Skip to content
go-libfossil Artifact [ea24dfe546]
Login

Artifact [ea24dfe546]

Artifact ea24dfe546b32e066a701381b8698de5333990cbd3c60ec7091d427a48d2f9f4:


package conformance

import (
	"context"
	"errors"
	"net/http/httptest"
	"path/filepath"
	"slices"
	"strings"
	"testing"
	"time"

	gofossil "github.com/danmestas/go-libfossil"
	"github.com/danmestas/go-libfossil/db"

	"github.com/danmestas/go-libfossil/internal/bisect"
	"github.com/danmestas/go-libfossil/internal/branch"
	"github.com/danmestas/go-libfossil/internal/cone"
	"github.com/danmestas/go-libfossil/internal/content"
	"github.com/danmestas/go-libfossil/internal/deck"
	libfossil "github.com/danmestas/go-libfossil/internal/fsltype"
	"github.com/danmestas/go-libfossil/internal/hash"
	"github.com/danmestas/go-libfossil/internal/horizon"
	"github.com/danmestas/go-libfossil/internal/horizon/seed"
	"github.com/danmestas/go-libfossil/internal/merge"
	"github.com/danmestas/go-libfossil/internal/repo"
	"github.com/danmestas/go-libfossil/internal/sync"
	"github.com/danmestas/go-libfossil/internal/tag"
	"github.com/danmestas/go-libfossil/internal/xfer"
)

// The merge-base query and deepen (draft-fossil-horizon-00 Appendix A step
// 6: §5.5, §5.7, §5.8, §5.9, §6.2.4) over hzStruct: go-libfossil's handler
// is the origin, its structural clone the client.

func horizonDeepenCases() []Case {
	return []Case{horizonMergeBaseQuery, horizonMergeBaseBound, horizonDeepen, horizonDeepenPoint, horizonDeepenFailed,
		horizonMergeCrossing, horizonMergeDeepen, horizonPushCheck, horizonSeedVerify,
		horizonBisectCrossing}
}

var (
	horizonMergeBaseQuery = Case{ID: "horizon.6.2.4.merge-base", Req: "HZ-6.2-7"}
	horizonMergeBaseBound = Case{ID: "horizon.8.merge-base-bound", Req: "HZ-8-1"}
	horizonDeepen         = Case{ID: "horizon.5.8.deepen", Req: "HZ-5.8-1"}
	horizonDeepenPoint    = Case{ID: "horizon.5.8.deepen-point", Req: "HZ-5.8-1"}
	horizonDeepenFailed   = Case{ID: "horizon.5.8.deepen-failed", Req: "HZ-5.8-1"}
	horizonMergeCrossing  = Case{ID: "horizon.5.5.merge-crossing", Req: "HZ-5.5-1"}
	horizonMergeDeepen    = Case{ID: "horizon.5.5.merge-deepen", Req: "HZ-5.5-2"}
	horizonPushCheck      = Case{ID: "horizon.5.7.push-check", Req: "HZ-5.7-2"}
	horizonSeedVerify     = Case{ID: "horizon.5.9.verify-seed", Req: "HZ-5.9-1"}
	horizonBisectCrossing = Case{ID: "horizon.5.5.bisect-crossing", Req: "HZ-5.5-1"}
)

func TestHorizonDeepen(t *testing.T) {
	check(t, horizonMergeBaseQuery, func() error { return hzMergeBaseQuery(t) })
	check(t, horizonMergeBaseBound, func() error { return hzMergeBaseBound(t) })
	check(t, horizonDeepen, func() error { return hzDeepen(t) })
	check(t, horizonDeepenPoint, func() error { return hzDeepenPoint(t) })
	check(t, horizonDeepenFailed, func() error { return hzDeepenFailed(t) })
	check(t, horizonMergeCrossing, func() error { return hzMergeCrossing(t) })
	check(t, horizonMergeDeepen, func() error { return hzMergeDeepen(t) })
	check(t, horizonPushCheck, func() error { return hzPushCheck(t) })
	check(t, horizonSeedVerify, func() error { return hzSeedVerify(t) })
	check(t, horizonBisectCrossing, func() error { return hzBisectCrossing(t) })
}

