Skip to content
quarry Artifact [ca38cbca5c]
Login

Artifact [ca38cbca5c]

Artifact ca38cbca5ca2f75ba428bb8c7ebc301df758afd89b11a06f2f0f8f01fc023052:


package api

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

	"craftdesign.group/quarry/internal/artifact"
	"craftdesign.group/quarry/internal/index"
	"craftdesign.group/quarry/internal/ops"
	"craftdesign.group/quarry/internal/zitcli"
)

// A check-in past largeDiffLines is answered by its file list and each
// file's counts, with no hunks computed; file= narrows zit's diff to
// that one path; unified=true is still the whole patch, for a script.
func TestALargeCheckinIsDiffedFileByFile(t *testing.T) {
	if _, err := exec.LookPath("zit"); err != nil {
		t.Skip("zit not on PATH")
	}
	dir := t.TempDir()
	zrun(t, dir, "init")
	zwrite(t, dir, "a.txt", "one\n")
	zwrite(t, dir, "b.txt", "two\n")
	zrun(t, dir, "add", "a.txt", "b.txt")
	zrun(t, dir, "commit", "-m", "first", "--user", "u")
	base := ztip(t, dir, "trunk")
	zwrite(t, dir, "a.txt", "one\nmore a\n")
	zwrite(t, dir, "b.txt", "TWO\n")
	zrun(t, dir, "commit", "-m", "second", "--user", "u")
	tip := ztip(t, dir, "trunk")

	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	if err := ix.FoldNow([]index.Parsed{
		{Hash: base, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-10-30T10:00:00", User: "u", Comment: "first"},
			Changes: []string{"a.txt", "b.txt"}},
		{Hash: tip, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-10-30T11:00:00", User: "u", Comment: "second", Parents: []string{base}},
			Changes: []string{"a.txt", "b.txt"}, Lines: map[string]index.LineCount{"a.txt": {Added: 1}, "b.txt": {Added: 1, Removed: 1}}},
	}); err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	deps := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: zitcli.New(filepath.Join(dir, ".zit"))}})}
	if err := RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	defer func(n int) { largeDiffLines = n }(largeDiffLines)
	largeDiffLines = 2
	diffOf := func(in diffGetIn) diffGetOut {
		t.Helper()
		in.Repo, in.Hash = "test", tip
		out, err := r.Invoke(context.Background(), "checkin.diff", in)
		if err != nil {
			t.Fatal(err)
		}
		return out.(diffGetOut)
	}
	whole := diffOf(diffGetIn{})
	if !whole.Large || len(whole.Files) != 2 || len(whole.Structured) != 0 || whole.Counts["b.txt"].Removed != 1 || whole.TotalLines != 3 {
		t.Errorf("FAIL: a large check-in answered large=%v files=%d hunks=%d counts=%v total=%d",
			whole.Large, len(whole.Files), len(whole.Structured), whole.Counts, whole.TotalLines)
	}
	one := diffOf(diffGetIn{File: "b.txt"})
	if one.Large || len(one.Structured) != 1 || one.Structured[0].Path != "b.txt" {
		t.Errorf("FAIL: file=b.txt answered large=%v with %d files", one.Large, len(one.Structured))
	}
	// A large check-in's whole patch is not built into one answer: the
	// op refuses and points at the streamed download (jva, ^3920).
	for _, in := range []diffGetIn{{Unified: true}, {Unified: true, Raw: true}, {Raw: true}} {
		in.Repo, in.Hash = "test", tip
		if _, err := r.Invoke(context.Background(), "checkin.diff", in); err == nil || !strings.Contains(err.Error(), "/patch") {
			t.Errorf("FAIL: a large check-in's whole unified diff answered (%v), not the pointer to /patch", err)
		}
	}
	largeDiffLines = 20000
	if small := diffOf(diffGetIn{}); small.Large || len(small.Structured) != 2 {
		t.Errorf("FAIL: a small check-in answered large=%v with %d files", small.Large, len(small.Structured))
	}
	// A small one still answers its whole patch, raw: nothing parsed.
	raw := diffOf(diffGetIn{Unified: true, Raw: true})
	if len(raw.Structured) != 0 || !strings.Contains(raw.Unified, "a.txt") || !strings.Contains(raw.Unified, "b.txt") {
		t.Errorf("FAIL: a small check-in's raw patch answered %d parsed files, %d bytes", len(raw.Structured), len(raw.Unified))
	}
	// The streamed download writes the same bytes, and names the check-in
	// before it writes any.
	var buf strings.Builder
	named := ""
	if err := deps.PatchTo(context.Background(), "test", tip, "", false, func(to string) {
		if buf.Len() != 0 {
			t.Error("FAIL: named after bytes were written")
		}
		named = to
	}, &buf); err != nil {
		t.Fatal(err)
	}
	if buf.String() != raw.Unified || named != tip {
		t.Errorf("FAIL: the streamed patch (%d bytes, named %.12s) differs from checkin.diff's (%d bytes)", buf.Len(), named, len(raw.Unified))
	}

	// file.get reads at most maxFileGetBytes: past it the answer says so
	// and carries no content (jva, ^3926), but still its true size, not
	// the bytes read (jva, ^4016).
	defer func(n int) { maxFileGetBytes = n }(maxFileGetBytes)
	maxFileGetBytes = 2
	if out, err := r.Invoke(context.Background(), "file.get", fileGetIn{Repo: "test", Hash: tip, Path: "b.txt"}); err != nil {
		t.Fatal(err)
	} else if f := out.(fileGetOut); !f.TooLarge || f.Content != "" || f.Size <= 2 {
		t.Errorf("FAIL: a file past the read bound answered tooLarge=%v size=%d content=%q", f.TooLarge, f.Size, f.Content)
	}
	maxFileGetBytes = 16 << 20

	// A range diff has no index count; it is sized by its file list.
	defer func(n int) { largeDiffFiles = n }(largeDiffFiles)
	largeDiffFiles = 1
	if rng := diffOf(diffGetIn{From: base}); !rng.Large || len(rng.Files) != 2 || len(rng.Structured) != 0 {
		t.Errorf("FAIL: a range past largeDiffFiles answered large=%v files=%d hunks=%d", rng.Large, len(rng.Files), len(rng.Structured))
	}
	largeDiffFiles = 500
	if rng := diffOf(diffGetIn{From: base}); rng.Large || len(rng.Structured) != 2 {
		t.Errorf("FAIL: a small range answered large=%v with %d files", rng.Large, len(rng.Structured))
	}
}

