Artifact 9a7ad5640cc379a96639616cbf58021e552c27c2868fab1e563e7ec0ec991b64:
- File
internal/zitcli/zitcli_test.go
— part of check-in
[c754084dc7]
at
2026-08-17 15:52:12
on branch trunk
— web: the files page was unusably slow; fix the query, memoise, cap the list
Three separate faults, worst first.
Branches() used a correlated subquery — one full scan of checkin per row. On tcl (42,074 check-ins, 1,855 branches) that took 434 SECONDS, which is what made the page look hung rather than slow. One grouped scan with SQLite's bare-column-with-max rule, plus an index on (branch, date): 0.042s.
'ls --long' costs ~4ms per file because it reads each blob for size and the binary flag, so tcl's 2,198-path tip took 8.5s. Fossil never pays this — browse.c builds a directory listing from its own indexed tables and shows no size at all — but quarry reads through the CLI by design, so the answer is to compute once: a check-in's file set is IMMUTABLE, so listings are memoised per version, the tip is warmed after each ingest, and the tree op resolves 'trunk' to its hash so the page asks for the key that was warmed.
Only a hash may be cached. Caching 'trunk' would serve a stale tree forever after the next commit — a correctness bug dressed as a performance fix — so the memo refuses anything that can move, with a test pinning it.
The branch selector rendered every branch: 1,855 <option> elements, 90% of a 239KB page, and unusable as a control. Now the 25 most recently active, plus the branch being viewed even when it is older.
tcl's repo home: 8.5s -> 0.38s, 239KB -> 29KB. sqlite: 5.0s -> 0.32s. (user: dmestas, size: 6370) [annotate] [blame] [check-ins using]
package zitcli import ( "errors" "os" "os/exec" "path/filepath" "strings" "testing" ) // testRepo builds a throwaway zit repository with n one-file commits and // returns its path. Requires the zit binary; skips when absent. func testRepo(t *testing.T, n int) string { t.Helper() if _, err := exec.LookPath("zit"); err != nil { t.Skip("zit not on PATH") } dir := t.TempDir() run(t, dir, "init") for i := 0; i < n; i++ { name := string(rune('a'+i)) + ".txt" if err := os.WriteFile(filepath.Join(dir, name), []byte(name+" body\n"), 0o644); err != nil { t.Fatal(err) } run(t, dir, "add", name) run(t, dir, "commit", "-m", "add "+name) } return dir } func run(t *testing.T, dir string, args ...string) { t.Helper() cmd := exec.Command("zit", args...) cmd.Dir = dir if out, err := cmd.CombinedOutput(); err != nil { t.Fatalf("zit %v: %v\n%s", args, err, out) } } func TestFramesBootstrapAndIncremental(t *testing.T) { repo := testRepo(t, 2) z := New(repo) // Bootstrap: full census. batch, err := z.Frames(Cursor{}) if err != nil { t.Fatal(err) } if len(batch.Artifacts) == 0 { t.Fatal("bootstrap emitted nothing") } for _, a := range batch.Artifacts { if len(a.Hash) != 64 { t.Fatalf("bad hash %q", a.Hash) } if len(a.Bytes) == 0 { t.Fatalf("%s: empty artifact", a.Hash) } } // Current cursor: no new artifacts, cursor unchanged. again, err := z.Frames(batch.Cursor) if err != nil { t.Fatal(err) } if len(again.Artifacts) != 0 { t.Fatalf("current cursor re-emitted %d artifacts", len(again.Artifacts)) } if again.Cursor != batch.Cursor { t.Fatalf("cursor moved without appends: %v -> %v", batch.Cursor, again.Cursor) } // Append: only the new artifacts stream. if err := os.WriteFile(filepath.Join(repo, "new.txt"), []byte("new\n"), 0o644); err != nil { t.Fatal(err) } run(t, repo, "add", "new.txt") run(t, repo, "commit", "-m", "add new") delta, err := z.Frames(batch.Cursor) if err != nil { t.Fatal(err) } if len(delta.Artifacts) == 0 || len(delta.Artifacts) >= len(batch.Artifacts) { t.Fatalf("delta emitted %d of %d total", len(delta.Artifacts), len(batch.Artifacts)) } if delta.Cursor == batch.Cursor { t.Fatal("cursor did not advance past appends") } } func TestFramesCursorInvalidAfterStoreRewrite(t *testing.T) { repo := testRepo(t, 3) z := New(repo) batch, err := z.Frames(Cursor{}) if err != nil { t.Fatal(err) } // Shun a blob out of the store — the reclaim removes bytes from the // consumed prefix, so the cursor must die. (A no-op `pack` on an // already-optimal store rightly leaves cursors valid, which is why // this test rewrites via shun instead.) head, err := exec.Command("zit", "ls", "-R", repo, "tip").Output() if err != nil { t.Fatal(err) } blob := strings.Fields(string(head))[0] run(t, repo, "shun", blob, "--force") _, err = z.Frames(batch.Cursor) if !errors.Is(err, ErrCursorInvalid) { t.Fatalf("want ErrCursorInvalid after pack, got %v", err) } // Recovery: bootstrap from zero succeeds. if _, err := z.Frames(Cursor{}); err != nil { t.Fatalf("bootstrap after invalidation failed: %v", err) } } func TestFramesBytesMatchArtifactRaw(t *testing.T) { repo := testRepo(t, 1) z := New(repo) batch, err := z.Frames(Cursor{}) if err != nil { t.Fatal(err) } a := batch.Artifacts[0] cmd := exec.Command("zit", "artifact", a.Hash, "--raw") cmd.Dir = repo raw, err := cmd.Output() if err != nil { t.Fatal(err) } if string(raw) != string(a.Bytes) { t.Fatalf("frames bytes differ from artifact --raw for %s", a.Hash) } } func TestFramesMissingRepo(t *testing.T) { if _, err := exec.LookPath("zit"); err != nil { t.Skip("zit not on PATH") } z := New(t.TempDir()) if _, err := z.Frames(Cursor{}); err == nil { t.Fatal("want error for non-repository, got nil") } } func TestLs(t *testing.T) { repo := testRepo(t, 2) z := New(repo) files, err := z.Ls("tip") if err != nil { t.Fatal(err) } if len(files) != 2 || files[0].Name != "a.txt" || files[1].Name != "b.txt" { t.Fatalf("files = %+v", files) } if len(files[0].Blob) != 64 { t.Fatalf("blob id = %q", files[0].Blob) } if _, err := z.Ls("nope"); err == nil { t.Fatal("want error for bad version") } } func TestCat(t *testing.T) { repo := testRepo(t, 2) z := New(repo) got, err := z.Cat("a.txt", "tip") if err != nil { t.Fatal(err) } if string(got) != "a.txt body\n" { t.Fatalf("content = %q", got) } if _, err := z.Cat("missing.txt", "tip"); err == nil { t.Fatal("want error for missing file") } } func TestDiff(t *testing.T) { repo := testRepo(t, 1) commitFileZ(t, repo, "a.txt", "a.txt body\nplus a line\n", "grow a") commitFileZ(t, repo, "new.txt", "fresh\n", "add new") head, err := exec.Command("zit", "timeline", "-R", repo).Output() if err != nil { t.Fatal(err) } lines := strings.Split(strings.TrimSpace(string(head)), "\n") first := strings.Fields(lines[len(lines)-1])[0] unified, brief, err := New(repo).Diff(first, "tip") if err != nil { t.Fatal(err) } if !strings.Contains(unified, "+plus a line") || !strings.Contains(unified, "+fresh") { t.Fatalf("unified missing changes:\n%s", unified) } if len(brief) != 2 || brief[0].Status != "M" || brief[1].Status != "A" { t.Fatalf("brief = %+v", brief) } } func commitFileZ(t *testing.T, repo, name, body, msg string) { t.Helper() if err := os.WriteFile(filepath.Join(repo, name), []byte(body), 0o644); err != nil { t.Fatal(err) } run(t, repo, "add", name) run(t, repo, "commit", "-m", msg) } // A listing may only be memoised under a version that cannot move. This // is the guard on a cache that would otherwise serve a stale file tree // forever after the next commit. func TestOnlyHashVersionsAreCacheable(t *testing.T) { for _, v := range []string{ "aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa", "bbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbbb", } { if !immutableVersion(v) { t.Errorf("hash %q rejected as cacheable", v) } } for _, v := range []string{ "trunk", "tip", "", "release-1.2", "2026-08-17", "aaaaaaaa", // a prefix still resolves by search, not identity "AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA", // uppercase is not the stored form "zzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzzz", } { if immutableVersion(v) { t.Errorf("%q accepted as cacheable", v) } } }