package app import ( "archive/zip" "bytes" "io" "os" "path/filepath" "sort" "testing" "time" ) func writeTestFile(t *testing.T, path string, content string, mtime time.Time) { t.Helper() if err := os.MkdirAll(filepath.Dir(path), 0755); err != nil { t.Fatalf("mkdir: %v", err) } if err := os.WriteFile(path, []byte(content), 0644); err != nil { t.Fatalf("write: %v", err) } if err := os.Chtimes(path, mtime, mtime); err != nil { t.Fatalf("chtimes: %v", err) } } func readZipEntries(t *testing.T, path string) map[string]string { t.Helper() r, err := zip.OpenReader(path) if err != nil { t.Fatalf("open zip %s: %v", path, err) } defer r.Close() out := map[string]string{} for _, f := range r.File { rc, err := f.Open() if err != nil { t.Fatalf("open entry %s: %v", f.Name, err) } data, err := io.ReadAll(rc) rc.Close() if err != nil { t.Fatalf("read entry %s: %v", f.Name, err) } out[f.Name] = string(data) } return out } func TestBuildZipArchiveRoundTrip(t *testing.T) { root := t.TempDir() mtime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) writeTestFile(t, filepath.Join(root, "a.txt"), "hello", mtime) writeTestFile(t, filepath.Join(root, "sub", "b.txt"), "world", mtime) dest := filepath.Join(t.TempDir(), "out.zip") if err := buildZipArchive(root, dest); err != nil { t.Fatalf("buildZipArchive: %v", err) } entries := readZipEntries(t, dest) if entries["a.txt"] != "hello" || entries["sub/b.txt"] != "world" { t.Fatalf("entries=%+v", entries) } } func TestBuildZipArchiveDeterministicWhenUnchanged(t *testing.T) { root := t.TempDir() mtime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) writeTestFile(t, filepath.Join(root, "a.txt"), "hello", mtime) writeTestFile(t, filepath.Join(root, "b.txt"), "world", mtime) dir := t.TempDir() p1 := filepath.Join(dir, "one.zip") p2 := filepath.Join(dir, "two.zip") if err := buildZipArchive(root, p1); err != nil { t.Fatalf("build 1: %v", err) } if err := buildZipArchive(root, p2); err != nil { t.Fatalf("build 2: %v", err) } b1, _ := os.ReadFile(p1) b2, _ := os.ReadFile(p2) if !bytes.Equal(b1, b2) { t.Fatalf("two builds of an unchanged tree produced different bytes (len %d vs %d)", len(b1), len(b2)) } } func TestPatchArchiveOnTargetIncrementalAndValid(t *testing.T) { root := t.TempDir() mtime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) writeTestFile(t, filepath.Join(root, "static.txt"), "does not change", mtime) writeTestFile(t, filepath.Join(root, "growing.txt"), "cycle-1 line\n", mtime) work := t.TempDir() cachedPath := filepath.Join(work, "cache.zip") newPath := filepath.Join(work, "new.zip") targetPath := filepath.Join(work, "target.zip") // Cycle 1: cold start, no cache, no target yet. if err := buildZipArchive(root, newPath); err != nil { t.Fatalf("build cycle1: %v", err) } if err := patchArchiveOnTarget(targetPath, newPath, cachedPath); err != nil { t.Fatalf("patch cycle1: %v", err) } if err := os.Rename(newPath, cachedPath); err != nil { t.Fatalf("rename cycle1: %v", err) } want1, _ := os.ReadFile(cachedPath) got1, _ := os.ReadFile(targetPath) if !bytes.Equal(want1, got1) { t.Fatalf("cycle1: target does not match freshly built zip") } entries1 := readZipEntries(t, targetPath) if entries1["static.txt"] != "does not change" || entries1["growing.txt"] != "cycle-1 line\n" { t.Fatalf("cycle1 entries=%+v", entries1) } // Cycle 2: only growing.txt changes (as it would with an appended log). writeTestFile(t, filepath.Join(root, "growing.txt"), "cycle-1 line\ncycle-2 line\n", mtime.Add(time.Minute)) if err := buildZipArchive(root, newPath); err != nil { t.Fatalf("build cycle2: %v", err) } prefixLen, err := commonPrefixLen(cachedPath, newPath) if err != nil { t.Fatalf("commonPrefixLen: %v", err) } newInfo, _ := os.Stat(newPath) if prefixLen <= 0 { t.Fatalf("expected a nonzero unchanged prefix (static.txt entry should be identical), got %d", prefixLen) } if prefixLen >= newInfo.Size() { t.Fatalf("expected the changed growing.txt entry to make the archives diverge before EOF, prefixLen=%d size=%d", prefixLen, newInfo.Size()) } if err := patchArchiveOnTarget(targetPath, newPath, cachedPath); err != nil { t.Fatalf("patch cycle2: %v", err) } if err := os.Rename(newPath, cachedPath); err != nil { t.Fatalf("rename cycle2: %v", err) } want2, _ := os.ReadFile(cachedPath) got2, _ := os.ReadFile(targetPath) if !bytes.Equal(want2, got2) { t.Fatalf("cycle2: target does not match freshly built zip after incremental patch") } entries2 := readZipEntries(t, targetPath) if entries2["growing.txt"] != "cycle-1 line\ncycle-2 line\n" { t.Fatalf("cycle2 growing.txt=%q", entries2["growing.txt"]) } if entries2["static.txt"] != "does not change" { t.Fatalf("cycle2 static.txt=%q", entries2["static.txt"]) } } func TestPatchArchiveOnTargetFallsBackOnDrift(t *testing.T) { root := t.TempDir() mtime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) writeTestFile(t, filepath.Join(root, "a.txt"), "hello", mtime) work := t.TempDir() cachedPath := filepath.Join(work, "cache.zip") newPath := filepath.Join(work, "new.zip") targetPath := filepath.Join(work, "target.zip") if err := buildZipArchive(root, newPath); err != nil { t.Fatalf("build: %v", err) } if err := patchArchiveOnTarget(targetPath, newPath, cachedPath); err != nil { t.Fatalf("patch: %v", err) } if err := os.Rename(newPath, cachedPath); err != nil { t.Fatalf("rename: %v", err) } // Simulate drift: something external truncated/replaced the target file // so its size no longer matches what our local cache expects. if err := os.WriteFile(targetPath, []byte("garbage"), 0644); err != nil { t.Fatalf("simulate drift: %v", err) } writeTestFile(t, filepath.Join(root, "a.txt"), "hello again", mtime.Add(time.Minute)) if err := buildZipArchive(root, newPath); err != nil { t.Fatalf("build2: %v", err) } if err := patchArchiveOnTarget(targetPath, newPath, cachedPath); err != nil { t.Fatalf("patch2: %v", err) } want, _ := os.ReadFile(newPath) got, _ := os.ReadFile(targetPath) if !bytes.Equal(want, got) { t.Fatalf("expected drift to trigger a full rewrite, target still corrupt/stale") } entries := readZipEntries(t, targetPath) if entries["a.txt"] != "hello again" { t.Fatalf("entries=%+v", entries) } } func TestCommonPrefixLenSortedIndependent(t *testing.T) { // Sanity check that WalkDir's lexical ordering means entry order in the // zip doesn't depend on directory-read order — build twice from the same // tree and expect the entry name list to already come out sorted. root := t.TempDir() mtime := time.Date(2026, 1, 1, 0, 0, 0, 0, time.UTC) names := []string{"z.txt", "a.txt", "m/one.txt", "m/two.txt"} for _, n := range names { writeTestFile(t, filepath.Join(root, n), n, mtime) } dest := filepath.Join(t.TempDir(), "out.zip") if err := buildZipArchive(root, dest); err != nil { t.Fatalf("build: %v", err) } r, err := zip.OpenReader(dest) if err != nil { t.Fatalf("open: %v", err) } defer r.Close() var got []string for _, f := range r.File { got = append(got, f.Name) } sorted := append([]string(nil), got...) sort.Strings(sorted) for i := range got { if got[i] != sorted[i] { t.Fatalf("entries not in lexical order: %v", got) } } }