// hzMergeBaseQuery: a structural clone asks its origin for the merge base of
// two check-ins, one below its horizon, and the origin answers from its own
// graph: the base, or - for check-ins sharing no ancestor.
func hzMergeBaseQuery(t *testing.T) error {
	x := newHZStruct(t)
	r, _, err := x.clone(t, x.peer())
	if err != nil {
		return err
	}
	for _, q := range [][3]string{{x.c4, x.feat1, x.c1}, {x.feat2, x.c4, x.c3}} {
		res, err := sync.Sync(context.Background(), r, x.peer(), sync.SyncOpts{Pull: true, MergeBaseOf: [2]string{q[0], q[1]}})
		if err != nil || !res.MergeBaseAnswered || res.MergeBase != q[2] {
			return expectf("base of %.10s and %.10s: %v answered=%v %q, want %.10s",
				q[0], q[1], err, res.MergeBaseAnswered, res.MergeBase, q[2])
		}
	}
	root := hzCommit(t, x.s.f, "unrelated", []libfossil.FslID{0}, nil)
	res, err := sync.Sync(context.Background(), r, x.peer(), sync.SyncOpts{Pull: true,
		MergeBaseOf: [2]string{x.c4, hzUUID(t, x.s.f.r, root)}})
	if err != nil || !res.MergeBaseAnswered || res.MergeBase != "" {
		return expectf("unrelated: %v answered=%v %q, want -", err, res.MergeBaseAnswered, res.MergeBase)
	}
	return nil
}

// hzMergeBaseBound: the walk behind an answer stops at its visited-node
// bound, as the origin's other graph walks do (§8).
func hzMergeBaseBound(t *testing.T) error {
	x := newHZStruct(t)
	if _, err := merge.FindCommonAncestorWithin(context.Background(), x.s.f.r, x.s.c4, x.s.feat1, 2); !errors.Is(err, merge.ErrBound) {
		return expectf("a walk past its bound: %v, want merge.ErrBound", err)
	}
	base, err := merge.FindCommonAncestorWithin(context.Background(), x.s.f.r, x.s.c4, x.s.feat1, 64)
	if err != nil || base != x.s.c1 {
		return expectf("within the bound: %d %v, want c1", base, err)
	}
	return nil
}

// hzDeepen: deepening a structural clone from C = c3 to its parent c2 moves
// the stored horizon, discards the seed for c3 before the synchronization
// (the request offers none), takes c2's manifest and the seed for c2, and
// replaces nothing held: K's check-ins derive densely, only c1 and feat1
// stay below.
func hzDeepen(t *testing.T) error {
	x := newHZStruct(t)
	r, _, err := x.clone(t, x.peer())
	if err != nil {
		return err
	}
	before, err := hzHeldNames(r)
	if err != nil {
		return err
	}
	peer := x.peer()
	res, err := sync.Deepen(context.Background(), r, peer, x.c2, sync.SyncOpts{})
	if err != nil || res.HorizonFallback || res.StructuralServed == 0 {
		return expectf("deepen to c2: %v %+v", err, res)
	}
	if err := hzStoredHorizon(r, x.c2, cone.DepthStructural); err != nil {
		return err
	}
	first := peer.sent()[0]
	if got := hzPragmas(first, "horizon-seed"); len(got) != 1 || got[0][0] != "-" {
		return expectf("the deepen's first request offers seed %q, want - (the seed for c3 discarded first)", got)
	}
	if got := hzPragmas(first, "horizon"); len(got) != 1 || got[0][0] != x.c2 {
		return expectf("the deepen's first request names horizon %q, want c2", got)
	}
	k, err := horizon.HeldSet(context.Background(), x.s.f.r.DB(), x.s.c2, cone.RuleDescendants)
	if err != nil {
		return err
	}
	_, want, err := seed.Mint(x.s.f.r.DB(), x.s.c2, k, seedLoginGo)
	if err != nil {
		return err
	}
	if got := mustHeld(t, r); string(got) != string(want) {
		return expectf("held seed after the deepen:\n%s\nwant the seed for c2:\n%s", got, want)
	}
	after, err := hzHeldNames(r)
	if err != nil {
		return err
	}
	for name := range before {
		if !after[name] {
			return expectf("the deepen dropped held artifact %.10s", name)
		}
	}
	if !after[x.c2] {
		return expectf("c2's manifest is not held after the deepen")
	}
	x.s.k = k
	if err := x.s.derivesDense(t, r); err != nil {
		return err
	}
	return hzElided(r, map[string]bool{x.c1: true, x.feat1: true})
}

