Files
mesh-sdk/go/delivery/fingerprint2_test.go
T
jschoubben af4e4e60a3 Carry mesh package versions in build asks and fingerprints
ADR 0573 §2 has every module build against the latest published version of
each mesh package it uses, chosen by the planner and written by the build,
never pinned by hand (issue 1585, S4). The ask, the fetch request and the
merge preview now carry the packages and their versions, and the new-form
fingerprint takes each version, so a package's trunk merge changes every
dependent's fingerprint. A module using no package keeps its fingerprint.

Claude-Session: https://claude.ai/code/session_01NcbgfJ9Gum47zkFKWRaovL
2026-11-01 01:57:26 +01:00

175 lines
8.0 KiB
Go

package delivery
import (
"strings"
"testing"
)
// The new form's inputs (novox/hq ADR 0429 clause 5 as ADR 0437 clause 5 changed it, ADR 0405 clause 7). The base and
// builder values are module fingerprints in the new form; a lock is the caller's hash of a lock file, which this
// function only enters (its form is the builder's to settle; the values here are stand-in tokens); the toolchain is a
// language, the installed toolchain's version and the build module's recipe version.
func inputs2() SourceInputs2 {
return SourceInputs2{
Module: "mesh-mail",
Tree: "t-module",
Bases: []string{SourcePrefix2 + strings.Repeat("a", 64), SourcePrefix2 + strings.Repeat("b", 64)},
Builder: SourcePrefix2 + strings.Repeat("c", 64),
Contexts: map[string]string{"mail": "t-root", "tool": "t-root2"},
Locks: map[string]string{"mail": "sha256:1111", "tool": "sha256:2222"},
Toolchains: map[string]Toolchain2{
"mail": {Language: "go", Version: "1.26.0", Recipe: "3"},
"tool": {Language: "typescript", Version: "5.9.2", Recipe: "1"},
},
}
}
func mustFP2(t *testing.T, in SourceInputs2) string {
t.Helper()
fp, none := SourceFingerprint2(in)
if fp == "" || none != "" {
t.Fatalf("fingerprint = %q, none = %q", fp, none)
}
return fp
}
// 1356/AC1: a fixed input has the fingerprint the test fixes, with the new prefix. The hex was computed from the function and checked against an independent sha256 of the lines;
// it is pinned here, so a change of the lines is a change of every fingerprint, and shows.
func TestFingerprint2Vector(t *testing.T) {
const want = "src2:56b6f2f6ffc1767225e0b277ab78539521e9a9fbaadfe4ec158d2fddee1d63ef"
if got := mustFP2(t, inputs2()); got != want {
t.Errorf("fingerprint = %s, want %s", got, want)
}
if SourcePrefix2 != "src2:" || SourcePrefix == SourcePrefix2 {
t.Errorf("the new prefix is %q", SourcePrefix2)
}
}
// 1356/AC2: each input on its own changes the fingerprint.
func TestFingerprint2Changes(t *testing.T) {
want := mustFP2(t, inputs2())
for name, f := range map[string]func(*SourceInputs2){
"module": func(in *SourceInputs2) { in.Module = "other" },
"tree": func(in *SourceInputs2) { in.Tree = "t-other" },
"a base": func(in *SourceInputs2) { in.Bases[0] = SourcePrefix2 + strings.Repeat("d", 64) },
"a base added": func(in *SourceInputs2) { in.Bases = append(in.Bases, SourcePrefix2+strings.Repeat("e", 64)) },
"the builder": func(in *SourceInputs2) { in.Builder = SourcePrefix2 + strings.Repeat("f", 64) },
"a lock": func(in *SourceInputs2) { in.Locks["mail"] = "sha256:changed" },
"a lock added": func(in *SourceInputs2) { in.Locks["extra"] = "sha256:3333" },
"a toolchain": func(in *SourceInputs2) { in.Toolchains["mail"] = Toolchain2{"go", "1.26.1", "3"} },
"a toolchain lang": func(in *SourceInputs2) { in.Toolchains["mail"] = Toolchain2{"typescript", "1.26.0", "3"} },
"a recipe": func(in *SourceInputs2) { in.Toolchains["mail"] = Toolchain2{"go", "1.26.0", "4"} },
"a context": func(in *SourceInputs2) { in.Contexts["mail"] = "t-changed" },
"a context added": func(in *SourceInputs2) { in.Contexts["extra"] = "t-root" },
"a toolchain added": func(in *SourceInputs2) { in.Toolchains["extra"] = Toolchain2{"go", "1.26.0", "3"} },
// Issue 1585 (ADR 0573 §2): a package's new published version, and a package used at all.
"a package added": func(in *SourceInputs2) { in.Packages = map[string]string{"git.novox.be/novox/mesh-sdk/go": "c1"} },
"a package version": func(in *SourceInputs2) { in.Packages = map[string]string{"git.novox.be/novox/mesh-sdk/go": "c2"} },
} {
in := inputs2()
f(&in)
if got := mustFP2(t, in); got == want {
t.Errorf("a changed %s keeps the fingerprint", name)
}
}
}
// 1356/AC3: a base given twice, and maps in any order, give the same fingerprint.
func TestFingerprint2OrderAndDuplicates(t *testing.T) {
want := mustFP2(t, inputs2())
in := inputs2()
in.Bases = []string{in.Bases[1], in.Bases[0]}
if got := mustFP2(t, in); got != want {
t.Errorf("the order of bases changes it: %s", got)
}
in = inputs2()
in.Bases = append(in.Bases, in.Bases[0], in.Bases[1])
if got := mustFP2(t, in); got != want {
t.Errorf("a base given twice changes it: %s", got)
}
for i := 0; i < 20; i++ {
in = inputs2()
in.Contexts, in.Locks, in.Toolchains = map[string]string{}, map[string]string{}, map[string]Toolchain2{}
for _, k := range []string{"tool", "mail"} {
in.Contexts[k] = inputs2().Contexts[k]
in.Locks[k] = inputs2().Locks[k]
in.Toolchains[k] = inputs2().Toolchains[k]
}
if got := mustFP2(t, in); got != want {
t.Fatalf("map order changes it: %s", got)
}
}
}
// 1356/AC4: an artifact that resolves packages when built, with no lock, gives no fingerprint and the reason "no lock";
// a module with no tree gets none with today's reason, and the reason the source is unpinned stands over both.
func TestFingerprint2None(t *testing.T) {
in := inputs2()
in.Locks["mail"] = ""
if fp, none := SourceFingerprint2(in); fp != "" || none != "no lock" {
t.Errorf("no lock: fp = %q, none = %q", fp, none)
}
in = inputs2()
in.Tree = ""
if fp, none := SourceFingerprint2(in); fp != "" || none != "no tree" {
t.Errorf("no tree: fp = %q, none = %q", fp, none)
}
// Issue 1585: a package with no published version gives none, naming it; an empty set of packages changes nothing.
in = inputs2()
in.Packages = map[string]string{"git.novox.be/novox/b/go": "", "git.novox.be/novox/a/go": "", "git.novox.be/novox/c/go": "c1"}
if fp, none := SourceFingerprint2(in); fp != "" || none != NoPackageVersion+"git.novox.be/novox/a/go, git.novox.be/novox/b/go" {
t.Errorf("no package version: fp = %q, none = %q", fp, none)
}
in = inputs2()
in.Packages = map[string]string{}
if fp, _ := SourceFingerprint2(in); fp != mustFP2(t, inputs2()) {
t.Errorf("no package changes the fingerprint: %s", fp)
}
in = inputs2()
in.Tree, in.Unpinned = "", "a context is a Go program's build source"
if _, none := SourceFingerprint2(in); none != in.Unpinned {
t.Errorf("the reason for unpinned stands over no tree: %q", none)
}
}
// 1356/AC5: src1 is unchanged by the new form; its vectors in fingerprint_test.go run unchanged. This pins that the
// two forms do not collide for equal module and tree, and that src1 keeps its prefix.
func TestSource1StaysSource1(t *testing.T) {
if SourcePrefix != "src1:" {
t.Fatalf("src1's prefix is %q", SourcePrefix)
}
fp1, _ := SourceFingerprint(SourceInputs{Module: "m", Tree: "t"})
fp2, _ := SourceFingerprint2(SourceInputs2{Module: "m", Tree: "t"})
if fp1 == "" || fp2 == "" || strings.TrimPrefix(fp1, SourcePrefix) == strings.TrimPrefix(fp2, SourcePrefix2) {
t.Errorf("src1 %q and src2 %q", fp1, fp2)
}
}
// 1356/AC6: the key part holds only bus key characters and parsing gives the fingerprint back; a src1 or malformed
// value is refused, naming it.
func TestFingerprintKeyPart(t *testing.T) {
fp := mustFP2(t, inputs2())
part, err := FingerprintKeyPart(fp)
if err != nil {
t.Fatal(err)
}
if len(part) != 64 || strings.Trim(part, "0123456789abcdef") != "" || !nameOK.MatchString(part) {
t.Errorf("key part %q is not 64 lowercase hex characters a key allows", part)
}
back, err := ParseFingerprintKeyPart(part)
if err != nil || back != fp {
t.Errorf("parse(%q) = %q, %v; want %q", part, back, err, fp)
}
src1, _ := SourceFingerprint(SourceInputs{Module: "m", Tree: "t"})
for _, bad := range []string{src1, "src2:", "src2:xyz", "", "src2:" + strings.Repeat("A", 64), "src2:" + strings.Repeat("a", 63)} {
if _, err := FingerprintKeyPart(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
t.Errorf("FingerprintKeyPart(%q) = %v, want a refusal naming it", bad, err)
}
}
for _, bad := range []string{src1, "src2:" + strings.Repeat("a", 64), strings.Repeat("a", 63), strings.Repeat("A", 64), "", "zz"} {
if _, err := ParseFingerprintKeyPart(bad); err == nil || !strings.Contains(err.Error(), "\""+bad+"\"") {
t.Errorf("ParseFingerprintKeyPart(%q) = %v, want a refusal naming it", bad, err)
}
}
}