diff options
| author | 2026-06-07 12:12:39 +0000 | |
|---|---|---|
| committer | 2026-06-07 12:17:59 +0000 | |
| commit | 58698a0e70c61c7447ec9a7b938fc63a94151db1 (patch) | |
| tree | 808873c6e9ea3a38bcd054ac34eca09bec34703f /object | |
| parent | object/tree: Reject duplicates (diff) | |
| signature | No signature | |
object/tree: Add tests
Diffstat (limited to 'object')
| -rw-r--r-- | object/tree/append_test.go | 48 | ||||
| -rw-r--r-- | object/tree/helpers_test.go | 131 | ||||
| -rw-r--r-- | object/tree/insert.go | 3 | ||||
| -rw-r--r-- | object/tree/insert_test.go | 102 | ||||
| -rw-r--r-- | object/tree/malformed_test.go | 59 | ||||
| -rw-r--r-- | object/tree/parse.go | 3 | ||||
| -rw-r--r-- | object/tree/parse_test.go | 44 | ||||
| -rw-r--r-- | object/tree/roundtrip_test.go | 81 |
8 files changed, 469 insertions, 2 deletions
diff --git a/object/tree/append_test.go b/object/tree/append_test.go new file mode 100644 index 00000000..babcd03b --- /dev/null +++ b/object/tree/append_test.go @@ -0,0 +1,48 @@ +package tree_test + +import ( + "bytes" + "testing" + + "lindenii.org/go/furgit/internal/testgit" + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/typ" +) + +func TestAppend(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + entries := mixedEntries(t, repo) + tr := buildTree(t, entries) + + rawBody, err := tr.AppendWithoutHeader(nil) + if err != nil { + t.Fatalf("AppendWithoutHeader: %v", err) + } + + treeID, err := repo.HashObject(t, typ.TypeTree, bytes.NewReader(rawBody)) + if err != nil { + t.Fatalf("HashObject(tree): %v", err) + } + + err = repo.Fsck(t, testgit.FsckOptions{ + Strict: true, + NoDangling: true, + }, treeID) + if err != nil { + t.Fatalf("Fsck: %v", err) + } + + assertGitDecode(t, repo, treeID, tr.Entries()) + }) + } +} diff --git a/object/tree/helpers_test.go b/object/tree/helpers_test.go new file mode 100644 index 00000000..c71f0117 --- /dev/null +++ b/object/tree/helpers_test.go @@ -0,0 +1,131 @@ +package tree_test + +import ( + "strconv" + "strings" + "testing" + + "lindenii.org/go/furgit/internal/testgit" + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/tree" + "lindenii.org/go/furgit/object/tree/mode" + "lindenii.org/go/furgit/object/typ" +) + +func mixedEntries(tb testing.TB, repo *testgit.Repo) []tree.Entry { + tb.Helper() + + blobA, err := repo.HashObject(tb, typ.TypeBlob, strings.NewReader("blob-A\n")) + if err != nil { + tb.Fatalf("HashObject(blob-A): %v", err) + } + + blobB, err := repo.HashObject(tb, typ.TypeBlob, strings.NewReader("blob-B\n")) + if err != nil { + tb.Fatalf("HashObject(blob-B): %v", err) + } + + blobC, err := repo.HashObject(tb, typ.TypeBlob, strings.NewReader("blob-C\n")) + if err != nil { + tb.Fatalf("HashObject(blob-C): %v", err) + } + + subTree, err := repo.MkTree(tb, []testgit.TreeEntry{ + {Mode: "100644", Type: typ.TypeBlob, OID: blobA, Name: "leaf"}, + }) + if err != nil { + tb.Fatalf("MkTree(subtree): %v", err) + } + + submodule, err := repo.CommitTree(tb, subTree, testgit.CommitTreeOptions{Message: "submodule"}) + if err != nil { + tb.Fatalf("CommitTree(submodule): %v", err) + } + + return []tree.Entry{ + {Mode: mode.Regular, Name: "z", ID: blobA}, + {Mode: mode.Regular, Name: "A", ID: blobB}, + {Mode: mode.Regular, Name: "aa", ID: blobC}, + {Mode: mode.Regular, Name: "a0", ID: blobA}, + {Mode: mode.Regular, Name: "a.", ID: blobC}, + {Mode: mode.Regular, Name: "Z", ID: blobB}, + {Mode: mode.Regular, Name: "0", ID: blobA}, + {Mode: mode.Regular, Name: "CAPS", ID: blobB}, + {Mode: mode.Regular, Name: "caps", ID: blobC}, + {Mode: mode.Regular, Name: "name with space", ID: blobB}, + {Mode: mode.Regular, Name: "name.with.dot", ID: blobA}, + {Mode: mode.Regular, Name: "这是一些非 ASCII 的字符", ID: blobC}, + {Mode: mode.Regular, Name: "Emoji 👀", ID: blobC}, + {Mode: mode.Regular, Name: ".hidden", ID: blobA}, + {Mode: mode.Executable, Name: "exec.sh", ID: blobB}, + {Mode: mode.Symlink, Name: "sym.link", ID: blobC}, + {Mode: mode.Gitlink, Name: "submodule", ID: submodule}, + {Mode: mode.Regular, Name: "dir-", ID: blobA}, + {Mode: mode.Directory, Name: "dir", ID: subTree}, + {Mode: mode.Regular, Name: "dir0", ID: blobB}, + } +} + +func mkTreeEntries(entries []tree.Entry) []testgit.TreeEntry { + out := make([]testgit.TreeEntry, len(entries)) + for i, entry := range entries { + out[i] = testgit.TreeEntry{ + Mode: strconv.FormatUint(uint64(entry.Mode), 8), + Type: entry.Mode.ObjectType(), + OID: entry.ID, + Name: entry.Name, + } + } + + return out +} + +func buildTree(tb testing.TB, entries []tree.Entry) *tree.Tree { + tb.Helper() + + tr := new(tree.Tree) + for _, entry := range entries { + err := tr.Insert(entry) + if err != nil { + tb.Fatalf("Insert(%q): %v", entry.Name, err) + } + } + + return tr +} + +func assertGitDecode(tb testing.TB, repo *testgit.Repo, treeID id.ObjectID, got []tree.Entry) { + tb.Helper() + + want, err := repo.LsTree(tb, treeID) + if err != nil { + tb.Fatalf("LsTree: %v", err) + } + + if len(got) != len(want) { + tb.Fatalf("entry count = %d, want %d", len(got), len(want)) + } + + for i := range want { + wantMode, err := strconv.ParseUint(want[i].Mode, 8, 32) + if err != nil { + tb.Fatalf("entry[%d] parse git mode %q: %v", i, want[i].Mode, err) + } + + if uint64(got[i].Mode) != wantMode { + tb.Fatalf("entry[%d] mode = %o, want %o", i, uint64(got[i].Mode), wantMode) + } + + if got[i].Mode.ObjectType() != want[i].Type { + tb.Fatalf("entry[%d] type = %v, want %v", i, got[i].Mode.ObjectType(), want[i].Type) + } + + if got[i].ID != want[i].OID { + tb.Fatalf("entry[%d] id = %s, want %s", i, got[i].ID, want[i].OID) + } + + if got[i].Name != want[i].Name { + tb.Fatalf("entry[%d] name = %q, want %q", i, got[i].Name, want[i].Name) + } + } +} diff --git a/object/tree/insert.go b/object/tree/insert.go index e7fce5e9..b5227938 100644 --- a/object/tree/insert.go +++ b/object/tree/insert.go @@ -17,7 +17,8 @@ var ErrInvalidTree = errors.New("object/tree: invalid tree") // It rejects entries with an invalid name or mode, // and entries whose name conflicts with one already present. func (tree *Tree) Insert(entry Entry) error { - if err := validateName(entry.Name); err != nil { + err := validateName(entry.Name) + if err != nil { return err } diff --git a/object/tree/insert_test.go b/object/tree/insert_test.go new file mode 100644 index 00000000..fbf65b84 --- /dev/null +++ b/object/tree/insert_test.go @@ -0,0 +1,102 @@ +package tree_test + +import ( + "errors" + "testing" + + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/tree" + "lindenii.org/go/furgit/object/tree/mode" +) + +func TestInsertRejects(t *testing.T) { + t.Parallel() + + zero := id.SupportedObjectFormats()[0].Zero() + + for _, tc := range []struct { + name string + entry tree.Entry + }{ + {name: "empty-name", entry: tree.Entry{Mode: mode.Regular, Name: "", ID: zero}}, + {name: "slash-name", entry: tree.Entry{Mode: mode.Regular, Name: "a/b", ID: zero}}, + {name: "nul-name", entry: tree.Entry{Mode: mode.Regular, Name: "a\x00b", ID: zero}}, + {name: "invalid-mode", entry: tree.Entry{Mode: mode.Mode(0o100640), Name: "file", ID: zero}}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var tr tree.Tree + + err := tr.Insert(tc.entry) + if !errors.Is(err, tree.ErrInvalidTree) { + t.Fatalf("Insert error = %v, want ErrInvalidTree", err) + } + }) + } +} + +func TestInsertRejectsConflict(t *testing.T) { + t.Parallel() + + zero := id.SupportedObjectFormats()[0].Zero() + + for _, tc := range []struct { + name string + first tree.Entry + second tree.Entry + }{ + { + name: "same-mode", + first: tree.Entry{Mode: mode.Regular, Name: "file", ID: zero}, + second: tree.Entry{Mode: mode.Regular, Name: "file", ID: zero}, + }, + { + name: "blob-then-tree", + first: tree.Entry{Mode: mode.Regular, Name: "name", ID: zero}, + second: tree.Entry{Mode: mode.Directory, Name: "name", ID: zero}, + }, + { + name: "tree-then-blob", + first: tree.Entry{Mode: mode.Directory, Name: "name", ID: zero}, + second: tree.Entry{Mode: mode.Regular, Name: "name", ID: zero}, + }, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + var tr tree.Tree + + err := tr.Insert(tc.first) + if err != nil { + t.Fatalf("Insert(first): %v", err) + } + + err = tr.Insert(tc.second) + if !errors.Is(err, tree.ErrInvalidTree) { + t.Fatalf("Insert(second) error = %v, want ErrInvalidTree", err) + } + }) + } +} + +func TestEntriesIsCopy(t *testing.T) { + t.Parallel() + + zero := id.SupportedObjectFormats()[0].Zero() + + var tr tree.Tree + + err := tr.Insert(tree.Entry{Mode: mode.Regular, Name: "file", ID: zero}) + if err != nil { + t.Fatalf("Insert: %v", err) + } + + entries := tr.Entries() + entries[0].Name = "mutated" + + again := tr.Entries() + if again[0].Name != "file" { + t.Fatalf("Entries()[0].Name = %q, want %q", again[0].Name, "file") + } +} diff --git a/object/tree/malformed_test.go b/object/tree/malformed_test.go new file mode 100644 index 00000000..ca00ea94 --- /dev/null +++ b/object/tree/malformed_test.go @@ -0,0 +1,59 @@ +package tree_test + +import ( + "bytes" + "errors" + "testing" + + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/tree" +) + +func TestParseMalformed(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + size := objectFormat.Size() + + record := func(mode, name string, idLen int) []byte { + var b bytes.Buffer + b.WriteString(mode) + b.WriteByte(' ') + b.WriteString(name) + b.WriteByte(0) + b.Write(make([]byte, idLen)) + + return b.Bytes() + } + + for _, tc := range []struct { + name string + body []byte + }{ + {name: "malformed-mode", body: record("10064x", "file", size)}, + {name: "zero-padded-mode", body: record("0100644", "file", size)}, + {name: "unsupported-mode", body: record("100640", "file", size)}, + {name: "empty-name", body: record("100644", "", size)}, + {name: "slash-name", body: record("100644", "a/b", size)}, + {name: "truncated-id", body: record("100644", "file", size-1)}, + {name: "missing-mode-terminator", body: []byte("100644")}, + {name: "missing-name-terminator", body: []byte("100644 file")}, + {name: "unsorted", body: append(record("100644", "b", size), record("100644", "a", size)...)}, + {name: "duplicate", body: append(record("100644", "a", size), record("100644", "a", size)...)}, + {name: "conflicting-tree-blob", body: append(record("100644", "foo", size), record("40000", "foo", size)...)}, + } { + t.Run(tc.name, func(t *testing.T) { + t.Parallel() + + _, err := tree.Parse(tc.body, objectFormat) + if !errors.Is(err, tree.ErrInvalidTree) { + t.Fatalf("Parse error = %v, want ErrInvalidTree", err) + } + }) + } + }) + } +} diff --git a/object/tree/parse.go b/object/tree/parse.go index 90519705..5b01fa05 100644 --- a/object/tree/parse.go +++ b/object/tree/parse.go @@ -41,7 +41,8 @@ func Parse(body []byte, objectFormat id.ObjectFormat) (*Tree, error) { name := string(body[i : i+nul]) i += nul + 1 - if err := validateName(name); err != nil { + err = validateName(name) + if err != nil { return nil, err } diff --git a/object/tree/parse_test.go b/object/tree/parse_test.go new file mode 100644 index 00000000..8d32b136 --- /dev/null +++ b/object/tree/parse_test.go @@ -0,0 +1,44 @@ +package tree_test + +import ( + "testing" + + "lindenii.org/go/furgit/internal/testgit" + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/tree" + "lindenii.org/go/furgit/object/typ" +) + +func TestParse(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + entries := mixedEntries(t, repo) + + treeID, err := repo.MkTree(t, mkTreeEntries(entries)) + if err != nil { + t.Fatalf("MkTree: %v", err) + } + + rawBody, err := repo.CatFile(t, typ.TypeTree, treeID) + if err != nil { + t.Fatalf("CatFile: %v", err) + } + + parsed, err := tree.Parse(rawBody, objectFormat) + if err != nil { + t.Fatalf("Parse: %v", err) + } + + assertGitDecode(t, repo, treeID, parsed.Entries()) + }) + } +} diff --git a/object/tree/roundtrip_test.go b/object/tree/roundtrip_test.go new file mode 100644 index 00000000..7fdc4140 --- /dev/null +++ b/object/tree/roundtrip_test.go @@ -0,0 +1,81 @@ +package tree_test + +import ( + "bytes" + "testing" + + "lindenii.org/go/furgit/internal/testgit" + "lindenii.org/go/furgit/object/id" + "lindenii.org/go/furgit/object/tree" + "lindenii.org/go/furgit/object/typ" +) + +func TestRoundTrip(t *testing.T) { + t.Parallel() + + for _, objectFormat := range id.SupportedObjectFormats() { + t.Run(objectFormat.String(), func(t *testing.T) { + t.Parallel() + + repo, err := testgit.NewRepo(t, testgit.RepoOptions{ObjectFormat: objectFormat}) + if err != nil { + t.Fatalf("NewRepo: %v", err) + } + + entries := mixedEntries(t, repo) + tr := buildTree(t, entries) + + rawBody, err := tr.AppendWithoutHeader(nil) + if err != nil { + t.Fatalf("AppendWithoutHeader: %v", err) + } + + treeID, err := repo.HashObject(t, typ.TypeTree, bytes.NewReader(rawBody)) + if err != nil { + t.Fatalf("HashObject(tree): %v", err) + } + + err = repo.Fsck(t, testgit.FsckOptions{ + Strict: true, + NoDangling: true, + }, treeID) + if err != nil { + t.Fatalf("Fsck: %v", err) + } + + gitBody, err := repo.CatFile(t, typ.TypeTree, treeID) + if err != nil { + t.Fatalf("CatFile: %v", err) + } + + parsed, err := tree.Parse(gitBody, objectFormat) + if err != nil { + t.Fatalf("Parse: %v", err) + } + + assertEntriesEqual(t, parsed.Entries(), tr.Entries()) + }) + } +} + +func assertEntriesEqual(t *testing.T, got []tree.Entry, want []tree.Entry) { + t.Helper() + + if len(got) != len(want) { + t.Fatalf("entry count = %d, want %d", len(got), len(want)) + } + + for i := range want { + if got[i].Mode != want[i].Mode { + t.Fatalf("entry[%d] mode = %o, want %o", i, got[i].Mode, want[i].Mode) + } + + if got[i].Name != want[i].Name { + t.Fatalf("entry[%d] name = %q, want %q", i, got[i].Name, want[i].Name) + } + + if got[i].ID != want[i].ID { + t.Fatalf("entry[%d] id = %s, want %s", i, got[i].ID, want[i].ID) + } + } +} |