// hzDeepenPoint: a deepen toward a check-in that is not an ancestor of C
// moves C to their merge base, so K only grows: feat1 lies off C's
// ancestry, the clone holds neither its manifest nor its ancestry, so it
// asks the origin, and C' is c1.
func hzDeepenPoint(t *testing.T) error {
	x := newHZStruct(t)
	r, _, err := x.clone(t, x.peer())
	if err != nil {
		return err
	}
	peer := x.peer()
	if _, err := sync.Deepen(context.Background(), r, peer, x.feat1, sync.SyncOpts{}); err != nil {
		return err
	}
	if err := hzStoredHorizon(r, x.c1, cone.DepthStructural); err != nil {
		return err
	}
	if got := hzPragmas(peer.sent()[0], "horizon-mergebase"); len(got) != 1 || got[0][0] != x.c3 || got[0][1] != x.feat1 {
		return expectf("the deepen asked %q, want the base of c3 and feat1", got)
	}
	return hzElided(r, nil)
}

// hzDeepenFailed: a deepen whose synchronization fails leaves the store as it
// was, the horizon and the seed for C both, as zit's does.
func hzDeepenFailed(t *testing.T) error {
	x := newHZStruct(t)
	r, _, err := x.clone(t, x.peer())
	if err != nil {
		return err
	}
	held := mustHeld(t, r)
	if _, err := sync.Deepen(context.Background(), r, hzFailingPeer{}, x.c2, sync.SyncOpts{}); err == nil {
		return expectf("a deepen through a failing peer succeeded")
	}
	if err := hzStoredHorizon(r, x.c3, cone.DepthStructural); err != nil {
		return err
	}
	if got := mustHeld(t, r); string(got) != string(held) {
		return expectf("the failed deepen left seed %q, want the seed for c3 back", got)
	}
	return x.s.derivesDense(t, r)
}

type hzFailingPeer struct{}

func (hzFailingPeer) Exchange(context.Context, *xfer.Message) (*xfer.Message, error) {
	return nil, errors.New("peer unreachable")
}

// hzStoredHorizon is nil when r's store cone carries a horizon at c, of the
// descendants rule, at depth d.
func hzStoredHorizon(r *repo.Repo, c string, d cone.HorizonDepth) error {
	sc, err := content.StoreCone(r.DB())
	if err != nil {
		return err
	}
	if sc == nil {
		return expectf("the store is dense")
	}
	h, ok := sc.Horizon()
	if !ok || h.Hash() != c || h.Rule() != cone.RuleDescendants || h.Depth() != d {
		return expectf("stored horizon %v %s %v %v, want %.10s descendants %v", ok, h.Hash(), h.Rule(), h.Depth(), c, d)
	}
	return nil
}

// hzHeldNames is every artifact r holds, by name.
func hzHeldNames(r *repo.Repo) (map[string]bool, error) {
	rows, err := r.DB().Query("SELECT uuid FROM blob WHERE size>=0")
	if err != nil {
		return nil, err
	}
	defer rows.Close()
	out := map[string]bool{}
	for rows.Next() {
		var u string
		if err := rows.Scan(&u); err != nil {
			return nil, err
		}
		out[u] = true
	}
	return out, rows.Err()
}

// hzMergeFixture is hzStruct's origin served over HTTP, with feat3, a child
// of feat1 committed after everything else; w and z, which merge feat3 into
// c4 and into feat2; and y, which merges feat1 into c4. Its client is a
// structural clone at C = c3 with a checkout at feat2.
//
// fossil's merge base (pivot.c), the most recent common ancestor, of w and z
// is feat3, below the horizon: the store holds w and z but not their base.
// That of y and feat2 is c3, which the store holds, but feat1, a common
// ancestor too, is below it: the store's walk crosses the horizon on the way.
type hzMergeFixture struct {
	x           *hzStruct
	feat3, w, z string
	y           string
	origin      *hzRecorder
	url         string
	r           *gofossil.Repo
	co          *gofossil.Checkout
}

