From 138a1e1c4baa772a18c03ba2c5772d7bb4877e20 Mon Sep 17 00:00:00 2001 From: jochen Date: Thu, 22 Oct 2026 14:15:53 +0200 Subject: [PATCH] issue 1838: refuse a node name of this mesh in the pacman and kernel modules A module is for any mesh; the check reads the list of names at the root and the covered modules, and the kernel module's fixtures now say what they show. --- checks/particulars/covered | 3 + checks/particulars/go.mod | 3 + checks/particulars/main.go | 146 ++++++++++++++++++ checks/particulars/main_test.go | 65 ++++++++ merge-check.sh | 3 + .../kernel/cmd/kernel-tools/hugepages_test.go | 10 +- .../kernel/cmd/kernel-tools/manifest_test.go | 2 +- particulars | 5 + 8 files changed, 231 insertions(+), 6 deletions(-) create mode 100644 checks/particulars/covered create mode 100644 checks/particulars/go.mod create mode 100644 checks/particulars/main.go create mode 100644 checks/particulars/main_test.go create mode 100644 particulars diff --git a/checks/particulars/covered b/checks/particulars/covered new file mode 100644 index 00000000..9ded26c5 --- /dev/null +++ b/checks/particulars/covered @@ -0,0 +1,3 @@ +# The modules the check covers (novox/hq ADR 0507 §9, issue 1838), one a line; issue 1838 widens it. +pacman +kernel diff --git a/checks/particulars/go.mod b/checks/particulars/go.mod new file mode 100644 index 00000000..310d5ebe --- /dev/null +++ b/checks/particulars/go.mod @@ -0,0 +1,3 @@ +module git.novox.be/novox/mesh-catalog/checks/particulars + +go 1.22 diff --git a/checks/particulars/main.go b/checks/particulars/main.go new file mode 100644 index 00000000..f20862b2 --- /dev/null +++ b/checks/particulars/main.go @@ -0,0 +1,146 @@ +// 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 +// fixture of a covered module names a node of this one. +// +// The names are in the file `particulars` at the catalogue's root, one a line, `#` comments — the one place +// they may be written. The modules covered are in `covered` beside this file, one a line. A name is matched +// whole and in any case; a hyphen joins a word, an underscore or a 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/) are blanked +// before a line is read. +// +// go run . +// +// Exits 1 naming file:line and the word, 2 when a list cannot be read. +package main + +import ( + "bufio" + "bytes" + "fmt" + "io/fs" + "os" + "path/filepath" + "regexp" + "sort" + "strings" +) + +var forge = regexp.MustCompile(`(?i)git\.novox\.be|\bnovox/[\w.-]+`) + +func main() { + root := "." + if len(os.Args) > 1 { + root = os.Args[1] + } + names, err := readList(filepath.Join(root, "particulars")) + if err == nil && len(names) == 0 { + err = fmt.Errorf("particulars lists no name — the check would pass on anything") + } + var covered []string + if err == nil { + covered, err = readList("covered") + if err == nil && len(covered) == 0 { + err = fmt.Errorf("covered lists no module — the check would pass on anything") + } + } + if err != nil { + fmt.Fprintln(os.Stderr, "particulars:", err) + os.Exit(2) + } + findings, err := run(root, names, covered) + if err != nil { + fmt.Fprintln(os.Stderr, "particulars:", err) + os.Exit(2) + } + for _, f := range findings { + fmt.Println(f) + } + if len(findings) > 0 { + 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)) + os.Exit(1) + } + fmt.Printf("particulars: none of %d node names in the %d covered modules\n", len(names), len(covered)) +} + +// readList reads a list: one entry a line; blank lines and lines starting with # are not entries. +func readList(path string) ([]string, error) { + f, err := os.Open(path) + if err != nil { + return nil, err + } + defer f.Close() + var out []string + sc := bufio.NewScanner(f) + for sc.Scan() { + line := strings.TrimSpace(sc.Text()) + if line == "" || strings.HasPrefix(line, "#") { + continue + } + out = append(out, line) + } + return out, sc.Err() +} + +// run reads every text file of each covered module and returns the findings, sorted. +func run(root string, names, covered []string) ([]string, error) { + var out []string + for _, m := range covered { + dir := filepath.Join(root, "modules", m) + err := filepath.WalkDir(dir, func(path string, d fs.DirEntry, err error) error { + if err != nil { + return err + } + if d.IsDir() { + switch d.Name() { + case "node_modules", "dist", "vendor", ".git": + return filepath.SkipDir + } + return nil + } + rel, _ := filepath.Rel(root, path) + // A path is read too: a testdata directory named after a node is a finding. + out = append(out, scan(rel+" (name)", filepath.ToSlash(rel), names)...) + if d.Name() == "go.sum" { + return nil + } + b, err := os.ReadFile(path) + if err != nil { + return err + } + if bytes.IndexByte(b, 0) >= 0 { + return nil + } + out = append(out, scan(filepath.ToSlash(rel), string(b), names)...) + return nil + }) + if err != nil { + return nil, err + } + } + for i, f := range out { + out[i] = strings.Replace(f, " (name):", ":0:", 1) + } + sort.Strings(out) + return out, nil +} + +// scan returns a finding for each node name in src, whole-word and in any case. +func scan(file, src string, names []string) []string { + var out []string + var rxs []*regexp.Regexp + for _, n := range names { + // Whole word, underscore a separator, hyphen a joiner; and a CamelCase part of an identifier (TestAce). + q := regexp.QuoteMeta(n) + camel := regexp.QuoteMeta(strings.ToUpper(n[:1]) + n[1:]) + rxs = append(rxs, regexp.MustCompile(`(?i:(?:^|[^A-Za-z0-9-])`+q+`(?:$|[^A-Za-z0-9-]))|[a-z0-9]`+camel+`(?:$|[^a-z])`)) + } + for i, line := range strings.Split(src, "\n") { + line = forge.ReplaceAllStringFunc(line, func(s string) string { return strings.Repeat(" ", len(s)) }) + for j, rx := range rxs { + if rx.MatchString(line) { + out = append(out, fmt.Sprintf("%s:%d: node name %q — %s", file, i+1, strings.ToLower(names[j]), strings.Join(strings.Fields(line), " "))) + } + } + } + return out +} diff --git a/checks/particulars/main_test.go b/checks/particulars/main_test.go new file mode 100644 index 00000000..6312cf36 --- /dev/null +++ b/checks/particulars/main_test.go @@ -0,0 +1,65 @@ +package main + +import ( + "os" + "path/filepath" + "strings" + "testing" +) + +var names = []string{"novox", "ace", "shanks", "g14"} + +func TestANodeNameIsRefusedWhereItIsWritten(t *testing.T) { + cases := map[string]struct{ file, src string }{ + "manifest string": {"modules/kernel/module.json", "{\n \"description\": \"as on Shanks\"\n}\n"}, + "file line": {"modules/kernel/files/boot.sh", "#!/bin/sh\necho run on g14\n"}, + "go identifier": {"modules/kernel/cmd/x/a.go", "package main\n\nfunc novox() {}\n"}, + "test name": {"modules/kernel/cmd/x/a_test.go", "package main\n\nfunc TestAce(t *testing.T) {}\n\nfunc TestOnAce_x(t *testing.T) {}\n"}, + } + for name, c := range cases { + if got := scan(c.file, c.src, names); len(got) == 0 { + t.Errorf("%s: %q not refused", name, c.src) + } + } +} + +func TestTheFindingNamesTheLineAndTheWord(t *testing.T) { + got := scan("f.sh", "a\nb\nrun on ACE now\n", names) + if len(got) != 1 || !strings.HasPrefix(got[0], "f.sh:3:") || !strings.Contains(got[0], "ace") { + t.Errorf("got %v", got) + } +} + +func TestTheForgeIsNotANodeName(t *testing.T) { + for _, s := range []string{ + "see novox/hq issue 5", + "module git.novox.be/novox/mesh-sdk/go v1.2.3", + "https://git.novox.be/novox/mesh-catalog", + "space, trace, surface, grace, ace-of, mesh-novox, g14x", + } { + if got := scan("f", s, names); len(got) != 0 && !strings.Contains(s, "ace-of") { + t.Errorf("%q refused: %v", s, got) + } + } + if got := scan("f", "space trace surface grace g14x", names); len(got) != 0 { + t.Errorf("longer words refused: %v", got) + } +} + +func TestOnlyCoveredModulesAreRead(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("modules/kernel/files/a", "on novox\n") + write("modules/other/files/a", "on novox\n") + got, err := run(root, names, []string{"kernel"}) + if err != nil { + t.Fatal(err) + } + if len(got) != 1 || !strings.Contains(got[0], "modules/kernel/files/a:1") { + t.Errorf("got %v", got) + } +} diff --git a/merge-check.sh b/merge-check.sh index fb1d9b8b..3fe55bd1 100755 --- a/merge-check.sh +++ b/merge-check.sh @@ -55,6 +55,9 @@ set -eu (cd checks/words && go test -count=1 . && go run . ../..) +# Node names (novox/hq ADR 0507 §9, issue 1838): no node name of this mesh in a covered module; the names are in +# `particulars`, the covered modules in checks/particulars/covered. +(cd checks/particulars && go test -count=1 . && go run . ../..) # The toolchains (novox/hq ADR 0477 clause 4): the package go only in modules/go, nodejs and npm only in modules/nodejs. (cd checks/toolchains && go test -count=1 . && go run . ../..) # The image builder (novox/hq ADR 0487 §3): only modules/buildah provides rootless-image-build, docker never. diff --git a/modules/kernel/cmd/kernel-tools/hugepages_test.go b/modules/kernel/cmd/kernel-tools/hugepages_test.go index 2d6359c7..4467b628 100644 --- a/modules/kernel/cmd/kernel-tools/hugepages_test.go +++ b/modules/kernel/cmd/kernel-tools/hugepages_test.go @@ -1,6 +1,6 @@ package main -// Over a stand-in tree holding novox's own files of 2026-10-14 23:25 (read through node-reads for +// Over a stand-in tree holding the files of a real machine of 2026-10-14 23:25 (read through node-reads for // novox/hq issue 746): the mode, the per-size controls, meminfo's huge page lines, and fail2ban's and // journald's memory.stat and io.stat, cut to the lines the tool reads. @@ -26,7 +26,7 @@ func tree(t *testing.T, files map[string]string) *Machine { return &Machine{Root: root} } -func novox(t *testing.T) *Machine { +func capturedMachine(t *testing.T) *Machine { thp := "sys/kernel/mm/transparent_hugepage/" cg := "sys/fs/cgroup/system.slice/" files := map[string]string{ @@ -53,8 +53,8 @@ func novox(t *testing.T) *Machine { return tree(t, files) } -func TestNovoxBeforeTheChange(t *testing.T) { - a, err := novox(t).HugePages(10) +func TestTheCapturedMachineBeforeTheChange(t *testing.T) { + a, err := capturedMachine(t).HugePages(10) if err != nil { t.Fatal(err) } @@ -86,7 +86,7 @@ func TestNovoxBeforeTheChange(t *testing.T) { } func TestUnitsAreBoundedAndNoneIsSaid(t *testing.T) { - a, err := novox(t).HugePages(1) + a, err := capturedMachine(t).HugePages(1) if err != nil || len(a.Units) != 1 { t.Fatalf("%v %+v", err, a) } diff --git a/modules/kernel/cmd/kernel-tools/manifest_test.go b/modules/kernel/cmd/kernel-tools/manifest_test.go index b68dacf5..a3594f35 100644 --- a/modules/kernel/cmd/kernel-tools/manifest_test.go +++ b/modules/kernel/cmd/kernel-tools/manifest_test.go @@ -129,7 +129,7 @@ func (b bed) file(t *testing.T, p string) string { } func TestApplySetsTheModeRotatesTheJournalOnceAndRestoreGivesBackWhatWasFound(t *testing.T) { - // novox's file on 2026-10-14, read through node-reads. + // a real machine's file on 2026-10-14, read through node-reads. b := newBed(t, "madvise", "[always] madvise never\n") out, err := b.run(t, "apply") if err != nil || !strings.Contains(out, "always -> madvise") { diff --git a/particulars b/particulars new file mode 100644 index 00000000..b2ab98b1 --- /dev/null +++ b/particulars @@ -0,0 +1,5 @@ +# This mesh's node names (novox/hq ADR 0507 §9, issue 1838): the one place they may be written. +novox +ace +shanks +g14