Artifact b7ac136d557eabf662f01f6c69bab46bdcf2cfe06fb60d57d5568a570080c55c:
- File
internal/cone/cone_test.go
— part of check-in
[027fc6c741]
at
2026-10-18 18:26:57
on branch feat/submanifest-reader
— feat(cone): the cone specification of draft-fossil-sparse-cone-00 ยง3
Inclusion and exclusion members as literal path prefixes, normalized through tracked-path validation, longest member deciding, dead exclusions and contradictions refused, and zit's canonical serialized form (version 1, and version 2 for exclusions) with its SHA3-256 digest. (user: [email protected], size: 12072) [annotate] [blame] [check-ins using] [more...]
package cone import ( "bytes" "encoding/hex" "errors" "fmt" "strings" "testing" "github.com/danmestas/go-libfossil/internal/hash" ) func mustNew(t *testing.T, include, exclude []string) *Cone { t.Helper() c, err := New(include, exclude) if err != nil { t.Fatalf("New(%q, %q): %v", include, exclude, err) } return c } func TestNewNormalizesSortsAndValidates(t *testing.T) { c := mustNew(t, []string{"src/vendor", "src", "docs/rfcs", "src/vendor/deep"}, nil) if got := strings.Join(c.Include(), ","); got != "docs/rfcs,src" { t.Fatalf("Include = %q, want docs/rfcs,src", got) } if len(c.Exclude()) != 0 { t.Fatalf("Exclude = %q, want none", c.Exclude()) } for _, tc := range []struct { include, exclude []string want error }{ {nil, nil, ErrBounds}, {[]string{"a//b"}, nil, ErrInvalidMember}, {[]string{"../out"}, nil, ErrInvalidMember}, {[]string{"./a"}, nil, ErrInvalidMember}, {[]string{"a/"}, nil, ErrInvalidMember}, {[]string{"/abs"}, nil, ErrInvalidMember}, {[]string{"a\\b"}, nil, ErrInvalidMember}, {[]string{""}, nil, ErrInvalidMember}, {[]string{"bad\xffutf8"}, nil, ErrInvalidMember}, {[]string{".fslckout"}, nil, ErrReservedMember}, {[]string{"sub/_FOSSIL_"}, nil, ErrReservedMember}, {[]string{"src"}, []string{"src//v"}, ErrInvalidMember}, } { if _, err := New(tc.include, tc.exclude); !errors.Is(err, tc.want) { t.Errorf("New(%q, %q) = %v, want %v", tc.include, tc.exclude, err, tc.want) } } } func TestNewBounds(t *testing.T) { names := make([]string, MaxMembers+1) for i := range names { names[i] = fmt.Sprintf("d%d", i) } if _, err := New(names[:MaxMembers], nil); err != nil { t.Fatalf("%d members: %v", MaxMembers, err) } if _, err := New(names, nil); !errors.Is(err, ErrBounds) { t.Fatalf("%d members: %v, want ErrBounds", len(names), err) } // The cap is joint across polarities. if _, err := New(names[:1], names[1:]); !errors.Is(err, ErrBounds) { t.Fatalf("joint cap: %v, want ErrBounds", err) } long := strings.Repeat("a", MaxMemberLen+1) if _, err := New([]string{long}, nil); !errors.Is(err, ErrInvalidMember) { t.Fatalf("long member: %v, want ErrInvalidMember", err) } if _, err := New([]string{long[:MaxMemberLen]}, nil); err != nil { t.Fatalf("member at the cap: %v", err) } } func TestNewExclusionNormalization(t *testing.T) { c := mustNew(t, []string{"src", "src/vendor/keep"}, []string{"src/vendor", "src/vendor/junk"}) // src/vendor/junk is redundant (its nearest covering member is an // exclusion too); src/vendor/keep is a re-inclusion and survives. if got := strings.Join(c.Include(), ","); got != "src,src/vendor/keep" { t.Fatalf("Include = %q", got) } if got := strings.Join(c.Exclude(), ","); got != "src/vendor" { t.Fatalf("Exclude = %q", got) } for _, tc := range []struct { include, exclude []string want error }{ {[]string{"src"}, []string{"tools"}, ErrDeadExclusion}, {[]string{"src"}, []string{"src"}, ErrContradiction}, {nil, []string{"src/vendor"}, ErrBounds}, // Covered by an exclusion, so a redundant exclusion, never dead. {[]string{"src", "src/a/b"}, []string{"src/a", "src/a/b/c"}, nil}, } { if _, err := New(tc.include, tc.exclude); !errors.Is(err, tc.want) { t.Errorf("New(%q, %q) = %v, want %v", tc.include, tc.exclude, err, tc.want) } } // A dash-sibling sorts between a member and its subtree ('-' < '/') and // must not sever the ancestor chain for later entries. dashed := mustNew(t, []string{"src", "src-other"}, []string{"src/vendor"}) if len(dashed.Include()) != 2 || len(dashed.Exclude()) != 1 { t.Fatalf("dashed = %q / %q", dashed.Include(), dashed.Exclude()) } if dashed.Covers("src/vendor/x") || !dashed.Covers("src-other/y") { t.Fatal("dash-sibling broke the exclusion") } // Exact duplicates collapse. dup := mustNew(t, []string{"a", "a"}, []string{"a/b", "a/b"}) if len(dup.Include()) != 1 || len(dup.Exclude()) != 1 { t.Fatalf("dup = %q / %q", dup.Include(), dup.Exclude()) } } func TestCoversBoundaries(t *testing.T) { c := mustNew(t, []string{"src", "docs/rfcs"}, nil) for path, want := range map[string]bool{ "src": true, "src/cone.go": true, "docs/rfcs/draft.txt": true, "src2/file": false, "srcx": false, "docs": false, "docs/plans/x": false, "src.txt": false, // '.' sorts before '/' } { if got := c.Covers(path); got != want { t.Errorf("Covers(%q) = %v, want %v", path, got, want) } } } func TestCoversLongestMatchAcrossPolarities(t *testing.T) { c := mustNew(t, []string{"src", "src/vendor/keep"}, []string{"src/vendor"}) for path, want := range map[string]bool{ "src/a.go": true, "src/vendor": false, "src/vendor/lib.c": false, "src/vendor/keep": true, "src/vendor/keep/deep.txt": true, "src/vendorx": true, "src/vendor.txt": true, "README": false, } { if got := c.Covers(path); got != want { t.Errorf("Covers(%q) = %v, want %v", path, got, want) } } } func TestDecideNamesTheDecidingMember(t *testing.T) { c := mustNew(t, []string{"src", "src/vendor/keep"}, []string{"src/vendor"}) for _, tc := range []struct { path, member string covered bool }{ {"src/a.go", "src", true}, {"src/vendor/lib.c", "src/vendor", false}, {"src/vendor/keep/deep.txt", "src/vendor/keep", true}, {"README", "", false}, } { d := c.Decide(tc.path) if d.Covered != tc.covered || d.Member != tc.member { t.Errorf("Decide(%q) = %+v, want covered=%v member=%q", tc.path, d, tc.covered, tc.member) } if d.Covered != c.Covers(tc.path) { t.Errorf("Decide and Covers disagree on %q", tc.path) } if d.Excluded() != (d.Member != "" && !d.Covered) { t.Errorf("Decide(%q).Excluded() = %v", tc.path, d.Excluded()) } } } func TestIntersectsSubtree(t *testing.T) { c := mustNew(t, []string{"src/lib"}, nil) for dir, want := range map[string]bool{ "src/lib": true, "src/lib/deep": true, "src": true, "srcx": false, "tools": false, } { if got := c.IntersectsSubtree(dir); got != want { t.Errorf("IntersectsSubtree(%q) = %v, want %v", dir, got, want) } } nested := mustNew(t, []string{"src", "src/vendor/keep"}, []string{"src/vendor"}) if !nested.IntersectsSubtree("src/vendor") { t.Error("a re-inclusion must keep its excluded ancestor intersecting") } } func TestMayCover(t *testing.T) { c := mustNew(t, []string{"src"}, []string{"src/vendor"}) for _, tc := range []struct { first, limit string want bool }{ {"src/a", "src/z", true}, {"tools/a", "", false}, {"README", "docs/z", false}, {"README", "tools", true}, {"README", "src", false}, // the limit is exclusive {"README", "src.txt", true}, {"src0", "", false}, // past "src/" + anything {"src.txt", "src/a", true}, {"a", "", true}, } { if got := c.MayCover(tc.first, tc.limit); got != tc.want { t.Errorf("MayCover(%q, %q) = %v, want %v", tc.first, tc.limit, got, tc.want) } } } // The serialized forms below are byte-for-byte what zit's cone.zig writes: // version 1 (count byte, then per member a u16 LE length and the bytes) for a // cone with no exclusions, and the 0x00-led version 2 form (version byte, // total count, then per member a polarity flag, u16 LE length and bytes, in // one sorted order across both polarities) for one with exclusions. func TestSerializeVectors(t *testing.T) { for _, tc := range []struct { include, exclude []string hex string }{ {[]string{"src"}, nil, "01" + "0300" + hex.EncodeToString([]byte("src"))}, {[]string{"b/dir", "a"}, nil, "02" + "0100" + "61" + "0500" + hex.EncodeToString([]byte("b/dir"))}, { []string{"src", "src/vendor/keep"}, []string{"src/vendor"}, "00" + "02" + "03" + "00" + "0300" + hex.EncodeToString([]byte("src")) + "01" + "0a00" + hex.EncodeToString([]byte("src/vendor")) + "00" + "0f00" + hex.EncodeToString([]byte("src/vendor/keep")), }, } { c := mustNew(t, tc.include, tc.exclude) if got := hex.EncodeToString(c.Serialize()); got != tc.hex { t.Errorf("Serialize(%q, %q) = %s, want %s", tc.include, tc.exclude, got, tc.hex) } } } func TestParseRoundTripAndFailsClosed(t *testing.T) { for _, c := range []*Cone{ mustNew(t, []string{"b/dir", "a"}, nil), mustNew(t, []string{"src", "src/vendor/keep"}, []string{"src/vendor"}), } { data := c.Serialize() back, err := Parse(data) if err != nil { t.Fatalf("Parse(%x): %v", data, err) } if !bytes.Equal(back.Serialize(), data) || !back.Equal(c) { t.Fatalf("round trip changed %x", data) } if _, err := Parse(append(append([]byte{}, data...), 'x')); !errors.Is(err, ErrInvalidMember) { t.Errorf("residue: %v", err) } if _, err := Parse(data[:len(data)-1]); !errors.Is(err, ErrInvalidMember) { t.Errorf("truncation: %v", err) } } // Not canonical: "a/sub" is covered by "a", so normalization would drop // it; stored state fails closed rather than self-repairs. forged := []byte{2, 1, 0, 'a', 5, 0, 'a', '/', 's', 'u', 'b'} if _, err := Parse(forged); !errors.Is(err, ErrInvalidMember) { t.Errorf("non-canonical: %v", err) } // Not canonical: members out of order. if _, err := Parse([]byte{2, 1, 0, 'b', 1, 0, 'a'}); !errors.Is(err, ErrInvalidMember) { t.Errorf("unsorted: %v", err) } for _, tc := range []struct { data []byte want error }{ {nil, ErrInvalidMember}, {[]byte{0}, ErrInvalidMember}, // a truncated versioned lead-in {[]byte{0, 2, 0}, ErrBounds}, // versioned, zero members {[]byte{0, 3, 1}, ErrInvalidMember}, // an unknown version is refused {[]byte{0, 2, 1, 2, 1, 0, 'a'}, ErrInvalidMember}, {[]byte{1, 0, 0}, ErrInvalidMember}, // a zero-length member } { if _, err := Parse(tc.data); !errors.Is(err, tc.want) { t.Errorf("Parse(%x) = %v, want %v", tc.data, err, tc.want) } } } func TestDigestStableUnderPermutation(t *testing.T) { forward := mustNew(t, []string{"src", "docs"}, nil) backward := mustNew(t, []string{"docs", "src", "src/sub"}, nil) other := mustNew(t, []string{"src", "docs/rfcs"}, nil) excluded := mustNew(t, []string{"src", "docs"}, []string{"src/vendor"}) if forward.Digest() != backward.Digest() { t.Error("digest depends on input order") } if forward.Digest() == other.Digest() || forward.Digest() == excluded.Digest() { t.Error("digest ignores coverage") } // SHA3-256 over the canonical form, as zit keys its caches; the // conformance suite pins the values against zit itself. if got, want := forward.Digest(), hash.SHA3(forward.Serialize()); got != want { t.Fatalf("Digest = %s, want %s", got, want) } } func TestAmendment(t *testing.T) { c := mustNew(t, []string{"src"}, []string{"src/vendor"}) wider, err := c.With([]string{"docs"}, nil) if err != nil { t.Fatal(err) } if got := strings.Join(wider.Include(), ","); got != "docs,src" { t.Fatalf("With: %q", got) } replaced := mustNew(t, []string{"docs", "src"}, []string{"src/vendor"}) if !wider.Equal(replaced) { t.Fatal("incremental and wholesale amendment disagree") } narrower, err := wider.Without([]string{"src/vendor", "docs"}) if err != nil { t.Fatal(err) } if !narrower.Equal(mustNew(t, []string{"src"}, nil)) { t.Fatalf("Without: %q / %q", narrower.Include(), narrower.Exclude()) } if _, err := wider.Without([]string{"src"}); !errors.Is(err, ErrDeadExclusion) { t.Fatalf("removing the inclusion over an exclusion: %v", err) } if _, err := c.Without([]string{"src"}); !errors.Is(err, ErrBounds) { t.Fatalf("removing every inclusion: %v", err) } // As in zit, a path that is not a member removes nothing. same, err := c.Without([]string{"tools"}) if err != nil || !same.Equal(c) { t.Fatalf("removing a non-member: %v", err) } } func TestStringNamesTheCone(t *testing.T) { c := mustNew(t, []string{"src", "src/vendor/keep"}, []string{"src/vendor"}) if got := c.String(); got != "src, !src/vendor, src/vendor/keep" { t.Fatalf("String = %q", got) } }