Skip to content
go-libfossil Artifact [72b5613936]
Login

Artifact [72b5613936]

Artifact 72b5613936c4ae2f382935010e05afc6c9ff7389882419db17fc53605aaed134:


package cli

import (
	"context"
	"io"
	"net/http"
	"net/http/httptest"
	"os"
	"path/filepath"
	"strings"
	"sync/atomic"
	"testing"

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

// A real byte-protocol HTTP peer must satisfy required structure even when
// optional autosync is disabled; ordinary check-in stdout stays unchanged.
func TestRepoCiRequiredContentNoSync(t *testing.T) {
	source := requiredHTTPSource(t)
	var requests atomic.Int64
	server := requiredHTTPServer(t, source, &requests)
	defer server.Close()
	path := filepath.Join(t.TempDir(), "target.fossil")
	target, _, err := libfossil.Clone(context.Background(), path,
		libfossil.NewHTTPTransport(server.URL), libfossil.CloneOpts{PullPhantoms: true})
	if err != nil {
		t.Fatal(err)
	}
	parent, err := target.ResolveVersion("tip")
	if err != nil {
		t.Fatal(err)
	}
	coverage := requiredHTTPElide(t, target, parent)
	dir := t.TempDir()
	co, err := target.CreateCheckout(dir, libfossil.CheckoutCreateOpts{Cone: coverage})
	if err != nil {
		t.Fatal(err)
	}
	if err := co.Extract(parent, libfossil.ExtractOpts{}); err != nil {
		t.Fatal(err)
	}
	name := filepath.Join(dir, coverage.Include()[0])
	if err := os.WriteFile(name, []byte("edited"), 0o644); err != nil {
		t.Fatal(err)
	}
	if err := co.Close(); err != nil {
		t.Fatal(err)
	}
	if err := target.SetConfig("last-sync-url", server.URL); err != nil {
		t.Fatal(err)
	}
	if err := target.Close(); err != nil {
		t.Fatal(err)
	}
	requests.Store(0)
	output := requiredHTTPStdout(t, func() error {
		return (&RepoCiCmd{NoSync: true, Message: "required recovery", User: "test", Dir: dir}).Run(
			&Globals{Repo: path})
	})
	if requests.Load() == 0 {
		t.Fatal("--no-sync suppressed required recovery")
	}
	if !strings.HasPrefix(output, "checkin ") || strings.Count(output, "\n") != 1 {
		t.Fatalf("requests=%d, stdout=%q", requests.Load(), output)
	}
}

func requiredHTTPServer(
	t *testing.T, source *libfossil.Repo, requests *atomic.Int64,
) *httptest.Server {
	t.Helper()
	handler := source.XferHandler()
	return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		requests.Add(1)
		handler.ServeHTTP(w, r)
	}))
}

func requiredHTTPStdout(t *testing.T, run func() error) string {
	t.Helper()
	file, err := os.CreateTemp(t.TempDir(), "stdout")
	if err != nil {
		t.Fatal(err)
	}
	defer func() {
		if err := file.Close(); err != nil {
			t.Error(err)
		}
	}()
	previous := os.Stdout
	os.Stdout = file
	defer func() { os.Stdout = previous }()
	if err := run(); err != nil {
		t.Fatal(err)
	}
	if _, err := file.Seek(0, io.SeekStart); err != nil {
		t.Fatal(err)
	}
	output, err := io.ReadAll(file)
	if err != nil {
		t.Fatal(err)
	}
	return string(output)
}

func requiredHTTPSource(t *testing.T) *libfossil.Repo {
	t.Helper()
	r, err := libfossil.Create(filepath.Join(t.TempDir(), "source.fossil"),
		libfossil.CreateOpts{User: "test"})
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() {
		if err := r.Close(); err != nil {
			t.Error(err)
		}
	})
	for key, value := range map[string]string{
		manifest.SettingEnableSubmanifests: "on", manifest.SettingSubmanifestK: "1",
	} {
		if err := r.SetConfig(key, value); err != nil {
			t.Fatal(err)
		}
	}
	var files []libfossil.FileToCommit
	for _, name := range []string{"f0000", "f0003", "f0004", "f0005", "f0006", "f0008",
		"f0010", "f0014", "f0015", "f0016", "f0017", "f0018", "f0019", "f0021",
		"f0022", "f0023", "f0024"} {
		files = append(files, libfossil.FileToCommit{Name: name, Content: []byte(name)})
	}
	if _, _, err := r.Commit(libfossil.CommitOpts{Files: files, User: "test"}); err != nil {
		t.Fatal(err)
	}
	return r
}

func requiredHTTPElide(t *testing.T, r *libfossil.Repo, parent int64) *libfossil.Cone {
	t.Helper()
	d, err := manifest.GetManifest(r.Inner(), fsltype.FslID(parent))
	if err != nil {
		t.Fatal(err)
	}
	if len(d.S) < 2 {
		t.Fatal("fixture requires actual sibling structural nodes")
	}
	rid, held := content.AvailableByUUID(r.Inner().DB(), d.S[0].UUID)
	if !held {
		t.Fatal("fixture first subtree unavailable")
	}
	data, err := content.Expand(r.Inner().DB(), rid)
	if err != nil {
		t.Fatal(err)
	}
	leaf, err := deck.Parse(data)
	if err != nil {
		t.Fatal(err)
	}
	var names []string
	for _, file := range leaf.F {
		names = append(names, file.Name)
	}
	coverage, err := libfossil.NewCone(names, nil)
	if err != nil {
		t.Fatal(err)
	}
	if err := r.SetStoreCone(coverage); err != nil {
		t.Fatal(err)
	}
	for _, sub := range d.S[1:] {
		if _, err := r.Inner().DB().Exec(
			"UPDATE blob SET size=-1,content=NULL WHERE uuid=?", sub.UUID); err != nil {
			t.Fatal(err)
		}
		if _, err := r.Inner().DB().Exec(
			"INSERT OR IGNORE INTO phantom SELECT rid FROM blob WHERE uuid=?", sub.UUID); err != nil {
			t.Fatal(err)
		}
	}
	return coverage
}