diff --git a/checks/noplacement/main.go b/checks/noplacement/main.go index e7af4e27..3de240c4 100644 --- a/checks/noplacement/main.go +++ b/checks/noplacement/main.go @@ -1,11 +1,13 @@ -// Command noplacement refuses what the repository path of placement left behind (novox/hq ADR 0567 §1, §8, §10). +// 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 manifest naming a state, stream, event or bucket of the indexer's placement (`mesh-delivery-indexer.placement…` +// 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`. // // go run . @@ -29,7 +31,10 @@ var Prefixes = []string{"mesh-delivery-indexer.placement", "mesh-delivery-indexe 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 §1)") + 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 { @@ -47,7 +52,7 @@ func Findings(root string) ([]string, error) { } 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 §8)", module, s)) + 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)) } } return out, nil diff --git a/checks/noplacement/main_test.go b/checks/noplacement/main_test.go index db3caf85..0ffc320f 100644 --- a/checks/noplacement/main_test.go +++ b/checks/noplacement/main_test.go @@ -15,6 +15,7 @@ func TestFindingsRefuseWhatThePlacementPathLeft(t *testing.T) { }{ {"clean", map[string]string{"modules/a/module.json": `{"module":"a","reads":["mesh-controller.assignments"]}`}, ""}, {"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"}, diff --git a/checks/particulars/main.go b/checks/particulars/main.go index 42525c96..e2f6d32e 100644 --- a/checks/particulars/main.go +++ b/checks/particulars/main.go @@ -3,7 +3,7 @@ // fixture of a covered 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 -// MESH_NODE_NAMES, comma-separated, from the node records it reads (novox/hq ADR 0567 §11). A run with none fails +// MESH_NODE_NAMES, comma-separated, from the node records it reads (novox/hq ADR 0567 §16). A run with none fails // loudly, since it would pass on anything. 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/) are blanked diff --git a/modules/mesh-build-go/cmd/mesh-build-go/asks_test.go b/modules/mesh-build-go/cmd/mesh-build-go/asks_test.go index 7759b452..d9efe887 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/asks_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/asks_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "regexp" "slices" "strings" "testing" @@ -126,7 +127,8 @@ func TestManifestClaimsTheGoBuildSeatAndReadsItsCapacity(t *testing.T) { raw, _ := os.ReadFile(moduleDir + "/module.json") var doc struct { Config map[string]struct { - Default json.RawMessage `json:"default"` + Default json.RawMessage `json:"default"` + Pattern, Description string } `json:"config"` } _ = json.Unmarshal(raw, &doc) @@ -144,6 +146,16 @@ func TestManifestClaimsTheGoBuildSeatAndReadsItsCapacity(t *testing.T) { if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" { t.Errorf("the settings template holds build hours %v (%s)", w, fault) } + // ADR 0567 §15: build-hours names no node (one window, or empty for any hour) and capacity defaults to 0. + window := regexp.MustCompile("^(?:" + doc.Config[buildstep.BuildHoursSetting].Pattern + ")$") + for value, ok := range map[string]bool{"": true, "18:00-06:00": true, "g14=18:00-06:00": false, "18:00": false} { + if window.MatchString(value) != ok { + t.Errorf("build-hours pattern on %q: accepted %v, want %v", value, !ok, ok) + } + } + if strings.Contains(doc.Config[buildstep.BuildHoursSetting].Description, "=") { + t.Errorf("the build-hours description names a node pair") + } var capacity float64 _ = json.Unmarshal(doc.Config[buildstep.CapacitySetting].Default, &capacity) if capacity != 0 { diff --git a/modules/mesh-build-go/cmd/mesh-build-go/digest.go b/modules/mesh-build-go/cmd/mesh-build-go/digest.go index dc6b4858..c3494d08 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/digest.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/digest.go @@ -38,7 +38,7 @@ const DigestVersion = "6" const besideVariable = "MESH_CHECK_BESIDE" // namesVariable is the same for the mesh's node names: a package whose tests reach a file naming it depends on the node -// list the ask carries, so its digest holds the sorted names and a new node misses a kept verdict (ADR 0567 §12). +// list the ask carries, so its digest holds the sorted names and a new node misses a kept verdict (ADR 0567 §15). const namesVariable = "MESH_NODE_NAMES" // ReadsOutsideMarker is the name of a file in a package's directory that says its tests read files outside it (a diff --git a/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go b/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go index 49d3f9a1..0a449589 100644 --- a/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go +++ b/modules/mesh-build-go/cmd/mesh-build-go/digest_test.go @@ -189,7 +189,7 @@ func TestAModuleOutsideTheTreeCountsByVersionAndChecksum(t *testing.T) { if d3 := digest(); d3 == d1 { t.Errorf("a new version left the digest as it was") } - // A different node list misses the cache only for a package whose tests read MESH_NODE_NAMES (ADR 0567 §12). + // A different node list misses the cache only for a package whose tests read MESH_NODE_NAMES (ADR 0567 §15). names = []string{"node-names a"} d4 := digest() names = []string{"node-names a", "node-names b"} diff --git a/modules/mesh-build-go/module.json b/modules/mesh-build-go/module.json index 6c72f508..88fbba9b 100644 --- a/modules/mesh-build-go/module.json +++ b/modules/mesh-build-go/module.json @@ -33,7 +33,7 @@ "build-hours": { "type": "string", "description": "When this node may take this module's build asks: one window HH:MM-HH:MM in the node's local time (an end before its start crosses midnight, 18:00-06:00). Outside it the node takes no new step and finishes the ones it runs; an empty value builds at any hour. It is set per node in the mesh's configuration.", - "pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*", + "pattern": "|([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]", "default": "" }, "cores-per-step": { diff --git a/modules/mesh-build-image/cmd/mesh-build-image/manifest_test.go b/modules/mesh-build-image/cmd/mesh-build-image/manifest_test.go index 9b424a22..b6da5a0a 100644 --- a/modules/mesh-build-image/cmd/mesh-build-image/manifest_test.go +++ b/modules/mesh-build-image/cmd/mesh-build-image/manifest_test.go @@ -1,8 +1,10 @@ package main import ( + "encoding/json" "os" "path/filepath" + "regexp" "slices" "strings" "testing" @@ -121,6 +123,30 @@ func TestManifestKeepsItsStatesClaimsItsBuildSeatAndDeclaresItsHealthTool(t *tes if strings.TrimSpace(string(tpl)) != "${seat:mesh-artifact-store:reach}" { t.Errorf("registry template = %q", tpl) } + raw, _ = os.ReadFile(filepath.Join(moduleDir, "module.json")) + var doc struct { + Config map[string]struct { + Default json.RawMessage `json:"default"` + Pattern, Description string + } `json:"config"` + } + _ = json.Unmarshal(raw, &doc) + var capacity float64 = -1 + _ = json.Unmarshal(doc.Config["capacity"].Default, &capacity) + if capacity != 0 { + t.Errorf("the capacity's default = %g, want 0", capacity) + } + // ADR 0567 §15: build-hours names no node (one window, or empty for any hour) and capacity defaults to 0. + window := regexp.MustCompile("^(?:" + doc.Config["build-hours"].Pattern + ")$") + for value, ok := range map[string]bool{"": true, "18:00-06:00": true, "g14=18:00-06:00": false, "18:00": false} { + if window.MatchString(value) != ok { + t.Errorf("build-hours pattern on %q: accepted %v, want %v", value, !ok, ok) + } + } + if strings.Contains(doc.Config["build-hours"].Description, "=") { + t.Errorf("the build-hours description names a node pair") + } + } // Every tool the manifest names is served, and every tool served is named. diff --git a/modules/mesh-build-image/module.json b/modules/mesh-build-image/module.json index 3e5321ff..54bc2fb0 100644 --- a/modules/mesh-build-image/module.json +++ b/modules/mesh-build-image/module.json @@ -28,7 +28,7 @@ "build-hours": { "type": "string", "description": "When this node may take this module's build asks: one window HH:MM-HH:MM in the node's local time (an end before its start crosses midnight, 18:00-06:00). Outside it the node takes no new step and finishes the ones it runs; an empty value builds at any hour. It is set per node in the mesh's configuration.", - "pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*", + "pattern": "|([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]", "default": "" }, "cores-per-step": { diff --git a/modules/mesh-build-node/cmd/mesh-build-node/asks_test.go b/modules/mesh-build-node/cmd/mesh-build-node/asks_test.go index 5a2f0172..75a03f2e 100644 --- a/modules/mesh-build-node/cmd/mesh-build-node/asks_test.go +++ b/modules/mesh-build-node/cmd/mesh-build-node/asks_test.go @@ -5,6 +5,7 @@ import ( "encoding/json" "fmt" "os" + "regexp" "strings" "testing" "time" @@ -84,7 +85,8 @@ func TestManifestTakesTheBuildCapacity(t *testing.T) { raw, _ := os.ReadFile(moduleDir + "/module.json") var doc struct { Config map[string]struct { - Default json.RawMessage `json:"default"` + Default json.RawMessage `json:"default"` + Pattern, Description string } `json:"config"` } _ = json.Unmarshal(raw, &doc) @@ -102,6 +104,16 @@ func TestManifestTakesTheBuildCapacity(t *testing.T) { if w, fault := buildstep.NodeBuildHours(moduleDir+"/files/settings.json", "g14", t.Errorf)(); w != nil || fault != "" { t.Errorf("the settings template holds build hours %v (%s)", w, fault) } + // ADR 0567 §15: build-hours names no node (one window, or empty for any hour) and capacity defaults to 0. + window := regexp.MustCompile("^(?:" + doc.Config[buildstep.BuildHoursSetting].Pattern + ")$") + for value, ok := range map[string]bool{"": true, "18:00-06:00": true, "g14=18:00-06:00": false, "18:00": false} { + if window.MatchString(value) != ok { + t.Errorf("build-hours pattern on %q: accepted %v, want %v", value, !ok, ok) + } + } + if strings.Contains(doc.Config[buildstep.BuildHoursSetting].Description, "=") { + t.Errorf("the build-hours description names a node pair") + } var capacity float64 _ = json.Unmarshal(doc.Config[buildstep.CapacitySetting].Default, &capacity) if capacity != 0 { diff --git a/modules/mesh-build-node/module.json b/modules/mesh-build-node/module.json index bbd38287..25f8611b 100644 --- a/modules/mesh-build-node/module.json +++ b/modules/mesh-build-node/module.json @@ -29,7 +29,7 @@ "build-hours": { "type": "string", "description": "When this node may take this module's build asks: one window HH:MM-HH:MM in the node's local time (an end before its start crosses midnight, 18:00-06:00). Outside it the node takes no new step and finishes the ones it runs; an empty value builds at any hour. It is set per node in the mesh's configuration.", - "pattern": "|[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9](,[a-z0-9_-]+=([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9])*", + "pattern": "|([01][0-9]|2[0-3]):[0-5][0-9]-([01][0-9]|2[0-3]):[0-5][0-9]", "default": "" }, "cores-per-step": {