func newHZMergeFixture(t *testing.T) *hzMergeFixture {
	t.Helper()
	x := newHZStruct(t)
	f := &hzMergeFixture{x: x, origin: &hzRecorder{r: x.s.f.r}}
	f.y = hzUUID(t, x.s.f.r, hzCommit(t, x.s.f, "y", []libfossil.FslID{x.s.c4, x.s.feat1}, nil))
	feat3 := hzCommit(t, x.s.f, "feat3", []libfossil.FslID{x.s.feat1}, nil)
	f.feat3 = hzUUID(t, x.s.f.r, feat3)
	f.w = hzUUID(t, x.s.f.r, hzCommit(t, x.s.f, "w", []libfossil.FslID{x.s.c4, feat3}, nil))
	f.z = hzUUID(t, x.s.f.r, hzCommit(t, x.s.f, "z", []libfossil.FslID{x.s.feat2, feat3}, nil))
	server := httptest.NewServer(f.origin)
	t.Cleanup(server.Close)
	f.url = server.URL
	hc, err := gofossil.NewHorizonCone(nil, nil, gofossil.Horizon{Checkin: x.c3, Structural: true})
	if err != nil {
		t.Fatal(err)
	}
	r, res, err := gofossil.Clone(context.Background(), filepath.Join(t.TempDir(), "structural.fossil"),
		gofossil.NewHTTPTransport(f.url), gofossil.CloneOpts{StoreCone: hc, PullPhantoms: true})
	if err != nil || res.HorizonFallback {
		t.Fatalf("structural clone: %v %+v", err, res)
	}
	t.Cleanup(func() { r.Close() })
	f.r = r
	co, err := r.CreateCheckout(t.TempDir(), gofossil.CheckoutCreateOpts{})
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { co.Close() })
	if err := co.Extract(f.rid(t, x.feat2), gofossil.ExtractOpts{}); err != nil {
		t.Fatal(err)
	}
	f.co = co
	f.origin.take()
	return f
}

func (f *hzMergeFixture) rid(t *testing.T, uuid string) int64 {
	t.Helper()
	rid, err := f.r.ResolveVersion(uuid)
	if err != nil {
		t.Fatal(err)
	}
	return rid
}

// hzMergeCrossing: a merge whose base lies below the structural horizon is
// detected before anything changes, and the diagnostic names the horizon and
// the remedy; so is a merge whose base the store holds when its walk to it
// crossed the horizon, where an older-looking ancestor below it could be the
// base. Neither names a base: the store cannot know it.
func hzMergeCrossing(t *testing.T) error {
	f := newHZMergeFixture(t)
	for _, m := range [][2]string{{f.z, f.w}, {f.x.feat2, f.y}} {
		if err := f.co.Extract(f.rid(t, m[0]), gofossil.ExtractOpts{}); err != nil {
			return err
		}
		_, err := f.co.Merge(gofossil.CheckoutMergeOpts{Version: f.rid(t, m[1])})
		if !errors.Is(err, gofossil.ErrBelowHorizon) {
			return expectf("merge of %.10s across the horizon: %v, want ErrBelowHorizon", m[1], err)
		}
		for _, want := range []string{"below the structural horizon at " + f.x.c3, "deepen"} {
			if !strings.Contains(err.Error(), want) {
				return expectf("diagnostic %q lacks %q", err, want)
			}
		}
		if v, _, err := f.co.Version(); err != nil || v != f.rid(t, m[0]) {
			return expectf("the refused merge moved the checkout to %d (%v)", v, err)
		}
		changed, err := f.co.HasChanges()
		if err != nil || changed {
			return expectf("the refused merge left changes %v (%v)", changed, err)
		}
	}
	return nil
}

