diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go index b873b4c2..407f642c 100644 --- a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/main.go @@ -7,8 +7,10 @@ package main import ( "cmp" "context" + "errors" "fmt" "io" + "io/fs" "log" "maps" "os" @@ -200,13 +202,9 @@ func run(ctx context.Context, getenv func(string) string, logOut io.Writer) erro b.Numbers = build.VersionNumbers{N: numberer} // The builder's toolchain waits (design 47 G §672): its state waits, and wait-held and wait-ended on its stream waits; its // bound is its setting toolchain-wait-minutes. - waitBound := defaultToolchainWait - if v := strings.TrimSpace(getenv("MESH_BUILDER_TOOLCHAIN_WAIT_MINUTES")); v != "" { - n, err := strconv.Atoi(v) - if err != nil || n < 1 { - return fmt.Errorf("MESH_BUILDER_TOOLCHAIN_WAIT_MINUTES %q is not a number of minutes", v) - } - waitBound = time.Duration(n) * time.Minute + waitBound, err := toolchainWaitOf(getenv("MESH_BUILDER_TOOLCHAIN_WAIT_FILE")) + if err != nil { + return err } waitsBucket := process.AwaitBucket(ctx, js, conn, build.WaitsBucket, "the toolchain waits are kept in memory only, and builder_waits shows none", logger.Printf) b.Waits = newWaitsKept(waitsBucket, func(ctx context.Context, event, id string, body any) error { @@ -569,3 +567,28 @@ func (s *sourceHeads) head(_ context.Context, rev uint64, repository string) (st } return "", nil } + +// toolchainWaitOf reads the toolchain wait's setting, toolchain-wait-minutes, from the file the manifest declares with the +// content ${config:toolchain-wait-minutes} (design 47 G §672, A-71), at the builder's start: no file, or an empty one, is +// the default; a content that is no number of minutes is an error naming the file. +func toolchainWaitOf(path string) (time.Duration, error) { + if path == "" { + return defaultToolchainWait, nil + } + raw, err := os.ReadFile(path) + if errors.Is(err, fs.ErrNotExist) { + return defaultToolchainWait, nil + } + if err != nil { + return 0, err + } + v := strings.TrimSpace(string(raw)) + if v == "" { + return defaultToolchainWait, nil + } + n, err := strconv.Atoi(v) + if err != nil || n < 1 { + return 0, fmt.Errorf("%s holds %q, which is not a number of minutes", path, v) + } + return time.Duration(n) * time.Minute, nil +} diff --git a/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/toolchainwait_test.go b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/toolchainwait_test.go new file mode 100644 index 00000000..2d63b4ab --- /dev/null +++ b/modules/mesh-delivery-builder/cmd/mesh-delivery-builder/toolchainwait_test.go @@ -0,0 +1,39 @@ +package main + +import ( + "os" + "path/filepath" + "testing" + "time" +) + +// A-71: the toolchain wait's setting is read at the start from the file the manifest declares, never from the environment. +func TestToolchainWaitIsReadFromItsFile(t *testing.T) { + dir := t.TempDir() + n := 0 + file := func(content string) string { + n++ + p := filepath.Join(dir, "w"+string(rune('a'+n))) + if err := os.WriteFile(p, []byte(content), 0o600); err != nil { + t.Fatal(err) + } + return p + } + for _, c := range []struct { + name, path string + want time.Duration + bad bool + }{ + {"no file named", "", defaultToolchainWait, false}, + {"file absent", filepath.Join(dir, "absent"), defaultToolchainWait, false}, + {"empty", file(" \n"), defaultToolchainWait, false}, + {"minutes", file("15\n"), 15 * time.Minute, false}, + {"not a number", file("soon"), 0, true}, + {"zero", file("0"), 0, true}, + } { + got, err := toolchainWaitOf(c.path) + if (err != nil) != c.bad || got != c.want { + t.Errorf("%s: %v, %v; want %v, error %v", c.name, got, err, c.want, c.bad) + } + } +} diff --git a/modules/mesh-delivery-builder/manifest_test.go b/modules/mesh-delivery-builder/manifest_test.go index 835fd25d..38debdc3 100644 --- a/modules/mesh-delivery-builder/manifest_test.go +++ b/modules/mesh-delivery-builder/manifest_test.go @@ -3,6 +3,7 @@ package manifest import ( "bytes" "encoding/json" + "fmt" "io/fs" "os" "path/filepath" @@ -221,8 +222,22 @@ func TestManifestProcess(t *testing.T) { // 1607/AC1 t.Errorf("process = %v", p) } env, _ := p["env"].(map[string]any) - if len(env) != 2 || env["MESH_BROKER_FILE"] != "${dir:state}/broker" || env["MESH_BUILDER_TOOLCHAIN_WAIT_MINUTES"] != "${config:toolchain-wait-minutes}" { - t.Errorf("env = %v, want the broker file and the toolchain wait's setting (design 47 G §672)", p["env"]) + if len(env) != 2 || env["MESH_BROKER_FILE"] != "${dir:state}/broker" || env["MESH_BUILDER_TOOLCHAIN_WAIT_FILE"] != "${dir:state}/toolchain-wait-minutes" { + t.Errorf("env = %v, want the broker file and the path of the toolchain wait's file, never the setting itself (design 47 G §672, A-71)", p["env"]) + } + for k, v := range env { + if strings.Contains(fmt.Sprint(v), "${config:") { + t.Errorf("env %s = %v: a setting reaches the process as a file, never its environment (A-71)", k, v) + } + } + found := false + for _, r := range m.Resources { + if r["type"] == "file" && r["path"] == "${dir:state}/toolchain-wait-minutes" && r["content"] == "${config:toolchain-wait-minutes}" { + found = true + } + } + if !found { + t.Errorf("no file resource with the content ${config:toolchain-wait-minutes}: A-71") } for _, r := range m.Resources { if r["id"] == "settings" { diff --git a/modules/mesh-delivery-builder/module.json b/modules/mesh-delivery-builder/module.json index e43d5995..8e65cc39 100644 --- a/modules/mesh-delivery-builder/module.json +++ b/modules/mesh-delivery-builder/module.json @@ -165,6 +165,14 @@ "place": ".", "unknown": "program" }, + { + "id": "toolchain-wait", + "type": "file", + "path": "${dir:state}/toolchain-wait-minutes", + "owner": "mesh-delivery-builder", + "mode": "0600", + "content": "${config:toolchain-wait-minutes}" + }, { "id": "builder", "type": "process", @@ -175,7 +183,7 @@ ], "env": { "MESH_BROKER_FILE": "${dir:state}/broker", - "MESH_BUILDER_TOOLCHAIN_WAIT_MINUTES": "${config:toolchain-wait-minutes}" + "MESH_BUILDER_TOOLCHAIN_WAIT_FILE": "${dir:state}/toolchain-wait-minutes" }, "user": "mesh-delivery-builder", "health": {