Merge pull request 'Refuse a node name of the mesh in the pacman and kernel modules (issue 1838)' (#441) from fix/1838-particulars into main
This commit was merged in pull request #441.
This commit is contained in:
@@ -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
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
module git.novox.be/novox/mesh-catalog/checks/particulars
|
||||||
|
|
||||||
|
go 1.22
|
||||||
@@ -0,0 +1,159 @@
|
|||||||
|
// 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` at checks/particulars/covered, one a line. A name is matched
|
||||||
|
// 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.
|
||||||
|
// The forge is not a node name: its host (git.novox.be) and the organisation path (novox/<repo>) are blanked
|
||||||
|
// before a line is read.
|
||||||
|
//
|
||||||
|
// go run . <catalogue root>
|
||||||
|
//
|
||||||
|
// 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"
|
||||||
|
)
|
||||||
|
|
||||||
|
// forge is the forge's host and organisation path, blanked before a line is read; a path segment novox/ elsewhere
|
||||||
|
// (testdata/novox/x, /srv/novox/data) is not blanked.
|
||||||
|
var forge = regexp.MustCompile(`(?i)git\.novox\.be(?:/novox/[\w.-]+)?|(?:^|[^/\w.-])novox/[\w.-]+`)
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
root := "."
|
||||||
|
if len(os.Args) > 1 {
|
||||||
|
root = os.Args[1]
|
||||||
|
}
|
||||||
|
os.Exit(check(root))
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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) int {
|
||||||
|
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(filepath.Join(root, "checks/particulars/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)
|
||||||
|
return 2
|
||||||
|
}
|
||||||
|
findings, err := run(root, names, covered)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Fprintln(os.Stderr, "particulars:", err)
|
||||||
|
return 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))
|
||||||
|
return 1
|
||||||
|
}
|
||||||
|
fmt.Printf("particulars: none of %d node names in the %d covered modules\n", len(names), len(covered))
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
for _, n := range hits(filepath.ToSlash(rel), names) {
|
||||||
|
out = append(out, fmt.Sprintf("%s:0: node name %q in the path", filepath.ToSlash(rel), n))
|
||||||
|
}
|
||||||
|
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
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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
|
||||||
|
for i, line := range strings.Split(src, "\n") {
|
||||||
|
for _, n := range hits(line, names) {
|
||||||
|
out = append(out, fmt.Sprintf("%s:%d: node name %q — %s", file, i+1, n, strings.Join(strings.Fields(line), " ")))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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.
|
||||||
|
func hits(line string, names []string) []string {
|
||||||
|
line = forge.ReplaceAllStringFunc(line, func(s string) string { return strings.Repeat(" ", len(s)) })
|
||||||
|
var out []string
|
||||||
|
for _, n := range names {
|
||||||
|
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]`)
|
||||||
|
if rx.MatchString(line) {
|
||||||
|
out = append(out, strings.ToLower(n))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return out
|
||||||
|
}
|
||||||
@@ -0,0 +1,116 @@
|
|||||||
|
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",
|
||||||
|
"git.novox.be",
|
||||||
|
"space trace surface grace g14x",
|
||||||
|
} {
|
||||||
|
if got := scan("f", s, names); len(got) != 0 {
|
||||||
|
t.Errorf("%q refused: %v", s, got)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestANodeNameInAPathOrJoinedIsRefused(t *testing.T) {
|
||||||
|
for _, s := range []string{
|
||||||
|
"modules/kernel/testdata/novox/meminfo",
|
||||||
|
"/srv/novox/data",
|
||||||
|
"testdata/shanks-meminfo.txt",
|
||||||
|
"on-shanks",
|
||||||
|
"shanks-desktop",
|
||||||
|
"mesh-novox",
|
||||||
|
"func novoxTree() {}",
|
||||||
|
"aceFiles := 1",
|
||||||
|
"g14Meminfo := 2",
|
||||||
|
"novox_tree := 3",
|
||||||
|
} {
|
||||||
|
if got := scan("f", s, names); len(got) == 0 {
|
||||||
|
t.Errorf("%q not refused", s)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestANodeNamedPathIsRefusedThroughRun(t *testing.T) {
|
||||||
|
root := t.TempDir()
|
||||||
|
p := filepath.Join(root, "modules/kernel/testdata/ace/x")
|
||||||
|
os.MkdirAll(filepath.Dir(p), 0o755)
|
||||||
|
os.WriteFile(p, []byte("clean\n"), 0o644)
|
||||||
|
got, err := run(root, names, []string{"kernel"})
|
||||||
|
if err != nil || len(got) != 1 || !strings.HasPrefix(got[0], "modules/kernel/testdata/ace/x:0: ") {
|
||||||
|
t.Errorf("got %v, %v", got, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestCheckReturnsOneOnAFindingAndTwoOnAnUnreadableList(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\n")
|
||||||
|
write("checks/particulars/covered", "kernel\n")
|
||||||
|
write("modules/kernel/a", "clean\n")
|
||||||
|
if got := check(root); got != 0 {
|
||||||
|
t.Errorf("clean = %d, want 0", got)
|
||||||
|
}
|
||||||
|
write("modules/kernel/a", "on ace\n")
|
||||||
|
if got := check(root); got != 1 {
|
||||||
|
t.Errorf("finding = %d, want 1", got)
|
||||||
|
}
|
||||||
|
os.Remove(filepath.Join(root, "particulars"))
|
||||||
|
if got := check(root); got != 2 {
|
||||||
|
t.Errorf("no list = %d, want 2", 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -55,6 +55,9 @@
|
|||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
(cd checks/words && go test -count=1 . && go run . ../..)
|
(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.
|
# 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 . ../..)
|
(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.
|
# The image builder (novox/hq ADR 0487 §3): only modules/buildah provides rootless-image-build, docker never.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
package main
|
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
|
// 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.
|
// 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}
|
return &Machine{Root: root}
|
||||||
}
|
}
|
||||||
|
|
||||||
func novox(t *testing.T) *Machine {
|
func capturedMachine(t *testing.T) *Machine {
|
||||||
thp := "sys/kernel/mm/transparent_hugepage/"
|
thp := "sys/kernel/mm/transparent_hugepage/"
|
||||||
cg := "sys/fs/cgroup/system.slice/"
|
cg := "sys/fs/cgroup/system.slice/"
|
||||||
files := map[string]string{
|
files := map[string]string{
|
||||||
@@ -53,8 +53,8 @@ func novox(t *testing.T) *Machine {
|
|||||||
return tree(t, files)
|
return tree(t, files)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestNovoxBeforeTheChange(t *testing.T) {
|
func TestTheCapturedMachineBeforeTheChange(t *testing.T) {
|
||||||
a, err := novox(t).HugePages(10)
|
a, err := capturedMachine(t).HugePages(10)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatal(err)
|
t.Fatal(err)
|
||||||
}
|
}
|
||||||
@@ -86,7 +86,7 @@ func TestNovoxBeforeTheChange(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestUnitsAreBoundedAndNoneIsSaid(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 {
|
if err != nil || len(a.Units) != 1 {
|
||||||
t.Fatalf("%v %+v", err, a)
|
t.Fatalf("%v %+v", err, a)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -129,7 +129,7 @@ func (b bed) file(t *testing.T, p string) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestApplySetsTheModeRotatesTheJournalOnceAndRestoreGivesBackWhatWasFound(t *testing.T) {
|
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")
|
b := newBed(t, "madvise", "[always] madvise never\n")
|
||||||
out, err := b.run(t, "apply")
|
out, err := b.run(t, "apply")
|
||||||
if err != nil || !strings.Contains(out, "always -> madvise") {
|
if err != nil || !strings.Contains(out, "always -> madvise") {
|
||||||
|
|||||||
@@ -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
|
||||||
Reference in New Issue
Block a user