// A check-in the index holds no counts for (folded when zit reported
// its files unknown: pkgsrc, jva ^4016) is sized by its file list, so
// it is refused whole and answered file by file like a branch.
func TestAnUncountedCheckinIsSizedByItsFiles(t *testing.T) {
	if _, err := exec.LookPath("zit"); err != nil {
		t.Skip("zit not on PATH")
	}
	dir := t.TempDir()
	zrun(t, dir, "init")
	zwrite(t, dir, "a.txt", "one\n")
	zwrite(t, dir, "b.txt", "two\n")
	zrun(t, dir, "add", "a.txt", "b.txt")
	zrun(t, dir, "commit", "-m", "first", "--user", "u")
	base := ztip(t, dir, "trunk")
	zwrite(t, dir, "a.txt", "one\nmore a\n")
	zwrite(t, dir, "b.txt", "TWO\n")
	zrun(t, dir, "commit", "-m", "second", "--user", "u")
	tip := ztip(t, dir, "trunk")

	ix, err := index.Open(filepath.Join(t.TempDir(), "index.db"))
	if err != nil {
		t.Fatal(err)
	}
	t.Cleanup(func() { ix.Close() })
	if err := ix.FoldNow([]index.Parsed{
		{Hash: base, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-10-30T10:00:00", User: "u", Comment: "first"}},
		{Hash: tip, Art: &artifact.Artifact{Type: artifact.Manifest, Date: "2026-10-30T11:00:00", User: "u", Comment: "second", Parents: []string{base}}},
	}); err != nil {
		t.Fatal(err)
	}
	r := ops.NewRegistry()
	deps := Deps{Version: "v", Repos: NewRepos(map[string]*Repo{"test": {Index: ix, Zit: zitcli.New(filepath.Join(dir, ".zit"))}})}
	if err := RegisterAll(r, deps); err != nil {
		t.Fatal(err)
	}
	defer func(n int) { largeDiffFiles = n }(largeDiffFiles)
	largeDiffFiles = 1
	for _, in := range []diffGetIn{{Unified: true}, {Raw: true}} {
		in.Repo, in.Hash = "test", tip
		if _, err := r.Invoke(context.Background(), "checkin.diff", in); err == nil || !strings.Contains(err.Error(), "/patch") {
			t.Errorf("FAIL: an uncounted large check-in's whole diff answered (%v), not the pointer to /patch", err)
		}
	}
	out, err := r.Invoke(context.Background(), "checkin.diff", diffGetIn{Repo: "test", Hash: tip})
	if err != nil {
		t.Fatal(err)
	}
	if d := out.(diffGetOut); !d.Large || len(d.Files) != 2 || len(d.Structured) != 0 {
		t.Errorf("FAIL: an uncounted large check-in answered large=%v files=%d hunks=%d", d.Large, len(d.Files), len(d.Structured))
	}
	largeDiffFiles = 500
	out, err = r.Invoke(context.Background(), "checkin.diff", diffGetIn{Repo: "test", Hash: tip, Unified: true, Raw: true})
	if err != nil {
		t.Fatal(err)
	}
	if d := out.(diffGetOut); !strings.Contains(d.Unified, "b.txt") {
		t.Errorf("FAIL: an uncounted small check-in did not answer its patch (%d bytes)", len(d.Unified))
	}
}