Skip to content
go-libfossil Artifact [982b5905cc]
Login

Artifact [982b5905cc]

Artifact 982b5905cc74d61bd17813db67b7c1cb067f35350f27a8e490f08669c891a5c1:


package checkout

import (
	"context"
	"errors"
	"os"
	"path/filepath"
	"strings"
	"testing"

	"github.com/danmestas/go-libfossil/internal/manifest"
)

func TestUpdateRequiredFilesDoesNotMutate(t *testing.T) {
	f := newSparseRepo(t, true)
	remote := f.commit(t, maps(sparseFiles, map[string]string{
		"src/a.c": "incoming\n", "tools/t.c": "outside incoming\n",
	}), f.c1)
	co := f.open(t, sparseCone(t), f.c1)
	files, err := manifest.ListFiles(f.repo, remote)
	if err != nil {
		t.Fatal(err)
	}
	var wanted string
	for _, file := range files {
		if file.Name == "src/a.c" || file.Name == "tools/t.c" {
			if file.Name == "src/a.c" {
				wanted = file.UUID
			}
			_, err := f.repo.DB().Exec(
				"UPDATE blob SET size=-1,content=NULL WHERE uuid=?", file.UUID,
			)
			if err != nil {
				t.Fatal(err)
			}
		}
	}
	before, err := co.RecoverySnapshot()
	if err != nil {
		t.Fatal(err)
	}
	target, needs, err := co.UpdateRequiredFiles(context.Background(), UpdateOpts{TargetRID: remote})
	if err != nil || target != remote {
		t.Fatalf("update readiness target = %d, %v", target, err)
	}
	found := false
	for _, file := range needs {
		if !co.covers(file.Name) {
			t.Fatalf("update requested excluded payload: %+v", file)
		}
		found = found || file.UUID == wanted
	}
	if !found {
		t.Fatal("update readiness omitted unavailable covered target")
	}
	after, err := co.RecoverySnapshot()
	if err != nil || before != after {
		t.Fatalf("update readiness mutated checkout: %q != %q: %v", before, after, err)
	}
}

func TestSparseUpdateDoesNotRequireExcludedPayload(t *testing.T) {
	f := newSparseRepo(t, false)
	remote := f.commit(t, maps(sparseFiles, map[string]string{"tools/t.c": "elided incoming\n"}), f.c1)
	co := f.open(t, sparseCone(t), f.c1)
	files, err := manifest.ListFiles(f.repo, remote)
	if err != nil {
		t.Fatal(err)
	}
	for _, file := range files {
		if file.Name == "tools/t.c" {
			_, err := f.repo.DB().Exec(
				"UPDATE blob SET size=-1,content=NULL WHERE uuid=?", file.UUID,
			)
			if err != nil {
				t.Fatal(err)
			}
		}
	}
	if _, err := co.Update(UpdateOpts{TargetRID: remote}); err != nil {
		t.Fatalf("record-only update demanded outside payload: %v", err)
	}
	if _, err := os.Stat(filepath.Join(co.Dir(), "tools/t.c")); !os.IsNotExist(err) {
		t.Fatalf("update materialized excluded path: %v", err)
	}
}

func TestUpdateRequiredFilesIncludesMergeBase(t *testing.T) {
	f := newSparseRepo(t, false)
	remote := f.commit(t, maps(sparseFiles, map[string]string{"src/a.c": "incoming\n"}), f.c1)
	co := f.open(t, sparseCone(t), f.c1)
	name := filepath.Join(co.Dir(), "src/a.c")
	if err := os.WriteFile(name, []byte("local edit\n"), 0o644); err != nil {
		t.Fatal(err)
	}
	before, err := co.RecoverySnapshot()
	if err != nil {
		t.Fatal(err)
	}
	base, err := manifest.ListFiles(f.repo, f.c1)
	if err != nil {
		t.Fatal(err)
	}
	_, needs, err := co.UpdateRequiredFiles(context.Background(), UpdateOpts{TargetRID: remote})
	if err != nil {
		t.Fatal(err)
	}
	for _, file := range base {
		if file.Name != "src/a.c" {
			continue
		}
		found := false
		for _, need := range needs {
			found = found || need.UUID == file.UUID
		}
		if !found {
			t.Fatal("update readiness omitted local merge base")
		}
	}
	after, err := co.RecoverySnapshot()
	if err != nil || before != after {
		t.Fatalf("readiness stored local scan: %q != %q: %v", before, after, err)
	}
}

