Skip to content
go-libfossil Artifact [0d540f2fb4]
Login

Artifact [0d540f2fb4]

Artifact 0d540f2fb4a1fea6c924d322327db205c3f67113519503cf2cab10b3d64c6eee:


package conformance

import (
	"context"
	"fmt"
	"path/filepath"
	"strings"
	"testing"

	"github.com/danmestas/go-libfossil/internal/content"
	"github.com/danmestas/go-libfossil/internal/hash"
	"github.com/danmestas/go-libfossil/internal/repo"
	"github.com/danmestas/go-libfossil/internal/sync"
	"github.com/danmestas/go-libfossil/internal/xfer"
)

// scOneArtifactRounds shrinks every clone batch to one artifact, so each
// check-in and each artifact its walk reaches lands in a round of its own.
type scOneArtifactRounds struct{}

func (scOneArtifactRounds) Check(site string, _ float64) bool {
	return site == "handler.emitCloneBatch.smallBatch"
}

func scProtocolParentDiffFlat(t *testing.T) error {
	return scProtocolParentDiff(t, newSCSyncRepo(t, scSyncFiles()), scSyncFiles())
}

func scProtocolParentDiffTree(t *testing.T) error {
	return scProtocolParentDiff(t, newSCSyncTreeRepo(t), scSyncTreeFiles())
}

// A check-in sends only what its primary parent's tree does not hold: an
// unchanged in-cone file crosses the wire once however many check-ins name
// it, while a check-in whose walk outgrows a round resumes in the next.
func scProtocolParentDiff(t *testing.T, origin *scRepo, base map[string]string) error {
	parent := origin.c1
	for i := 0; i < 3; i++ {
		base[fmt.Sprintf("src/v%d", i)] = fmt.Sprintf("version %d\n", i)
		base[fmt.Sprintf("zzz/v%d", i)] = fmt.Sprintf("outside %d\n", i)
		parent = origin.commit(t, base, parent)
	}
	peer := &scSyncPeer{r: origin.r, opts: sync.HandleOpts{Buggify: scOneArtifactRounds{}}}
	r, _ := scSyncClone(t, peer, sync.CloneOpts{StoreCone: scCone(t, []string{"src"}, nil)})
	if err := scNoTransit(r); err != nil {
		return err
	}
	unchanged := scTextUUID(origin.r, "inside\n")
	sent, resumed := 0, false
	for _, resp := range peer.replies {
		if _, ok := scSyncPayloads(resp)[unchanged]; ok {
			sent++
		}
		for _, card := range resp.Cards {
			if c, ok := card.(*xfer.CookieCard); ok && strings.Count(c.Value, ":") == 3 {
				resumed = true
			}
		}
	}
	if sent != 1 || !resumed {
		return fmt.Errorf("unchanged in-cone file sent %d times, walk resumed=%v; want once, true", sent, resumed)
	}
	for text, want := range map[string]bool{"inside\n": true, "version 2\n": true, "outside 2\n": false} {
		_, held, err := content.CheckAvailableByUUID(r.DB(), scTextUUID(origin.r, text))
		if err != nil || held != want {
			return fmt.Errorf("holding %q = %v, want %v (error %v)", text, held, want, err)
		}
	}
	return nil
}

// scTextUUID names text as origin stored it, under either hash.
func scTextUUID(origin *repo.Repo, text string) string {
	uuid := hash.SHA3([]byte(text))
	if _, ok := content.AvailableByUUID(origin.DB(), uuid); ok {
		return uuid
	}
	return hash.SHA1([]byte(text))
}

// Committing deltifies each manifest against its successor, so the first
// check-in's chain puts every later manifest on the wire ahead of its row.
// Each still walks its tree: without pulling phantoms afterwards, the clone
// holds every check-in's in-cone files.
func scProtocolDeltaManifestFlat(t *testing.T) error {
	return scProtocolDeltaManifestWalk(t, newSCSyncRepo(t, scSyncFiles()), scSyncFiles())
}

func scProtocolDeltaManifestTree(t *testing.T) error {
	return scProtocolDeltaManifestWalk(t, newSCSyncTreeRepo(t), scSyncTreeFiles())
}

func scProtocolDeltaManifestWalk(t *testing.T, origin *scRepo, files map[string]string) error {
	parent := origin.c1
	for i := 0; i < 3; i++ {
		files["src/a"] = fmt.Sprintf("inside v%d\n", i)
		parent = origin.commit(t, files, parent)
	}
	peer := &scSyncPeer{r: origin.r}
	r, _ := scSyncClone(t, peer, sync.CloneOpts{StoreCone: scCone(t, []string{"src"}, nil)})
	if err := scNoTransit(r); err != nil {
		return err
	}
	for i := 0; i < 3; i++ {
		text := fmt.Sprintf("inside v%d\n", i)
		if _, held, err := content.CheckAvailableByUUID(r.DB(), scTextUUID(origin.r, text)); err != nil || !held {
			return fmt.Errorf("holding %q = %v (error %v), want held", text, held, err)
		}
	}
	return nil
}

// scNoTransit holds a clone made without pulling phantoms to the walk alone:
// whatever the cone needs arrived in the clone rounds themselves.
func scNoTransit(r *repo.Repo) error {
	a, err := content.LoadAbsences(r.DB())
	if err != nil {
		return err
	}
	if n := a.Count(content.Transit); n != 0 {
		return fmt.Errorf("clone rounds left %d transit phantoms", n)
	}
	return nil
}

// scCapOnlyPeer echoes only the snapshot bound of a clone-cap cookie, as a
// client predating the sparse walk's progress fields might.
type scCapOnlyPeer struct{ *scSyncPeer }

func (p scCapOnlyPeer) Exchange(ctx context.Context, req *xfer.Message) (*xfer.Message, error) {
	cards := make([]xfer.Card, 0, len(req.Cards))
	for _, card := range req.Cards {
		if c, ok := card.(*xfer.CookieCard); ok && strings.HasPrefix(c.Value, "lf-clone-cap:") {
			fields := strings.SplitN(c.Value, ":", 3)
			card = &xfer.CookieCard{Value: fields[0] + ":" + fields[1]}
		}
		cards = append(cards, card)
	}
	return p.scSyncPeer.Exchange(ctx, &xfer.Message{Cards: cards})
}

// A client that drops the walk's progress still converges: the round that
// returns to a part-walked check-in steps past it, and its next pull fetches
// what the walk still owed by name.
func scProtocolResumeDropped(t *testing.T) error {
	origin := newSCSyncRepo(t, scSyncFiles())
	files := scSyncFiles()
	for i := 0; i < 4; i++ {
		files[fmt.Sprintf("src/n%d", i)] = fmt.Sprintf("new %d\n", i)
	}
	origin.commit(t, files, origin.c1)
	peer := scCapOnlyPeer{&scSyncPeer{r: origin.r, opts: sync.HandleOpts{Buggify: scOneArtifactRounds{}}}}
	r, _, err := sync.Clone(context.Background(), filepath.Join(t.TempDir(), "clone.fossil"), peer,
		sync.CloneOpts{StoreCone: scCone(t, []string{"src"}, nil), PullPhantoms: true})
	if err != nil {
		return err
	}
	defer r.Close()
	for i := 0; i < 4; i++ {
		if err := scSyncHolding(r, fmt.Sprintf("new %d\n", i), true); err != nil {
			return err
		}
	}
	return scSyncHolding(r, "outside\n", false)
}