Artifact eb8d0b74e7720c8a88e68586b049b9d26d9eba34668a1553e60261e06bf18251:
- File
dst/clone_test.go
— part of check-in
[f5744359d3]
at
2026-04-19 16:30:54
on branch trunk
— Initial open source release
libfossil is a pure-Go implementation of the Fossil SCM, reading and writing the same .fossil SQLite repository format as upstream Fossil.
This is the first public release of code that previously lived in a private repository. The module path is github.com/danmestas/libfossil and the binary is named libfossil to avoid $PATH collisions with upstream fossil.
Included: - Repo lifecycle (create, clone), checkout/checkin, timeline, merge, diff, annotate, shun, tag, branch, bisect, stash, undo/redo - Sync-over-HTTP protocol (client and handler) - Two SQLite drivers: modernc (pure Go, default) and ncruces (wasm-capable) - Observer interfaces (SyncObserver, CheckoutObserver) with an optional OpenTelemetry implementation in observer/otel (separate submodule) - Deterministic simulation testing harness with BUGGIFY fault injection - Kong-based embeddable CLI under cli/ and a standalone binary under cmd/libfossil/ (user: [email protected], size: 4047) [annotate] [blame] [check-ins using]
package dst import ( "context" "fmt" "math/rand" "os" "path/filepath" "testing" "github.com/danmestas/libfossil/db" "github.com/danmestas/libfossil/internal/blob" "github.com/danmestas/libfossil/internal/repo" libsync "github.com/danmestas/libfossil/internal/sync" "github.com/danmestas/libfossil/simio" _ "github.com/danmestas/libfossil/internal/testdriver" ) // runCloneDST exercises Clone under deterministic fault injection. // // 1. Create server repo, seed with 50 blobs (deterministic content from seed). // 2. Create MockFossil with server-side SeededBuggify (seed+1000). // 3. Call sync.Clone() with client-side SeededBuggify (seed+2000). // 4. On success: verify blob convergence + no phantoms. // 5. On failure: verify repo file deleted (error cleanup). func runCloneDST(t *testing.T, seed int64) { t.Helper() // 1. Create and seed the server repo. serverPath := filepath.Join(t.TempDir(), "server.fossil") serverRepo, err := repo.Create(serverPath, "server", simio.CryptoRand{}) if err != nil { t.Fatalf("seed %d: repo.Create server: %v", seed, err) } t.Cleanup(func() { serverRepo.Close() }) rng := rand.New(rand.NewSource(seed)) seededUUIDs := make([]string, 0, 50) for i := range 50 { size := 64 + rng.Intn(512) data := make([]byte, size) rng.Read(data) var uuid string err := serverRepo.WithTx(func(tx *db.Tx) error { _, u, err := blob.Store(tx, data) if err != nil { return err } uuid = u return nil }) if err != nil { t.Fatalf("seed %d: store blob %d: %v", seed, i, err) } seededUUIDs = append(seededUUIDs, uuid) } // 2. Wire up MockFossil with server-side buggify. mf := NewMockFossil(serverRepo) serverBuggify := &SeededBuggify{rng: rand.New(rand.NewSource(seed + 1000))} mf.SetBuggify(serverBuggify) // 3. Clone with client-side buggify. clonePath := filepath.Join(t.TempDir(), "clone.fossil") clientBuggify := &SeededBuggify{rng: rand.New(rand.NewSource(seed + 2000))} ctx := context.Background() cloneRepo, result, cloneErr := libsync.Clone(ctx, clonePath, mf, libsync.CloneOpts{ Buggify: clientBuggify, }) if cloneErr != nil { // 5. On failure: verify repo file was cleaned up. t.Logf("seed %d: clone failed (expected under buggify): %v", seed, cloneErr) if _, statErr := os.Stat(clonePath); !os.IsNotExist(statErr) { t.Errorf("seed %d: clone file should be deleted on error, stat returned: %v", seed, statErr) } return } // 4. On success: verify convergence. t.Cleanup(func() { cloneRepo.Close() }) t.Logf("seed %d: clone OK — %d rounds, %d blobs received", seed, result.Rounds, result.BlobsRecvd) // Verify blob convergence. Under buggify, dropFile can silently // discard received blobs, so a "successful" clone may have gaps. // We log missing blobs and only fail if zero blobs arrived. missing := 0 for _, uuid := range seededUUIDs { _, exists := blob.Exists(cloneRepo.DB(), uuid) if !exists { missing++ } } if missing > 0 { t.Logf("seed %d: %d/%d blobs missing (expected under buggify dropFile)", seed, missing, len(seededUUIDs)) } if result.BlobsRecvd == 0 { t.Errorf("seed %d: clone reported success but received 0 blobs", seed) } // Check phantom table — under buggify, residual phantoms are possible // when dropFile discards a blob that the protocol considers delivered. var phantomCount int row := cloneRepo.DB().QueryRow("SELECT COUNT(*) FROM phantom") if err := row.Scan(&phantomCount); err != nil { t.Fatalf("seed %d: query phantom count: %v", seed, err) } if phantomCount > 0 { t.Logf("seed %d: %d residual phantoms (expected under buggify)", seed, phantomCount) } } // TestCloneDST runs a single deterministic clone with fault injection. func TestCloneDST(t *testing.T) { runCloneDST(t, 42) } // TestCloneDSTSeedSweep runs 20 seeds to explore diverse fault combinations. func TestCloneDSTSeedSweep(t *testing.T) { for seed := int64(0); seed < 20; seed++ { t.Run(fmt.Sprintf("seed_%d", seed), func(t *testing.T) { runCloneDST(t, seed) }) } }