| index c39cfe04d..23f688095 100644 |
| --- a/internal/release/program_test.go |
| +++ b/internal/release/program_test.go |
| @@ -2,12 +2,14 @@ package release |
| |
| import ( |
| "context" |
| + "encoding/json" |
| "errors" |
| "os" |
| "os/exec" |
| "path/filepath" |
| "strings" |
| "testing" |
| + "time" |
| ) |
| |
| // tinyProgram builds a stripped linux/arm64 Go binary whose main package is |
| @@ -96,6 +98,140 @@ func TestCheckShipProgram(t *testing.T) { |
| } |
| } |
| |
| +// sameProgram is b with one byte appended: different bytes (a different |
| +// sha256, so a different release) whose build info still names the same |
| +// program. |
| +func sameProgram(b []byte) []byte { |
| + return append(b[:len(b):len(b)], 0) |
| +} |
| + |
| +// Ruling G1: --program-change is recorded whenever it is given, even when the |
| +// newest release already has the same program. Ship A, then B1 with the flag, |
| +// then B2 with the flag: B2 must carry the consent, because a box that missed |
| +// B1 is still on A and decides by its own history. |
| +func TestCheckShipProgramRecordsTheFlagWhenThePredecessorAgrees(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + a, b1 := tinyProgram(t, "example.com/console"), tinyProgram(t, "example.com/carlos") |
| + b2 := sameProgram(b1) |
| + if err := shipWithProgram(t, s, "va", a, false); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := shipWithProgram(t, s, "vb1", b1, true); err != nil { |
| + t.Fatal(err) |
| + } |
| + if err := shipWithProgram(t, s, "vb2", b2, true); err != nil { |
| + t.Fatal(err) |
| + } |
| + m, err := GetManifest(context.Background(), s, "bab", "console", "vb2") |
| + if err != nil || m.Program != "example.com/carlos" || !m.ProgramChange { |
| + t.Fatalf("vb2 manifest = %+v, %v; want program_change true though vb1 already names the program", m, err) |
| + } |
| + // And without the flag a same-program ship records no change. |
| + if err := shipWithProgram(t, s, "vb3", sameProgram(b2), false); err != nil { |
| + t.Fatal(err) |
| + } |
| + if m, err := GetManifest(context.Background(), s, "bab", "console", "vb3"); err != nil || m.ProgramChange { |
| + t.Fatalf("vb3 manifest = %+v, %v; want no program_change", m, err) |
| + } |
| +} |
| + |
| +// Every Go artifact of a release agreeing on one program passes, and the |
| +// manifest names it. |
| +func TestCheckShipProgramAgreeingArtifactsPass(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + arm := tinyProgram(t, "example.com/console") |
| + files := map[string][]byte{"linux-arm64": arm, "linux-amd64": sameProgram(arm)} |
| + m := Manifest{Schema: 1, App: "console", Version: "v1", Kind: "binary", |
| + Artifacts: map[string]Artifact{"linux-arm64": {}, "linux-amd64": {}}} |
| + if err := CheckShipProgram(context.Background(), s, "bab", &m, files, false); err != nil { |
| + t.Fatalf("agreeing artifacts: %v", err) |
| + } |
| + if m.Program != "example.com/console" { |
| + t.Fatalf("program = %q, want example.com/console", m.Program) |
| + } |
| +} |
| + |
| +// A static release is never judged or stamped, whatever its bytes are. |
| +func TestCheckShipProgramSkipsStatic(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + if err := shipWithProgram(t, s, "v1", tinyProgram(t, "example.com/console"), false); err != nil { |
| + t.Fatal(err) |
| + } |
| + files := map[string][]byte{"site.tar.gz": tinyProgram(t, "example.com/carlos")} |
| + m := Manifest{Schema: 1, App: "console", Version: "v2", Kind: "static", |
| + Artifacts: map[string]Artifact{"site.tar.gz": {}}} |
| + if err := CheckShipProgram(context.Background(), s, "bab", &m, files, false); err != nil { |
| + t.Fatalf("static release judged: %v", err) |
| + } |
| + if m.Program != "" || m.ProgramChange { |
| + t.Fatalf("static manifest stamped: program %q change %v", m.Program, m.ProgramChange) |
| + } |
| +} |
| + |
| +// putManifest writes a raw manifest for app console under account bab. |
| +func putManifest(t *testing.T, s Store, ver string, m Manifest) { |
| + t.Helper() |
| + raw, err := json.Marshal(m) |
| + if err != nil { |
| + t.Fatal(err) |
| + } |
| + if _, err := s.Put(context.Background(), ManifestKey("bab", "console", ver), raw, ""); err != nil { |
| + t.Fatal(err) |
| + } |
| +} |
| + |
| +// PreviousProgram picks by built_at, not by the order the store lists |
| +// versions in. "z-new" was built last, but its manifest object is the older |
| +// one (a re-upload, a copy between buckets), so the store's own ranking — |
| +// newest object first, ties by version — lists "a-old" first. |
| +func TestPreviousProgramOrdersByBuiltAt(t *testing.T) { |
| + root := t.TempDir() |
| + s := NewDirStore(root) |
| + art := map[string]Artifact{adoptPlatform: {File: adoptPlatform, SHA256: strings.Repeat("a", 64), Size: 1}} |
| + putManifest(t, s, "z-new", Manifest{Schema: 1, App: "console", Version: "z-new", Kind: "binary", |
| + BuiltAt: "2026-06-01T00:00:00Z", Artifacts: art, Program: "example.com/new"}) |
| + putManifest(t, s, "a-old", Manifest{Schema: 1, App: "console", Version: "a-old", Kind: "binary", |
| + BuiltAt: "2026-01-01T00:00:00Z", Artifacts: art, Program: "example.com/old"}) |
| + older, newer := time.Now().Add(-time.Hour), time.Now() |
| + for ver, mod := range map[string]time.Time{"z-new": older, "a-old": newer} { |
| + p := filepath.Join(root, filepath.FromSlash(ManifestKey("bab", "console", ver))) |
| + if err := os.Chtimes(p, mod, mod); err != nil { |
| + t.Fatal(err) |
| + } |
| + } |
| + if vers, _, err := VersionCandidates(context.Background(), s, "bab", "console", 20); err != nil || len(vers) != 2 || vers[0] != "a-old" { |
| + t.Fatalf("listing = %v, %v; the test needs a-old listed first", vers, err) |
| + } |
| + got, ver, err := PreviousProgram(context.Background(), s, "bab", "console") |
| + if err != nil || got != "example.com/new" || ver != "z-new" { |
| + t.Fatalf("PreviousProgram = %q %q %v, want example.com/new from z-new", got, ver, err) |
| + } |
| +} |
| + |
| +// A manifest that parses but fails validation, and one that names another |
| +// app or version than its key, are skipped; the older valid one answers. |
| +func TestPreviousProgramSkipsInvalidAndMisnamedManifests(t *testing.T) { |
| + s := NewDirStore(t.TempDir()) |
| + if err := shipWithProgram(t, s, "v1", tinyProgram(t, "example.com/console"), false); err != nil { |
| + t.Fatal(err) |
| + } |
| + art := map[string]Artifact{adoptPlatform: {File: adoptPlatform, SHA256: strings.Repeat("a", 64), Size: 1}} |
| + late := "2999-01-01T00:00:00Z" |
| + // Fails validateManifest: schema 2. |
| + putManifest(t, s, "v2", Manifest{Schema: 2, App: "console", Version: "v2", Kind: "binary", |
| + BuiltAt: late, Artifacts: art, Program: "example.com/invalid"}) |
| + // Valid, but declares another app. |
| + putManifest(t, s, "v3", Manifest{Schema: 1, App: "other", Version: "v3", Kind: "binary", |
| + BuiltAt: late, Artifacts: art, Program: "example.com/other-app"}) |
| + // Valid, but declares another version. |
| + putManifest(t, s, "v4", Manifest{Schema: 1, App: "console", Version: "v9", Kind: "binary", |
| + BuiltAt: late, Artifacts: art, Program: "example.com/other-version"}) |
| + got, ver, err := PreviousProgram(context.Background(), s, "bab", "console") |
| + if err != nil || got != "example.com/console" || ver != "v1" { |
| + t.Fatalf("PreviousProgram = %q %q %v, want example.com/console from v1", got, ver, err) |
| + } |
| +} |
| + |
| // A release from an old client has no program and must not switch the check |
| // off for the next ship. |
| func TestPreviousProgramSkipsReleasesWithoutOne(t *testing.T) { |