func TestUpdateContextCancellationRollsBackFiles(t *testing.T) {
	f := newSparseRepo(t, false)
	remote := f.commit(t, maps(sparseFiles, map[string]string{"src/a.c": "incoming\n"}), f.c1)
	co := f.open(t, sparseCone(t), f.c1)
	name := filepath.Join(co.Dir(), "src/a.c")
	beforeBytes, err := os.ReadFile(name)
	if err != nil {
		t.Fatal(err)
	}
	before, err := co.RecoverySnapshot()
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	called := false
	_, err = co.UpdateContext(ctx, UpdateOpts{
		TargetRID: remote,
		Callback: func(string, UpdateChange) error {
			called = true
			cancel()
			return nil
		},
	})
	if !called || !errors.Is(err, context.Canceled) {
		t.Fatalf("canceled update callback=%v, error=%v", called, err)
	}
	afterBytes, err := os.ReadFile(name)
	if err != nil || string(beforeBytes) != string(afterBytes) {
		t.Fatalf("canceled update changed bytes: %q != %q: %v", beforeBytes, afterBytes, err)
	}
	after, err := co.RecoverySnapshot()
	if err != nil || before != after {
		t.Fatalf("canceled update changed metadata: %q != %q: %v", before, after, err)
	}
}

func TestUpdateContextCancellationRestoresBackup(t *testing.T) {
	f := newSparseRepo(t, false)
	remote := f.commit(t, maps(sparseFiles, map[string]string{"src/new.c": "incoming\n"}), f.c1)
	co := f.open(t, sparseCone(t), f.c1)
	original := map[string]string{
		"src/new.c": "untracked\n", "src/new.c-original": "existing backup\n",
	}
	for name, data := range original {
		if err := os.WriteFile(filepath.Join(co.Dir(), name), []byte(data), 0o644); err != nil {
			t.Fatal(err)
		}
	}
	ctx, cancel := context.WithCancel(context.Background())
	defer cancel()
	_, err := co.UpdateContext(ctx, UpdateOpts{
		TargetRID: remote,
		Callback:  func(string, UpdateChange) error { cancel(); return nil },
	})
	if !errors.Is(err, context.Canceled) {
		t.Fatalf("canceled update = %v", err)
	}
	for name, want := range original {
		data, err := os.ReadFile(filepath.Join(co.Dir(), name))
		if err != nil || string(data) != want {
			t.Fatalf("rollback %s = %q, %v; want %q", name, data, err, want)
		}
	}
}

func TestUpdateContextFinalizationFailureRestoresFiles(t *testing.T) {
	f := newSparseRepo(t, false)
	remote := f.commit(t, maps(sparseFiles, map[string]string{"src/a.c": "incoming\n"}), f.c1)
	co := f.open(t, sparseCone(t), f.c1)
	name := filepath.Join(co.Dir(), "src/a.c")
	beforeBytes, err := os.ReadFile(name)
	if err != nil {
		t.Fatal(err)
	}
	before, err := co.RecoverySnapshot()
	if err != nil {
		t.Fatal(err)
	}
	if _, err := co.db.Exec(`CREATE TRIGGER reject_update BEFORE INSERT ON vvar
		WHEN NEW.name='checkout' BEGIN SELECT RAISE(ABORT, 'injected update failure'); END`); err != nil {
		t.Fatal(err)
	}
	_, err = co.UpdateContext(context.Background(), UpdateOpts{TargetRID: remote})
	if err == nil || !strings.Contains(err.Error(), "injected update failure") {
		t.Fatalf("update error = %v, want injected finalization failure", err)
	}
	afterBytes, err := os.ReadFile(name)
	if err != nil || string(beforeBytes) != string(afterBytes) {
		t.Fatalf("failed finalization changed bytes: %q != %q: %v", beforeBytes, afterBytes, err)
	}
	after, err := co.RecoverySnapshot()
	if err != nil || before != after {
		t.Fatalf("failed finalization changed metadata: %q != %q: %v", before, after, err)
	}
}

func TestUpdateContextCanceledBeforeMutation(t *testing.T) {
	f := newSparseRepo(t, false)
	remote := f.commit(t, maps(sparseFiles, map[string]string{"src/a.c": "incoming\n"}), f.c1)
	co := f.open(t, sparseCone(t), f.c1)
	before, err := co.RecoverySnapshot()
	if err != nil {
		t.Fatal(err)
	}
	ctx, cancel := context.WithCancel(context.Background())
	cancel()
	_, err = co.UpdateContext(ctx, UpdateOpts{TargetRID: remote})
	if !errors.Is(err, context.Canceled) {
		t.Fatalf("canceled update = %v", err)
	}
	after, err := co.RecoverySnapshot()
	if err != nil || before != after {
		t.Fatalf("canceled update mutated checkout: %q != %q: %v", before, after, err)
	}
}