diff options
| author | 2025-11-16 00:00:00 +0000 | |
|---|---|---|
| committer | 2025-11-16 00:00:00 +0000 | |
| commit | bad0f9715556a470d0de2a22c7040181e3a033ba (patch) | |
| tree | 21463072ce5bc85682a887ce0cae26d833941af3 /repo_test.go | |
| parent | EntryRecursive should return ErrNotFound instead of nil, nil (diff) | |
| signature | ||
Use actual git for tests and enhance Head
Diffstat (limited to 'repo_test.go')
| -rw-r--r-- | repo_test.go | 537 |
1 files changed, 23 insertions, 514 deletions
diff --git a/repo_test.go b/repo_test.go index b1b48df4..0e9de49e 100644 --- a/repo_test.go +++ b/repo_test.go @@ -1,539 +1,48 @@ package furgit import ( - "bytes" - "crypto/sha1" - "crypto/sha256" - "encoding/binary" - "errors" - "fmt" - "math" - "os" - "path/filepath" - "sort" "testing" ) -func writeLooseBlob(t *testing.T, repo *Repository, data []byte) Hash { - blob := &Blob{Data: data} - id, err := repo.WriteLooseObject(blob) - if err != nil { - t.Fatalf("WriteLooseObject: %v", err) - } - return id -} - -func TestOpenRepositoryAndLooseRead(t *testing.T) { - root := t.TempDir() - setupRepoConfig(t, root) - repo, err := OpenRepository(root) - if err != nil { - t.Fatalf("OpenRepository error: %v", err) - } - t.Cleanup(func() { _ = repo.Close() }) - - id := writeLooseBlob(t, repo, []byte("loose blob payload")) - obj, err := repo.looseRead(id) - if err != nil { - t.Fatalf("looseRead error: %v", err) - } - blob, ok := obj.(*StoredBlob) - if !ok { - t.Fatalf("expected StoredBlob, got %T", obj) - } - if string(blob.Data) != "loose blob payload" { - t.Fatalf("blob data mismatch: %q", blob.Data) - } -} - -func TestResolveRefLooseAndPacked(t *testing.T) { - root := t.TempDir() - setupRepoConfig(t, root) - repo, err := OpenRepository(root) - if err != nil { - t.Fatalf("OpenRepository error: %v", err) - } - t.Cleanup(func() { _ = repo.Close() }) - - looseID := hashWithByte(0xa0) - loosePath := filepath.Join(root, "refs", "heads") - if err := os.MkdirAll(loosePath, 0o755); err != nil { - t.Fatalf("mkdir refs: %v", err) - } - if err := os.WriteFile(filepath.Join(loosePath, "master"), []byte(looseID.String()+"\n"), 0o644); err != nil { - t.Fatalf("write ref: %v", err) - } - id, err := repo.ResolveRef("refs/heads/master") - if err != nil || id != looseID { - t.Fatalf("ResolveRef loose mismatch (id=%v err=%v)", id, err) - } - - packedID := hashWithByte(0xb0) - packed := fmt.Sprintf("%s refs/tags/v1\n", packedID.String()) - if err := os.WriteFile(filepath.Join(root, "packed-refs"), []byte(packed), 0o644); err != nil { - t.Fatalf("write packed refs: %v", err) - } - id, err = repo.resolvePackedRef("refs/tags/v1") - if err != nil || id != packedID { - t.Fatalf("resolvePackedRef direct mismatch (id=%v err=%v)", id, err) - } - id, err = repo.ResolveRef("refs/tags/v1") - if err != nil || id != packedID { - t.Fatalf("ResolveRef packed mismatch (id=%v err=%v)", id, err) - } - - if _, err := repo.ResolveRef("refs/heads/missing"); !errors.Is(err, ErrInvalidObject) { - t.Fatalf("expected ErrInvalidObject for missing ref, got %v", err) - } -} - -func TestResolveHEAD(t *testing.T) { - root := t.TempDir() - setupRepoConfig(t, root) - repo, err := OpenRepository(root) - if err != nil { - t.Fatalf("OpenRepository error: %v", err) - } - t.Cleanup(func() { _ = repo.Close() }) - - headPath := filepath.Join(root, "HEAD") - if err := os.WriteFile(headPath, []byte("ref: refs/heads/master\n"), 0o644); err != nil { - t.Fatalf("write HEAD: %v", err) - } - ref, err := repo.ResolveHEAD() - if err != nil || ref != "refs/heads/master" { - t.Fatalf("ResolveHEAD mismatch (ref=%q err=%v)", ref, err) - } - if err := os.WriteFile(headPath, []byte("detached\n"), 0o644); err != nil { - t.Fatalf("write HEAD detached: %v", err) - } - if _, err := repo.ResolveHEAD(); err == nil { - t.Fatal("expected error for detached HEAD") - } -} - -func TestReadObjectTypeSizeLoose(t *testing.T) { - t.Parallel() - root := t.TempDir() - setupRepoConfig(t, root) - repo, err := OpenRepository(root) - if err != nil { - t.Fatalf("OpenRepository error: %v", err) - } - t.Cleanup(func() { _ = repo.Close() }) - - data := []byte("header-only read") - id := writeLooseBlob(t, repo, data) - ty, size, err := repo.ReadObjectTypeSize(id) - if err != nil { - t.Fatalf("ReadObjectTypeSize loose error: %v", err) - } - if ty != ObjectTypeBlob || size != int64(len(data)) { - t.Fatalf("unexpected loose metadata ty=%d size=%d", ty, size) - } -} - -func TestReadObjectTypeSizePackedObjects(t *testing.T) { - t.Parallel() - root := t.TempDir() - setupRepoConfig(t, root) - - objs := []testPackObject{ - {finalType: ObjectTypeBlob, body: []byte("packed base payload")}, - { - finalType: ObjectTypeBlob, - body: []byte("packed delta payload with extra bytes"), - encoding: packEncodingOfsDelta, - baseIndex: 0, - }, - } - ids := writeTestPack(t, root, "pack-basic", objs) - - repo, err := OpenRepository(root) - if err != nil { - t.Fatalf("OpenRepository error: %v", err) - } - t.Cleanup(func() { _ = repo.Close() }) - - ty, size, err := repo.ReadObjectTypeSize(ids[0]) - if err != nil { - t.Fatalf("ReadObjectTypeSize base error: %v", err) - } - if ty != ObjectTypeBlob || size != int64(len(objs[0].body)) { - t.Fatalf("unexpected base metadata ty=%d size=%d", ty, size) - } - - ty, size, err = repo.ReadObjectTypeSize(ids[1]) - if err != nil { - t.Fatalf("ReadObjectTypeSize delta error: %v", err) - } - if ty != ObjectTypeBlob || size != int64(len(objs[1].body)) { - t.Fatalf("unexpected delta metadata ty=%d size=%d", ty, size) - } -} - -func TestReadObjectTypeSizePackRefDeltaLooseBase(t *testing.T) { - t.Parallel() - root := t.TempDir() - setupRepoConfig(t, root) - - repo, err := OpenRepository(root) - if err != nil { - t.Fatalf("OpenRepository error: %v", err) - } - t.Cleanup(func() { _ = repo.Close() }) - - looseBody := []byte("loose base for ref delta") - baseID := writeLooseBlob(t, repo, looseBody) - - objs := []testPackObject{ - { - finalType: ObjectTypeBlob, - body: []byte("ref delta rewritten body"), - encoding: packEncodingRefDelta, - baseHash: baseID, - baseBody: looseBody, - }, - } - ids := writeTestPack(t, root, "pack-ref", objs) - - ty, size, err := repo.ReadObjectTypeSize(ids[0]) - if err != nil { - t.Fatalf("ReadObjectTypeSize ref delta error: %v", err) - } - if ty != ObjectTypeBlob || size != int64(len(objs[0].body)) { - t.Fatalf("unexpected ref delta metadata ty=%d size=%d", ty, size) - } -} - -func TestWriteLooseObjectAllTypes(t *testing.T) { - root := t.TempDir() - setupRepoConfig(t, root) - repo, err := OpenRepository(root) - if err != nil { - t.Fatalf("OpenRepository error: %v", err) - } - t.Cleanup(func() { _ = repo.Close() }) - - // Blob - blob := &Blob{Data: []byte("test blob data")} - blobID, err := repo.WriteLooseObject(blob) - if err != nil { - t.Fatalf("WriteLooseObject Blob error: %v", err) - } - readBlob, err := repo.ReadObject(blobID) - if err != nil { - t.Fatalf("ReadObject Blob error: %v", err) - } - if rb, ok := readBlob.(*StoredBlob); !ok { - t.Fatalf("expected StoredBlob, got %T", readBlob) - } else if string(rb.Data) != "test blob data" { - t.Fatalf("blob data mismatch: %q", rb.Data) - } - - // Tree - tree := &Tree{ - Entries: []TreeEntry{ - {Mode: 0100644, Name: []byte("file.txt"), ID: blobID}, - }, - } - treeID, err := repo.WriteLooseObject(tree) - if err != nil { - t.Fatalf("WriteLooseObject Tree error: %v", err) - } - readTree, err := repo.ReadObject(treeID) - if err != nil { - t.Fatalf("ReadObject Tree error: %v", err) - } - if rt, ok := readTree.(*StoredTree); !ok { - t.Fatalf("expected StoredTree, got %T", readTree) - } else if len(rt.Entries) != 1 { - t.Fatalf("tree entries mismatch: %d", len(rt.Entries)) - } +func TestRepositoryOpen(t *testing.T) { + repoPath, cleanup := setupTestRepo(t) + defer cleanup() - // Commit - commit := &Commit{ - Tree: treeID, - Author: Ident{ - Name: []byte("Test Author"), - Email: []byte("test@example.com"), - WhenUnix: 1700000000, - OffsetMinutes: 0, - }, - Committer: Ident{ - Name: []byte("Test Author"), - Email: []byte("test@example.com"), - WhenUnix: 1700000000, - OffsetMinutes: 0, - }, - Message: []byte("Test commit message\n"), - } - commitID, err := repo.WriteLooseObject(commit) - if err != nil { - t.Fatalf("WriteLooseObject Commit error: %v", err) - } - readCommit, err := repo.ReadObject(commitID) + repo, err := OpenRepository(repoPath) if err != nil { - t.Fatalf("ReadObject Commit error: %v", err) - } - if rc, ok := readCommit.(*StoredCommit); !ok { - t.Fatalf("expected StoredCommit, got %T", readCommit) - } else if rc.Tree != treeID { - t.Fatalf("commit tree mismatch") + t.Fatalf("OpenRepository failed: %v", err) } + defer func() { _ = repo.Close() }() - // Tag - tag := &Tag{ - Target: commitID, - TargetType: ObjectTypeCommit, - Name: []byte("v1.0.0"), - Tagger: &Ident{ - Name: []byte("Test Tagger"), - Email: []byte("tagger@example.com"), - WhenUnix: 1700000000, - OffsetMinutes: 0, - }, - Message: []byte("Test tag message\n"), - } - tagID, err := repo.WriteLooseObject(tag) - if err != nil { - t.Fatalf("WriteLooseObject Tag error: %v", err) - } - readTag, err := repo.ReadObject(tagID) - if err != nil { - t.Fatalf("ReadObject Tag error: %v", err) + if repo.rootPath != repoPath { + t.Errorf("rootPath: got %q, want %q", repo.rootPath, repoPath) } - if rtag, ok := readTag.(*StoredTag); !ok { - t.Fatalf("expected StoredTag, got %T", readTag) - } else if rtag.Target != commitID { - t.Fatalf("tag target mismatch") + if repo.hashSize != 32 && repo.hashSize != 20 { + t.Errorf("hashSize: got %d, want 32 (SHA-256) or 20 (SHA-1)", repo.hashSize) } } -type packObjectEncoding uint8 - -const ( - packEncodingFull packObjectEncoding = iota - packEncodingOfsDelta - packEncodingRefDelta -) - -type testPackObject struct { - finalType ObjectType - body []byte - encoding packObjectEncoding - baseIndex int - baseHash Hash - baseBody []byte -} - -func writeTestPack(t *testing.T, root, name string, objs []testPackObject) []Hash { - t.Helper() - repo := &Repository{hashSize: testHashSize} - packDir := filepath.Join(root, "objects", "pack") - err := os.MkdirAll(packDir, 0o750) - if err != nil { - t.Fatalf("mkdir pack dir: %v", err) - } - - var buf bytes.Buffer - buf.Write([]byte{'P', 'A', 'C', 'K'}) - err = binary.Write(&buf, binary.BigEndian, uint32(packVersion2)) - if err != nil { - t.Fatalf("write pack version: %v", err) +func TestRepositoryOpenInvalid(t *testing.T) { + _, err := OpenRepository("/nonexistent/path") + if err == nil { + t.Fatal("expected error for nonexistent path") } - objCount := len(objs) - if objCount > math.MaxUint32 { - t.Fatalf("too many objects: %d", len(objs)) - } - count32 := uint32(objCount) //#nosec G115 - err = binary.Write(&buf, binary.BigEndian, count32) - if err != nil { - t.Fatalf("write pack count: %v", err) - } - - offsets := make([]uint64, len(objs)) - ids := make([]Hash, len(objs)) - - for i, obj := range objs { - offset := buf.Len() - if offset < 0 { - t.Fatalf("negative buffer length") - } - offsets[i] = uint64(offset) - header, err := headerForType(obj.finalType, obj.body) - if err != nil { - t.Fatalf("headerForType: %v", err) - } - raw := make([]byte, len(header)+len(obj.body)) - copy(raw, header) - copy(raw[len(header):], obj.body) - ids[i] = repo.computeRawHash(raw) - - switch obj.encoding { - case packEncodingFull: - buf.Write(encodePackHeader(obj.finalType, len(obj.body))) - buf.Write(compressBytes(t, obj.body)) - case packEncodingOfsDelta: - if obj.baseIndex < 0 || obj.baseIndex >= i { - t.Fatalf("invalid base index %d for ofs delta %d", obj.baseIndex, i) - } - buf.Write(encodePackHeader(ObjectTypeOfsDelta, len(obj.body))) - dist := offsets[i] - offsets[obj.baseIndex] - buf.Write(encodeOfsDistance(dist)) - baseBody := objs[obj.baseIndex].body - delta := buildInsertOnlyDelta(len(baseBody), obj.body) - buf.Write(compressBytes(t, delta)) - case packEncodingRefDelta: - if obj.baseHash == (Hash{}) { - t.Fatalf("ref delta %d missing base hash", i) - } - baseBody := obj.baseBody - if len(baseBody) == 0 { - t.Fatalf("ref delta %d missing base body", i) - } - buf.Write(encodePackHeader(ObjectTypeRefDelta, len(obj.body))) - buf.Write(obj.baseHash.data[:testHashSize]) - delta := buildInsertOnlyDelta(len(baseBody), obj.body) - buf.Write(compressBytes(t, delta)) - default: - t.Fatalf("unknown encoding %d", obj.encoding) - } - } - - packContent := append([]byte(nil), buf.Bytes()...) - packChecksum := repo.computeRawHash(packContent) - buf.Write(packChecksum.data[:testHashSize]) - packBytes := buf.Bytes() - - packPath := filepath.Join(packDir, name+".pack") - err = os.WriteFile(packPath, packBytes, 0o600) - if err != nil { - t.Fatalf("write pack file: %v", err) - } - - writeTestPackIndex(t, packDir, name, ids, offsets, packChecksum) - return ids } -func writeTestPackIndex(t *testing.T, packDir, name string, ids []Hash, offsets []uint64, packChecksum Hash) { - t.Helper() - repo := &Repository{hashSize: testHashSize} - type idxEntry struct { - id Hash - offset uint64 - } - entries := make([]idxEntry, len(ids)) - for i := range ids { - entries[i] = idxEntry{id: ids[i], offset: offsets[i]} - } - sort.Slice(entries, func(i, j int) bool { - return bytes.Compare(entries[i].id.data[:testHashSize], entries[j].id.data[:testHashSize]) < 0 - }) +func TestRepositoryClose(t *testing.T) { + repoPath, cleanup := setupTestRepo(t) + defer cleanup() - var buf bytes.Buffer - err := binary.Write(&buf, binary.BigEndian, uint32(idxMagic)) - if err != nil { - t.Fatalf("write idx magic: %v", err) - } - err = binary.Write(&buf, binary.BigEndian, uint32(idxVersion2)) + repo, err := OpenRepository(repoPath) if err != nil { - t.Fatalf("write idx version: %v", err) - } - - var fanout [256]uint32 - for _, entry := range entries { - first := int(entry.id.data[0]) - for i := first; i < len(fanout); i++ { - fanout[i]++ - } + t.Fatalf("OpenRepository failed: %v", err) } - for _, count := range fanout { - err = binary.Write(&buf, binary.BigEndian, count) - if err != nil { - t.Fatalf("write fanout: %v", err) - } - } - - for _, entry := range entries { - buf.Write(entry.id.data[:testHashSize]) - } - - buf.Write(make([]byte, len(entries)*4)) - for _, entry := range entries { - if entry.offset >= 0x80000000 { - t.Fatalf("offset too large for 32-bit table") - } - var word [4]byte - binary.BigEndian.PutUint32(word[:], uint32(entry.offset)) - buf.Write(word[:]) + if err := repo.Close(); err != nil { + t.Fatalf("Close failed: %v", err) } - idxData := append([]byte(nil), buf.Bytes()...) - idxChecksum := repo.computeRawHash(idxData) - buf.Write(packChecksum.data[:testHashSize]) - buf.Write(idxChecksum.data[:testHashSize]) - - idxPath := filepath.Join(packDir, name+".idx") - err = os.WriteFile(idxPath, buf.Bytes(), 0o600) - if err != nil { - t.Fatalf("write idx file: %v", err) - } -} - -func buildInsertOnlyDelta(srcLen int, dst []byte) []byte { - var buf bytes.Buffer - buf.Write(encodeVarint(srcLen)) - buf.Write(encodeVarint(len(dst))) - remaining := dst - for len(remaining) > 0 { - chunk := remaining - if len(chunk) > 127 { - chunk = remaining[:127] - } - buf.WriteByte(byte(len(chunk))) - buf.Write(chunk) - remaining = remaining[len(chunk):] - } - return buf.Bytes() -} - -func encodeOfsDistance(dist uint64) []byte { - if dist == 0 { - return []byte{0} - } - var out []byte - out = append(out, byte(dist&0x7f)) - for dist >>= 7; dist != 0; dist >>= 7 { - out = append(out, byte(((dist-1)&0x7f)|0x80)) - } - for i, j := 0, len(out)-1; i < j; i, j = i+1, j-1 { - out[i], out[j] = out[j], out[i] - } - return out -} - -func setupRepoConfig(t *testing.T, root string) { - var algo string - switch testHashSize { - case sha1.Size: - algo = "sha1" - case sha256.Size: - algo = "sha256" - default: - t.Fatalf("unsupported testHashSize: %d", testHashSize) - } - - cfg := fmt.Sprintf(` -[core] - repositoryformatversion = 0 -[extensions] - objectformat = %s -`, algo) - - err := os.WriteFile(filepath.Join(root, "config"), []byte(cfg), 0o644) - if err != nil { - t.Fatalf("write config: %v", err) + if err := repo.Close(); err != nil { + t.Fatalf("second Close failed: %v", err) } } |
