Artifact 0bbc61bca3433c10e8d81bd3e04e00e8066cfa555768d7152bb64381c5d745f4:
- File
integration_test.go
— part of check-in
[04f41a4c9c]
at
2026-07-28 22:08:51
on branch trunk
— fix(manifest): unify mlink derivation (#216)
Route direct check-ins and internal rebuilds through canonical mlink derivation. Update sparse-history consumers and preserve legacy conflict-fork rows with an explicit RID discriminator migration.\n\nCloses #213 (user: [email protected], size: 8189) [annotate] [blame] [check-ins using]
package libfossil_test import ( "bytes" "fmt" "os/exec" "path/filepath" "testing" "time" libfossil "github.com/danmestas/go-libfossil" "github.com/danmestas/go-libfossil/internal/blob" "github.com/danmestas/go-libfossil/internal/content" "github.com/danmestas/go-libfossil/internal/manifest" "github.com/danmestas/go-libfossil/internal/repo" _ "github.com/danmestas/go-libfossil/internal/testdriver" "github.com/danmestas/go-libfossil/simio" "github.com/danmestas/go-libfossil/testutil" ) func TestPhaseA_EndToEnd(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } fossilBin := testutil.RequireFossilBin(t) // 1. Create a repo with Go path := filepath.Join(t.TempDir(), "phase-a.fossil") r, err := repo.Create(path, "testuser", simio.CryptoRand{}, "") if err != nil { t.Fatalf("Create: %v", err) } defer r.Close() // 2. Store full-text blobs content1 := []byte("first blob with enough content to make deltas meaningful in testing") rid1, uuid1, err := blob.Store(r.DB(), content1) if err != nil { t.Fatalf("Store blob 1: %v", err) } content2 := []byte("second blob with entirely different content for variety") rid2, _, err := blob.Store(r.DB(), content2) if err != nil { t.Fatalf("Store blob 2: %v", err) } // 3. Store delta blob (against blob 1) content3 := []byte("first blob with MODIFIED content to make deltas meaningful in testing") rid3, _, err := blob.StoreDelta(r.DB(), content3, rid1) if err != nil { t.Fatalf("StoreDelta: %v", err) } // Also store a regular blob (for fossil rebuild compatibility) content4 := []byte("fourth blob without delta") _, _, err = blob.Store(r.DB(), content4) if err != nil { t.Fatalf("Store blob 4: %v", err) } // 4. Store phantom _, err = blob.StorePhantom(r.DB(), "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa") if err != nil { t.Fatalf("StorePhantom: %v", err) } // 5. Verify content retrieval got1, err := content.Expand(r.DB(), rid1) if err != nil { t.Fatalf("Expand rid1: %v", err) } if !bytes.Equal(got1, content1) { t.Fatalf("Expand rid1 mismatch") } got2, err := content.Expand(r.DB(), rid2) if err != nil { t.Fatalf("Expand rid2: %v", err) } if !bytes.Equal(got2, content2) { t.Fatalf("Expand rid2 mismatch") } // 6. Expand delta chain got3, err := content.Expand(r.DB(), rid3) if err != nil { t.Fatalf("Expand rid3 (delta): %v", err) } if !bytes.Equal(got3, content3) { t.Fatalf("Expand rid3 mismatch") } // 7. Verify blob integrity if err := content.Verify(r.DB(), rid1); err != nil { t.Fatalf("Verify rid1: %v", err) } if err := content.Verify(r.DB(), rid3); err != nil { t.Fatalf("Verify rid3: %v", err) } // 8. Verify Fossil CLI can read the repository r.Close() // Verify blob exists via SQL query query := fmt.Sprintf("SELECT uuid, size FROM blob WHERE uuid = '%s'", uuid1) cmd := exec.Command(fossilBin, "sql", "-R", path, query) out, err := cmd.Output() if err != nil { t.Fatalf("fossil sql: %v", err) } if len(out) == 0 { t.Fatalf("fossil sql: blob %s not found", uuid1) } // 9. fossil rebuild should process the repository // Note: Fossil 2.28 may segfault on repos with deltas created via direct DB writes // This is a known limitation - fossil rebuild works on fossil-generated repos cmd = exec.Command(fossilBin, "rebuild", path) out, err = cmd.CombinedOutput() if err != nil { // Log the error but don't fail - the important validation is that: // 1. We can create repos // 2. We can store/retrieve content correctly // 3. Fossil CLI can query the database t.Logf("fossil rebuild had issues (expected with manually-created deltas): %v\nOutput: %s", err, out) } else { t.Log("fossil rebuild succeeded") } } func TestPhaseA_FossilCreatedRepo(t *testing.T) { if testing.Short() { t.Skip("skipping integration test") } fossilBin := testutil.RequireFossilBin(t) path := filepath.Join(t.TempDir(), "fossil-created.fossil") exec.Command(fossilBin, "new", path).CombinedOutput() r, err := repo.Open(path) if err != nil { t.Fatalf("Open fossil-created: %v", err) } defer r.Close() var blobCount int r.DB().QueryRow("SELECT count(*) FROM blob").Scan(&blobCount) t.Logf("fossil-created repo has %d blobs", blobCount) } func TestPhaseB_Integration(t *testing.T) { testutil.RequireFossilBin(t) path := filepath.Join(t.TempDir(), "integration-b.fossil") r, err := repo.Create(path, "integration-user", simio.CryptoRand{}, "") if err != nil { t.Fatalf("Create: %v", err) } defer r.Close() // Create 3 checkins with accumulating file sets var lastRid libfossil.FslID rids := make([]libfossil.FslID, 3) // Commit 1: 2 files rids[0], _, err = manifest.Checkin(r, manifest.CheckinOpts{ Files: []manifest.File{ {Name: "file.txt", Content: []byte("v1")}, {Name: "file1.txt", Content: []byte("new-1")}, }, Comment: "commit 1", User: "integration-user", Time: time.Date(2024, 1, 15, 11, 0, 0, 0, time.UTC), }) if err != nil { t.Fatalf("Checkin 1: %v", err) } lastRid = rids[0] // Commit 2: 3 files (file.txt updated, file1.txt carried, file2.txt added) rids[1], _, err = manifest.Checkin(r, manifest.CheckinOpts{ Files: []manifest.File{ {Name: "file.txt", Content: []byte("v2")}, {Name: "file1.txt", Content: []byte("new-1")}, {Name: "file2.txt", Content: []byte("new-2")}, }, Comment: "commit 2", User: "integration-user", Parent: lastRid, Time: time.Date(2024, 1, 15, 12, 0, 0, 0, time.UTC), }) if err != nil { t.Fatalf("Checkin 2: %v", err) } lastRid = rids[1] // Commit 3: 4 files (all previous + file3.txt) rids[2], _, err = manifest.Checkin(r, manifest.CheckinOpts{ Files: []manifest.File{ {Name: "file.txt", Content: []byte("v3")}, {Name: "file1.txt", Content: []byte("new-1")}, {Name: "file2.txt", Content: []byte("new-2")}, {Name: "file3.txt", Content: []byte("new-3")}, }, Comment: "commit 3", User: "integration-user", Parent: lastRid, Time: time.Date(2024, 1, 15, 13, 0, 0, 0, time.UTC), }) if err != nil { t.Fatalf("Checkin 3: %v", err) } lastRid = rids[2] // Verify database structure var eventCount, plinkCount, mlinkCount int r.DB().QueryRow("SELECT count(*) FROM event WHERE type='ci'").Scan(&eventCount) r.DB().QueryRow("SELECT count(*) FROM plink").Scan(&plinkCount) r.DB().QueryRow("SELECT count(*) FROM mlink").Scan(&mlinkCount) if eventCount != 3 { t.Fatalf("event count = %d, want 3", eventCount) } if plinkCount != 2 { t.Fatalf("plink count = %d, want 2", plinkCount) } // Canonical sparse mlink count: 2 root, 2 second-checkin changes, // 2 third-checkin changes = 6 total. if mlinkCount != 6 { t.Fatalf("mlink count = %d, want 6", mlinkCount) } // Verify parent-child relationships via plink var pid libfossil.FslID r.DB().QueryRow("SELECT pid FROM plink WHERE cid=?", rids[1]).Scan(&pid) if pid != rids[0] { t.Fatalf("checkin 2 parent = %d, want %d", pid, rids[0]) } // Verify ListFiles works correctly for each commit files1, err := manifest.ListFiles(r, rids[0]) if err != nil { t.Fatalf("ListFiles rids[0]: %v", err) } if len(files1) != 2 { t.Fatalf("ListFiles rids[0] count = %d, want 2", len(files1)) } files2, err := manifest.ListFiles(r, rids[1]) if err != nil { t.Fatalf("ListFiles rids[1]: %v", err) } if len(files2) != 3 { t.Fatalf("ListFiles rids[1] count = %d, want 3", len(files2)) } files3, err := manifest.ListFiles(r, rids[2]) if err != nil { t.Fatalf("ListFiles rids[2]: %v", err) } if len(files3) != 4 { t.Fatalf("ListFiles rids[2] count = %d, want 4", len(files3)) } // Verify Log traverses the chain entries, err := manifest.Log(r, manifest.LogOpts{Start: lastRid}) if err != nil { t.Fatalf("Log: %v", err) } if len(entries) != 3 { t.Fatalf("Log count = %d, want 3", len(entries)) } if entries[0].Comment != "commit 3" || entries[2].Comment != "commit 1" { t.Fatalf("Log order incorrect: %q, %q, %q", entries[0].Comment, entries[1].Comment, entries[2].Comment) } // Verify fossil rebuild processes without errors r.Close() if err := testutil.FossilRebuild(path); err != nil { t.Fatalf("rebuild: %v", err) } }