Compare commits
58
Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
170fad2e93 | ||
|
|
fab57411c5 | ||
|
|
212c8e4f52 | ||
|
|
7220cbc787 | ||
|
|
4713d984ea | ||
|
|
ceb7d920b6 | ||
|
|
9d5ddcd050 | ||
|
|
007dd17910 | ||
|
|
30e6435dea | ||
|
|
ba3635307c | ||
|
|
c88b75a822 | ||
|
|
bd5bf861f1 | ||
|
|
c55e705357 | ||
|
|
d4e9f699d1 | ||
|
|
c09b7c0513 | ||
|
|
0a398579e2 | ||
|
|
219c6b1a47 | ||
|
|
649865b7d2 | ||
|
|
098a0f6542 | ||
|
|
4b50f5171b | ||
|
|
a8e62b52fd | ||
|
|
653eb10d41 | ||
|
|
afc919e47e | ||
|
|
8d92433f20 | ||
|
|
3209cfa6fb | ||
|
|
4d017422dc | ||
|
|
5c138c693f | ||
|
|
04a85108e5 | ||
|
|
389630265b | ||
|
|
4847c96b58 | ||
|
|
161abbc621 | ||
|
|
de31c6a9d1 | ||
|
|
4909c9c8c7 | ||
|
|
171148a59f | ||
|
|
1db1965073 | ||
|
|
120c38427c | ||
|
|
e7112ad575 | ||
|
|
1758a2cb4e | ||
|
|
87144173a0 | ||
|
|
14a5c1c80a | ||
|
|
1267c74fec | ||
|
|
f7971f0f4b | ||
|
|
dd074ecb18 | ||
|
|
228b8669d0 | ||
|
|
96b7b6551c | ||
|
|
7c4891cd13 | ||
|
|
dfb9336ced | ||
|
|
6a0e08bec1 | ||
|
|
7ad9e531d4 | ||
|
|
5db896c813 | ||
|
|
e54bca6575 | ||
|
|
dbe28dceba | ||
|
|
39d724fd77 | ||
|
|
3b89d398fa | ||
|
|
0c98e627c2 | ||
|
|
d33e6baa73 | ||
|
|
e7f1a99188 | ||
|
|
0bdbdd8772 |
-13
@@ -14,7 +14,6 @@ modules/claude-code/claude-code
|
|||||||
modules/mesh-watcher/mesh-watcher
|
modules/mesh-watcher/mesh-watcher
|
||||||
modules/dnsmasq/dnsmasq
|
modules/dnsmasq/dnsmasq
|
||||||
modules/mesh-issues/cmd/mesh-issues/mesh-issues
|
modules/mesh-issues/cmd/mesh-issues/mesh-issues
|
||||||
modules/mesh-issues/mesh-issues
|
|
||||||
modules/mounts/cmd/mounts/mounts
|
modules/mounts/cmd/mounts/mounts
|
||||||
modules/mesh-knowledge/cmd/mesh-knowledge/mesh-knowledge
|
modules/mesh-knowledge/cmd/mesh-knowledge/mesh-knowledge
|
||||||
modules/mesh-knowledge/mesh-knowledge
|
modules/mesh-knowledge/mesh-knowledge
|
||||||
@@ -29,15 +28,3 @@ modules/brother/cmd/brother/brother
|
|||||||
modules/mesh-build-go/cmd/mesh-build-go/mesh-build-go
|
modules/mesh-build-go/cmd/mesh-build-go/mesh-build-go
|
||||||
modules/mesh-config/mesh-config
|
modules/mesh-config/mesh-config
|
||||||
modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
|
modules/mesh-build-image/cmd/mesh-build-image/mesh-build-image
|
||||||
*.test
|
|
||||||
|
|
||||||
# binaries a local go build leaves in a module root
|
|
||||||
/checks/paths/paths
|
|
||||||
/modules/mesh-build-go/mesh-build-go
|
|
||||||
/modules/mesh-delivery-gate/mesh-delivery-gate
|
|
||||||
modules/mesh-config/cmd/mesh-config/mesh-config
|
|
||||||
|
|
||||||
# Built by their modules' build step (issue 1577), never committed.
|
|
||||||
modules/forticlient/cmd/forticlient-tools/forticlient-tools
|
|
||||||
modules/networkmanager/uplink-tools
|
|
||||||
checks/words/words
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest.
|
|
||||||
modules/*/module.json
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/config
|
module git.novox.be/novox/mesh-catalog/checks/config
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
|
|
||||||
|
|
||||||
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func Check(root string) (map[string][]string, error) {
|
|||||||
// The modules taking a whole layer were found after the list was first written and before the trunk held a copy
|
// The modules taking a whole layer were found after the list was first written and before the trunk held a copy
|
||||||
// (issue 735), so they stand in it too.
|
// (issue 735), so they stand in it too.
|
||||||
var seed = map[string]bool{
|
var seed = map[string]bool{
|
||||||
"fail2ban": true, "influxdb": true, "keycloak": true, "mailu": true, "searxng": true,
|
"betterbird": true, "fail2ban": true, "influxdb": true, "keycloak": true, "mailu": true, "searxng": true,
|
||||||
"baserow": true, "confluence": true, "gitea": true, "gitlab": true, "grafana": true, "home-assistant": true,
|
"baserow": true, "confluence": true, "gitea": true, "gitlab": true, "grafana": true, "home-assistant": true,
|
||||||
"icecast": true, "jira": true, "letta": true, "matrix": true, "nodered": true, "supabase": true, "unifi": true,
|
"icecast": true, "jira": true, "letta": true, "matrix": true, "nodered": true, "supabase": true, "unifi": true,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest and the files it names.
|
|
||||||
modules
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-catalog/checks/trunk"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The script's comparison of this list with the trunk's (novox/hq ADR 0529 clause 2): the list of modules not yet moved gains none
|
|
||||||
// the trunk's copy at the merge base does not hold. The catalogue itself is judged against the list by
|
|
||||||
// TestTheCatalogueSaysConfigInEveryMovedModule. Skipped, and said, where no trunk is beside.
|
|
||||||
func TestTheUnmovedListHoldsNoModuleTheTrunksDoesNot(t *testing.T) {
|
|
||||||
path, ok := trunk.Copy(t, "checks/config/unmoved")
|
|
||||||
found, err := Check(filepath.Join("..", ".."))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
unmoved, err := Unmoved("unmoved")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
base := seed
|
|
||||||
if ok {
|
|
||||||
if base, err = Unmoved(path); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, _, added := Judge(found, unmoved, base)
|
|
||||||
for _, m := range added {
|
|
||||||
t.Errorf("checks/config/unmoved gains %s, which the trunk's copy does not hold: the list only shrinks", m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module checks/credentialform
|
|
||||||
|
|
||||||
go 1.26.0
|
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
@@ -1,98 +0,0 @@
|
|||||||
// Package credentialform refuses what the catalogue's tree holds about credentials and routes that no manifest judge
|
|
||||||
// sees: a module that provides and keeps a `bound` state the provisioner does not read, and a route received as a file.
|
|
||||||
// It judges no offer's credential form: that rule (an own secret, an issuer, or `form` pair or none; the lead,
|
|
||||||
// 2026-10-29) is part of the catalogue's rules, which the checker's judge runs on every change (CatalogueProblems,
|
|
||||||
// novox/hq ADR 0578 §2); this package called it itself until 2026-11.
|
|
||||||
//
|
|
||||||
// It refuses a route received as a file too (issue 774, research 072 row 34, the operator on 2026-10-31: "there is only
|
|
||||||
// 1 path, the other is simply deprecated/invalid"): a route is a provision, served from the seat mesh-provisioner's
|
|
||||||
// plans alone, and a manifest's `receives.route` is the deprecated path. A module still moving off it is listed in
|
|
||||||
// stillReceivingRoutes with why; every refusal names them all, and a listed module that no longer receives one is
|
|
||||||
// refused too, so the list only shrinks.
|
|
||||||
package credentialform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Problems is every module of the manifests under root/modules that keeps a `bound` state the provisioner does not read,
|
|
||||||
// and every route received as a file, one line each.
|
|
||||||
func Problems(root string) ([]string, error) {
|
|
||||||
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sort.Strings(files)
|
|
||||||
var out []string
|
|
||||||
var receiving []string
|
|
||||||
reads := map[string]bool{}
|
|
||||||
var binders []string
|
|
||||||
for _, f := range files {
|
|
||||||
raw, err := os.ReadFile(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
m, err := catalogue.Decode(raw)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %w", f, err)
|
|
||||||
}
|
|
||||||
if _, received := m.Receives["route"]; received {
|
|
||||||
receiving = append(receiving, m.Module)
|
|
||||||
} else if _, listed := stillReceivingRoutes[m.Module]; listed {
|
|
||||||
out = append(out, fmt.Sprintf("%s no longer receives route; take it off stillReceivingRoutes", m.Module))
|
|
||||||
}
|
|
||||||
var kept struct {
|
|
||||||
Reads []string `json:"reads"`
|
|
||||||
State []json.RawMessage `json:"state"`
|
|
||||||
}
|
|
||||||
_ = json.Unmarshal(raw, &kept) // reads may hold objects in another module; only the provisioner's are read
|
|
||||||
if m.Module == "mesh-provisioner" {
|
|
||||||
for _, r := range kept.Reads {
|
|
||||||
reads[r] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(m.Provides) > 0 && keepsBound(kept.State) {
|
|
||||||
binders = append(binders, m.Module)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, module := range binders {
|
|
||||||
if !reads[module+".bound"] {
|
|
||||||
out = append(out, fmt.Sprintf("%s provides and keeps a `bound` state, and mesh-provisioner's manifest does not read %s.bound", module, module))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, module := range receiving {
|
|
||||||
if _, listed := stillReceivingRoutes[module]; !listed {
|
|
||||||
out = append(out, fmt.Sprintf("%s receives route as a file: a route is a provision, served from the seat "+
|
|
||||||
"mesh-provisioner's plans alone (issue 774); still on the deprecated path: %s", module,
|
|
||||||
strings.Join(receiving, ", ")))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// stillReceivingRoutes is every module still receiving its routes as a file, and why it has not moved yet.
|
|
||||||
var stillReceivingRoutes = map[string]string{
|
|
||||||
"route-adapter": "migration scaffolding writing into a predecessor's proxy (novox/hq ADR 0104), deleted when that " +
|
|
||||||
"migration ends; it reads the received file its TypeScript adapter turns into the predecessor's route files",
|
|
||||||
}
|
|
||||||
|
|
||||||
// keepsBound says whether a manifest's `state` names `bound`, as a plain name or an object with a name.
|
|
||||||
func keepsBound(state []json.RawMessage) bool {
|
|
||||||
for _, s := range state {
|
|
||||||
var name string
|
|
||||||
var obj struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if json.Unmarshal(s, &name) == nil && name == "bound" || json.Unmarshal(s, &obj) == nil && obj.Name == "bound" {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
@@ -1,54 +0,0 @@
|
|||||||
package credentialform
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func aCatalogue(t *testing.T, manifests map[string]string) string {
|
|
||||||
t.Helper()
|
|
||||||
root := t.TempDir()
|
|
||||||
for module, body := range manifests {
|
|
||||||
dir := filepath.Join(root, "modules", module)
|
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
// A binder the provisioner does not read is refused and named; an offer that says no form is the judge's to refuse, not
|
|
||||||
// this check's; a route received as a file is refused but for the module still moving off it; and the real catalogue is
|
|
||||||
// clean.
|
|
||||||
func TestNoBinderIsUnreadAndNoRouteIsReceivedAsAFile(t *testing.T) {
|
|
||||||
root := aCatalogue(t, map[string]string{
|
|
||||||
"silent": `{"module":"silent","provides":[{"name":"thing","scope":"mesh"}]}`,
|
|
||||||
"binder": `{"module":"binder","provides":[{"name":"d","credential":{"form":"pair"}}],"state":["bound"]}`,
|
|
||||||
"mesh-provisioner": `{"module":"mesh-provisioner","reads":["other.bound"]}`,
|
|
||||||
"says": `{"module":"says","provides":[{"name":"a","credential":{"form":"pair"}},{"name":"b","credential":{"form":"none"}},{"name":"c","credential":{"own":"jwt"}}]}`,
|
|
||||||
// A route received as a file (issue 774, research 072 row 34): refused, naming every module still on it; the one
|
|
||||||
// listed as still moving passes.
|
|
||||||
"old-proxy": `{"module":"old-proxy","receives":{"route":"/var/lib/old-proxy/mesh.json"}}`,
|
|
||||||
"route-adapter": `{"module":"route-adapter","receives":{"route":"/var/lib/route-adapter/mesh.json"}}`,
|
|
||||||
})
|
|
||||||
got, err := Problems(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(got) != 2 || !strings.Contains(got[0], "binder") || !strings.Contains(got[0], "mesh-provisioner") ||
|
|
||||||
!strings.HasPrefix(got[1], "old-proxy receives route") || !strings.Contains(got[1], "old-proxy, route-adapter") {
|
|
||||||
t.Fatalf("refused %v, wanted the binder the provisioner does not read and the received route", got)
|
|
||||||
}
|
|
||||||
real, err := Problems(filepath.Join("..", ".."))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(real) != 0 {
|
|
||||||
t.Fatalf("the catalogue has binders or routes it must not:\n%s", strings.Join(real, "\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (issue 1936).
|
|
||||||
# every manifest, for its bound state and its received routes.
|
|
||||||
modules/*/module.json
|
|
||||||
@@ -1,128 +0,0 @@
|
|||||||
// Package failurekit refuses a build module whose tests do not run mesh-sdk's conformance kit (issue 1901, its thorough
|
|
||||||
// fix): a module that claims a node-build-* seat and serves a test step must hold a test that calls conformance.Run, so
|
|
||||||
// its parse of the test runner's output is held to the failure contract (a failing test named with its cause, never a
|
|
||||||
// passing line in its place). A build module with no test step (mesh-build-image builds images and runs no tests) is
|
|
||||||
// exempt from that, and said so. Every build module, test step or not, must also hold a test that calls
|
|
||||||
// conformance.LeftRunning with its own start sweep, so a step a previous process left running is ended at its start
|
|
||||||
// (issue 1890).
|
|
||||||
package failurekit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Judged is what the check says of one build module: Refused says why it fails the check, "" when it passes; Exempt
|
|
||||||
// says why the kit does not apply.
|
|
||||||
type Judged struct {
|
|
||||||
Module, Refused, Exempt string
|
|
||||||
}
|
|
||||||
|
|
||||||
type manifest struct {
|
|
||||||
Claims []struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
} `json:"claims"`
|
|
||||||
Tools []json.RawMessage `json:"tools"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Judge reads every modules/<m>/module.json of the catalogue at root and judges each module that claims a node-build-*
|
|
||||||
// seat.
|
|
||||||
func Judge(root string) ([]Judged, error) {
|
|
||||||
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sort.Strings(manifests)
|
|
||||||
var out []Judged
|
|
||||||
for _, p := range manifests {
|
|
||||||
raw, err := os.ReadFile(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var m manifest
|
|
||||||
if err := json.Unmarshal(raw, &m); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %w", p, err)
|
|
||||||
}
|
|
||||||
seat := ""
|
|
||||||
for _, c := range m.Claims {
|
|
||||||
if strings.HasPrefix(c.Name, "node-build-") {
|
|
||||||
seat = c.Name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if seat == "" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dir := filepath.Dir(p)
|
|
||||||
j := Judged{Module: filepath.Base(dir)}
|
|
||||||
var refused []string
|
|
||||||
test := testTool(m.Tools)
|
|
||||||
switch {
|
|
||||||
case test == "":
|
|
||||||
j.Exempt = fmt.Sprintf("it claims %s and serves no test step, so the failure contract's cases do not apply", seat)
|
|
||||||
default:
|
|
||||||
runs, err := runsTheKit(dir, "conformance.Run(")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !runs {
|
|
||||||
refused = append(refused, fmt.Sprintf("it claims %s and serves the test step %s, and no test of it calls conformance.Run "+
|
|
||||||
"(mesh-sdk go/buildstep/conformance): run the kit with the module's own parse path (issue 1901)", seat, test))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Every build module keeps steps, so every one runs the left-running case, test step or not (issue 1890).
|
|
||||||
sweeps, err := runsTheKit(dir, "conformance.LeftRunning(")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if !sweeps {
|
|
||||||
refused = append(refused, fmt.Sprintf("it claims %s, and no test of it calls conformance.LeftRunning "+
|
|
||||||
"(mesh-sdk go/buildstep/conformance): run the kit's left-running case with the module's own start sweep (issue 1890)", seat))
|
|
||||||
}
|
|
||||||
j.Refused = strings.Join(refused, "; ")
|
|
||||||
out = append(out, j)
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// testTool is the name of the module's test step tool (<prefix>_test), or "".
|
|
||||||
func testTool(tools []json.RawMessage) string {
|
|
||||||
for _, raw := range tools {
|
|
||||||
var name string
|
|
||||||
if json.Unmarshal(raw, &name) != nil {
|
|
||||||
var t struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
}
|
|
||||||
if json.Unmarshal(raw, &t) != nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
name = t.Name
|
|
||||||
}
|
|
||||||
if strings.HasSuffix(name, "_test") {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// runsTheKit says a _test.go file under dir calls the kit's function call (such as "conformance.Run(").
|
|
||||||
func runsTheKit(dir, call string) (bool, error) {
|
|
||||||
found := false
|
|
||||||
err := filepath.WalkDir(dir, func(p string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil || found || d.IsDir() || !strings.HasSuffix(p, "_test.go") {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
raw, err := os.ReadFile(p)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
found = strings.Contains(string(raw), call) &&
|
|
||||||
strings.Contains(string(raw), `"git.novox.be/novox/mesh-sdk/go/buildstep/conformance"`)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return found, err
|
|
||||||
}
|
|
||||||
@@ -1,77 +0,0 @@
|
|||||||
package failurekit
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// 1901/AC7 and 1890/AC8: a build module with a test step and no kit is refused, one that runs the kit passes, one with no
|
|
||||||
// test step is exempt from the failure cases but not from the left-running case, a module that runs only the failure
|
|
||||||
// cases is refused, and a module that claims no node-build-* seat is not judged.
|
|
||||||
func TestABuildModuleWithATestStepMustRunTheKit(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
const imp = "package main\n\nimport \"git.novox.be/novox/mesh-sdk/go/buildstep/conformance\"\n\n"
|
|
||||||
const sweep = "func TestSweep(t *testing.T) { conformance.LeftRunning(t, nil) }\n"
|
|
||||||
kit := imp + "func TestKit(t *testing.T) { conformance.Run(t, conformance.GoTest, nil) }\n" + sweep
|
|
||||||
for path, body := range map[string]string{
|
|
||||||
"modules/a/module.json": `{"claims":[{"name":"node-build-a"}],"tools":["build_a_fetch","build_a_test"]}`,
|
|
||||||
"modules/a/cmd/a/check_test.go": "package main\n",
|
|
||||||
"modules/b/module.json": `{"claims":[{"name":"node-build-b"}],"tools":[{"name":"build_b_test"}]}`,
|
|
||||||
"modules/b/cmd/b/kit_test.go": kit,
|
|
||||||
"modules/c/module.json": `{"claims":[{"name":"node-build-c"}],"tools":["build_c_build"]}`,
|
|
||||||
"modules/c/cmd/c/kit_test.go": imp + sweep,
|
|
||||||
"modules/f/module.json": `{"claims":[{"name":"node-build-f"}],"tools":["build_f_build"]}`,
|
|
||||||
"modules/g/module.json": `{"claims":[{"name":"node-build-g"}],"tools":["build_g_test"]}`,
|
|
||||||
"modules/g/cmd/g/kit_test.go": imp + "func TestKit(t *testing.T) { conformance.Run(t, conformance.GoTest, nil) }\n",
|
|
||||||
"modules/d/module.json": `{"claims":[{"name":"other"}],"tools":["d_test"]}`,
|
|
||||||
"modules/e/module.json": `{"claims":[{"name":"node-build-e"}],"tools":["build_e_test"]}`,
|
|
||||||
"modules/e/cmd/e/kit.go": kit, // not a test file
|
|
||||||
} {
|
|
||||||
p := filepath.Join(root, path)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
got, err := Judge(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
want := map[string]string{"a": "refused", "b": "passes", "c": "exempt", "e": "refused", "f": "refused", "g": "refused"}
|
|
||||||
if len(got) != len(want) {
|
|
||||||
t.Fatalf("judged %+v, want a, b, c, e, f and g", got)
|
|
||||||
}
|
|
||||||
for _, j := range got {
|
|
||||||
is := "passes"
|
|
||||||
if j.Refused != "" {
|
|
||||||
is = "refused"
|
|
||||||
} else if j.Exempt != "" {
|
|
||||||
is = "exempt"
|
|
||||||
}
|
|
||||||
if is != want[j.Module] {
|
|
||||||
t.Errorf("%s %s (%+v), want it %s", j.Module, is, j, want[j.Module])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Every build module of this catalogue with a test step runs the kit.
|
|
||||||
func TestTheCataloguesBuildModulesRunTheKit(t *testing.T) {
|
|
||||||
got, err := Judge(filepath.Join("..", ".."))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(got) == 0 {
|
|
||||||
t.Fatal("no module claims a node-build-* seat: the check reads the wrong tree")
|
|
||||||
}
|
|
||||||
for _, j := range got {
|
|
||||||
switch {
|
|
||||||
case j.Refused != "":
|
|
||||||
t.Errorf("%s: %s", j.Module, j.Refused)
|
|
||||||
case j.Exempt != "":
|
|
||||||
t.Logf("%s is exempt: %s", j.Module, j.Exempt)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/failurekit
|
|
||||||
|
|
||||||
go 1.22
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Judge("../.."): every manifest, and the directory of each module claiming a node-build-* seat, walked for a test that
|
|
||||||
# calls the kit. The build modules are the mesh-build-* directories; a build module named otherwise needs its line here.
|
|
||||||
modules/*/module.json
|
|
||||||
modules/mesh-build-*
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# The calls of the controller's hand-act log a manifest of this catalogue may still invoke, each `<module> <verb> <why>`:
|
|
||||||
# the fallback of a recorder (mesh-sdk handacts.Recorder) while the seat mesh-hand-acts has no holder that answers, until
|
|
||||||
# the controller's lease is written to a holder that is no controller. The check of this directory (handacts) refuses any
|
|
||||||
# other, and an entry whose module no longer invokes it: the list only shrinks, and it is empty before the lease write
|
|
||||||
# (novox/hq ADR 0565 §11; issue 1953, issue 774). It is empty: no manifest invokes the controller's log (issue 1953, 1955).
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/handacts
|
|
||||||
|
|
||||||
go 1.22
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// Command handacts refuses a manifest that records into the controller's hand-act log (novox/hq ADR 0565 §11): every
|
|
||||||
// recorder records through the seat mesh-hand-acts from its delivery, so no manifest invokes
|
|
||||||
// `seat:mesh-controller.hand-act`, `.warranted` or `.drill`. The recorders' fallbacks to the controller's log while the
|
|
||||||
// seat has no holder are named in checks/handacts/fallbacks, each module and verb with why; that list only shrinks, and
|
|
||||||
// an entry whose module no longer invokes the verb is refused too.
|
|
||||||
//
|
|
||||||
// go run . <catalog root>
|
|
||||||
//
|
|
||||||
// Exits 1 on a finding.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Verbs are the controller's hand-act verbs, as a manifest invokes them, and Allowed the list of named fallbacks.
|
|
||||||
var Verbs = []string{"hand-act", "warranted", "drill"}
|
|
||||||
|
|
||||||
const (
|
|
||||||
invokePrefix = "seat:mesh-controller."
|
|
||||||
Allowed = "checks/handacts/fallbacks"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Findings is every refusal in the catalog at root, sorted.
|
|
||||||
func Findings(root string) ([]string, error) {
|
|
||||||
allowed := map[string]bool{}
|
|
||||||
raw, err := os.ReadFile(filepath.Join(root, Allowed))
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, line := range strings.Split(string(raw), "\n") {
|
|
||||||
if f := strings.Fields(line); len(f) >= 3 && !strings.HasPrefix(f[0], "#") {
|
|
||||||
allowed[f[0]+" "+f[1]] = true
|
|
||||||
} else if len(f) > 0 && !strings.HasPrefix(f[0], "#") {
|
|
||||||
return nil, fmt.Errorf("%s: %q is not `<module> <verb> <why>`", Allowed, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var out []string
|
|
||||||
invoked := map[string]bool{}
|
|
||||||
for _, path := range manifests {
|
|
||||||
raw, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var m struct {
|
|
||||||
Module string `json:"module"`
|
|
||||||
Invokes []string `json:"invokes"`
|
|
||||||
}
|
|
||||||
if json.Unmarshal(raw, &m) != nil || m.Module == "mesh-controller" {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, inv := range m.Invokes {
|
|
||||||
verb, ok := strings.CutPrefix(strings.TrimSpace(inv), invokePrefix)
|
|
||||||
if !ok || !contains(Verbs, verb) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
invoked[m.Module+" "+verb] = true
|
|
||||||
if !allowed[m.Module+" "+verb] {
|
|
||||||
out = append(out, fmt.Sprintf("%s invokes %s%s: record through seat:mesh-hand-acts.%s (novox/hq ADR 0565 §11), "+
|
|
||||||
"or name the fallback in %s", m.Module, invokePrefix, verb, verb, Allowed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for entry := range allowed {
|
|
||||||
if !invoked[entry] {
|
|
||||||
out = append(out, fmt.Sprintf("%s names %q, which is no longer invoked: take it off the list, which only shrinks",
|
|
||||||
Allowed, entry))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(out)
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func contains(list []string, s string) bool {
|
|
||||||
for _, x := range list {
|
|
||||||
if x == s {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if len(os.Args) != 2 {
|
|
||||||
fmt.Fprintln(os.Stderr, "usage: handacts <catalog root>")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
found, err := Findings(os.Args[1])
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
fmt.Println(f)
|
|
||||||
}
|
|
||||||
if len(found) > 0 {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println("no manifest records into the controller's hand-act log but the fallbacks named")
|
|
||||||
}
|
|
||||||
@@ -1,59 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// No manifest invokes the controller's hand-act verbs but a named fallback; a named fallback no longer invoked is
|
|
||||||
// refused (novox/hq ADR 0565 §11's catalog test).
|
|
||||||
func TestFindings(t *testing.T) {
|
|
||||||
for _, row := range []struct {
|
|
||||||
name string
|
|
||||||
files map[string]string
|
|
||||||
want string // empty: clean
|
|
||||||
}{
|
|
||||||
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-hand-acts.hand-act"]}`}, ""},
|
|
||||||
{"an unnamed call", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.warranted"]}`},
|
|
||||||
"a invokes seat:mesh-controller.warranted"},
|
|
||||||
{"a named fallback, and the controller's own", map[string]string{
|
|
||||||
"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.drill"]}`,
|
|
||||||
"modules/mesh-controller/module.json": `{"module":"mesh-controller","invokes":["seat:mesh-controller.hand-act"]}`,
|
|
||||||
Allowed: "# why\na drill fallback until the lease write\n"}, ""},
|
|
||||||
{"a fallback no longer invoked", map[string]string{Allowed: "gone hand-act fallback\n"}, `names "gone hand-act"`},
|
|
||||||
{"another verb of the controller", map[string]string{"modules/a/module.json": `{"module":"a","invokes":["seat:mesh-controller.conditions"]}`}, ""},
|
|
||||||
} {
|
|
||||||
t.Run(row.name, func(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
for path, text := range row.files {
|
|
||||||
p := filepath.Join(root, path)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
got, err := Findings(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if all := strings.Join(got, "\n"); (row.want == "") != (all == "") || !strings.Contains(all, row.want) {
|
|
||||||
t.Fatalf("findings = %q, want one containing %q", all, row.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The run of this check over the real tree.
|
|
||||||
func TestTheCatalogRecordsOnlyThroughTheSeat(t *testing.T) {
|
|
||||||
got, err := Findings(filepath.Join("..", ".."))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, f := range got {
|
|
||||||
t.Error(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (issue 1936).
|
|
||||||
# every manifest, for what it invokes.
|
|
||||||
modules/*/module.json
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module checks/health
|
|
||||||
|
|
||||||
go 1.26.0
|
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
// Package health is the catalogue's count of long-running resources that do not say how they are ready (novox/hq ADR 0240 rule
|
|
||||||
// 8), kept in health-undeclared and judged by a test over the real tree (ADR 0529 clauses 2 and 4): it replaces the step of
|
|
||||||
// merge-check.sh that read it out of the controller's `module check`. Every manifest is decoded with mesh-sdk's catalogue
|
|
||||||
// package, the one the controller counts with, and judged by nothing here (novox/hq ADR 0578 §3): a resource's `health` is
|
|
||||||
// in the manifest itself, not in a template's text.
|
|
||||||
package health
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Count is the long-running resources without `health` of every module under root/modules.
|
|
||||||
func Count(root string) (int, error) {
|
|
||||||
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
sort.Strings(manifests)
|
|
||||||
if len(manifests) == 0 {
|
|
||||||
return 0, fmt.Errorf("no manifest under %s: the count would be zero for any catalogue", filepath.Join(root, "modules"))
|
|
||||||
}
|
|
||||||
n := 0
|
|
||||||
for _, path := range manifests {
|
|
||||||
raw, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
m, err := catalogue.Decode(raw)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("%s: %w", path, err)
|
|
||||||
}
|
|
||||||
n += len(catalogue.Undeclared(m))
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Kept is the number the file keeps: the first number of its first line.
|
|
||||||
func Kept(path string) (int, error) {
|
|
||||||
raw, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
first, _, _ := strings.Cut(string(raw), "\n")
|
|
||||||
digits := ""
|
|
||||||
for _, r := range strings.TrimSpace(first) {
|
|
||||||
if r < '0' || r > '9' {
|
|
||||||
break
|
|
||||||
}
|
|
||||||
digits += string(r)
|
|
||||||
}
|
|
||||||
n, err := strconv.Atoi(digits)
|
|
||||||
if err != nil {
|
|
||||||
return 0, fmt.Errorf("%s does not begin with the number it keeps: %q", path, first)
|
|
||||||
}
|
|
||||||
return n, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Judge says what is wrong with a count against the kept number; empty when they agree. It only goes down: a rise is refused,
|
|
||||||
// and a fall asks the new number written in health-undeclared, so the count cannot rise again.
|
|
||||||
func Judge(kept, counted int) string {
|
|
||||||
switch {
|
|
||||||
case counted > kept:
|
|
||||||
return fmt.Sprintf("the long-running resources without health rose from %d to %d: declare how each new one is ready (ADR 0240 rule 8)", kept, counted)
|
|
||||||
case counted < kept:
|
|
||||||
return fmt.Sprintf("the long-running resources without health fell from %d to %d: write %d in health-undeclared, so the count cannot rise again", kept, counted, counted)
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package health
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func aCatalogue(t *testing.T, manifests map[string]string) string {
|
|
||||||
t.Helper()
|
|
||||||
root := t.TempDir()
|
|
||||||
for module, body := range manifests {
|
|
||||||
dir := filepath.Join(root, "modules", module)
|
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "module.json"), []byte(body), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return root
|
|
||||||
}
|
|
||||||
|
|
||||||
// A service that stays up and does not say how it is ready is counted; one that says, and a file, are not.
|
|
||||||
func TestOnlyALongRunningResourceWithoutHealthIsCounted(t *testing.T) {
|
|
||||||
root := aCatalogue(t, map[string]string{
|
|
||||||
"quiet": `{"module":"quiet","resources":[{"id":"svc","type":"service","unit":"quiet.service","state":"running"}]}`,
|
|
||||||
"says": `{"module":"says","resources":[{"id":"svc","type":"service","unit":"says.service","state":"running",` +
|
|
||||||
`"health":{"kind":"unit"}}]}`,
|
|
||||||
"file": `{"module":"file","resources":[{"id":"cfg","type":"file","path":"/etc/file.conf","content":"x"}]}`,
|
|
||||||
})
|
|
||||||
counted, err := Count(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if counted != 1 {
|
|
||||||
t.Fatalf("counted %d, wanted the one quiet service", counted)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTheCountMayOnlyGoDown(t *testing.T) {
|
|
||||||
for _, c := range []struct {
|
|
||||||
kept, counted int
|
|
||||||
says string
|
|
||||||
}{
|
|
||||||
{58, 58, ""},
|
|
||||||
{58, 59, "rose from 58 to 59"},
|
|
||||||
{58, 57, "fell from 58 to 57"},
|
|
||||||
} {
|
|
||||||
got := Judge(c.kept, c.counted)
|
|
||||||
if (c.says == "") != (got == "") || !strings.Contains(got, c.says) {
|
|
||||||
t.Errorf("kept %d counted %d: %q, wanted %q", c.kept, c.counted, got, c.says)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTheKeptCountIsTheFirstNumberOfTheFile(t *testing.T) {
|
|
||||||
path := filepath.Join(t.TempDir(), "health-undeclared")
|
|
||||||
if err := os.WriteFile(path, []byte("58\nThe long-running resources ...\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if n, err := Kept(path); err != nil || n != 58 {
|
|
||||||
t.Fatalf("%d %v", n, err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(path, []byte("many\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := Kept(path); err == nil {
|
|
||||||
t.Fatal("a file with no number gave a count")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The script's `module check` run over the real tree (novox/hq ADR 0240 rule 8, ADR 0529 clause 2 and 4): the long-running
|
|
||||||
// resources that do not say how they are ready, counted as the controller counts them, are the number health-undeclared keeps.
|
|
||||||
func TestTheCatalogueSaysHowItsLongRunningResourcesAreReady(t *testing.T) {
|
|
||||||
kept, err := Kept(filepath.Join("..", "..", "health-undeclared"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
counted, err := Count(filepath.Join("..", ".."))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if say := Judge(kept, counted); say != "" {
|
|
||||||
t.Fatal(say)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# every manifest, decoded; and the root's kept count of resources that do not say how they are ready.
|
|
||||||
modules/*/module.json
|
|
||||||
health-undeclared
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest.
|
|
||||||
modules/*/module.json
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest, and whether the path each image artifact builds from is there.
|
|
||||||
modules
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/locks
|
|
||||||
|
|
||||||
go 1.22
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
// Command locks refuses a TypeScript module that names packages beyond the SDK in its package.json and commits no
|
|
||||||
// package-lock.json (novox/hq ADR 0469 §5, issue 1868): the Node build module installs a module's own packages only
|
|
||||||
// from its committed lock, so such a module is refused by the builder and could not be delivered.
|
|
||||||
//
|
|
||||||
// go run . <catalogue root>
|
|
||||||
//
|
|
||||||
// Exits 1 on a finding. The packages that count are those the build module counts (mesh-build-node fetch.go
|
|
||||||
// ownDependencies): the `dependencies` beyond the SDK, typescript and the @types packages.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Finding is one module refused: it names Packages beyond the SDK and has no lock file.
|
|
||||||
type Finding struct {
|
|
||||||
Module string
|
|
||||||
Packages []string
|
|
||||||
}
|
|
||||||
|
|
||||||
// Check reads every module under root/modules that builds a TypeScript artifact.
|
|
||||||
func Check(root string) ([]Finding, error) {
|
|
||||||
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sort.Strings(manifests)
|
|
||||||
var out []Finding
|
|
||||||
for _, path := range manifests {
|
|
||||||
raw, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var m struct {
|
|
||||||
Module string `json:"module"`
|
|
||||||
Build struct {
|
|
||||||
Artifacts []struct {
|
|
||||||
Language string `json:"language"`
|
|
||||||
} `json:"artifacts"`
|
|
||||||
} `json:"build"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(raw, &m); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %w", path, err)
|
|
||||||
}
|
|
||||||
typescript := false
|
|
||||||
for _, a := range m.Build.Artifacts {
|
|
||||||
typescript = typescript || a.Language == "typescript"
|
|
||||||
}
|
|
||||||
if !typescript {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
dir := filepath.Dir(path)
|
|
||||||
pkgRaw, err := os.ReadFile(filepath.Join(dir, "package.json"))
|
|
||||||
if os.IsNotExist(err) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var pkg struct {
|
|
||||||
Dependencies map[string]json.RawMessage `json:"dependencies"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(pkgRaw, &pkg); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: package.json: %w", m.Module, err)
|
|
||||||
}
|
|
||||||
var names []string
|
|
||||||
for n := range pkg.Dependencies {
|
|
||||||
if n != "@novox/mesh-sdk" && n != "typescript" && !strings.HasPrefix(n, "@types/") {
|
|
||||||
names = append(names, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(names) == 0 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if _, err := os.Stat(filepath.Join(dir, "package-lock.json")); err != nil {
|
|
||||||
sort.Strings(names)
|
|
||||||
out = append(out, Finding{Module: m.Module, Packages: names})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
root := "."
|
|
||||||
if len(os.Args) > 1 {
|
|
||||||
root = os.Args[1]
|
|
||||||
}
|
|
||||||
found, err := Check(root)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
fmt.Printf("%s names packages beyond the SDK (%s) and has no package-lock.json: commit one made by the build module's toolchain (novox/hq ADR 0469 §5)\n", f.Module, strings.Join(f.Packages, ", "))
|
|
||||||
}
|
|
||||||
if len(found) > 0 {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println("locks: every TypeScript module that names packages beyond the SDK commits its lock")
|
|
||||||
}
|
|
||||||
@@ -1,75 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"reflect"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// module writes modules/<name> with a TypeScript artifact, the package.json given ("" for none) and, when lock, a lock.
|
|
||||||
func module(t *testing.T, root, name, language, pkg string, lock bool) {
|
|
||||||
t.Helper()
|
|
||||||
dir := filepath.Join(root, "modules", name)
|
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
write := func(file, body string) {
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, file), []byte(body), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
write("module.json", `{"module":"`+name+`","build":{"artifacts":[{"name":"tools","kind":"bundle","language":"`+language+`","from":"index.ts"}]}}`)
|
|
||||||
if pkg != "" {
|
|
||||||
write("package.json", pkg)
|
|
||||||
}
|
|
||||||
if lock {
|
|
||||||
write("package-lock.json", "{}")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
const sdkOnly = `{"dependencies":{"@novox/mesh-sdk":"^0.1.1"},"devDependencies":{"typescript":"^5","@types/node":"^22"}}`
|
|
||||||
const withPg = `{"dependencies":{"@novox/mesh-sdk":"^0.1.1","pg":"^8","@types/pg":"^8"}}`
|
|
||||||
|
|
||||||
func TestAModuleNamingOnlyTheSDKNeedsNoLock(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
module(t, root, "plain", "typescript", sdkOnly, false)
|
|
||||||
module(t, root, "none", "typescript", "", false)
|
|
||||||
got, err := Check(root)
|
|
||||||
if err != nil || len(got) != 0 {
|
|
||||||
t.Fatalf("%v %v", got, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAModuleNamingMorePackagesWithoutALockIsRefusedNamingThem(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
module(t, root, "db", "typescript", withPg, false)
|
|
||||||
module(t, root, "db-locked", "typescript", withPg, true)
|
|
||||||
got, err := Check(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if want := []Finding{{Module: "db", Packages: []string{"pg"}}}; !reflect.DeepEqual(got, want) {
|
|
||||||
t.Fatalf("got %v, want %v", got, want)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAModuleThatIsNotTypeScriptIsNotJudged(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
module(t, root, "go", "go", withPg, false)
|
|
||||||
if got, err := Check(root); err != nil || len(got) != 0 {
|
|
||||||
t.Fatalf("%v %v", got, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): every TypeScript module that names packages
|
|
||||||
// beyond the SDK commits its lock.
|
|
||||||
func TestTheCatalogueCommitsTheLocksItNeeds(t *testing.T) {
|
|
||||||
found, err := Check("../..")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
t.Errorf("%s names packages beyond the SDK (%v) and has no package-lock.json: commit one made by the build module's toolchain (novox/hq ADR 0469 §5)", f.Module, f.Packages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest, a TypeScript module's package.json and whether its lock is there.
|
|
||||||
modules/*/module.json
|
|
||||||
modules/*/package.json
|
|
||||||
modules/*/package-lock.json
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest.
|
|
||||||
modules/*/module.json
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module checks/manifests
|
|
||||||
|
|
||||||
go 1.26.0
|
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// Package manifests reads every module's manifest as the catalogue's tree holds it and refuses what no judge could
|
|
||||||
// read: a module.json that does not decode, and two manifests that name one module.
|
|
||||||
//
|
|
||||||
// It judges no manifest (novox/hq ADR 0578 §2): the checker judges every change's manifests with the indexer's judge, the
|
|
||||||
// one judge, so the field, seat, bus and catalogue rules are said once, there. Until 2026-11 this check parsed each
|
|
||||||
// manifest with mesh-sdk's strict parser and ran the rules between manifests (issue 774, power's bar item of
|
|
||||||
// mesh-catalog ca8d6f33); that judgement moved to the checker, and what stays is the two refusals that are about the
|
|
||||||
// tree, not a manifest's rules.
|
|
||||||
package manifests
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/catalogue"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Problems is what is wrong with the tree's manifests under root/modules, one line each: a module.json whose bytes are
|
|
||||||
// no manifest (catalogue.Decode: not JSON, or a field of the wrong shape), then a module named by two manifests.
|
|
||||||
func Problems(root string) ([]string, error) {
|
|
||||||
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sort.Strings(files)
|
|
||||||
var out []string
|
|
||||||
named := map[string]bool{}
|
|
||||||
for _, f := range files {
|
|
||||||
raw, err := os.ReadFile(f)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
rel, _ := filepath.Rel(root, f)
|
|
||||||
m, err := catalogue.Decode(raw)
|
|
||||||
if err != nil {
|
|
||||||
out = append(out, fmt.Sprintf("%s: %v", rel, err))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if named[m.Module] {
|
|
||||||
out = append(out, fmt.Sprintf("%s: %s was already given; two manifests name one module", rel, m.Module))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
named[m.Module] = true
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
package manifests
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The tree's manifests are ones a judge can read, and no module is named twice. A row is a catalogue of its own: a
|
|
||||||
// manifest that is no JSON and two manifests of one module are refused, naming the file; a manifest with a field this
|
|
||||||
// check does not know is not (it judges nothing: the checker does, ADR 0578 §2). The catalogue itself passes.
|
|
||||||
func TestEveryManifestDecodesAndNamesOneModule(t *testing.T) {
|
|
||||||
for _, row := range []struct {
|
|
||||||
name string
|
|
||||||
files map[string]string
|
|
||||||
refused string
|
|
||||||
}{
|
|
||||||
{name: "a manifest the judge can read", files: map[string]string{"a": `{"module":"a","version":"1"}`}},
|
|
||||||
{name: "a field no part knows is the judge's to refuse", files: map[string]string{"a": `{"module":"a","version":"1","unheard":true}`}},
|
|
||||||
{name: "bytes that are no manifest", files: map[string]string{"a": `{"module":`}, refused: "modules/a/module.json"},
|
|
||||||
{name: "two manifests of one module", files: map[string]string{"a": `{"module":"x","version":"1"}`, "b": `{"module":"x","version":"1"}`},
|
|
||||||
refused: "two manifests name one module"},
|
|
||||||
} {
|
|
||||||
root := t.TempDir()
|
|
||||||
for dir, manifest := range row.files {
|
|
||||||
if err := os.MkdirAll(filepath.Join(root, "modules", dir), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(root, "modules", dir, "module.json"), []byte(manifest), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
got, err := Problems(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
joined := strings.Join(got, "\n")
|
|
||||||
if (row.refused == "") != (len(got) == 0) || !strings.Contains(joined, row.refused) {
|
|
||||||
t.Errorf("%s: refused %q, want %q", row.name, joined, row.refused)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
real, err := Problems(filepath.Join("..", ".."))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(real) != 0 {
|
|
||||||
t.Fatalf("the catalogue holds manifests no judge can read:\n%s", strings.Join(real, "\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (issue 1936).
|
|
||||||
# every module's directory (its manifest), decoded.
|
|
||||||
modules
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/nobinaries
|
|
||||||
|
|
||||||
go 1.22
|
|
||||||
@@ -1,84 +0,0 @@
|
|||||||
// Package nobinaries refuses a compiled program anywhere in the repository (issue 1577; first a checks/ binary slipped past a modules/-only rule): it is build output, made by the module's build step,
|
|
||||||
// and `go build` in a module's folder writes one beside go.mod where `git add -A` takes it.
|
|
||||||
package nobinaries
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Kind says what executable format the first bytes of a file are, or "" when they are none.
|
|
||||||
func Kind(head []byte) string {
|
|
||||||
switch {
|
|
||||||
case bytes.HasPrefix(head, []byte("\x7fELF")):
|
|
||||||
return "ELF"
|
|
||||||
case bytes.HasPrefix(head, []byte("MZ")):
|
|
||||||
return "PE"
|
|
||||||
case bytes.HasPrefix(head, []byte{0xcf, 0xfa, 0xed, 0xfe}), bytes.HasPrefix(head, []byte{0xce, 0xfa, 0xed, 0xfe}),
|
|
||||||
bytes.HasPrefix(head, []byte{0xfe, 0xed, 0xfa, 0xcf}), bytes.HasPrefix(head, []byte{0xfe, 0xed, 0xfa, 0xce}),
|
|
||||||
bytes.HasPrefix(head, []byte{0xca, 0xfe, 0xba, 0xbe}) && len(head) >= 8 && head[7] < 0x20 && head[6] == 0 && head[5] == 0 && head[4] == 0:
|
|
||||||
return "Mach-O"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// Files are the files of the catalogue's root that the change holds, anywhere: the tracked ones when the root is a git
|
|
||||||
// checkout (an ignored binary a local build left is not the change's), else every file there but .git's.
|
|
||||||
func Files(root string) ([]string, error) {
|
|
||||||
cmd := exec.Command("git", "-C", root, "ls-files", "-z")
|
|
||||||
if out, err := cmd.Output(); err == nil && len(out) > 0 {
|
|
||||||
var files []string
|
|
||||||
for _, f := range strings.Split(strings.TrimRight(string(out), "\x00"), "\x00") {
|
|
||||||
if _, err := os.Lstat(filepath.Join(root, f)); err == nil { // a file the change deletes is not there
|
|
||||||
files = append(files, f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return files, nil
|
|
||||||
}
|
|
||||||
var files []string
|
|
||||||
err := filepath.WalkDir(root, func(p string, d os.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if d.IsDir() && d.Name() == ".git" {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
if d.IsDir() || !d.Type().IsRegular() {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
rel, _ := filepath.Rel(root, p)
|
|
||||||
files = append(files, rel)
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
return files, err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Find names each file of files (paths from root) that is a compiled program, with its format.
|
|
||||||
func Find(root string, files []string) ([]string, error) {
|
|
||||||
var found []string
|
|
||||||
for _, f := range files {
|
|
||||||
p := filepath.Join(root, f)
|
|
||||||
if st, err := os.Lstat(p); err != nil || !st.Mode().IsRegular() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
fh, err := os.Open(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
head := make([]byte, 8)
|
|
||||||
n, err := io.ReadFull(fh, head)
|
|
||||||
fh.Close()
|
|
||||||
if err != nil && err != io.ErrUnexpectedEOF && err != io.EOF {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if k := Kind(head[:n]); k != "" {
|
|
||||||
found = append(found, fmt.Sprintf("%s (%s)", f, k))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found, nil
|
|
||||||
}
|
|
||||||
@@ -1,89 +0,0 @@
|
|||||||
package nobinaries
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"os/exec"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFindNamesEachCompiledProgramAndNothingElse(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
files := map[string][]byte{
|
|
||||||
"modules/a/a": []byte("\x7fELF\x02\x01\x01\x00rest"),
|
|
||||||
"modules/b/cmd/b/b.exe": []byte("MZ\x90\x00"),
|
|
||||||
"modules/c/c": {0xcf, 0xfa, 0xed, 0xfe, 7, 0, 0, 1},
|
|
||||||
"modules/d/main.go": []byte("package main\n"),
|
|
||||||
"modules/e/short": []byte("\x7f"),
|
|
||||||
"modules/f/empty": {},
|
|
||||||
"modules/g/Foo.class": {0xca, 0xfe, 0xba, 0xbe, 0, 0, 0, 52}, // Java, not Mach-O
|
|
||||||
"checks/words/words": []byte("\x7fELF\x02\x01\x01\x00rest"), // outside modules/ too
|
|
||||||
}
|
|
||||||
var names []string
|
|
||||||
for f, b := range files {
|
|
||||||
p := filepath.Join(root, f)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(p, b, 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
names = append(names, f)
|
|
||||||
}
|
|
||||||
got, err := Find(root, names)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(got) != 4 {
|
|
||||||
t.Fatalf("found %v; wanted modules/a/a, modules/b/cmd/b/b.exe and modules/c/c and checks/words/words only", got)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The catalogue's own tree holds no compiled program: a compiled program is build output, so remove it and add it to .gitignore.
|
|
||||||
func TestTheCatalogueHoldsNoCompiledProgram(t *testing.T) {
|
|
||||||
root := filepath.Join("..", "..")
|
|
||||||
files, err := Files(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
found, err := Find(root, files)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
t.Errorf("%s: a compiled program is build output: remove it and add it to .gitignore (issue 1577)", f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// A compiled program outside modules/ is refused too (a binary under checks/ slipped past the modules/-only rule): Files holds
|
|
||||||
// every tracked file of a git checkout and every file of a plain folder, and Find names the binary in both.
|
|
||||||
func TestAProgramUnderChecksIsRefused(t *testing.T) {
|
|
||||||
for _, git := range []bool{true, false} {
|
|
||||||
root := t.TempDir()
|
|
||||||
p := filepath.Join(root, "checks", "words", "words")
|
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(p, []byte("\x7fELF\x02\x01\x01\x00rest"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if git {
|
|
||||||
for _, args := range [][]string{{"init", "-q"}, {"add", "-A"}} {
|
|
||||||
if out, err := exec.Command("git", append([]string{"-C", root}, args...)...).CombinedOutput(); err != nil {
|
|
||||||
t.Fatalf("git %v: %v: %s", args, err, out)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
files, err := Files(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
found, err := Find(root, files)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(found) != 1 {
|
|
||||||
t.Errorf("git=%v: found %v; wanted checks/words/words (ELF)", git, found)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
# every tracked file of the repository (git ls-files, no path limit): the whole tree, so this package's verdict is never kept.
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# The modules of this catalogue that still read the controller's assignments (mesh-controller.assignments), each with
|
|
||||||
# why. The check of this directory (noplacement) refuses any other module that reads them, and an entry here whose module no longer
|
|
||||||
# does: the list only shrinks, and it is empty before the controller's lease is written to a holder that is no
|
|
||||||
# controller (novox/hq ADR 0549 §3, §6; issue 774).
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/noplacement
|
|
||||||
|
|
||||||
go 1.22
|
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
|
|
||||||
|
|
||||||
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
|
|
||||||
@@ -1,170 +0,0 @@
|
|||||||
// Command noplacement refuses what the repository path of placement left behind (novox/hq ADR 0567 §10, §12).
|
|
||||||
//
|
|
||||||
// Placement, which module runs on which node, is bus state held by the seat mesh-placement; no file of the
|
|
||||||
// catalogue says it, and no manifest reads it from the delivery indexer, which no longer keeps it. It refuses:
|
|
||||||
//
|
|
||||||
// 1. a `placement` directory (or file) at the root of the catalogue;
|
|
||||||
//
|
|
||||||
// 2. a `particulars` file at the root (the mesh's node names come from the checker's run, §16);
|
|
||||||
//
|
|
||||||
// 3. a manifest naming a state, stream, event or bucket of the indexer's placement (`mesh-delivery-indexer.placement…`
|
|
||||||
// or `mesh-delivery-indexer_placement…`) in any string value, such as `reads`;
|
|
||||||
//
|
|
||||||
// 4. a manifest, other than mesh-controller's, that reads the controller's assignments (`mesh-controller.assignments`):
|
|
||||||
// every reader takes placement from the seat mesh-placement (novox/hq ADR 0549 §3, ADR 0567), and the lease write is
|
|
||||||
// refused while one still reads them (ADR 0549 §6; issue 774). The modules that still read them for what the
|
|
||||||
// placement does not carry are named in checks/noplacement/assignments-readers, each with why; that list only
|
|
||||||
// shrinks: an entry whose module no longer reads them is refused too.
|
|
||||||
//
|
|
||||||
// go run . <catalogue root>
|
|
||||||
//
|
|
||||||
// Exits 1 on a finding.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Prefixes are the names of the indexer's placement parts; a manifest string starting with one names them.
|
|
||||||
var Prefixes = []string{"mesh-delivery-indexer.placement", "mesh-delivery-indexer_placement"}
|
|
||||||
|
|
||||||
// Findings is every refusal in the catalogue at root, sorted.
|
|
||||||
func Findings(root string) ([]string, error) {
|
|
||||||
var out []string
|
|
||||||
if _, err := os.Lstat(filepath.Join(root, "placement")); err == nil {
|
|
||||||
out = append(out, "placement: the repository holds no placement; it is bus state of the seat mesh-placement (novox/hq ADR 0567 §10)")
|
|
||||||
}
|
|
||||||
if _, err := os.Lstat(filepath.Join(root, "particulars")); err == nil {
|
|
||||||
out = append(out, "particulars: the repository lists no node names; the checker hands them to the check (novox/hq ADR 0567 §16)")
|
|
||||||
}
|
|
||||||
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
sort.Strings(manifests)
|
|
||||||
for _, file := range manifests {
|
|
||||||
raw, err := os.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var doc any
|
|
||||||
if err := json.Unmarshal(raw, &doc); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %w", file, err)
|
|
||||||
}
|
|
||||||
module := filepath.Base(filepath.Dir(file))
|
|
||||||
for _, s := range named(doc, nil) {
|
|
||||||
out = append(out, fmt.Sprintf("%s: names %q, which the indexer no longer keeps; placement is the seat mesh-placement's (novox/hq ADR 0567 §12)", module, s))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
readers, err := assignmentsReaders(root)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
out = append(out, readers...)
|
|
||||||
sort.Strings(out)
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// AssignmentsRead is the controller's assignments as a manifest reads them, and Allowed the file, relative to the
|
|
||||||
// catalogue's root, that names the modules still reading them: `<module> <why>` per line, `#` a comment.
|
|
||||||
const (
|
|
||||||
AssignmentsRead = "mesh-controller.assignments"
|
|
||||||
Allowed = "checks/noplacement/assignments-readers"
|
|
||||||
)
|
|
||||||
|
|
||||||
// assignmentsReaders is every finding on the reads of the controller's assignments: a module other than the controller
|
|
||||||
// reading them that Allowed does not name, and a module Allowed names that no longer reads them (issue 774).
|
|
||||||
func assignmentsReaders(root string) ([]string, error) {
|
|
||||||
allowed := map[string]bool{}
|
|
||||||
raw, err := os.ReadFile(filepath.Join(root, Allowed))
|
|
||||||
if err != nil && !os.IsNotExist(err) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
for _, line := range strings.Split(string(raw), "\n") {
|
|
||||||
if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") {
|
|
||||||
allowed[f[0]] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var found []string
|
|
||||||
reads := map[string]bool{}
|
|
||||||
for _, path := range manifests {
|
|
||||||
raw, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var m struct {
|
|
||||||
Module string `json:"module"`
|
|
||||||
Reads []string `json:"reads"`
|
|
||||||
}
|
|
||||||
if json.Unmarshal(raw, &m) != nil || m.Module == "mesh-controller" {
|
|
||||||
continue // a manifest that does not read is the manifest checks' to refuse
|
|
||||||
}
|
|
||||||
for _, r := range m.Reads {
|
|
||||||
if strings.TrimSpace(r) != AssignmentsRead {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
reads[m.Module] = true
|
|
||||||
if !allowed[m.Module] {
|
|
||||||
found = append(found, fmt.Sprintf("%s reads %s: read where modules run from mesh-delivery-assigner.placement, "+
|
|
||||||
"the seat mesh-placement's (novox/hq ADR 0549 §3, ADR 0567); the lease write is refused while a module reads "+
|
|
||||||
"the assignments (ADR 0549 §6)", m.Module, AssignmentsRead))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for module := range allowed {
|
|
||||||
if !reads[module] {
|
|
||||||
found = append(found, fmt.Sprintf("%s names %s, which no longer reads %s: take it off the list, which only shrinks",
|
|
||||||
Allowed, module, AssignmentsRead))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return found, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// named collects the strings of a decoded manifest that start with one of the Prefixes.
|
|
||||||
func named(v any, into []string) []string {
|
|
||||||
switch v := v.(type) {
|
|
||||||
case string:
|
|
||||||
for _, p := range Prefixes {
|
|
||||||
if strings.HasPrefix(v, p) {
|
|
||||||
return append(into, v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
case []any:
|
|
||||||
for _, e := range v {
|
|
||||||
into = named(e, into)
|
|
||||||
}
|
|
||||||
case map[string]any:
|
|
||||||
for _, e := range v {
|
|
||||||
into = named(e, into)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return into
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
if len(os.Args) != 2 {
|
|
||||||
fmt.Fprintln(os.Stderr, "usage: noplacement <catalogue root>")
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
found, err := Findings(os.Args[1])
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
fmt.Println(f)
|
|
||||||
}
|
|
||||||
if len(found) > 0 {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
fmt.Println("no placement directory, no manifest reads the indexer's placement, and only the modules listed read the controller's assignments")
|
|
||||||
}
|
|
||||||
@@ -1,88 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-catalog/checks/trunk"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) {
|
|
||||||
for _, row := range []struct {
|
|
||||||
name string
|
|
||||||
files map[string]string
|
|
||||||
want string // empty: clean
|
|
||||||
}{
|
|
||||||
{"clean", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-assigner.placement"]}`}, ""},
|
|
||||||
{"a placement directory", map[string]string{"placement/ace.json": `{}`}, "placement: the repository holds no placement"},
|
|
||||||
{"a particulars file", map[string]string{"particulars": "ace\n"}, "particulars: the repository lists no node names"},
|
|
||||||
{"a read of the indexer's placement", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-delivery-indexer.placement"]}`}, `a: names "mesh-delivery-indexer.placement"`},
|
|
||||||
{"a read of its candidates", map[string]string{"modules/b/module.json": `{"module":"b","reads":["mesh-delivery-indexer.placement-candidates"]}`}, "placement-candidates"},
|
|
||||||
{"its event", map[string]string{"modules/c/module.json": `{"module":"c","on":[{"event":"mesh-delivery-indexer.placement-changed"}]}`}, "placement-changed"},
|
|
||||||
// Issue 774 (ADR 0549 §3, §6): a reader of the controller's assignments is refused unless the list names it, the
|
|
||||||
// controller's own manifest is not judged, and a name on the list whose module no longer reads them is refused.
|
|
||||||
{"an unlisted reader of the assignments", map[string]string{"modules/r/module.json": `{"module":"r","reads":["mesh-controller.assignments"]}`}, "r reads mesh-controller.assignments"},
|
|
||||||
{"a listed reader and the controller", map[string]string{"modules/l/module.json": `{"module":"l","reads":["mesh-controller.assignments"]}`,
|
|
||||||
"modules/mesh-controller/module.json": `{"module":"mesh-controller","reads":["mesh-controller.assignments"]}`, Allowed: "# why\nl its plan\n"}, ""},
|
|
||||||
{"a listed module that reads them no more", map[string]string{Allowed: "gone no longer reads\n"}, "names gone, which no longer reads"},
|
|
||||||
{"the seat's own state is allowed", map[string]string{"modules/d/module.json": `{"module":"d","reads":["mesh-placement.placement"]}`}, ""},
|
|
||||||
} {
|
|
||||||
t.Run(row.name, func(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
for path, text := range row.files {
|
|
||||||
p := filepath.Join(root, path)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
got, err := Findings(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if all := strings.Join(got, "\n"); (row.want == "") != (all == "") || !strings.Contains(all, row.want) {
|
|
||||||
t.Fatalf("findings = %q, want one containing %q", all, row.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2).
|
|
||||||
func TestTheCatalogueHoldsNoPlacementOfTheRepositoryPath(t *testing.T) {
|
|
||||||
got, err := Findings(filepath.Join("..", ".."))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, f := range got {
|
|
||||||
t.Error(f)
|
|
||||||
}
|
|
||||||
// Issue 774 (ADR 0549 §3, §6; the comparison of novox/hq ADR 0529 clause 2): the list of modules still reading the
|
|
||||||
// controller's assignments gains none the trunk's copy at the merge base does not hold. Its first copy gains nothing.
|
|
||||||
path, ok := trunk.Copy(t, Allowed)
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
names := func(path string) map[string]bool {
|
|
||||||
raw, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
out := map[string]bool{}
|
|
||||||
for _, line := range strings.Split(string(raw), "\n") {
|
|
||||||
if f := strings.Fields(line); len(f) > 0 && !strings.HasPrefix(f[0], "#") {
|
|
||||||
out[f[0]] = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
base := names(path)
|
|
||||||
for module := range names(filepath.Join("..", "..", Allowed)) {
|
|
||||||
if !base[module] {
|
|
||||||
t.Errorf("%s gains %s, which the trunk's copy does not hold: the list only shrinks", Allowed, module)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# the root, for a placement directory, and every manifest, for a read of the indexer's placement states.
|
|
||||||
?placement
|
|
||||||
modules/*/module.json
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module checks/noscripts
|
|
||||||
|
|
||||||
go 1.22
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
package noscripts
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The catalogue's checks are Go tests (novox/hq ADR 0529 clause 5, ADR 0445): the checker runs every nested Go module's tests by
|
|
||||||
// the language's convention and no script at the root says what else to run. A script back at the root would be a check nobody
|
|
||||||
// runs (ADR 0503 §3: since R4e the checker has not run them), so it is refused.
|
|
||||||
func TestNoScriptOfTheOldCheckStandsAtTheRoot(t *testing.T) {
|
|
||||||
for _, name := range []string{"merge-check.sh", "merge-check-typescript.sh", "check-nested-modules.sh"} {
|
|
||||||
if _, err := os.Stat(filepath.Join("..", "..", name)); !errors.Is(err, os.ErrNotExist) {
|
|
||||||
t.Errorf("%s stands at the catalogue's root (%v): what it ran is a Go test in checks/ or a step of the build module", name, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# that none of the old check scripts stands at the root.
|
|
||||||
?merge-check.sh
|
|
||||||
?merge-check-typescript.sh
|
|
||||||
?check-nested-modules.sh
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/owned
|
module git.novox.be/novox/mesh-catalog/checks/owned
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
|
|
||||||
|
|
||||||
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
|
|
||||||
|
|||||||
+9
-177
@@ -11,23 +11,13 @@
|
|||||||
// another module, after ${dir:<id>} is resolved within each module and both homes are read as one root.
|
// another module, after ${dir:<id>} is resolved within each module and both homes are read as one root.
|
||||||
// Archives are not containers for this rule. This holds for ~/.ssh what checks/sshdir held (ADR 0415 §6).
|
// Archives are not containers for this rule. This holds for ~/.ssh what checks/sshdir held (ADR 0415 §6).
|
||||||
//
|
//
|
||||||
// 5. a directory that is, lies inside or holds a directory a system package owns under /var/lib, or any other
|
// go run . <catalogue root>
|
||||||
// resource inside one (issue 1841: pacman's state directory, placed with "place": ".", was /var/lib/pacman, the
|
|
||||||
// package manager's own database directory). A placed directory is resolved as the mesh resolves it with the
|
|
||||||
// default data root: "." is /var/lib/<module>, "mesh" /var/lib/mesh/<module>, neither /var/lib/<module>/<id>.
|
|
||||||
// The system directories are checks/owned/system, captured from a machine (pacman -Qo, /usr/lib/tmpfiles.d);
|
|
||||||
// checks/owned/system-known names the exceptions still to fix, by module, resource and issue. That list only
|
|
||||||
// shrinks: an entry that no longer collides is refused, and with -base one the trunk's copy does not hold.
|
|
||||||
//
|
|
||||||
// go run . [-base <trunk's system-known>] <catalogue root>
|
|
||||||
//
|
//
|
||||||
// Exits 1 on a finding.
|
// Exits 1 on a finding.
|
||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"flag"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
@@ -37,12 +27,6 @@ import (
|
|||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// The lists of rule 5, relative to the catalogue's root.
|
|
||||||
const (
|
|
||||||
SystemList = "checks/owned/system"
|
|
||||||
SystemKnown = "checks/owned/system-known"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Home is the one root both machine homes are read as.
|
// Home is the one root both machine homes are read as.
|
||||||
const Home = "${home}"
|
const Home = "${home}"
|
||||||
|
|
||||||
@@ -59,7 +43,7 @@ type Finding struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type resource struct {
|
type resource struct {
|
||||||
ID, Type, Path, Unknown, Place string
|
ID, Type, Path, Unknown string
|
||||||
Knows []string
|
Knows []string
|
||||||
HasKnows bool
|
HasKnows bool
|
||||||
}
|
}
|
||||||
@@ -81,7 +65,6 @@ func load(file string) (*parsed, error) {
|
|||||||
ID string `json:"id"`
|
ID string `json:"id"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
Place string `json:"place"`
|
|
||||||
Unknown *string `json:"unknown"`
|
Unknown *string `json:"unknown"`
|
||||||
Knows []string `json:"knows"`
|
Knows []string `json:"knows"`
|
||||||
} `json:"resources"`
|
} `json:"resources"`
|
||||||
@@ -98,7 +81,7 @@ func load(file string) (*parsed, error) {
|
|||||||
out.Module = filepath.Base(filepath.Dir(file))
|
out.Module = filepath.Base(filepath.Dir(file))
|
||||||
}
|
}
|
||||||
for _, r := range m.Resources {
|
for _, r := range m.Resources {
|
||||||
x := resource{ID: r.ID, Type: r.Type, Path: r.Path, Place: r.Place, Knows: r.Knows, HasKnows: r.Knows != nil}
|
x := resource{ID: r.ID, Type: r.Type, Path: r.Path, Knows: r.Knows, HasKnows: r.Knows != nil}
|
||||||
if r.Unknown != nil {
|
if r.Unknown != nil {
|
||||||
x.Unknown = *r.Unknown
|
x.Unknown = *r.Unknown
|
||||||
if x.Unknown == "" {
|
if x.Unknown == "" {
|
||||||
@@ -160,23 +143,14 @@ type declared struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Check reads every manifest under root/modules and answers what it refuses.
|
// Check reads every manifest under root/modules and answers what it refuses.
|
||||||
func Check(root string, base []string) ([]Finding, error) {
|
func Check(root string) ([]Finding, error) {
|
||||||
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
files, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
sort.Strings(files)
|
sort.Strings(files)
|
||||||
system, err := readSystem(filepath.Join(root, SystemList))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
known, err := readLines(filepath.Join(root, SystemKnown))
|
|
||||||
if err != nil && !errors.Is(err, os.ErrNotExist) {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var out []Finding
|
var out []Finding
|
||||||
var paths []declared
|
var paths []declared
|
||||||
collided := map[string]bool{}
|
|
||||||
type container struct{ module, path string }
|
type container struct{ module, path string }
|
||||||
var containers []container
|
var containers []container
|
||||||
for _, file := range files {
|
for _, file := range files {
|
||||||
@@ -196,7 +170,6 @@ func Check(root string, base []string) ([]Finding, error) {
|
|||||||
dirs[r.ID] = "/placed/" + m.Module + "/" + r.ID
|
dirs[r.ID] = "/placed/" + m.Module + "/" + r.ID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
out = append(out, inSystem(m, system, known, collided)...)
|
|
||||||
for _, r := range m.Resources {
|
for _, r := range m.Resources {
|
||||||
p := norm(r.Path, dirs)
|
p := norm(r.Path, dirs)
|
||||||
if p != "" {
|
if p != "" {
|
||||||
@@ -239,28 +212,6 @@ func Check(root string, base []string) ([]Finding, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
for _, k := range known {
|
|
||||||
if !collided[k] {
|
|
||||||
module, id, _ := strings.Cut(k, " ")
|
|
||||||
id, _, _ = strings.Cut(id, " ")
|
|
||||||
out = append(out, Finding{5, module, id, fmt.Sprintf("is listed in %s, and no longer lies in a system "+
|
|
||||||
"package's directory: take the line out, so the list only shrinks", SystemKnown)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if base != nil {
|
|
||||||
held := map[string]bool{}
|
|
||||||
for _, b := range base {
|
|
||||||
held[b] = true
|
|
||||||
}
|
|
||||||
for _, k := range known {
|
|
||||||
if !held[k] {
|
|
||||||
module, id, _ := strings.Cut(k, " ")
|
|
||||||
id, _, _ = strings.Cut(id, " ")
|
|
||||||
out = append(out, Finding{5, module, id, fmt.Sprintf("is a new line in %s, which the trunk's copy "+
|
|
||||||
"does not hold: a module's directory is placed outside a system package's, never listed", SystemKnown)})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, d := range paths {
|
for _, d := range paths {
|
||||||
for _, c := range containers {
|
for _, c := range containers {
|
||||||
if c.module != d.module && (d.path == c.path || strings.HasPrefix(d.path, c.path+"/")) {
|
if c.module != d.module && (d.path == c.path || strings.HasPrefix(d.path, c.path+"/")) {
|
||||||
@@ -271,140 +222,21 @@ func Check(root string, base []string) ([]Finding, error) {
|
|||||||
return out, nil
|
return out, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// systemDir is one directory a system package owns, and what says so.
|
|
||||||
type systemDir struct{ Path, Source string }
|
|
||||||
|
|
||||||
// readLines is a list's lines, without comments and blank lines.
|
|
||||||
func readLines(file string) ([]string, error) {
|
|
||||||
raw, err := os.ReadFile(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var lines []string
|
|
||||||
for _, line := range strings.Split(string(raw), "\n") {
|
|
||||||
if line = strings.TrimSpace(line); line != "" && !strings.HasPrefix(line, "#") {
|
|
||||||
lines = append(lines, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return lines, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// readSystem is the list of system directories: "<absolute path> <package>: <how it was read>" per line. Absent or
|
|
||||||
// empty, the check fails: a list that says nothing would let every module through.
|
|
||||||
func readSystem(file string) ([]systemDir, error) {
|
|
||||||
lines, err := readLines(file)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("rule 5 reads the system packages' directories from %s: %w", file, err)
|
|
||||||
}
|
|
||||||
if len(lines) == 0 {
|
|
||||||
return nil, fmt.Errorf("%s names no system package's directory, so rule 5 would pass everything", file)
|
|
||||||
}
|
|
||||||
var out []systemDir
|
|
||||||
for _, line := range lines {
|
|
||||||
p, source, _ := strings.Cut(line, " ")
|
|
||||||
if !strings.HasPrefix(p, "/var/lib/") || strings.TrimSpace(source) == "" {
|
|
||||||
return nil, fmt.Errorf("%s: %q is not \"/var/lib/<directory> <package>: <how it was read>\"", file, line)
|
|
||||||
}
|
|
||||||
out = append(out, systemDir{path.Clean(p), strings.TrimSpace(source)})
|
|
||||||
}
|
|
||||||
return out, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// placedAt is where the mesh places a pathless directory with the default data root (mesh-sdk catalogue dirsFor).
|
|
||||||
func placedAt(module string, r resource) string {
|
|
||||||
switch r.Place {
|
|
||||||
case ".":
|
|
||||||
return "/var/lib/" + module
|
|
||||||
case "mesh":
|
|
||||||
return "/var/lib/mesh/" + module
|
|
||||||
}
|
|
||||||
return "/var/lib/" + module + "/" + r.ID
|
|
||||||
}
|
|
||||||
|
|
||||||
// inSystem is rule 5 for one module: each resource whose resolved path collides with a system package's directory,
|
|
||||||
// unless system-known lists it (then it is marked collided, so a stale line is found).
|
|
||||||
func inSystem(m *parsed, system []systemDir, known []string, collided map[string]bool) []Finding {
|
|
||||||
real := map[string]string{}
|
|
||||||
for _, r := range m.Resources {
|
|
||||||
if r.Type == "directory" && r.Path == "" && r.ID != "" {
|
|
||||||
real[r.ID] = placedAt(m.Module, r)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, r := range m.Resources {
|
|
||||||
if r.ID != "" && r.Path != "" {
|
|
||||||
real[r.ID] = r.Path
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var out []Finding
|
|
||||||
for _, r := range m.Resources {
|
|
||||||
p := ""
|
|
||||||
if r.Path != "" {
|
|
||||||
p = norm(r.Path, real)
|
|
||||||
} else if r.Type == "directory" {
|
|
||||||
p = real[r.ID]
|
|
||||||
}
|
|
||||||
if !strings.HasPrefix(p, "/") {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, s := range system {
|
|
||||||
inside := p == s.Path || strings.HasPrefix(p, s.Path+"/")
|
|
||||||
holds := r.Type == "directory" && strings.HasPrefix(s.Path, p+"/")
|
|
||||||
if !inside && !holds {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
listed := ""
|
|
||||||
for _, k := range known {
|
|
||||||
if strings.HasPrefix(k, m.Module+" "+r.ID+" ") || k == m.Module+" "+r.ID {
|
|
||||||
listed = k
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if listed != "" {
|
|
||||||
collided[listed] = true
|
|
||||||
break
|
|
||||||
}
|
|
||||||
how := "is or lies inside"
|
|
||||||
if holds {
|
|
||||||
how = "holds"
|
|
||||||
}
|
|
||||||
out = append(out, Finding{5, m.Module, r.ID, fmt.Sprintf("resolves to %q, which %s %s, the directory of the "+
|
|
||||||
"system package %s; place it under /var/lib/mesh/%s (\"place\": \"mesh\") or a path of its own", p, how,
|
|
||||||
s.Path, s.Source, m.Module)})
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
baseFile := flag.String("base", "", "the trunk's copy of "+SystemKnown+": a line it does not hold is refused")
|
|
||||||
flag.Parse()
|
|
||||||
root := "."
|
root := "."
|
||||||
if flag.NArg() > 0 {
|
if len(os.Args) > 1 {
|
||||||
root = flag.Arg(0)
|
root = os.Args[1]
|
||||||
}
|
}
|
||||||
var base []string
|
found, err := Check(root)
|
||||||
if *baseFile != "" {
|
|
||||||
lines, err := readLines(*baseFile)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
base = append([]string{}, lines...)
|
|
||||||
}
|
|
||||||
found, err := Check(root, base)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, err)
|
fmt.Fprintln(os.Stderr, err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
for _, f := range found {
|
for _, f := range found {
|
||||||
cite := "novox/hq ADR 0422 §8, issue 1024"
|
fmt.Printf("%s: %s: %s (rule %d, novox/hq ADR 0422 §8, issue 1024)\n", f.Module, f.ID, f.Why, f.Rule)
|
||||||
if f.Rule == 5 {
|
|
||||||
cite = "issue 1841"
|
|
||||||
}
|
|
||||||
fmt.Printf("%s: %s: %s (rule %d, %s)\n", f.Module, f.ID, f.Why, f.Rule, cite)
|
|
||||||
}
|
}
|
||||||
if len(found) > 0 {
|
if len(found) > 0 {
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
fmt.Println("owned paths: every directory and archive says how it treats unknown files; no module declares inside another's directory, or in a system package's")
|
fmt.Println("owned paths: every directory and archive says how it treats unknown files; no module declares inside another's directory")
|
||||||
}
|
}
|
||||||
|
|||||||
+1
-102
@@ -20,10 +20,7 @@ func manifest(t *testing.T, root, module, body string) {
|
|||||||
|
|
||||||
func check(t *testing.T, root string) []Finding {
|
func check(t *testing.T, root string) []Finding {
|
||||||
t.Helper()
|
t.Helper()
|
||||||
if _, err := os.Stat(filepath.Join(root, SystemList)); os.IsNotExist(err) {
|
found, err := Check(root)
|
||||||
lists(t, root, "/var/lib/pacman pacman: pacman -Qo\n", "")
|
|
||||||
}
|
|
||||||
found, err := Check(root, nil)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -38,20 +35,6 @@ func only(t *testing.T, found []Finding, rule int, module, id string) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// lists writes the catalogue's list of system directories and its known exceptions.
|
|
||||||
func lists(t *testing.T, root, system, known string) {
|
|
||||||
t.Helper()
|
|
||||||
if err := os.MkdirAll(filepath.Join(root, "checks", "owned"), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(root, SystemList), []byte(system), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(root, SystemKnown), []byte(known), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func dir(id, path, rest string) string {
|
func dir(id, path, rest string) string {
|
||||||
return `{"id":"` + id + `","type":"directory","path":"` + path + `"` + rest + `}`
|
return `{"id":"` + id + `","type":"directory","path":"` + path + `"` + rest + `}`
|
||||||
}
|
}
|
||||||
@@ -162,87 +145,3 @@ func TestPlacedDirectoriesOfTwoModulesAreTheirOwn(t *testing.T) {
|
|||||||
t.Fatalf("refused %+v", found)
|
t.Fatalf("refused %+v", found)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 1841/AC18: a module's directory that resolves to a /var/lib directory a system package owns, lies inside one or
|
|
||||||
// holds one, is refused, whether placed (place ".", or by default beneath the module's own) or stated; a placed
|
|
||||||
// directory saying place "mesh" lies under /var/lib/mesh and passes (issue 1841: pacman's state was /var/lib/pacman).
|
|
||||||
func TestADirectoryOfASystemPackageIsRefused(t *testing.T) {
|
|
||||||
system := "/var/lib/pacman pacman: pacman -Qo\n/var/lib/systemd/coredump systemd: tmpfiles.d/systemd.conf\n"
|
|
||||||
for name, resources := range map[string][]string{
|
|
||||||
"placed-own": {`{"id":"state","type":"directory","place":".","unknown":"program"}`},
|
|
||||||
"placed-beneath": {`{"id":"coredump","type":"directory","unknown":"program"}`},
|
|
||||||
"stated-inside": {dir("d", "/var/lib/pacman/keeper", `,"unknown":"program"`)},
|
|
||||||
"stated-holding": {dir("d", "/var/lib/systemd", `,"unknown":"program"`)},
|
|
||||||
"file-inside": {`{"id":"state","type":"directory","place":"mesh","unknown":"program"}`,
|
|
||||||
`{"id":"f","type":"file","path":"/var/lib/pacman/keeper.json"}`},
|
|
||||||
} {
|
|
||||||
module := map[string]string{"placed-own": "pacman", "placed-beneath": "systemd"}[name]
|
|
||||||
if module == "" {
|
|
||||||
module = "keeper"
|
|
||||||
}
|
|
||||||
root := t.TempDir()
|
|
||||||
lists(t, root, system, "")
|
|
||||||
manifest(t, root, module, mod(module, resources...))
|
|
||||||
found := check(t, root)
|
|
||||||
if len(found) != 1 || found[0].Rule != 5 || found[0].Module != module ||
|
|
||||||
!strings.Contains(found[0].Why, "/var/lib/") || !strings.Contains(found[0].Why, "package") {
|
|
||||||
t.Errorf("%s: found %+v; want one finding of rule 5 for %s naming the path and the package", name, found, module)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
root := t.TempDir()
|
|
||||||
lists(t, root, system, "")
|
|
||||||
manifest(t, root, "pacman", mod("pacman",
|
|
||||||
`{"id":"state","type":"directory","place":"mesh","unknown":"program"}`,
|
|
||||||
`{"id":"f","type":"file","path":"${dir:state}/keeper.json"}`,
|
|
||||||
dir("other", "/var/lib/pacman-other", `,"unknown":"program"`)))
|
|
||||||
if found := check(t, root); len(found) != 0 {
|
|
||||||
t.Fatalf("a directory under /var/lib/mesh/pacman was refused: %+v", found)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1841/AC18: a known exception passes; one that no longer collides is refused, so the list only shrinks; one the trunk's
|
|
||||||
// copy does not hold is refused; and an absent list of system directories fails the check rather than passing it.
|
|
||||||
func TestKnownSystemExceptionsOnlyShrink(t *testing.T) {
|
|
||||||
system := "/var/lib/systemd systemd: tmpfiles.d/systemd.conf\n"
|
|
||||||
known := "# comment\nsystemd state issue 1873\n"
|
|
||||||
root := t.TempDir()
|
|
||||||
lists(t, root, system, known)
|
|
||||||
manifest(t, root, "systemd", mod("systemd", `{"id":"state","type":"directory","place":".","unknown":"program"}`))
|
|
||||||
if found := check(t, root); len(found) != 0 {
|
|
||||||
t.Fatalf("a known exception was refused: %+v", found)
|
|
||||||
}
|
|
||||||
if found, err := Check(root, []string{"systemd state issue 1873"}); err != nil || len(found) != 0 {
|
|
||||||
t.Fatalf("a known exception held by the trunk was refused: %+v %v", found, err)
|
|
||||||
}
|
|
||||||
if found, err := Check(root, []string{}); err != nil || len(found) != 1 || found[0].Rule != 5 {
|
|
||||||
t.Fatalf("an exception the trunk does not hold passed: %+v %v", found, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
manifest(t, root, "systemd", mod("systemd", `{"id":"state","type":"directory","place":"mesh","unknown":"program"}`))
|
|
||||||
only(t, check(t, root), 5, "systemd", "state")
|
|
||||||
|
|
||||||
root = t.TempDir()
|
|
||||||
manifest(t, root, "a", mod("a"))
|
|
||||||
if err := os.MkdirAll(filepath.Join(root, "checks", "owned"), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(root, SystemList), []byte("# nothing\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if _, err := Check(root, nil); err == nil {
|
|
||||||
t.Fatal("an empty list of system directories passed")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1841/AC18: the catalogue as it stands passes rule 5 with its own lists.
|
|
||||||
func TestTheCatalogueKeepsOutOfSystemDirectories(t *testing.T) {
|
|
||||||
found, err := Check("../..", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
if f.Rule == 5 {
|
|
||||||
t.Errorf("%s: %s: %s", f.Module, f.ID, f.Why)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest; its lists are this package's own files.
|
|
||||||
modules/*/module.json
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-catalog/checks/trunk"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): every rule holds.
|
|
||||||
func TestTheCatalogueKeepsEveryRuleOfOwnedPaths(t *testing.T) {
|
|
||||||
found, err := Check("../..", nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
t.Errorf("%s: %s: %s (rule %d, novox/hq ADR 0422 §8)", f.Module, f.ID, f.Why, f.Rule)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// And the list of system directories' known exceptions holds no line the trunk's copy at the merge base does not (rule 5). A trunk
|
|
||||||
// with none had no list: its first copy. Skipped, and said, where no trunk is beside.
|
|
||||||
func TestTheSystemListHoldsNoLineTheTrunksDoesNot(t *testing.T) {
|
|
||||||
path, ok := trunk.Copy(t, "checks/owned/system-known")
|
|
||||||
var base []string
|
|
||||||
if ok {
|
|
||||||
lines, err := readLines(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
base = append([]string{}, lines...)
|
|
||||||
}
|
|
||||||
found, err := Check("../..", base)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
if f.Rule == 5 {
|
|
||||||
t.Errorf("%s: %s: %s (rule %d, issue 1841)", f.Module, f.ID, f.Why, f.Rule)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
# The directories under /var/lib that a system package owns (rule 5 of checks/owned, issue 1841). A module's
|
|
||||||
# directory, placed or stated, may not be one of these, lie inside one or hold one: the package's program and the
|
|
||||||
# package manager keep their own files there, and the node-engine would judge, chmod or chown the directory.
|
|
||||||
#
|
|
||||||
# One line per directory: "<path> <package>: <how it was read>". Captured on the workstation on 2026-10-22, through
|
|
||||||
# the mesh: pacman's own database by `pacman -Qo` (pacman_owns), the rest from the `d`, `D` and `Q` lines of
|
|
||||||
# /usr/lib/tmpfiles.d (node-reads.search for "/var/lib/"). A directory found to belong to a package is added here.
|
|
||||||
/var/lib/pacman pacman: pacman -Qo /var/lib/pacman/ (pacman 7.1.0)
|
|
||||||
/var/lib/alsa alsa-utils: tmpfiles.d/alsa-utils.conf
|
|
||||||
/var/lib/colord colord: tmpfiles.d/colord.conf
|
|
||||||
/var/lib/containers podman: tmpfiles.d/podman.conf
|
|
||||||
/var/lib/dbus dbus: tmpfiles.d/dbus.conf
|
|
||||||
/var/lib/geoclue geoclue: tmpfiles.d/geoclue.conf
|
|
||||||
/var/lib/lastlog util-linux: tmpfiles.d/lastlog2.conf
|
|
||||||
/var/lib/libuuid util-linux: tmpfiles.d/uuidd.conf
|
|
||||||
/var/lib/machines systemd: tmpfiles.d/systemd-nspawn.conf
|
|
||||||
/var/lib/openvpn/chroot networkmanager-openvpn: tmpfiles.d/nm-openvpn-tmpfiles.conf
|
|
||||||
/var/lib/portables systemd: tmpfiles.d/portables.conf
|
|
||||||
/var/lib/postgres postgresql: tmpfiles.d/postgresql.conf
|
|
||||||
/var/lib/private systemd: tmpfiles.d/systemd.conf
|
|
||||||
/var/lib/rpcbind rpcbind: tmpfiles.d/rpcbind.conf
|
|
||||||
/var/lib/systemd systemd: tmpfiles.d/systemd.conf
|
|
||||||
/var/lib/tpm2-tss/system/keystore tpm2-tss: tmpfiles.d/tpm2-tss-fapi.conf
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# Modules whose directory lies in a system package's directory today (rule 5 of checks/owned, issue 1841), each with
|
|
||||||
# the issue that moves it out: "<module> <resource id> issue <number>". This list only shrinks: a line whose module
|
|
||||||
# no longer collides is refused, and so is a line the trunk's copy does not hold.
|
|
||||||
postgres state issue 1873
|
|
||||||
systemd state issue 1873
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# The modules the check covers (novox/hq ADR 0507 §9, issue 1838), one a line; issue 1838 widens it.
|
|
||||||
pacman
|
|
||||||
kernel
|
|
||||||
+168
-46
@@ -1,13 +1,23 @@
|
|||||||
// Command particulars keeps this mesh's node names out of the modules it covers (novox/hq ADR 0507 §9,
|
// Command particulars keeps this mesh's node names out of the modules it covers (novox/hq ADR 0507 §9,
|
||||||
// issue 1838): a module is for any mesh, so no manifest, file, code string, identifier, comment, test name or
|
// issue 1838): a module is for any mesh, so no manifest, file, code string, identifier, comment, test name or
|
||||||
// fixture of a covered module names a node of this one.
|
// fixture of any module names a node of this one.
|
||||||
//
|
//
|
||||||
// The names are the mesh's own, not the repository's: the checker hands them in the environment value
|
// The names are in the file `particulars` at the catalogue's root, one a line, `#` comments — the one place
|
||||||
// MESH_NODE_NAMES, comma-separated, from the node records it reads (novox/hq ADR 0567 §16). A run with none fails
|
// they may be written. Every directory under modules/ is read, with no list of covered modules, so a new module
|
||||||
// loudly, since it would pass on anything. The modules covered are in `covered` at checks/particulars/covered, one a line. A name is matched
|
// is covered from its first commit. A name is matched whole and in any case; a hyphen, an underscore or a
|
||||||
// whole and in any case; a hyphen, an underscore or a CamelCase boundary parts one, so a longer word holding a name is another word.
|
// CamelCase boundary parts one, so a longer word holding a name is another word.
|
||||||
// The forge is not a node name: its host (git.novox.be) and the organisation path (novox/<repo>) are blanked
|
// Some text holds a node name and is not one, so it is blanked before a line is read, each form exact: the
|
||||||
// before a line is read.
|
// software's module path (git.novox.be/novox/<repo>), the name of the software's record (novox/hq, followed by
|
||||||
|
// anything), the hardware model (asus-zephyrus-g14, ROG Zephyrus G14), and the software's own
|
||||||
|
// name and npm scope (Novox Mesh, novox-mesh, @novox/mesh-sdk with nothing before the @). Any other novox/<x>,
|
||||||
|
// the forge's host alone and a node after a word's @ (database@novox/postgres) are findings.
|
||||||
|
//
|
||||||
|
// A module may declare its hardware model's word in `particulars`, as a line `model <module> <word>`: the word,
|
||||||
|
// one of the names, is then no finding in that module's directory and still one in every other
|
||||||
|
// (asus-zephyrus-g14's adopted paths such as /etc/modprobe.d/g14-nvidia-power.conf). A declaration naming no
|
||||||
|
// module or no listed name makes the list unreadable.
|
||||||
|
// A hardware's own name holding a name is declared as a line `hardware <word>` (asus-g14): that exact word, in
|
||||||
|
// any case and not part of a longer hyphened word, is no finding in any module.
|
||||||
//
|
//
|
||||||
// go run . <catalogue root>
|
// go run . <catalogue root>
|
||||||
//
|
//
|
||||||
@@ -22,54 +32,57 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
"slices"
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
// forge is the forge's host and organisation path, blanked before a line is read; a path segment novox/ elsewhere
|
// forge is the software's module path on its forge and the name of its record (novox/hq, in any form: a citation,
|
||||||
// (testdata/novox/x, /srv/novox/data) is not blanked.
|
// a review, a URL path), blanked before a line is read; any other novox/<x> (a repository in a fixture,
|
||||||
var forge = regexp.MustCompile(`(?i)git\.novox\.be(?:/novox/[\w.-]+)?|(?:^|[^/\w.-])novox/[\w.-]+`)
|
// testdata/novox/x) is not blanked, nor novox/hqx.
|
||||||
|
var forge = regexp.MustCompile(`(?i:git\.novox\.be/novox/[\w.-]+)|novox/hq(?:$|[^\w-])`)
|
||||||
|
|
||||||
|
// ownNames are the hardware model's and the software's own names, blanked before a line is read: the model
|
||||||
|
// in any separator and case (asus-zephyrus-g14, ASUS_ZEPHYRUS_G14, ROG Zephyrus G14), the software as two
|
||||||
|
// words or hyphened, whole (Novox Mesh, novox mesh, novox-mesh), so novox-mesh-vault stays a node name joined;
|
||||||
|
// and the software's npm scope (@novox/mesh-sdk, @novox:registry=), its package path as the Go module path is,
|
||||||
|
// where no word stands before the @, so a session's who@node (fix-1@novox) is still a node name.
|
||||||
|
var ownNames = regexp.MustCompile(`(?i)(?:asus[-_ ])?zephyrus[-_ ]g14|(?:^|[^\w-])novox[- ]mesh(?:$|[^\w-])|(?:^|[^\w.-])@novox(?:$|[^\w-])`)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
root := "."
|
root := "."
|
||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
root = os.Args[1]
|
root = os.Args[1]
|
||||||
}
|
}
|
||||||
os.Exit(check(root, namesFromEnv()))
|
os.Exit(check(root))
|
||||||
}
|
|
||||||
|
|
||||||
// NamesEnv is the environment value that carries the mesh's node names (mesh-sdk delivery.NodeNamesEnv).
|
|
||||||
const NamesEnv = "MESH_NODE_NAMES"
|
|
||||||
|
|
||||||
// namesFromEnv is the names NamesEnv holds, comma-separated, trimmed, empty entries left out.
|
|
||||||
func namesFromEnv() []string {
|
|
||||||
var out []string
|
|
||||||
for _, n := range strings.Split(os.Getenv(NamesEnv), ",") {
|
|
||||||
if n = strings.TrimSpace(n); n != "" {
|
|
||||||
out = append(out, n)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// check runs the check over a catalogue and returns the exit code: 0 clean, 1 a finding, 2 a list unreadable.
|
// check runs the check over a catalogue and returns the exit code: 0 clean, 1 a finding, 2 a list unreadable.
|
||||||
func check(root string, names []string) int {
|
func check(root string) int {
|
||||||
var err error
|
entries, err := readList(filepath.Join(root, "particulars"))
|
||||||
if len(names) == 0 {
|
names, models, hardware := splitModels(entries)
|
||||||
err = fmt.Errorf("no node names (%s is empty) — the check would pass on anything", NamesEnv)
|
if err == nil && len(names) == 0 {
|
||||||
|
err = fmt.Errorf("particulars lists no name — the check would pass on anything")
|
||||||
}
|
}
|
||||||
var covered []string
|
var modules []string
|
||||||
if err == nil {
|
if err == nil {
|
||||||
covered, err = readList(filepath.Join(root, "checks/particulars/covered"))
|
modules, err = moduleDirs(root)
|
||||||
if err == nil && len(covered) == 0 {
|
if err == nil && len(modules) == 0 {
|
||||||
err = fmt.Errorf("covered lists no module — the check would pass on anything")
|
err = fmt.Errorf("modules/ holds no module — the check would pass on anything")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if err == nil {
|
||||||
|
err = checkModels(models, names, modules)
|
||||||
|
}
|
||||||
|
var blanks *regexp.Regexp
|
||||||
|
if err == nil {
|
||||||
|
blanks, err = hardwareWords(hardware)
|
||||||
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "particulars:", err)
|
fmt.Fprintln(os.Stderr, "particulars:", err)
|
||||||
return 2
|
return 2
|
||||||
}
|
}
|
||||||
findings, err := run(root, names, covered)
|
findings, err := run(root, names, modules, models, blanks)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "particulars:", err)
|
fmt.Fprintln(os.Stderr, "particulars:", err)
|
||||||
return 2
|
return 2
|
||||||
@@ -81,7 +94,7 @@ func check(root string, names []string) int {
|
|||||||
fmt.Printf("particulars: %d node name(s) in a module for any mesh (novox/hq ADR 0507 §9) — say what it shows, not where it was seen\n", len(findings))
|
fmt.Printf("particulars: %d node name(s) in a module for any mesh (novox/hq ADR 0507 §9) — say what it shows, not where it was seen\n", len(findings))
|
||||||
return 1
|
return 1
|
||||||
}
|
}
|
||||||
fmt.Printf("particulars: none of %d node names in the %d covered modules\n", len(names), len(covered))
|
fmt.Printf("particulars: none of %d node names in the %d modules\n", len(names), len(modules))
|
||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -104,10 +117,104 @@ func readList(path string) ([]string, error) {
|
|||||||
return out, sc.Err()
|
return out, sc.Err()
|
||||||
}
|
}
|
||||||
|
|
||||||
// run reads every text file of each covered module and returns the findings, sorted.
|
// splitModels parts the list's entries into the names, the models' words by module (`model <module> <word>`)
|
||||||
func run(root string, names, covered []string) ([]string, error) {
|
// and the hardware's words (`hardware <word>`, kept whole to be checked).
|
||||||
|
func splitModels(entries []string) (names []string, models map[string][]string, hardware []string) {
|
||||||
|
models = map[string][]string{}
|
||||||
|
for _, e := range entries {
|
||||||
|
f := strings.Fields(e)
|
||||||
|
if len(f) > 0 && f[0] == "hardware" {
|
||||||
|
hardware = append(hardware, e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if len(f) > 0 && f[0] == "model" {
|
||||||
|
if len(f) != 3 {
|
||||||
|
models[""] = append(models[""], e)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
models[f[1]] = append(models[f[1]], f[2])
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
names = append(names, e)
|
||||||
|
}
|
||||||
|
return names, models, hardware
|
||||||
|
}
|
||||||
|
|
||||||
|
// hardwareWords is the matcher of the declared hardware words, nil when none is declared; a line that is not
|
||||||
|
// `hardware <word>` makes the list unreadable.
|
||||||
|
func hardwareWords(lines []string) (*regexp.Regexp, error) {
|
||||||
|
var words []string
|
||||||
|
for _, l := range lines {
|
||||||
|
f := strings.Fields(l)
|
||||||
|
if len(f) != 2 {
|
||||||
|
return nil, fmt.Errorf("particulars: %q is not `hardware <word>`", l)
|
||||||
|
}
|
||||||
|
words = append(words, regexp.QuoteMeta(f[1]))
|
||||||
|
}
|
||||||
|
if len(words) == 0 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
return regexp.MustCompile(`(?i)(?:^|[^A-Za-z0-9-])(?:` + strings.Join(words, "|") + `)(?:$|[^A-Za-z0-9-])`), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// blankWith blanks what rx matches in s, keeping every other byte where it was.
|
||||||
|
func blankWith(rx *regexp.Regexp, s string) string {
|
||||||
|
if rx == nil {
|
||||||
|
return s
|
||||||
|
}
|
||||||
|
return rx.ReplaceAllStringFunc(s, func(m string) string { return strings.Repeat(" ", len(m)) })
|
||||||
|
}
|
||||||
|
|
||||||
|
// checkModels refuses a model declaration that is malformed, names no module or names a word that is no name.
|
||||||
|
func checkModels(models map[string][]string, names, modules []string) error {
|
||||||
|
for m, words := range models {
|
||||||
|
if m == "" {
|
||||||
|
return fmt.Errorf("particulars: %q is not `model <module> <word>`", words[0])
|
||||||
|
}
|
||||||
|
if !slices.Contains(modules, m) {
|
||||||
|
return fmt.Errorf("particulars: model %s %s names no module under modules/", m, words[0])
|
||||||
|
}
|
||||||
|
for _, w := range words {
|
||||||
|
if !slices.ContainsFunc(names, func(n string) bool { return strings.EqualFold(n, w) }) {
|
||||||
|
return fmt.Errorf("particulars: model %s %s: %q is not a listed name", m, w, w)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// without returns the names but the words, in any case.
|
||||||
|
func without(names, words []string) []string {
|
||||||
var out []string
|
var out []string
|
||||||
for _, m := range covered {
|
for _, n := range names {
|
||||||
|
if !slices.ContainsFunc(words, func(w string) bool { return strings.EqualFold(n, w) }) {
|
||||||
|
out = append(out, n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// moduleDirs returns every directory under the catalogue's modules/, sorted.
|
||||||
|
func moduleDirs(root string) ([]string, error) {
|
||||||
|
entries, err := os.ReadDir(filepath.Join(root, "modules"))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var out []string
|
||||||
|
for _, e := range entries {
|
||||||
|
if e.IsDir() {
|
||||||
|
out = append(out, e.Name())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// run reads every text file of each module named and returns the findings, sorted; a module's model words are
|
||||||
|
// not read as names in it.
|
||||||
|
func run(root string, all, modules []string, models map[string][]string, hardware *regexp.Regexp) ([]string, error) {
|
||||||
|
var out []string
|
||||||
|
for _, m := range modules {
|
||||||
|
names := without(all, models[m])
|
||||||
dir := filepath.Join(root, "modules", m)
|
dir := filepath.Join(root, "modules", m)
|
||||||
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -122,7 +229,7 @@ func run(root string, names, covered []string) ([]string, error) {
|
|||||||
}
|
}
|
||||||
rel, _ := filepath.Rel(root, path)
|
rel, _ := filepath.Rel(root, path)
|
||||||
// A path is read too: a testdata directory named after a node is a finding.
|
// A path is read too: a testdata directory named after a node is a finding.
|
||||||
for _, n := range hits(filepath.ToSlash(rel), names) {
|
for _, n := range hits(blankWith(hardware, filepath.ToSlash(rel)), names) {
|
||||||
out = append(out, fmt.Sprintf("%s:0: node name %q in the path", filepath.ToSlash(rel), n))
|
out = append(out, fmt.Sprintf("%s:0: node name %q in the path", filepath.ToSlash(rel), n))
|
||||||
}
|
}
|
||||||
if d.Name() == "go.sum" {
|
if d.Name() == "go.sum" {
|
||||||
@@ -135,7 +242,7 @@ func run(root string, names, covered []string) ([]string, error) {
|
|||||||
if bytes.IndexByte(b, 0) >= 0 {
|
if bytes.IndexByte(b, 0) >= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
out = append(out, scan(filepath.ToSlash(rel), string(b), names)...)
|
out = append(out, scan(filepath.ToSlash(rel), blankWith(hardware, string(b)), names)...)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -158,17 +265,32 @@ func scan(file, src string, names []string) []string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// hits returns the node names one line holds: as a word (a hyphen or underscore parts it), as a part of a
|
// hits returns the node names one line holds: as a word (a hyphen or underscore parts it), as a part of a
|
||||||
// CamelCase identifier (TestAce, novoxTree), the forge's host and organisation path blanked first.
|
// CamelCase identifier (TestAce, novoxTree), the forge's host and organisation path and the hardware's and the
|
||||||
|
// software's own names blanked first.
|
||||||
func hits(line string, names []string) []string {
|
func hits(line string, names []string) []string {
|
||||||
line = forge.ReplaceAllStringFunc(line, func(s string) string { return strings.Repeat(" ", len(s)) })
|
blank := func(s string) string { return strings.Repeat(" ", len(s)) }
|
||||||
|
line = forge.ReplaceAllStringFunc(line, blank)
|
||||||
|
line = ownNames.ReplaceAllStringFunc(line, blank)
|
||||||
var out []string
|
var out []string
|
||||||
for _, n := range names {
|
for _, n := range names {
|
||||||
q := regexp.QuoteMeta(n)
|
if matcher(n).MatchString(line) {
|
||||||
camel := regexp.QuoteMeta(strings.ToUpper(n[:1]) + n[1:])
|
|
||||||
rx := regexp.MustCompile(`(?i:(?:^|[^A-Za-z0-9])` + q + `(?:$|[^A-Za-z0-9]))|[a-z0-9]` + camel + `(?:$|[^a-z])|(?:^|[^A-Za-z0-9])` + q + `[A-Z0-9]`)
|
|
||||||
if rx.MatchString(line) {
|
|
||||||
out = append(out, strings.ToLower(n))
|
out = append(out, strings.ToLower(n))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// matchers holds each name's matcher, compiled once: the check reads every line of every module.
|
||||||
|
var matchers = map[string]*regexp.Regexp{}
|
||||||
|
|
||||||
|
// matcher is the matcher of one name: as a word, or as a part of a CamelCase identifier.
|
||||||
|
func matcher(n string) *regexp.Regexp {
|
||||||
|
if rx, ok := matchers[n]; ok {
|
||||||
|
return rx
|
||||||
|
}
|
||||||
|
q := regexp.QuoteMeta(n)
|
||||||
|
camel := regexp.QuoteMeta(strings.ToUpper(n[:1]) + n[1:])
|
||||||
|
rx := regexp.MustCompile(`(?i:(?:^|[^A-Za-z0-9])` + q + `(?:$|[^A-Za-z0-9]))|[a-z0-9]` + camel + `(?:$|[^a-z])|(?:^|[^A-Za-z0-9])` + q + `[A-Z0-9]`)
|
||||||
|
matchers[n] = rx
|
||||||
|
return rx
|
||||||
|
}
|
||||||
|
|||||||
+204
-16
@@ -35,7 +35,6 @@ func TestTheForgeIsNotANodeName(t *testing.T) {
|
|||||||
"see novox/hq issue 5",
|
"see novox/hq issue 5",
|
||||||
"module git.novox.be/novox/mesh-sdk/go v1.2.3",
|
"module git.novox.be/novox/mesh-sdk/go v1.2.3",
|
||||||
"https://git.novox.be/novox/mesh-catalog",
|
"https://git.novox.be/novox/mesh-catalog",
|
||||||
"git.novox.be",
|
|
||||||
"space trace surface grace g14x",
|
"space trace surface grace g14x",
|
||||||
} {
|
} {
|
||||||
if got := scan("f", s, names); len(got) != 0 {
|
if got := scan("f", s, names); len(got) != 0 {
|
||||||
@@ -68,34 +67,35 @@ func TestANodeNamedPathIsRefusedThroughRun(t *testing.T) {
|
|||||||
p := filepath.Join(root, "modules/kernel/testdata/ace/x")
|
p := filepath.Join(root, "modules/kernel/testdata/ace/x")
|
||||||
os.MkdirAll(filepath.Dir(p), 0o755)
|
os.MkdirAll(filepath.Dir(p), 0o755)
|
||||||
os.WriteFile(p, []byte("clean\n"), 0o644)
|
os.WriteFile(p, []byte("clean\n"), 0o644)
|
||||||
got, err := run(root, names, []string{"kernel"})
|
got, err := run(root, names, []string{"kernel"}, nil, nil)
|
||||||
if err != nil || len(got) != 1 || !strings.HasPrefix(got[0], "modules/kernel/testdata/ace/x:0: ") {
|
if err != nil || len(got) != 1 || !strings.HasPrefix(got[0], "modules/kernel/testdata/ace/x:0: ") {
|
||||||
t.Errorf("got %v, %v", got, err)
|
t.Errorf("got %v, %v", got, err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestCheckReturnsOneOnAFindingAndTwoOnNoNames(t *testing.T) {
|
func TestCheckReturnsOneOnAFindingAndTwoOnAnUnreadableList(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
write := func(p, s string) {
|
write := func(p, s string) {
|
||||||
p = filepath.Join(root, p)
|
p = filepath.Join(root, p)
|
||||||
os.MkdirAll(filepath.Dir(p), 0o755)
|
os.MkdirAll(filepath.Dir(p), 0o755)
|
||||||
os.WriteFile(p, []byte(s), 0o644)
|
os.WriteFile(p, []byte(s), 0o644)
|
||||||
}
|
}
|
||||||
write("checks/particulars/covered", "kernel\n")
|
write("particulars", "ace\n")
|
||||||
write("modules/kernel/a", "clean\n")
|
write("modules/kernel/a", "clean\n")
|
||||||
if got := check(root, []string{"ace"}); got != 0 {
|
if got := check(root); got != 0 {
|
||||||
t.Errorf("clean = %d, want 0", got)
|
t.Errorf("clean = %d, want 0", got)
|
||||||
}
|
}
|
||||||
write("modules/kernel/a", "on ace\n")
|
write("modules/kernel/a", "on ace\n")
|
||||||
if got := check(root, []string{"ace"}); got != 1 {
|
if got := check(root); got != 1 {
|
||||||
t.Errorf("finding = %d, want 1", got)
|
t.Errorf("finding = %d, want 1", got)
|
||||||
}
|
}
|
||||||
if got := check(root, nil); got != 2 {
|
os.Remove(filepath.Join(root, "particulars"))
|
||||||
t.Errorf("no names = %d, want 2", got)
|
if got := check(root); got != 2 {
|
||||||
|
t.Errorf("no list = %d, want 2", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOnlyCoveredModulesAreRead(t *testing.T) {
|
func TestRunReadsOnlyTheModulesItIsGiven(t *testing.T) {
|
||||||
root := t.TempDir()
|
root := t.TempDir()
|
||||||
write := func(p, s string) {
|
write := func(p, s string) {
|
||||||
p = filepath.Join(root, p)
|
p = filepath.Join(root, p)
|
||||||
@@ -104,7 +104,7 @@ func TestOnlyCoveredModulesAreRead(t *testing.T) {
|
|||||||
}
|
}
|
||||||
write("modules/kernel/files/a", "on novox\n")
|
write("modules/kernel/files/a", "on novox\n")
|
||||||
write("modules/other/files/a", "on novox\n")
|
write("modules/other/files/a", "on novox\n")
|
||||||
got, err := run(root, names, []string{"kernel"})
|
got, err := run(root, names, []string{"kernel"}, nil, nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -113,12 +113,200 @@ func TestOnlyCoveredModulesAreRead(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no node name of this mesh in a covered module.
|
// 1838/AC1: the check reads every directory under modules/ and no list of covered modules, so a module is
|
||||||
func TestTheCoveredModulesNameNoNode(t *testing.T) {
|
// covered from its first commit.
|
||||||
if _, set := os.LookupEnv(NamesEnv); !set {
|
func TestEveryModuleIsReadWithoutAList(t *testing.T) {
|
||||||
t.Skip("ADR 0567 §16: this checker hands no node names (before D14 is delivered); the skip goes once it is")
|
root := t.TempDir()
|
||||||
|
write := func(p, s string) {
|
||||||
|
p = filepath.Join(root, p)
|
||||||
|
os.MkdirAll(filepath.Dir(p), 0o755)
|
||||||
|
os.WriteFile(p, []byte(s), 0o644)
|
||||||
}
|
}
|
||||||
if code := check("../..", namesFromEnv()); code != 0 {
|
write("particulars", "novox\n")
|
||||||
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
|
write("modules/kernel/files/a", "clean\n")
|
||||||
|
if got := check(root); got != 0 {
|
||||||
|
t.Errorf("clean, no covered list = %d, want 0", got)
|
||||||
|
}
|
||||||
|
write("modules/new-module/files/a", "on novox\n")
|
||||||
|
if got := check(root); got != 1 {
|
||||||
|
t.Errorf("a finding in a module no list names = %d, want 1", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1838/AC2: the hardware model's own name and the software's own name are not node names.
|
||||||
|
func TestTheHardwareModelAndTheSoftwareAreNotNodeNames(t *testing.T) {
|
||||||
|
for _, s := range []string{
|
||||||
|
"modules/asus-zephyrus-g14/README.md",
|
||||||
|
"the asus-zephyrus-g14 module",
|
||||||
|
"ASUS_ZEPHYRUS_G14=1",
|
||||||
|
"product_family: ROG Zephyrus G14",
|
||||||
|
"# This machine is a node of a Novox mesh",
|
||||||
|
"Novox Mesh turns a handful of personal machines into one system",
|
||||||
|
"<!-- novox mesh: this file was changed on two nodes",
|
||||||
|
`const sealInfo = "novox-mesh sealed box v1"`,
|
||||||
|
"module git.novox.be/novox/mesh-catalog/modules/kernel",
|
||||||
|
} {
|
||||||
|
if got := scan("f", s, names); len(got) != 0 {
|
||||||
|
t.Errorf("%q refused: %v", s, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1838/AC2: a node name standing alone is still one, beside the hardware's or the software's name too.
|
||||||
|
func TestANodeNameBesideTheHardwareOrTheSoftwareIsRefused(t *testing.T) {
|
||||||
|
cases := map[string]string{
|
||||||
|
"g14": "the asus-zephyrus-g14 module on g14",
|
||||||
|
"shanks": "Novox Mesh on shanks",
|
||||||
|
"novox": "resolves under novox.internal",
|
||||||
|
"ace": "ROG Zephyrus G14 beside ace",
|
||||||
|
"novox mesh-ish": "mesh.mod.gitea.call.x.novox-mesh-vault",
|
||||||
|
}
|
||||||
|
for want, s := range cases {
|
||||||
|
got := scan("modules/m/a.go", s, names)
|
||||||
|
if len(got) != 1 || !strings.HasPrefix(got[0], "modules/m/a.go:1: ") {
|
||||||
|
t.Errorf("%s: %q gave %v, want one finding naming file:line", want, s, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1838/AC2: the software's npm scope (@novox/mesh-sdk, @novox:registry=) is its package path, as the Go module
|
||||||
|
// path is; a session's who@node naming a node is still a node name.
|
||||||
|
func TestTheSoftwaresNpmScopeIsNotANodeName(t *testing.T) {
|
||||||
|
for _, s := range []string{
|
||||||
|
"printf '%s\\n' ${shellQuote(`@novox:registry=${registry}`)}",
|
||||||
|
"const url = `${asked}@novox%2fmesh-sdk`;",
|
||||||
|
"# The @novox scope resolves from the mesh's own package registry",
|
||||||
|
`"dependencies": { "@novox/mesh-sdk": "^0.1.0" }`,
|
||||||
|
} {
|
||||||
|
if got := scan("f", s, names); len(got) != 0 {
|
||||||
|
t.Errorf("%q refused: %v", s, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range []string{"by fix-1@novox", "jochen@novox:"} {
|
||||||
|
if got := scan("f", s, names); len(got) != 1 {
|
||||||
|
t.Errorf("%q gave %v, want one finding", s, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1838/AC1: reading every module, the check compiles its matchers once, not once a line: a line read costs a
|
||||||
|
// few allocations, not a regexp compiled per name (the whole catalogue took 41 s with one per line).
|
||||||
|
func TestALineIsReadWithoutCompilingAMatcher(t *testing.T) {
|
||||||
|
line := "a line of code that names nothing at all, with git.novox.be/novox/mesh-sdk in it"
|
||||||
|
if n := testing.AllocsPerRun(100, func() { hits(line, names) }); n > 20 {
|
||||||
|
t.Errorf("a line costs %.0f allocations, want at most 20", n)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1838/AC2: a module may declare its hardware model's word in `particulars` (model <module> <word>); the word is
|
||||||
|
// then no finding in that module, and still one in every other.
|
||||||
|
func TestAModelsWordIsAllowedInItsModuleOnly(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
write := func(p, s string) {
|
||||||
|
p = filepath.Join(root, p)
|
||||||
|
os.MkdirAll(filepath.Dir(p), 0o755)
|
||||||
|
os.WriteFile(p, []byte(s), 0o644)
|
||||||
|
}
|
||||||
|
write("particulars", "ace\ng14\nmodel asus-zephyrus-g14 g14\n")
|
||||||
|
write("modules/asus-zephyrus-g14/README.md", "the adopted /etc/modprobe.d/g14-nvidia-power.conf; a second G14\n")
|
||||||
|
write("modules/kernel/a", "clean\n")
|
||||||
|
if got := check(root); got != 0 {
|
||||||
|
t.Errorf("the model's word in its own module = %d, want 0", got)
|
||||||
|
}
|
||||||
|
write("modules/asus-zephyrus-g14/a", "on ace\n")
|
||||||
|
if got := check(root); got != 1 {
|
||||||
|
t.Errorf("another node name in the model's module = %d, want 1", got)
|
||||||
|
}
|
||||||
|
write("modules/asus-zephyrus-g14/a", "clean\n")
|
||||||
|
write("modules/kernel/a", "i3's 20-g14.conf\n")
|
||||||
|
if got := check(root); got != 1 {
|
||||||
|
t.Errorf("the model's word in another module = %d, want 1", got)
|
||||||
|
}
|
||||||
|
write("modules/kernel/a", "clean\n")
|
||||||
|
write("particulars", "ace\ng14\nmodel no-such-module g14\n")
|
||||||
|
if got := check(root); got != 2 {
|
||||||
|
t.Errorf("a model of a module that does not exist = %d, want 2", got)
|
||||||
|
}
|
||||||
|
write("particulars", "ace\ng14\nmodel asus-zephyrus-g14 shanks\n")
|
||||||
|
if got := check(root); got != 2 {
|
||||||
|
t.Errorf("a model's word that is no listed name = %d, want 2", got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1838/AC2: only the module path git.novox.be/novox/<repo>, a package name or scope with nothing before its @, and
|
||||||
|
// the exact record citation novox/hq ADR|issue|design|to-be|research are the forge's or the software's; any
|
||||||
|
// other novox/<x>, the forge's host alone, and a node after a word's @ are findings.
|
||||||
|
func TestOnlyTheModulePathThePackageAndTheCitationAreNotNodeNames(t *testing.T) {
|
||||||
|
for _, s := range []string{
|
||||||
|
"module git.novox.be/novox/mesh-sdk/go v1.2.3",
|
||||||
|
"https://git.novox.be/novox/mesh-catalog",
|
||||||
|
`import { registerModuleTools } from "@novox/mesh-sdk/tools";`,
|
||||||
|
`"dependencies": { "@novox/ui": "^2.0.0" }`,
|
||||||
|
"// novox/hq ADR 0507 §9",
|
||||||
|
"(novox/hq issue 1838)",
|
||||||
|
"novox/hq design 47 and novox/hq to-be 53, novox/hq research 12",
|
||||||
|
} {
|
||||||
|
if got := scan("f", s, names); len(got) != 0 {
|
||||||
|
t.Errorf("%q refused: %v", s, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range []string{
|
||||||
|
`handedWith("node-b", "gitea", "postgres-database@novox/postgres+pair")`,
|
||||||
|
"delivery.novox/mesh-controller",
|
||||||
|
"see novox/mesh-catalog #441",
|
||||||
|
"git.novox.be",
|
||||||
|
} {
|
||||||
|
if got := scan("f", s, names); len(got) != 1 {
|
||||||
|
t.Errorf("%q gave %v, want one finding", s, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1838/AC2: novox/hq is the name of the software's own record, no finding in any form; another novox/<repo> is.
|
||||||
|
func TestTheRecordsNameIsNoFindingInAnyForm(t *testing.T) {
|
||||||
|
for _, s := range []string{
|
||||||
|
"novox/hq review round 2",
|
||||||
|
"novox/hq issues 1665 and 1666",
|
||||||
|
"repository: novox/hq",
|
||||||
|
"[the record](https://forge.example.org/novox/hq/src/branch/main/00-META/glossary.md)",
|
||||||
|
"novox/hq#7 at aaa",
|
||||||
|
"git.anchor.internal:222/novox/hq.git",
|
||||||
|
} {
|
||||||
|
if got := scan("f", s, names); len(got) != 0 {
|
||||||
|
t.Errorf("%q refused: %v", s, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, s := range []string{`repo := "novox/mesh-catalog"`, "novox/hqx", "novox/hq-mirror"} {
|
||||||
|
if got := scan("f", s, names); len(got) != 1 {
|
||||||
|
t.Errorf("%q gave %v, want one finding", s, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 1838/AC2: a hardware word declared in particulars (hardware <word>) is no finding anywhere; a name standing
|
||||||
|
// alone still is.
|
||||||
|
func TestAHardwareWordIsNoFindingAnywhere(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
write := func(p, s string) {
|
||||||
|
p = filepath.Join(root, p)
|
||||||
|
os.MkdirAll(filepath.Dir(p), 0o755)
|
||||||
|
os.WriteFile(p, []byte(s), 0o644)
|
||||||
|
}
|
||||||
|
write("particulars", "g14\nhardware asus-g14\n")
|
||||||
|
write("modules/triggerhappy/a", "/etc/triggerhappy/triggers.d/asus-g14.conf and ASUS-G14\n")
|
||||||
|
if got := check(root); got != 0 {
|
||||||
|
t.Errorf("the hardware word = %d, want 0", got)
|
||||||
|
}
|
||||||
|
write("modules/triggerhappy/a", "asus-g14.conf on g14\n")
|
||||||
|
if got := check(root); got != 1 {
|
||||||
|
t.Errorf("g14 standing alone beside it = %d, want 1", got)
|
||||||
|
}
|
||||||
|
write("modules/triggerhappy/a", "my-asus-g14-x\n")
|
||||||
|
if got := check(root); got != 1 {
|
||||||
|
t.Errorf("a longer word holding it = %d, want 1", got)
|
||||||
|
}
|
||||||
|
write("particulars", "g14\nhardware\n")
|
||||||
|
if got := check(root); got != 2 {
|
||||||
|
t.Errorf("a hardware line with no word = %d, want 2", got)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# check("../.."): every file of the modules the covered list names. The node names come from the run (MESH_NODE_NAMES).
|
|
||||||
modules
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/paths
|
module git.novox.be/novox/mesh-catalog/checks/paths
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
|
|
||||||
|
|
||||||
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
|
|
||||||
|
|||||||
@@ -251,8 +251,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
var grown []string
|
var grown []string
|
||||||
if baseDir == "" {
|
if baseDir == "" {
|
||||||
fmt.Fprintln(os.Stderr, "NOT COMPARED: no base branch's lists were given (-base <dir>), so additions to them were not judged; that is a failure, not a pass (issue 1295)")
|
fmt.Println("NOT COMPARED: no base branch's lists were given, so additions to them were not judged")
|
||||||
os.Exit(2)
|
|
||||||
} else {
|
} else {
|
||||||
baseShared, _, err1 := list(filepath.Join(baseDir, "shared"))
|
baseShared, _, err1 := list(filepath.Join(baseDir, "shared"))
|
||||||
baseKnown, _, err2 := list(filepath.Join(baseDir, "known"))
|
baseKnown, _, err2 := list(filepath.Join(baseDir, "known"))
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest and every file of a module.
|
|
||||||
modules
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-catalog/checks/trunk"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no module names another's state on its machine,
|
|
||||||
// and the known list holds only what still occurs.
|
|
||||||
func TestNoModuleNamesAnothersState(t *testing.T) {
|
|
||||||
root := filepath.Join("..", "..")
|
|
||||||
found, err := Check(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
shared, _, err := list("shared")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
known, _, err := list("known")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
refused, gone := Judge(found, shared, known)
|
|
||||||
for _, f := range refused {
|
|
||||||
t.Error(f)
|
|
||||||
}
|
|
||||||
for _, k := range gone {
|
|
||||||
t.Errorf("%q is in checks/paths/known and no longer occurs: take it out, so it cannot come back", k)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// And neither list gains a line the trunk's copies at the merge base do not hold, unless the line says why after `# allow:`. Fails, saying
|
|
||||||
// NOT COMPARED, where no trunk is beside (issue 1295).
|
|
||||||
func TestTheListsOfPathsHoldNoLineTheTrunksDoNot(t *testing.T) {
|
|
||||||
sharedPath, okShared := trunk.Copy(t, "checks/paths/shared")
|
|
||||||
knownPath, okKnown := trunk.Copy(t, "checks/paths/known")
|
|
||||||
if !okShared || !okKnown {
|
|
||||||
t.Log("the trunk holds not both lists: their first copies, to which nothing was added")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
shared, sharedWhy, err := list("shared")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
known, knownWhy, err := list("known")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
baseShared, _, err := list(sharedPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
baseKnown, _, err := list(knownPath)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, g := range append(Grown("shared", shared, sharedWhy, baseShared), Grown("known", known, knownWhy, baseKnown)...) {
|
|
||||||
t.Error(g)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +1,3 @@
|
|||||||
# Paths every program on a machine reads as the machine's own, not as a message from the module that
|
# Paths every program on a machine reads as the machine's own, not as a message from the module that
|
||||||
# writes them (checks/paths, novox/hq ADR 0260). One per line, with why.
|
# writes them (checks/paths, novox/hq ADR 0260). One per line, with why.
|
||||||
/etc/hosts # every resolver on the machine reads it; the hostname module writes it for all of them
|
/etc/hosts # every resolver on the machine reads it; the hostname module writes it for all of them
|
||||||
/var/lib/mesh-build-node-cache # allow: the Go build module gives its integration level the node's installed Node toolchains read-only, so the Node build module's tests build offline (issue 1891, ADR 0533)
|
|
||||||
|
|||||||
@@ -1,3 +0,0 @@
|
|||||||
module checks/planhands
|
|
||||||
|
|
||||||
go 1.26.0
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
// Package planhands refuses a module's line that takes a plan existing for its module handed to the provisioner (issue
|
|
||||||
// 774). Whether a plan hands its module is mesh-sdk's one rule, catalogue.Plan.Hands: since the seat mesh-provisioner keys a
|
|
||||||
// provision its provider's helper answers for a module not handed, a plan naming provisions alone exists for modules
|
|
||||||
// whose files and own secrets stay their node's (on 2026-10-30 the vault read every such plan as handed and answered
|
|
||||||
// twenty modules' own secrets to nobody, and the provisioner client refused each as a plan it could not act on). A line
|
|
||||||
// that tests whether a plan exists either asks Hands on the same line, or says "not handing": it reads only who binds
|
|
||||||
// whom, or that there is no plan at all.
|
|
||||||
package planhands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// existence is a test of whether an assignment's or a plan's `provisioning` is there: nil, or an empty raw value.
|
|
||||||
var existence = regexp.MustCompile(`Provisioning\s*[!=]=\s*nil|len\([^)]*Provisioning\)\s*[!=]=\s*0`)
|
|
||||||
|
|
||||||
// Problems is each such line under root's modules/ that neither asks Hands nor says "not handing", as
|
|
||||||
// `path:line: text`, sorted. Test files are left out: a test may build a plan by hand.
|
|
||||||
func Problems(root string) ([]string, error) {
|
|
||||||
var out []string
|
|
||||||
err := filepath.WalkDir(filepath.Join(root, "modules"), func(p string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if d.IsDir() || !strings.HasSuffix(p, ".go") || strings.HasSuffix(p, "_test.go") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
raw, err := os.ReadFile(p)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
rel, _ := filepath.Rel(root, p)
|
|
||||||
for i, line := range strings.Split(string(raw), "\n") {
|
|
||||||
if existence.MatchString(line) && !strings.Contains(line, ".Hands()") && !strings.Contains(line, "not handing") {
|
|
||||||
out = append(out, fmt.Sprintf("%s:%d: %s", rel, i+1, strings.TrimSpace(line)))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
sort.Strings(out)
|
|
||||||
return out, err
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
package planhands
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A line that takes a plan existing for its module handed is refused and named; one that asks Hands, one marked "not
|
|
||||||
// handing" and any test file pass; and the real catalogue is clean.
|
|
||||||
func TestNoModuleTakesAPlanExistingForItsModuleHanded(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
for f, body := range map[string]string{
|
|
||||||
"modules/vault/own.go": "if a.Provisioning != nil {\n",
|
|
||||||
"modules/client/plan.go": "if len(a.Provisioning) == 0 {\n",
|
|
||||||
"modules/vault/rule.go": "return a.Provisioning != nil && a.Provisioning.Hands(), nil\n",
|
|
||||||
"modules/client/binds.go": "if a.Provisioning != nil { // binds, not handing\n",
|
|
||||||
"modules/vault/own_test.go": "if a.Provisioning == nil {\n",
|
|
||||||
"modules/client/notgo/plan.md": "if a.Provisioning == nil {\n",
|
|
||||||
} {
|
|
||||||
p := filepath.Join(root, f)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(p, []byte(body), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
got, err := Problems(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(got) != 2 || !strings.HasPrefix(got[0], "modules/client/plan.go:1:") || !strings.HasPrefix(got[1], "modules/vault/own.go:1:") {
|
|
||||||
t.Fatalf("refused %v, wanted the client's and the vault's existence tests alone", got)
|
|
||||||
}
|
|
||||||
real, err := Problems(filepath.Join("..", ".."))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(real) > 0 {
|
|
||||||
t.Errorf("a module takes a plan existing for its module handed; ask catalogue.Plan.Hands, or mark a line that reads "+
|
|
||||||
"only who binds whom \"not handing\":\n%s", strings.Join(real, "\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (issue 1936).
|
|
||||||
# every module whole (a directory is read whole, so a module's Go source at any depth enters the digest), for a line that takes a plan existing for its module handed.
|
|
||||||
modules
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/predefined
|
module git.novox.be/novox/mesh-catalog/checks/predefined
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
|
|
||||||
|
|
||||||
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
|
|
||||||
|
|||||||
@@ -1,6 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# the root's list of predefined keys and every manifest that names one.
|
|
||||||
predefined.json
|
|
||||||
modules/*/module.json
|
|
||||||
@@ -1,41 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"sort"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-catalog/checks/trunk"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): predefined.json removes no key a module still
|
|
||||||
// names, judged against the trunk's copy at the merge base. A trunk with none had no keys; with no trunk to read, nothing can be
|
|
||||||
// seen removed, and the test says so.
|
|
||||||
func TestThePredefinedListRemovesNoKeyAModuleStillNames(t *testing.T) {
|
|
||||||
tree, err := Keys(filepath.Join("..", "..", File))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
path, ok := trunk.Copy(t, File)
|
|
||||||
if !ok {
|
|
||||||
t.Log("no trunk copy of " + File + ": no removal can be seen")
|
|
||||||
return
|
|
||||||
}
|
|
||||||
base, err := Keys(path)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
named, err := Named(filepath.Join("..", ".."))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
removed := Removed(base, tree, named)
|
|
||||||
keys := make([]string, 0, len(removed))
|
|
||||||
for k := range removed {
|
|
||||||
keys = append(keys, k)
|
|
||||||
}
|
|
||||||
sort.Strings(keys)
|
|
||||||
for _, k := range keys {
|
|
||||||
t.Errorf("%q is removed from %s and still named by %v", k, File, removed[k])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
module checks/readable
|
|
||||||
|
|
||||||
go 1.26.0
|
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62
|
|
||||||
@@ -1,2 +0,0 @@
|
|||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62 h1:R5C8ICjiaDITHRFR/uIATwARPPXKxssUC70m0svstb0=
|
|
||||||
git.novox.be/novox/mesh-sdk/go v0.1.14-0.20261101020750-62cddf4efc62/go.mod h1:ouERNpZizzwZ0HLEtAypsY9FR20FyUgUYRni6J7kVCY=
|
|
||||||
@@ -1,333 +0,0 @@
|
|||||||
// Command readable refuses a catalogue where a module reads another module's state that the owner does not announce
|
|
||||||
// readable (novox/hq ADR 0495 clause 3, issue 1790): the controller grants a read only from the owner's announcement, so
|
|
||||||
// such a read is refused on the live mesh, said only in the controller's journal. The sweep of 2026-10-30 (issue 774)
|
|
||||||
// found three: gitea's `bound` (read by mesh-provisioner, so no forge account was bound), disk-load's `readings` (read by
|
|
||||||
// restic, nfs-server and mounts) and restic's `good` (read by the self-check). The merge gate's check of reads against
|
|
||||||
// the owner's manifest went with clause 5, and announcetest holds a module to its own list, not to its readers.
|
|
||||||
//
|
|
||||||
// An owner announces from its code. A Go module announces when its code imports mesh-sdk's go/announce: every
|
|
||||||
// announce.State literal there is read, its Name a string, a constant of the package, or a constant of mesh-sdk this
|
|
||||||
// check knows (Known). A TypeScript module announces when its code says the event `states` or calls the SDK's keeps:
|
|
||||||
// every `{ name: "...", readable: true }` there is read. An owner that announces nothing from code is read as before
|
|
||||||
// on the mesh, and is not judged; neither is an owner outside the catalogue (another repository's).
|
|
||||||
//
|
|
||||||
// go run . <catalog root>
|
|
||||||
//
|
|
||||||
// Exits 1 on a finding.
|
|
||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
|
||||||
"go/parser"
|
|
||||||
"go/token"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-sdk/go/health"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Known are the mesh-sdk constants an announcement may name a state by, by the selector the code writes.
|
|
||||||
var Known = map[string]string{
|
|
||||||
"health.GoodState": health.GoodState,
|
|
||||||
"health.ReadingsState": health.ReadingsState,
|
|
||||||
}
|
|
||||||
|
|
||||||
const announcePkg = `"git.novox.be/novox/mesh-sdk/go/announce"`
|
|
||||||
|
|
||||||
// Announced is what one owner's code announces: each state, readable or not.
|
|
||||||
type Announced map[string]bool
|
|
||||||
|
|
||||||
// Findings is every refusal in the catalogue at root, sorted.
|
|
||||||
func Findings(root string) ([]string, error) {
|
|
||||||
manifests, err := filepath.Glob(filepath.Join(root, "modules", "*", "module.json"))
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
dirs := map[string]string{}
|
|
||||||
reads := map[string][]string{}
|
|
||||||
for _, path := range manifests {
|
|
||||||
raw, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var m struct {
|
|
||||||
Module string `json:"module"`
|
|
||||||
Reads []json.RawMessage `json:"reads"`
|
|
||||||
}
|
|
||||||
if err := json.Unmarshal(raw, &m); err != nil {
|
|
||||||
return nil, fmt.Errorf("%s: %w", path, err)
|
|
||||||
}
|
|
||||||
dirs[m.Module] = filepath.Dir(path)
|
|
||||||
for _, r := range m.Reads {
|
|
||||||
var s string
|
|
||||||
if json.Unmarshal(r, &s) == nil {
|
|
||||||
reads[m.Module] = append(reads[m.Module], s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var out []string
|
|
||||||
owners := map[string]Announced{}
|
|
||||||
for reader, rs := range reads {
|
|
||||||
for _, r := range rs {
|
|
||||||
owner, state, ok := strings.Cut(r, ".")
|
|
||||||
dir, inCatalogue := dirs[owner]
|
|
||||||
if !ok || !inCatalogue || owner == reader {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
a, seen := owners[owner]
|
|
||||||
if !seen {
|
|
||||||
var problems []string
|
|
||||||
a, problems, err = AnnouncedBy(dir)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
owners[owner] = a
|
|
||||||
for _, p := range problems {
|
|
||||||
out = append(out, owner+": "+p)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if a == nil {
|
|
||||||
continue // announces nothing from code: read as before
|
|
||||||
}
|
|
||||||
readable, keeps := a[state]
|
|
||||||
switch {
|
|
||||||
case !keeps:
|
|
||||||
out = append(out, fmt.Sprintf("%s reads %s.%s, which %s's code does not announce: the read is refused on the mesh", reader, owner, state, owner))
|
|
||||||
case !readable:
|
|
||||||
out = append(out, fmt.Sprintf("%s reads %s.%s, which %s's code announces not readable: the read is refused on the mesh; announce it readable", reader, owner, state, owner))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(out)
|
|
||||||
return dedupe(out), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func dedupe(in []string) []string {
|
|
||||||
var out []string
|
|
||||||
for i, s := range in {
|
|
||||||
if i == 0 || s != in[i-1] {
|
|
||||||
out = append(out, s)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
tsAnnounces = regexp.MustCompile(`emit\(\s*"states"|\bkeeps\(`)
|
|
||||||
tsState = regexp.MustCompile(`\{\s*name:\s*"([a-z0-9][a-z0-9-]*)"\s*(,\s*readable:\s*(true|false)\s*)?\}`)
|
|
||||||
)
|
|
||||||
|
|
||||||
// AnnouncedBy is what the module in dir announces from its code, nil when it announces nothing; problems are
|
|
||||||
// announcements whose state's name cannot be read.
|
|
||||||
func AnnouncedBy(dir string) (Announced, []string, error) {
|
|
||||||
var out Announced
|
|
||||||
var problems []string
|
|
||||||
pkgs := map[string][]*ast.File{}
|
|
||||||
fset := token.NewFileSet()
|
|
||||||
err := filepath.WalkDir(dir, func(path string, d os.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if d.IsDir() {
|
|
||||||
if n := d.Name(); path != dir && (n == "node_modules" || n == "dist" || n == "test" || n == "testdata" || strings.HasPrefix(n, ".")) {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
name := d.Name()
|
|
||||||
switch {
|
|
||||||
case strings.HasSuffix(name, ".go") && !strings.HasSuffix(name, "_test.go"):
|
|
||||||
f, err := parser.ParseFile(fset, path, nil, 0)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
pkgs[filepath.Dir(path)] = append(pkgs[filepath.Dir(path)], f)
|
|
||||||
case strings.HasSuffix(name, ".ts") && !strings.HasSuffix(name, ".d.ts") && !strings.HasSuffix(name, ".test.ts"):
|
|
||||||
raw, err := os.ReadFile(path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if !tsAnnounces.Match(raw) && !strings.Contains(string(raw), "KEPT") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
for _, m := range tsState.FindAllStringSubmatch(string(raw), -1) {
|
|
||||||
if out == nil {
|
|
||||||
out = Announced{}
|
|
||||||
}
|
|
||||||
out[m[1]] = out[m[1]] || m[3] == "true"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, err
|
|
||||||
}
|
|
||||||
for _, files := range pkgs {
|
|
||||||
imports := false
|
|
||||||
consts := map[string]ast.Expr{}
|
|
||||||
for _, f := range files {
|
|
||||||
for _, im := range f.Imports {
|
|
||||||
if im.Path.Value == announcePkg && (im.Name == nil || im.Name.Name == "announce") {
|
|
||||||
imports = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, decl := range f.Decls {
|
|
||||||
g, ok := decl.(*ast.GenDecl)
|
|
||||||
if !ok || g.Tok != token.CONST {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, spec := range g.Specs {
|
|
||||||
v := spec.(*ast.ValueSpec)
|
|
||||||
for i, n := range v.Names {
|
|
||||||
if i < len(v.Values) {
|
|
||||||
consts[n.Name] = v.Values[i]
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !imports {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if out == nil {
|
|
||||||
out = Announced{}
|
|
||||||
}
|
|
||||||
for _, f := range files {
|
|
||||||
ast.Inspect(f, func(n ast.Node) bool {
|
|
||||||
lit, ok := n.(*ast.CompositeLit)
|
|
||||||
if !ok {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
states := []*ast.CompositeLit{}
|
|
||||||
switch {
|
|
||||||
case isAnnounceState(lit.Type):
|
|
||||||
states = append(states, lit)
|
|
||||||
case isStateSlice(lit.Type):
|
|
||||||
for _, e := range lit.Elts {
|
|
||||||
if c, ok := e.(*ast.CompositeLit); ok && (c.Type == nil || isAnnounceState(c.Type)) {
|
|
||||||
states = append(states, c)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
for _, s := range states {
|
|
||||||
name, readable, problem := stateOf(s, consts)
|
|
||||||
if problem != "" {
|
|
||||||
problems = append(problems, fmt.Sprintf("%s: %s", fset.Position(s.Pos()), problem))
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
out[name] = out[name] || readable
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out, problems, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func isAnnounceState(e ast.Expr) bool {
|
|
||||||
s, ok := e.(*ast.SelectorExpr)
|
|
||||||
if !ok {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
x, ok := s.X.(*ast.Ident)
|
|
||||||
return ok && x.Name == "announce" && s.Sel.Name == "State"
|
|
||||||
}
|
|
||||||
|
|
||||||
func isStateSlice(e ast.Expr) bool {
|
|
||||||
a, ok := e.(*ast.ArrayType)
|
|
||||||
return ok && isAnnounceState(a.Elt)
|
|
||||||
}
|
|
||||||
|
|
||||||
// stateOf is one announce.State literal's name and whether it is readable (Readable true, or Readers named).
|
|
||||||
func stateOf(lit *ast.CompositeLit, consts map[string]ast.Expr) (string, bool, string) {
|
|
||||||
var name string
|
|
||||||
var readable bool
|
|
||||||
named := false
|
|
||||||
for _, e := range lit.Elts {
|
|
||||||
kv, ok := e.(*ast.KeyValueExpr)
|
|
||||||
if !ok {
|
|
||||||
return "", false, "an announce.State without field names cannot be read; name its fields"
|
|
||||||
}
|
|
||||||
key, _ := kv.Key.(*ast.Ident)
|
|
||||||
if key == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
switch key.Name {
|
|
||||||
case "Name":
|
|
||||||
v, ok := resolve(kv.Value, consts, 0)
|
|
||||||
if !ok {
|
|
||||||
return "", false, fmt.Sprintf("the state's name %s cannot be read: name it by a string, a constant of the package, or a mesh-sdk constant in checks/readable's Known", exprString(kv.Value))
|
|
||||||
}
|
|
||||||
name, named = v, true
|
|
||||||
case "Readable":
|
|
||||||
if id, ok := kv.Value.(*ast.Ident); ok {
|
|
||||||
readable = readable || id.Name == "true"
|
|
||||||
}
|
|
||||||
case "Readers":
|
|
||||||
readable = true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if !named {
|
|
||||||
return "", false, "an announce.State without a Name"
|
|
||||||
}
|
|
||||||
return name, readable, ""
|
|
||||||
}
|
|
||||||
|
|
||||||
func resolve(e ast.Expr, consts map[string]ast.Expr, depth int) (string, bool) {
|
|
||||||
if depth > 8 {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
switch v := e.(type) {
|
|
||||||
case *ast.BasicLit:
|
|
||||||
if v.Kind == token.STRING {
|
|
||||||
s, err := strconv.Unquote(v.Value)
|
|
||||||
return s, err == nil
|
|
||||||
}
|
|
||||||
case *ast.Ident:
|
|
||||||
if c, ok := consts[v.Name]; ok {
|
|
||||||
return resolve(c, consts, depth+1)
|
|
||||||
}
|
|
||||||
case *ast.SelectorExpr:
|
|
||||||
s, ok := Known[exprString(v)]
|
|
||||||
return s, ok
|
|
||||||
}
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
func exprString(e ast.Expr) string {
|
|
||||||
switch v := e.(type) {
|
|
||||||
case *ast.Ident:
|
|
||||||
return v.Name
|
|
||||||
case *ast.SelectorExpr:
|
|
||||||
return exprString(v.X) + "." + v.Sel.Name
|
|
||||||
case *ast.BasicLit:
|
|
||||||
return v.Value
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%T", e)
|
|
||||||
}
|
|
||||||
|
|
||||||
func main() {
|
|
||||||
root := "."
|
|
||||||
if len(os.Args) > 1 {
|
|
||||||
root = os.Args[1]
|
|
||||||
}
|
|
||||||
found, err := Findings(root)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, err)
|
|
||||||
os.Exit(2)
|
|
||||||
}
|
|
||||||
for _, f := range found {
|
|
||||||
fmt.Println(f)
|
|
||||||
}
|
|
||||||
if len(found) > 0 {
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// A read of another module's state is refused here when the owner's code announces that state not readable, or not at
|
|
||||||
// all (the sweep of 2026-10-30, issue 774); an owner that announces nothing from code, or lives in another repository,
|
|
||||||
// is not judged.
|
|
||||||
func TestFindings(t *testing.T) {
|
|
||||||
const goOwner = "package main\n\nimport \"git.novox.be/novox/mesh-sdk/go/announce\"\n\n"
|
|
||||||
for _, row := range []struct {
|
|
||||||
name string
|
|
||||||
files map[string]string
|
|
||||||
want string // empty: clean
|
|
||||||
}{
|
|
||||||
{"go, readable by a constant", map[string]string{
|
|
||||||
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
|
|
||||||
"modules/o/module.json": `{"module":"o"}`,
|
|
||||||
"modules/o/cmd/o/main.go": goOwner + "const S = \"s\"\n\nvar _ = announce.State{Name: S, Readable: true}\n",
|
|
||||||
"modules/o/cmd/o/main_test.go": goOwner + "var _ = announce.State{Name: \"s\"}\n"}, ""},
|
|
||||||
{"go, not readable", map[string]string{
|
|
||||||
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
|
|
||||||
"modules/o/module.json": `{"module":"o"}`,
|
|
||||||
"modules/o/cmd/o/main.go": goOwner + "var Kept = []announce.State{{Name: \"t\", Readable: true}, {Name: \"s\"}}\n"},
|
|
||||||
"r reads o.s, which o's code announces not readable"},
|
|
||||||
{"go, a known mesh-sdk name and readers", map[string]string{
|
|
||||||
"modules/r/module.json": `{"module":"r","reads":["o.good","o.s"]}`,
|
|
||||||
"modules/o/module.json": `{"module":"o"}`,
|
|
||||||
"modules/o/cmd/o/main.go": goOwner + "var _ = []announce.State{{Name: health.GoodState, Readable: true}, {Name: \"s\", Readers: []string{\"r\"}}}\n"}, ""},
|
|
||||||
{"go, a name that cannot be read", map[string]string{
|
|
||||||
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
|
|
||||||
"modules/o/module.json": `{"module":"o"}`,
|
|
||||||
"modules/o/cmd/o/main.go": goOwner + "var _ = announce.State{Name: other.S, Readable: true}\n"},
|
|
||||||
"the state's name other.S cannot be read"},
|
|
||||||
{"go, a state not announced", map[string]string{
|
|
||||||
"modules/r/module.json": `{"module":"r","reads":["o.s"]}`,
|
|
||||||
"modules/o/module.json": `{"module":"o"}`,
|
|
||||||
"modules/o/cmd/o/main.go": goOwner + "var _ = announce.State{Name: \"t\"}\n"},
|
|
||||||
"r reads o.s, which o's code does not announce"},
|
|
||||||
{"typescript, not readable", map[string]string{
|
|
||||||
"modules/r/module.json": `{"module":"r","reads":["o.bound"]}`,
|
|
||||||
"modules/o/module.json": `{"module":"o"}`,
|
|
||||||
"modules/o/kept.ts": `export const KEPT = [{ name: "announced" }, { name: "bound" }];`},
|
|
||||||
"r reads o.bound, which o's code announces not readable"},
|
|
||||||
{"typescript, readable", map[string]string{
|
|
||||||
"modules/r/module.json": `{"module":"r","reads":["o.bound"]}`,
|
|
||||||
"modules/o/module.json": `{"module":"o"}`,
|
|
||||||
"modules/o/index.ts": `await keeps([{ name: "bound", readable: true }]);`}, ""},
|
|
||||||
{"an owner that announces nothing, and one outside the catalogue", map[string]string{
|
|
||||||
"modules/r/module.json": `{"module":"r","reads":["o.s","mesh-controller.lease"]}`,
|
|
||||||
"modules/o/module.json": `{"module":"o"}`,
|
|
||||||
"modules/o/cmd/o/main.go": "package main\n\nvar _ = 1\n"}, ""},
|
|
||||||
} {
|
|
||||||
t.Run(row.name, func(t *testing.T) {
|
|
||||||
root := t.TempDir()
|
|
||||||
for path, text := range row.files {
|
|
||||||
p := filepath.Join(root, path)
|
|
||||||
if err := os.MkdirAll(filepath.Dir(p), 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(p, []byte(text), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
got, err := Findings(root)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
joined := strings.Join(got, "\n")
|
|
||||||
if row.want == "" && len(got) != 0 || row.want != "" && !strings.Contains(joined, row.want) {
|
|
||||||
t.Errorf("findings %q, want %q", joined, row.want)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// The catalogue itself: every state a module reads of another module that announces from code is announced readable.
|
|
||||||
func TestTheCatalogueReadsOnlyWhatItsOwnersAnnounceReadable(t *testing.T) {
|
|
||||||
got, err := Findings(filepath.Join("..", ".."))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
for _, f := range got {
|
|
||||||
t.Error(f)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (issue 1936).
|
|
||||||
# every manifest, for what it reads; and every module's Go and TypeScript code, for the states it announces.
|
|
||||||
modules/*/module.json
|
|
||||||
modules/*/cmd
|
|
||||||
modules/*/*.go
|
|
||||||
modules/*/*.ts
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/sdkjudge
|
|
||||||
|
|
||||||
go 1.22
|
|
||||||
@@ -1,189 +0,0 @@
|
|||||||
// Package sdkjudge refuses mesh-sdk a manifest judge (novox/hq ADR 0578 §4): a package every module compiles in carries
|
|
||||||
// the contract only, so it holds no function that gathers a manifest's problems and no table of what a manifest may
|
|
||||||
// declare. The judge is a package of mesh-delivery that only the indexer and the checker use, and a reader decodes a
|
|
||||||
// manifest and takes the indexer's verdict (ADR 0578 §3).
|
|
||||||
//
|
|
||||||
// It reads mesh-sdk's source as the checker places it beside this check (MESH_CHECK_BESIDE, the repository mesh-sdk: at its
|
|
||||||
// trunk, or at the head of the group's pull request when the group holds one), so a change adding either to mesh-sdk in a
|
|
||||||
// delivery group with a catalogue change is refused here. A change to mesh-sdk alone is checked by mesh-sdk's own check,
|
|
||||||
// which does not run this package: the catalogue's check sees the SDK only beside a catalogue change.
|
|
||||||
//
|
|
||||||
// What it refuses, in the non-test Go files of mesh-sdk's go/ tree:
|
|
||||||
// - a function or method named for problems (a name ending in Problems or Problem) that takes a Manifest or a Shelf, and
|
|
||||||
// a function named ParseManifest, ParseManifestWith, ParseManifestForInputs, ValidateManifest... or ValidateShelf...: the
|
|
||||||
// judge's entry points;
|
|
||||||
// - in the package catalogue, a package-level variable named for what is allowed, permitted, removable, reserved or
|
|
||||||
// forbidden whose value is a map or a list: an allow-list table.
|
|
||||||
//
|
|
||||||
// The composer's refusals of composition itself (what a machine can be composed with) are not a judgement of a manifest
|
|
||||||
// (ADR 0578 §4, clause 5; issue 1585 tracks moving them): they are named in composition, each with why, and the list only
|
|
||||||
// shrinks, since a name that no longer exists is refused too.
|
|
||||||
package sdkjudge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"go/ast"
|
|
||||||
"go/parser"
|
|
||||||
"go/token"
|
|
||||||
"io/fs"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"regexp"
|
|
||||||
"sort"
|
|
||||||
"strings"
|
|
||||||
)
|
|
||||||
|
|
||||||
// composition is every function or table the check lets stay in mesh-sdk, as "path-from-go/:name", and why. It names no
|
|
||||||
// manifest judgement: each is the composer's refusal of composition itself (ADR 0578 clause 5).
|
|
||||||
var composition = map[string]string{
|
|
||||||
"catalogue/consumers.go:consumersPlaceholderProblems": "composition: rendering a consumer's template",
|
|
||||||
"catalogue/collections.go:collectedPlaceholderProblems": "composition: placeholders a contribution renders",
|
|
||||||
"catalogue/environment_into.go:placeholderProblems": "composition: placeholders a contribution renders",
|
|
||||||
"catalogue/seat_contributions.go:seatPlaceholderProblems": "composition: placeholders a contribution renders",
|
|
||||||
"catalogue/collections.go:itemTemplateProblem": "composition: filling an item's template",
|
|
||||||
"catalogue/setting_takes.go:takenSettingsProblems": "composition: whether stored settings still compose",
|
|
||||||
"catalogue/template.go:HeldTemplateProblem": "composition: the take-in refusal of a resolved build",
|
|
||||||
}
|
|
||||||
|
|
||||||
// Problems is what mesh-sdk's Go tree under root (its go/ directory) holds of a manifest judge, one line each, but for
|
|
||||||
// what composition lets stay (the map the caller gives; the test of the real tree gives the package's), and the
|
|
||||||
// entries of composition it no longer holds.
|
|
||||||
func Problems(root string, composition map[string]string) ([]string, error) {
|
|
||||||
var out []string
|
|
||||||
seen := map[string]bool{}
|
|
||||||
err := filepath.WalkDir(root, func(path string, d fs.DirEntry, err error) error {
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if d.IsDir() {
|
|
||||||
if n := d.Name(); n == "testdata" || n == "vendor" || strings.HasPrefix(n, ".") {
|
|
||||||
return filepath.SkipDir
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(path, ".go") || strings.HasSuffix(path, "_test.go") {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
rel, _ := filepath.Rel(root, path)
|
|
||||||
file, err := parser.ParseFile(token.NewFileSet(), path, nil, parser.SkipObjectResolution)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%s: %w", rel, err)
|
|
||||||
}
|
|
||||||
inCatalogue := filepath.Base(filepath.Dir(path)) == "catalogue"
|
|
||||||
for _, d := range file.Decls {
|
|
||||||
switch d := d.(type) {
|
|
||||||
case *ast.FuncDecl:
|
|
||||||
if why := judgeFunc(d); why != "" {
|
|
||||||
out = append(out, note(composition, seen, rel, d.Name.Name, why))
|
|
||||||
}
|
|
||||||
case *ast.GenDecl:
|
|
||||||
if !inCatalogue || d.Tok != token.VAR {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for _, spec := range d.Specs {
|
|
||||||
v, _ := spec.(*ast.ValueSpec)
|
|
||||||
if v == nil {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
for i, name := range v.Names {
|
|
||||||
if i < len(v.Values) && allowName.MatchString(name.Name) && isTable(v.Values[i]) {
|
|
||||||
out = append(out, note(composition, seen, rel, name.Name,
|
|
||||||
"a table of what a manifest may declare (an allow-list)"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
var kept []string
|
|
||||||
for _, line := range out {
|
|
||||||
if line != "" {
|
|
||||||
kept = append(kept, line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for key, why := range composition {
|
|
||||||
if !seen[key] {
|
|
||||||
kept = append(kept, fmt.Sprintf("composition lists %s (%s), which mesh-sdk no longer holds: take it off the list", key, why))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
sort.Strings(kept)
|
|
||||||
return kept, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// note is the line refusing name in rel for why, or "" when composition lets it stay.
|
|
||||||
func note(composition map[string]string, seen map[string]bool, rel, name, why string) string {
|
|
||||||
key := rel + ":" + name
|
|
||||||
if _, ok := composition[key]; ok {
|
|
||||||
seen[key] = true
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return fmt.Sprintf("%s: %s is %s; mesh-sdk carries the contract only, the judge is the indexer's and the checker's (ADR 0578 §4)", rel, name, why)
|
|
||||||
}
|
|
||||||
|
|
||||||
// allowName is what names a table of what is allowed.
|
|
||||||
var allowName = regexp.MustCompile(`(?i)allow|permit|removable|reserved|forbid|denied`)
|
|
||||||
|
|
||||||
// isTable says whether an expression is a map or a list literal of more than one entry.
|
|
||||||
func isTable(e ast.Expr) bool {
|
|
||||||
c, ok := e.(*ast.CompositeLit)
|
|
||||||
if !ok || len(c.Elts) < 2 {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
switch c.Type.(type) {
|
|
||||||
case *ast.MapType, *ast.ArrayType:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// judgeFunc says why a function is a manifest judge, or "".
|
|
||||||
func judgeFunc(f *ast.FuncDecl) string {
|
|
||||||
name := f.Name.Name
|
|
||||||
if judgeEntry.MatchString(name) {
|
|
||||||
return "a function that judges a manifest"
|
|
||||||
}
|
|
||||||
if !strings.HasSuffix(name, "Problems") && !strings.HasSuffix(name, "Problem") {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
if takes(f.Recv) || takes(f.Type.Params) {
|
|
||||||
return "a function that gathers a manifest's problems"
|
|
||||||
}
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
|
|
||||||
// judgeEntry is the names of the judge's entry points: the strict parsers and the validators of a manifest or a shelf.
|
|
||||||
var judgeEntry = regexp.MustCompile(`^(ParseManifest(With|ForInputs)?|Validate(Manifest|Shelf)\w*)$`)
|
|
||||||
|
|
||||||
// takes says whether a list of parameters names a Manifest or a Shelf anywhere in a type.
|
|
||||||
func takes(fields *ast.FieldList) bool {
|
|
||||||
if fields == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
found := false
|
|
||||||
for _, f := range fields.List {
|
|
||||||
ast.Inspect(f.Type, func(n ast.Node) bool {
|
|
||||||
if id, ok := n.(*ast.Ident); ok && (id.Name == "Manifest" || id.Name == "Shelf") {
|
|
||||||
found = true
|
|
||||||
}
|
|
||||||
return !found
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return found
|
|
||||||
}
|
|
||||||
|
|
||||||
// BesideDir is where the checker places mesh-sdk's clone, as a directory under MESH_CHECK_BESIDE; its go/ tree is judged.
|
|
||||||
func BesideDir() (string, error) {
|
|
||||||
beside := os.Getenv("MESH_CHECK_BESIDE")
|
|
||||||
if beside == "" {
|
|
||||||
return "", fmt.Errorf("NOT CHECKED: MESH_CHECK_BESIDE is not set, so mesh-sdk's source is not beside this check; the checker places the SDK there (besideRules)")
|
|
||||||
}
|
|
||||||
dir := filepath.Join(beside, "mesh-sdk", "go")
|
|
||||||
if _, err := os.Stat(dir); err != nil {
|
|
||||||
return "", fmt.Errorf("NOT CHECKED: mesh-sdk is not beside this check in MESH_CHECK_BESIDE=%s: %w", beside, err)
|
|
||||||
}
|
|
||||||
return dir, nil
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
package sdkjudge
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// mesh-sdk carries the contract only (ADR 0578 §4). A row is a source tree of its own: a function gathering a manifest's
|
|
||||||
// problems, a manifest parser that judges and a table of what a manifest may declare are refused, named; a Problems
|
|
||||||
// function over something that is no manifest, a test file, a table outside the catalogue package and a function that
|
|
||||||
// merely decodes are not. Then the SDK beside this check is judged.
|
|
||||||
func TestMeshSDKCarriesNoManifestJudge(t *testing.T) {
|
|
||||||
for _, row := range []struct {
|
|
||||||
name, dir, file, src, refused string
|
|
||||||
}{
|
|
||||||
{name: "a function gathering a manifest's problems", dir: "catalogue", refused: "StreamProblems is a function that gathers a manifest's problems",
|
|
||||||
src: "package catalogue\ntype Manifest struct{}\nfunc StreamProblems(m Manifest) []string { return nil }\n"},
|
|
||||||
{name: "a method over a manifest", dir: "catalogue", refused: "healthProblems is a function that gathers",
|
|
||||||
src: "package catalogue\ntype Manifest struct{}\nfunc (m Manifest) healthProblems() []string { return nil }\n"},
|
|
||||||
{name: "a function over the shelf", dir: "catalogue", refused: "CatalogueProblems is a function that gathers",
|
|
||||||
src: "package catalogue\ntype Shelf map[string]int\nfunc CatalogueProblems(s Shelf) []string { return nil }\n"},
|
|
||||||
{name: "a parser that judges", dir: "catalogue", refused: "ParseManifestWith is a function that judges a manifest",
|
|
||||||
src: "package catalogue\nfunc ParseManifestWith(raw []byte) error { return nil }\n"},
|
|
||||||
{name: "a table of what may be removed", dir: "catalogue", refused: "removable is a table of what a manifest may declare",
|
|
||||||
src: "package catalogue\nvar removable = map[string]bool{\"A\": true, \"B\": true}\n"},
|
|
||||||
{name: "a key parser", dir: "delivery", src: "package delivery\nfunc ParseManifestsKey(k string) error { return nil }\n"},
|
|
||||||
{name: "a decoder", dir: "catalogue", src: "package catalogue\ntype Manifest struct{}\nfunc Decode(raw []byte) (Manifest, error) { return Manifest{}, nil }\n"},
|
|
||||||
{name: "problems of something else", dir: "asks", src: "package asks\ntype Condition struct{}\nfunc DecodeProblems(c Condition) []string { return nil }\n"},
|
|
||||||
{name: "a table beside the catalogue", dir: "asks", src: "package asks\nvar allowed = []string{\"a\", \"b\"}\n"},
|
|
||||||
{name: "a test file", dir: "catalogue", file: "x_test.go",
|
|
||||||
src: "package catalogue\ntype Manifest struct{}\nfunc StreamProblems(m Manifest) []string { return nil }\n"},
|
|
||||||
} {
|
|
||||||
root := t.TempDir()
|
|
||||||
dir := filepath.Join(root, row.dir)
|
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if row.file == "" {
|
|
||||||
row.file = "x.go"
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, row.file), []byte(row.src), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
got, err := Problems(root, nil)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
joined := strings.Join(got, "\n")
|
|
||||||
if (row.refused == "") != (len(got) == 0) || !strings.Contains(joined, row.refused) {
|
|
||||||
t.Errorf("%s: refused %q, want %q", row.name, joined, row.refused)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
dir, err := BesideDir()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
got, err := Problems(dir, composition)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if len(got) != 0 {
|
|
||||||
t.Fatalf("mesh-sdk holds a manifest judge:\n%s", strings.Join(got, "\n"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# None: the tests read mesh-sdk's source, the clone beside this repository (MESH_CHECK_BESIDE), which is no path of it.
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/secrets
|
module git.novox.be/novox/mesh-catalog/checks/secrets
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
|
|
||||||
|
|
||||||
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest and the files it names.
|
|
||||||
modules
|
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-catalog/checks/trunk"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): the known list gains no line the trunk's copy
|
|
||||||
// at the merge base does not hold. Without a trunk copy the lines in seed are the trunk's.
|
|
||||||
func TestTheKnownListHoldsNoLineTheTrunksDoesNot(t *testing.T) {
|
|
||||||
known, err := Known("known")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
base := seed
|
|
||||||
if path, ok := trunk.Copy(t, "checks/secrets/known"); ok {
|
|
||||||
if base, err = Known(path); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
for _, line := range Added(known, base) {
|
|
||||||
t.Errorf("checks/secrets/known gains %q, which the trunk's copy does not hold: the list only shrinks — protect the key instead (novox/hq issue 342)", line)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest.
|
|
||||||
modules/*/module.json
|
|
||||||
@@ -1,7 +1,3 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/templates
|
module git.novox.be/novox/mesh-catalog/checks/templates
|
||||||
|
|
||||||
go 1.22
|
go 1.22
|
||||||
|
|
||||||
require git.novox.be/novox/mesh-catalog/checks/trunk v0.0.0
|
|
||||||
|
|
||||||
replace git.novox.be/novox/mesh-catalog/checks/trunk => ../trunk
|
|
||||||
|
|||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest and its templates.
|
|
||||||
modules
|
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"git.novox.be/novox/mesh-catalog/checks/trunk"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The script's comparison of this list with the trunk's (novox/hq ADR 0529 clause 2): the list of modules keeping file text inline
|
|
||||||
// gains none the trunk's copy at the merge base does not hold. The catalogue itself is judged against the list by
|
|
||||||
// TestTheCatalogueKeepsFileTextInlineOnlyWhereListed. Skipped, and said, where no trunk is beside.
|
|
||||||
func TestTheInlineListHoldsNoModuleTheTrunksDoesNot(t *testing.T) {
|
|
||||||
path, ok := trunk.Copy(t, "checks/templates/inline")
|
|
||||||
found, err := Check("../..")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
listed, err := Listed(listFile)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
base := seed
|
|
||||||
if ok {
|
|
||||||
if base, err = Listed(path); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_, _, added := Judge(found, listed, base)
|
|
||||||
for _, m := range added {
|
|
||||||
t.Errorf("checks/templates/inline gains %s, which the trunk's copy does not hold: the list only shrinks", m)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# Check("../.."): every manifest.
|
|
||||||
modules/*/module.json
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
module git.novox.be/novox/mesh-catalog/checks/trunk
|
|
||||||
|
|
||||||
go 1.22
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Package trunk is where a catalogue check finds the trunk's copy of a list that may only shrink (novox/hq ADR 0529 clause 2):
|
|
||||||
// the file as the catalogue's base commit holds it, to judge a change's list against.
|
|
||||||
//
|
|
||||||
// The checker fetches the change's own repository at its base commit beside the change, as it fetches a sibling, and says where
|
|
||||||
// in MESH_CHECK_BESIDE; the copy is under the repository's name (the same convention the other repositories' tests use). With
|
|
||||||
// MESH_CHECK_BESIDE unset, or set without the catalogue's copy in it, the test fails saying NOT COMPARED WITH THE TRUNK and
|
|
||||||
// why: the checker places the change's own repository since R4g (issue 1295), so a missing copy is a fault, never a pass. A trunk that holds no such file had
|
|
||||||
// none: the list's first copy, which nothing was added to.
|
|
||||||
package trunk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Repository is the catalogue's name beside a check.
|
|
||||||
const Repository = "mesh-catalog"
|
|
||||||
|
|
||||||
// BesideEnv is where a merge check says the clones beside the one checked are.
|
|
||||||
const BesideEnv = "MESH_CHECK_BESIDE"
|
|
||||||
|
|
||||||
// Copy is the trunk's copy of the file at rel (a path from the catalogue's root), written to a file of the test's own: its path,
|
|
||||||
// and whether the trunk holds one (false: its first copy). It fails the test when no copy of the catalogue is beside.
|
|
||||||
func Copy(t testing.TB, rel string) (string, bool) {
|
|
||||||
t.Helper()
|
|
||||||
raw, found, err := Read(rel)
|
|
||||||
switch {
|
|
||||||
case err != nil:
|
|
||||||
t.Fatal(err)
|
|
||||||
case !found:
|
|
||||||
t.Logf("the trunk holds no %s: its first copy", rel)
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
path := filepath.Join(t.TempDir(), filepath.Base(rel))
|
|
||||||
if err := os.WriteFile(path, raw, 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
return path, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Read is the trunk's copy of rel and whether the trunk holds one. err is a trunk not beside the check (MESH_CHECK_BESIDE unset,
|
|
||||||
// or without the catalogue's directory), or a copy unreadable: a comparison that cannot be made is never a pass.
|
|
||||||
func Read(rel string) (content []byte, found bool, err error) {
|
|
||||||
beside := os.Getenv(BesideEnv)
|
|
||||||
if beside == "" {
|
|
||||||
return nil, false, fmt.Errorf("NOT COMPARED WITH THE TRUNK: %s is not set, so the trunk's copy of %s is not beside this run; the checker places the change's own repository there (issue 1295)", BesideEnv, rel)
|
|
||||||
}
|
|
||||||
dir := filepath.Join(beside, Repository)
|
|
||||||
if _, err := os.Stat(dir); errors.Is(err, os.ErrNotExist) {
|
|
||||||
return nil, false, fmt.Errorf("NOT COMPARED WITH THE TRUNK: %s is not beside this check in %s=%s, so %s is not judged against the trunk's copy; the checker places the change's own repository there at its trunk (issue 1295)", Repository, BesideEnv, beside, rel)
|
|
||||||
} else if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
raw, err := os.ReadFile(filepath.Join(dir, rel))
|
|
||||||
if errors.Is(err, os.ErrNotExist) {
|
|
||||||
return nil, false, nil
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return nil, false, err
|
|
||||||
}
|
|
||||||
return raw, true, nil
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
package trunk
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestTheCloneBesideTheCheckIsTheTrunk(t *testing.T) {
|
|
||||||
beside := t.TempDir()
|
|
||||||
dir := filepath.Join(beside, Repository, "checks", "x")
|
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
if err := os.WriteFile(filepath.Join(dir, "known"), []byte("a\nb\n"), 0o644); err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
t.Setenv(BesideEnv, beside)
|
|
||||||
raw, found, err := Read("checks/x/known")
|
|
||||||
if err != nil || !found || string(raw) != "a\nb\n" {
|
|
||||||
t.Fatalf("read %q found %v err %v", raw, found, err)
|
|
||||||
}
|
|
||||||
if _, found, err := Read("checks/x/absent"); err != nil || found {
|
|
||||||
t.Fatalf("a file the trunk does not hold: found %v err %v; wanted its first copy", found, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestWithNothingBesideTheComparisonFailsAndSaysNotMade(t *testing.T) {
|
|
||||||
t.Setenv(BesideEnv, "")
|
|
||||||
_, found, err := Read("checks/x/known")
|
|
||||||
if found || err == nil || !strings.Contains(err.Error(), "NOT COMPARED") {
|
|
||||||
t.Fatalf("found %v, err %v; wanted a failure saying NOT COMPARED", found, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestACloneBesideWithoutTheCatalogueFailsAndSaysNotMade(t *testing.T) {
|
|
||||||
t.Setenv(BesideEnv, t.TempDir()) // the siblings are there, the catalogue's own base is not
|
|
||||||
_, found, err := Read("checks/x/known")
|
|
||||||
if found || err == nil || !strings.Contains(err.Error(), "NOT COMPARED") || !strings.Contains(err.Error(), "not beside") {
|
|
||||||
t.Fatalf("found %v, err %v; wanted a failure naming the missing copy", found, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
# The modules of the catalogue whose texts still name the build agent or the toolchain images, and why (checks/words,
|
|
||||||
# buildagent_test.go). One module directory a line, with its reason after a #. It only shrinks.
|
|
||||||
modules/mesh-build-go # build.go:45 says "the build agent's recipe" in a step's description: the asks lane's module (issue 1887), not E6's to edit
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
package main
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// The build agent and the toolchain images are gone (novox/hq ADR 0524 §5, issue 1295 and E6 of milestone R4g): no text a module
|
|
||||||
// can show names them. Where the word stands for a reason that is another lane's or still waits for its change, buildagent-allowed
|
|
||||||
// names the module's directory with that reason and the date it is dated from; the list only shrinks: a directory on it that no
|
|
||||||
// longer names the word is refused, so the line is taken out.
|
|
||||||
var gone = []string{"build-agent", "build agent", "toolchain image"}
|
|
||||||
|
|
||||||
func allowedModules(t *testing.T) map[string]string {
|
|
||||||
t.Helper()
|
|
||||||
raw, err := os.ReadFile("buildagent-allowed")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
out := map[string]string{}
|
|
||||||
for _, line := range strings.Split(string(raw), "\n") {
|
|
||||||
entry, why, _ := strings.Cut(line, "#")
|
|
||||||
if entry = strings.TrimSpace(entry); entry != "" && !strings.HasPrefix(line, "#") {
|
|
||||||
if strings.TrimSpace(why) == "" {
|
|
||||||
t.Fatalf("%q is allowed without a reason", entry)
|
|
||||||
}
|
|
||||||
out[entry] = strings.TrimSpace(why)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNoTextOfTheCatalogueNamesTheBuildAgentOutsideTheModulesAllowed(t *testing.T) {
|
|
||||||
texts, _, err := collect("../..")
|
|
||||||
if err != nil {
|
|
||||||
t.Fatal(err)
|
|
||||||
}
|
|
||||||
allowed := allowedModules(t)
|
|
||||||
seen := map[string]bool{}
|
|
||||||
for _, f := range check(gone, texts) {
|
|
||||||
module := f
|
|
||||||
if rest, ok := strings.CutPrefix(f, "modules/"); ok {
|
|
||||||
module = "modules/" + strings.SplitN(rest, "/", 2)[0]
|
|
||||||
}
|
|
||||||
if _, ok := allowed[module]; ok {
|
|
||||||
seen[module] = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
t.Errorf("%s (the build agent and the toolchain images are gone: say what stands in their place)", f)
|
|
||||||
}
|
|
||||||
for module, why := range allowed {
|
|
||||||
if !seen[module] {
|
|
||||||
t.Errorf("buildagent-allowed names %s (%s), which names the word no more: take the line out, so it cannot come back", module, why)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -58,9 +58,6 @@ func checkLines(lines []retiredLine, texts []text) []string {
|
|||||||
for _, l := range lines {
|
for _, l := range lines {
|
||||||
rx := linePattern(l)
|
rx := linePattern(l)
|
||||||
for _, t := range texts {
|
for _, t := range texts {
|
||||||
if !strings.Contains(t.s, l.daemon) { // the pattern holds the daemon's name whole: the cheap test first
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if m := rx.FindStringSubmatchIndex(t.s); m != nil {
|
if m := rx.FindStringSubmatchIndex(t.s); m != nil {
|
||||||
line := t.line
|
line := t.line
|
||||||
if line > 0 {
|
if line > 0 {
|
||||||
|
|||||||
+36
-71
@@ -43,63 +43,18 @@ func main() {
|
|||||||
if len(os.Args) > 1 {
|
if len(os.Args) > 1 {
|
||||||
root = os.Args[1]
|
root = os.Args[1]
|
||||||
}
|
}
|
||||||
os.Exit(run(root))
|
|
||||||
}
|
|
||||||
|
|
||||||
// run reads every text a module can show under root and answers the command's exit code: 0 clean, 1 a finding, 2 a list or a
|
|
||||||
// file unreadable. The command prints what it found; a test of the real catalogue reads the code.
|
|
||||||
func run(root string) int {
|
|
||||||
words, err := readList(filepath.Join(root, "retired-words"))
|
words, err := readList(filepath.Join(root, "retired-words"))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Fprintln(os.Stderr, "words:", err)
|
fmt.Fprintln(os.Stderr, "words:", err)
|
||||||
return 2
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
if len(words) == 0 {
|
if len(words) == 0 {
|
||||||
fmt.Fprintln(os.Stderr, "words: retired-words lists no word — the check would pass on anything")
|
fmt.Fprintln(os.Stderr, "words: retired-words lists no word — the check would pass on anything")
|
||||||
return 2
|
os.Exit(2)
|
||||||
}
|
}
|
||||||
texts, files, err := collect(root)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, "words:", err)
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
findings := check(words, texts)
|
|
||||||
for _, f := range findings {
|
|
||||||
fmt.Println(f)
|
|
||||||
}
|
|
||||||
if len(findings) > 0 {
|
|
||||||
fmt.Printf("words: %d use(s) of a word the glossary retired (novox/hq ADR 0244) — say it in the glossary's word\n", len(findings))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
// And no daemon's line (novox/hq ADR 0272): what a module tells the operator to type is a `nox` line.
|
|
||||||
lines, err := readLines(filepath.Join(root, "retired-lines"))
|
|
||||||
if err != nil {
|
|
||||||
fmt.Fprintln(os.Stderr, "words:", err)
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
if len(lines) == 0 {
|
|
||||||
fmt.Fprintln(os.Stderr, "words: retired-lines lists no line — the check would pass on anything")
|
|
||||||
return 2
|
|
||||||
}
|
|
||||||
if findings := checkLines(lines, texts); len(findings) > 0 {
|
|
||||||
for _, f := range findings {
|
|
||||||
fmt.Println(f)
|
|
||||||
}
|
|
||||||
fmt.Printf("words: %d daemon's line(s) in what a module shows — the operator types one command, nox (novox/hq ADR 0272)\n",
|
|
||||||
len(findings))
|
|
||||||
return 1
|
|
||||||
}
|
|
||||||
fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines),
|
|
||||||
len(texts), files)
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// collect reads every text a module can show under root: the strings of its own code that is not a test, and every string of
|
|
||||||
// its manifests.
|
|
||||||
func collect(root string) ([]text, int, error) {
|
|
||||||
var texts []text
|
var texts []text
|
||||||
files := 0
|
files := 0
|
||||||
err := filepath.WalkDir(filepath.Join(root, "modules"), func(path string, d fs.DirEntry, err error) error {
|
err = filepath.WalkDir(filepath.Join(root, "modules"), func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -132,7 +87,38 @@ func collect(root string) ([]text, int, error) {
|
|||||||
texts = append(texts, found...)
|
texts = append(texts, found...)
|
||||||
return nil
|
return nil
|
||||||
})
|
})
|
||||||
return texts, files, err
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "words:", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
findings := check(words, texts)
|
||||||
|
for _, f := range findings {
|
||||||
|
fmt.Println(f)
|
||||||
|
}
|
||||||
|
if len(findings) > 0 {
|
||||||
|
fmt.Printf("words: %d use(s) of a word the glossary retired (novox/hq ADR 0244) — say it in the glossary's word\n", len(findings))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
// And no daemon's line (novox/hq ADR 0272): what a module tells the operator to type is a `nox` line.
|
||||||
|
lines, err := readLines(filepath.Join(root, "retired-lines"))
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "words:", err)
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
if len(lines) == 0 {
|
||||||
|
fmt.Fprintln(os.Stderr, "words: retired-lines lists no line — the check would pass on anything")
|
||||||
|
os.Exit(2)
|
||||||
|
}
|
||||||
|
if findings := checkLines(lines, texts); len(findings) > 0 {
|
||||||
|
for _, f := range findings {
|
||||||
|
fmt.Println(f)
|
||||||
|
}
|
||||||
|
fmt.Printf("words: %d daemon's line(s) in what a module shows — the operator types one command, nox (novox/hq ADR 0272)\n",
|
||||||
|
len(findings))
|
||||||
|
os.Exit(1)
|
||||||
|
}
|
||||||
|
fmt.Printf("words: %d retired words and %d daemons' lines, none in the %d strings of %d files\n", len(words), len(lines),
|
||||||
|
len(texts), files)
|
||||||
}
|
}
|
||||||
|
|
||||||
// readList reads retired-words: one word or phrase a line; blank lines and lines starting with # are not words.
|
// readList reads retired-words: one word or phrase a line; blank lines and lines starting with # are not words.
|
||||||
@@ -166,22 +152,9 @@ func pattern(word string) *regexp.Regexp {
|
|||||||
|
|
||||||
func check(words []string, texts []text) []string {
|
func check(words []string, texts []text) []string {
|
||||||
var out []string
|
var out []string
|
||||||
// A text can hold a word only if it holds the word's first part, in any case: the regular expression is run on those alone,
|
|
||||||
// which keeps the check of the whole catalogue within the unit level's bound.
|
|
||||||
lower := make([]string, len(texts))
|
|
||||||
for i, t := range texts {
|
|
||||||
lower[i] = strings.ToLower(t.s)
|
|
||||||
}
|
|
||||||
for _, w := range words {
|
for _, w := range words {
|
||||||
rx := pattern(w)
|
rx := pattern(w)
|
||||||
first := ""
|
for _, t := range texts {
|
||||||
if parts := strings.Fields(w); len(parts) > 0 {
|
|
||||||
first = strings.ToLower(parts[0])
|
|
||||||
}
|
|
||||||
for i, t := range texts {
|
|
||||||
if !strings.Contains(lower[i], first) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if m := rx.FindStringSubmatchIndex(t.s); m != nil {
|
if m := rx.FindStringSubmatchIndex(t.s); m != nil {
|
||||||
line := t.line
|
line := t.line
|
||||||
if line > 0 {
|
if line > 0 {
|
||||||
@@ -206,9 +179,6 @@ func excerpt(s string, from, to int) string {
|
|||||||
return strings.Join(strings.Fields(s[a:b]), " ")
|
return strings.Join(strings.Fields(s[a:b]), " ")
|
||||||
}
|
}
|
||||||
|
|
||||||
// modulePath matches a string that is only a module or import path ("git.example.org/org/repo/go/pkg").
|
|
||||||
var modulePath = regexp.MustCompile(`^[a-z0-9.-]+\.[a-z]+/[A-Za-z0-9_./-]+$`)
|
|
||||||
|
|
||||||
// goStrings returns a Go file's string literals, adjacent ones joined across `+` as the program joins them.
|
// goStrings returns a Go file's string literals, adjacent ones joined across `+` as the program joins them.
|
||||||
func goStrings(path, rel string) ([]text, error) {
|
func goStrings(path, rel string) ([]text, error) {
|
||||||
src, err := os.ReadFile(path)
|
src, err := os.ReadFile(path)
|
||||||
@@ -234,11 +204,6 @@ func goStrings(path, rel string) ([]text, error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
v = lit
|
v = lit
|
||||||
}
|
}
|
||||||
if modulePath.MatchString(v) {
|
|
||||||
// An import path or another module path names code, not words an agent reads.
|
|
||||||
cur, joining = nil, false
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if cur != nil && joining {
|
if cur != nil && joining {
|
||||||
cur.s += v
|
cur.s += v
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
@@ -87,11 +87,3 @@ func TestRetiredLinesAreReadAsDaemonAndCommand(t *testing.T) {
|
|||||||
t.Fatal("a line with no command was read")
|
t.Fatal("a line with no command was read")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// The script's run of this check over the real tree (novox/hq ADR 0529 clause 2): no word the glossary retired, and no
|
|
||||||
// daemon's line, in any text a module's tools can show an agent.
|
|
||||||
func TestTheCatalogueHoldsNoRetiredWordAndNoDaemonsLine(t *testing.T) {
|
|
||||||
if code := run("../.."); code != 0 {
|
|
||||||
t.Fatalf("the check of the real catalogue exits %d (see what it printed above)", code)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +0,0 @@
|
|||||||
# What these tests read outside their package, as paths from the repository's root (a glob, or a directory read whole;
|
|
||||||
# ? before a path whose absence is read). mesh-build-go hashes them into the package's digest, so its verdict is kept
|
|
||||||
# until one of them changes (issue 1936). An empty list, or a line matching nothing, keeps nothing.
|
|
||||||
# run("../.."): the root's lists of retired words and lines, and every module's texts.
|
|
||||||
retired-words
|
|
||||||
retired-lines
|
|
||||||
modules
|
|
||||||
+1
-1
@@ -1,5 +1,5 @@
|
|||||||
58
|
58
|
||||||
The long-running resources of this catalogue that do not say how they are ready (`health`, novox/hq ADR 0240
|
The long-running resources of this catalogue that do not say how they are ready (`health`, novox/hq ADR 0240
|
||||||
rule 8), as `module check` counts them. It may only go down: checks/health fails a change that raises it, and
|
rule 8), as `module check` counts them. It may only go down: merge-check.sh fails a change that raises it, and
|
||||||
one that lowers it writes the new number on the first line. From 2026-11-18, or once it is 0, a long-running
|
one that lowers it writes the new number on the first line. From 2026-11-18, or once it is 0, a long-running
|
||||||
resource without `health` is refused.
|
resource without `health` is refused.
|
||||||
|
|||||||
@@ -0,0 +1,140 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
# The catalogue's TypeScript part of its own check (novox/hq issue 302's `mesh-check-also`), declared in
|
||||||
|
# merge-check.sh's header and run by the build seat in the TypeScript toolchain, after the Go part. The media
|
||||||
|
# catalogue's twin is issue 311. Before this, a TypeScript module's change ran nothing of its own before it
|
||||||
|
# merged: the Go part said "NOT TESTED HERE", and a type error in the forge module would first have shown in
|
||||||
|
# its walk.
|
||||||
|
#
|
||||||
|
# For every touched module that has a package.json, in a copy of it outside the clone:
|
||||||
|
#
|
||||||
|
# 1. its dependencies are the toolchain's own (/app/node_modules: the mesh's SDK, Node's types and what the
|
||||||
|
# runtime carries), copied beside it, never fetched: the check holds no credential for the package
|
||||||
|
# registry. A module that depends on a package the toolchain does not hold **fails the check**, by name:
|
||||||
|
# it could not be type-checked or tested, and what could not run is a failure, never a note (novox/hq
|
||||||
|
# issue 385);
|
||||||
|
# 2. every source that is not a test is type-checked with the toolchain's compiler and the flags a build
|
||||||
|
# compiles with — the same check the build makes, before the merge rather than in the walk;
|
||||||
|
# 3. its tests (test/*.test.ts) run under Node's own runner with types stripped, as its own `npm test` does
|
||||||
|
# after its build: the sources are compiled to dist/ and beside themselves first, so a test that imports
|
||||||
|
# the compiled module, or a source that imports its sibling by its .js name, finds it.
|
||||||
|
#
|
||||||
|
# Said, never passed silently: a module with no tests is said NOT TESTED on every run that touches it —
|
||||||
|
# nothing of it could fail. A module whose tests exist and could not run fails (novox/hq issue 385).
|
||||||
|
#
|
||||||
|
# By hand: MESH_CHECK_MODULES=gitea sh merge-check-typescript.sh, in the toolchain image (it reads
|
||||||
|
# /app/node_modules), or with TOOLCHAIN_MODULES pointing at a node_modules that holds the same.
|
||||||
|
#
|
||||||
|
# **In an agent's session, outside the toolchain image** (novox/hq issue 742), the same node_modules is
|
||||||
|
# assembled first, in a throwaway directory under the check's own workspace, the way the image's `deps` stage
|
||||||
|
# assembles it: the toolchain's own package.json and lock (mesh-tools' node-tools/, read from the forge this
|
||||||
|
# clone came from, so the list is never copied here) installed with `npm ci`, then the SDK the mesh last
|
||||||
|
# published (`@novox/mesh-sdk@latest`), the @novox scope from the forge's package registry, read without a
|
||||||
|
# credential. Node and npm are the dev-tools module's; nothing is installed on the machine, no npm
|
||||||
|
# configuration or cache of the account's is read or written. Only when neither TOOLCHAIN_MODULES nor
|
||||||
|
# /app/node_modules is there: the forge's check, in the image, fetches nothing, as before. The forge is
|
||||||
|
# MESH_FORGE (default: the origin remote's), the owner MESH_FORGE_OWNER (default: the origin's owner).
|
||||||
|
set -eu
|
||||||
|
|
||||||
|
work=$(mktemp -d)
|
||||||
|
trap 'rm -rf "$work"' EXIT
|
||||||
|
|
||||||
|
deps="${TOOLCHAIN_MODULES:-/app/node_modules}"
|
||||||
|
if [ -z "${TOOLCHAIN_MODULES:-}" ] && [ ! -f "$deps/typescript/bin/tsc" ]; then
|
||||||
|
origin=$(git remote get-url origin 2>/dev/null || true)
|
||||||
|
forge="${MESH_FORGE:-$(printf '%s\n' "$origin" | sed -n 's#^\(https\{0,1\}://[^/]*\)/.*#\1#p')}"
|
||||||
|
owner="${MESH_FORGE_OWNER:-$(printf '%s\n' "$origin" | sed -n 's#^https\{0,1\}://[^/]*/\([^/]*\)/.*#\1#p')}"
|
||||||
|
if [ -z "$forge" ] || [ -z "$owner" ]; then
|
||||||
|
echo "NOT CHECKED: no TypeScript toolchain at $deps, and no forge to assemble one from (origin is '$origin'; set MESH_FORGE and MESH_FORGE_OWNER, or TOOLCHAIN_MODULES)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "no TypeScript toolchain at $deps: assembling one from $forge/$owner/mesh-tools node-tools/ and $owner's package registry (novox/hq issue 742)"
|
||||||
|
tc="$work/toolchain"
|
||||||
|
mkdir -p "$tc"
|
||||||
|
printf '@%s:registry=%s/api/packages/%s/npm/\n' "$owner" "$forge" "$owner" >"$work/npmrc"
|
||||||
|
# Read with Node's own fetch (dev-tools declares Node, not curl); a status that is not 2xx fails.
|
||||||
|
if ! { node -e '
|
||||||
|
const [base, dir] = process.argv.slice(1);
|
||||||
|
Promise.all(["package.json", "package-lock.json"].map(async (f) => {
|
||||||
|
const r = await fetch(base + f);
|
||||||
|
if (!r.ok) throw new Error(base + f + ": " + r.status);
|
||||||
|
require("fs").writeFileSync(dir + "/" + f, await r.text());
|
||||||
|
})).catch((e) => { console.error(e.message); process.exit(1); });' \
|
||||||
|
"$forge/$owner/mesh-tools/raw/branch/main/node-tools/" "$tc" &&
|
||||||
|
(cd "$tc" && export npm_config_userconfig="$work/npmrc" npm_config_cache="$work/npm-cache" &&
|
||||||
|
npm ci --ignore-scripts --no-audit --no-fund --loglevel=error &&
|
||||||
|
npm install --ignore-scripts --no-audit --no-fund --no-save --loglevel=error "@$owner/mesh-sdk@latest"); }; then
|
||||||
|
echo "NOT CHECKED: the TypeScript toolchain could not be assembled from $forge (novox/hq issue 742)"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
deps="$tc/node_modules"
|
||||||
|
fi
|
||||||
|
tsc="$deps/typescript/bin/tsc"
|
||||||
|
if [ ! -f "$tsc" ]; then
|
||||||
|
echo "NOT CHECKED: no TypeScript compiler at $tsc — this part runs in the TypeScript toolchain"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
# The modules the change reaches: the planner's answer (hq ADR 0238), and every module a changed file is in —
|
||||||
|
# a change to a module's tests alone moves no build (hq ADR 0267), and its tests must still run.
|
||||||
|
touched=$({
|
||||||
|
printf '%s\n' "${MESH_CHECK_MODULES:-}" | tr ',' '\n' | sed -e 's#^modules/##' -e '/\//d'
|
||||||
|
printf '%s\n' "${MESH_CHECK_CHANGED:-}" | tr ',' '\n' | sed -n 's#^modules/\([^/]*\)/.*#\1#p'
|
||||||
|
} | sed '/^$/d' | sort -u)
|
||||||
|
|
||||||
|
failed=""
|
||||||
|
for m in $touched; do
|
||||||
|
d="modules/$m"
|
||||||
|
[ -f "$d/package.json" ] || continue
|
||||||
|
copy="$work/$m"
|
||||||
|
mkdir -p "$copy"
|
||||||
|
(cd "$d" && tar --exclude=./node_modules --exclude=./dist -cf - .) | (cd "$copy" && tar -xf -)
|
||||||
|
# What it depends on, against what the toolchain holds.
|
||||||
|
missing=$(node -e '
|
||||||
|
const p = require(process.argv[1]); const have = require("fs").readdirSync(process.argv[2]);
|
||||||
|
const scoped = (n) => n.startsWith("@") ? n.split("/")[0] : n;
|
||||||
|
const names = Object.keys({ ...(p.dependencies || {}), ...(p.devDependencies || {}) });
|
||||||
|
const held = (n) => have.includes(n) || (n.startsWith("@") && have.includes(scoped(n)) &&
|
||||||
|
require("fs").existsSync(process.argv[2] + "/" + n));
|
||||||
|
console.log(names.filter((n) => n !== "typescript" && !held(n)).join(" "));' "$copy/package.json" "$deps")
|
||||||
|
if [ -n "$missing" ]; then
|
||||||
|
echo "FAILED: $d could not be checked — it depends on $missing, which the toolchain does not hold and the check cannot fetch (novox/hq issue 385)"
|
||||||
|
failed="$failed $m"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
mkdir -p "$copy/node_modules"
|
||||||
|
for p in "$deps"/*; do
|
||||||
|
case "$(basename "$p")" in typescript | esbuild | @esbuild) continue ;; esac
|
||||||
|
cp -r "$p" "$copy/node_modules/"
|
||||||
|
done
|
||||||
|
sources=$(cd "$copy" && find . -name '*.ts' ! -name '*.d.ts' ! -path './test/*' ! -path './node_modules/*' ! -path './dist/*' | sort)
|
||||||
|
if [ -n "$sources" ]; then
|
||||||
|
echo "type-checking $d (TypeScript)"
|
||||||
|
# shellcheck disable=SC2086 # the module's sources, none with a space in its name
|
||||||
|
if ! (cd "$copy" && node "$tsc" --noEmit --module NodeNext --moduleResolution NodeNext --target ES2022 \
|
||||||
|
--types node $sources); then
|
||||||
|
failed="$failed $m"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo "$d has no TypeScript sources of its own to type-check: its tests read its files"
|
||||||
|
fi
|
||||||
|
if ! ls "$copy"/test/*.test.ts >/dev/null 2>&1; then
|
||||||
|
echo "NOT TESTED: $d is TypeScript and has no tests of its own"
|
||||||
|
continue
|
||||||
|
fi
|
||||||
|
if [ -n "$sources" ]; then
|
||||||
|
# shellcheck disable=SC2086
|
||||||
|
(cd "$copy" && node "$tsc" --module NodeNext --moduleResolution NodeNext --target ES2022 --types node \
|
||||||
|
--rootDir . --outDir dist $sources && node "$tsc" --module NodeNext --moduleResolution NodeNext \
|
||||||
|
--target ES2022 --types node --rootDir . --outDir . $sources)
|
||||||
|
fi
|
||||||
|
echo "testing $d (TypeScript)"
|
||||||
|
if ! (cd "$copy" && node --test --test-reporter=spec --experimental-strip-types \
|
||||||
|
--no-warnings=ExperimentalWarning test/*.test.ts); then
|
||||||
|
failed="$failed $m"
|
||||||
|
fi
|
||||||
|
done
|
||||||
|
if [ -n "$failed" ]; then
|
||||||
|
echo "TypeScript modules that failed:$failed"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user