// hzMergeDeepen: given a peer, the merge asks the origin for the base,
// deepens to a common ancestor of C and the base, and merges over the real
// base it then holds. Where the store holds the base the origin names, it
// merges over it without deepening.
func hzMergeDeepen(t *testing.T) error {
	f := newHZMergeFixture(t)
	peer := gofossil.RequiredContentOpts{Peer: gofossil.NewHTTPTransport(f.url)}
	res, err := f.co.MergeContext(context.Background(), gofossil.CheckoutMergeOpts{Version: f.rid(t, f.y)}, peer)
	if err != nil {
		return expectf("merge of y with a peer: %v", err)
	}
	if res.Ancestor != f.rid(t, f.x.c3) {
		return expectf("merged y against rid %d, want c3's", res.Ancestor)
	}
	if err := hzAskedMergeBase(f, [2]string{f.x.feat2, f.y}); err != nil {
		return err
	}
	if err := hzStoredHorizon(f.r.Inner(), f.x.c3, cone.DepthStructural); err != nil {
		return expectf("merging over a held base deepened: %v", err)
	}
	f = newHZMergeFixture(t)
	peer = gofossil.RequiredContentOpts{Peer: gofossil.NewHTTPTransport(f.url)}
	if err := f.co.Extract(f.rid(t, f.z), gofossil.ExtractOpts{}); err != nil {
		return err
	}
	res, err = f.co.MergeContext(context.Background(), gofossil.CheckoutMergeOpts{Version: f.rid(t, f.w)}, peer)
	if err != nil {
		return expectf("merge of w with a peer: %v", err)
	}
	if res.Ancestor != f.rid(t, f.feat3) {
		return expectf("merged w against rid %d, want feat3's", res.Ancestor)
	}
	if err := hzAskedMergeBase(f, [2]string{f.z, f.w}); err != nil {
		return err
	}
	sc, err := f.r.StoreCone()
	if err != nil {
		return err
	}
	if h, ok := sc.Horizon(); !ok || h.Checkin != f.x.c1 || !h.Structural {
		return expectf("store horizon after the merge %+v %v, want c1 structural", h, ok)
	}
	return nil
}

// hzAskedMergeBase is nil when the origin was asked the base of q first. A
// deepen may ask another, for its deepen point.
func hzAskedMergeBase(f *hzMergeFixture, q [2]string) error {
	var asked [][]string
	for _, e := range f.origin.take() {
		asked = append(asked, hzPragmas(e.request, "horizon-mergebase")...)
	}
	if len(asked) == 0 || !slices.Equal(asked[0], q[:]) {
		return expectf("the merge asked the origin %q, first the base of %.10s and %.10s", asked, q[0], q[1])
	}
	return nil
}

// hzPushCheck: the origin moves c4 to branch moved after a structural clone
// took its seed. The clone, re-seeded by nothing yet, creates a branch from
// c4 cancelling sym-trunk; the origin refuses the push, naming the stale seed
// and moved, and stores nothing. After a pull re-seeds the clone, the clone
// writes the control artifact cancelling sym-moved on its check-in and
// pushes again, and the origin admits both and derives the check-in on the
// new branch alone.
func hzPushCheck(t *testing.T) error {
	x := newHZStruct(t)
	r, _, err := x.clone(t, x.peer())
	if err != nil {
		return err
	}
	x.s.tag(t, x.s.c4, "branch", tag.TagPropagating, "moved")
	x.s.tag(t, x.s.c4, "sym-moved", tag.TagPropagating, "")
	x.s.tag(t, x.s.c4, "sym-trunk", tag.TagCancel, "")
	c4, err := hzRID(r, x.c4)
	if err != nil {
		return err
	}
	when := x.s.f.when.Add(24 * time.Hour)
	nb, _, err := branch.Create(r, branch.CreateOpts{Name: "nb", Parent: c4, User: "u", Time: when})
	if err != nil {
		return err
	}
	nbUUID := hzUUID(t, r, nb)
	res, err := sync.Sync(context.Background(), r, x.peer(), sync.SyncOpts{Push: true})
	if !hzRefusedStale(err, res, "'moved'") || !hzRefusedStale(err, res, "'trunk'") {
		return expectf("push of a stale cancellation: %v %+v", err, res)
	}
	if names, err := hzHeldNames(x.s.f.r); err != nil || names[nbUUID] {
		return expectf("the origin holds the refused check-in (%v)", err)
	}
	if res, err := sync.Sync(context.Background(), r, x.peer(), sync.SyncOpts{Pull: true}); err != nil || res.HorizonFallback {
		return expectf("re-sync: %v %+v", err, res)
	}
	if _, err := tag.AddTag(r, tag.TagOpts{TargetRID: nb, TagName: "sym-moved", TagType: tag.TagCancel,
		User: "u", Time: when.Add(time.Hour)}); err != nil {
		return err
	}
	res, err = sync.Sync(context.Background(), r, x.peer(), sync.SyncOpts{Push: true})
	if err != nil || len(res.Errors) != 0 {
		return expectf("push with the correction: %v %q", err, res.Errors)
	}
	got, err := hzTagState(x.s.f.r.DB(), nbUUID)
	if err != nil || !slices.Contains(got, "branch/2=nb") || slices.ContainsFunc(got, func(s string) bool {
		return strings.HasPrefix(s, "sym-moved") || strings.HasPrefix(s, "sym-trunk")
	}) {
		return expectf("the origin derives the corrected check-in as %v (%v)", got, err)
	}
	return nil
}

// hzSeedVerify: verification by deepening (§5.9) holds the real tag artifacts
// and primary-parent chains below C, derives every entry's tag state from
// them and compares it with what the seed asserted. An honest seed verifies;
// one whose hue was altered after delivery is reported at its entry. The
// store is left a content-horizon store holding no seed.
func hzSeedVerify(t *testing.T) error {
	x := newHZStruct(t)
	r, _, err := x.clone(t, x.peer())
	if err != nil {
		return err
	}
	report, err := sync.VerifySeed(context.Background(), r, x.peer(), sync.SyncOpts{})
	if err != nil || report.Checked == 0 || len(report.Mismatches) != 0 {
		return expectf("an honest seed: %v %+v", err, report)
	}
	if err := hzStoredHorizon(r, x.c3, cone.DepthContent); err != nil {
		return err
	}
	if held, err := seed.Held(r.DB()); err != nil || held != nil {
		return expectf("a verified store still holds a seed (%v)", err)
	}

	x = newHZStruct(t)
	r, _, err = x.clone(t, x.peer())
	if err != nil {
		return err
	}
	d, err := deck.Parse(mustHeld(t, r))
	if err != nil {
		return err
	}
	for i := range d.T {
		if d.T[i].Name == "hue" {
			d.T[i].Value = "green"
		}
	}
	forged, err := d.Marshal()
	if err != nil {
		return err
	}
	if err := r.WithTx(func(tx *db.Tx) error { return seed.Hold(tx, hash.SHA3(forged), forged, nil) }); err != nil {
		return err
	}
	report, err = sync.VerifySeed(context.Background(), r, x.peer(), sync.SyncOpts{})
	if err != nil {
		return err
	}
	if len(report.Mismatches) == 0 {
		return expectf("an altered seed verified: %+v", report)
	}
	for _, m := range report.Mismatches {
		if m.Tag != "hue" || m.Seeded != "*green" || m.Derived != "*red" {
			return expectf("mismatch %+v, want hue seeded *green, derived *red", m)
		}
	}
	return nil
}

// hzBisectCrossing: a bisection whose good end lies below the structural
// horizon is refused before it begins, naming the horizon and the deepen;
// one within K runs.
func hzBisectCrossing(t *testing.T) error {
	x := newHZStruct(t)
	r, _, err := x.clone(t, x.peer())
	if err != nil {
		return err
	}
	if _, err := r.DB().Exec("CREATE TABLE IF NOT EXISTS vvar(name TEXT PRIMARY KEY, value CLOB) WITHOUT ROWID"); err != nil {
		return err
	}
	rid := func(uuid string) libfossil.FslID {
		id, err := hzRID(r, uuid)
		if err != nil {
			t.Fatal(err)
		}
		return id
	}
	s := bisect.NewSession(r.DB().SqlDB())
	if err := s.MarkGood(rid(x.c2)); err != nil {
		return err
	}
	if err := s.MarkBad(rid(x.c4)); err != nil {
		return err
	}
	_, err = s.Next()
	if !errors.Is(err, content.ErrBelowHorizon) || !strings.Contains(err.Error(), "below the structural horizon at "+x.c3) ||
		!strings.Contains(err.Error(), "repo cone deepen "+x.c2) {
		return expectf("bisect from below the horizon: %v, want a diagnostic naming the horizon and the deepen", err)
	}
	if err := s.MarkGood(rid(x.c3)); err != nil {
		return err
	}
	if _, err := s.Next(); err != nil && !errors.Is(err, bisect.ErrBisectComplete) {
		return expectf("bisect within K: %v", err)
	}
	return nil
}