Files
bee/audit/internal/app/blackbox_archive_test.go
T
mchusandClaude Sonnet 5 8a91f0f783 fix(webui): repair broken scenario Run button onclick, dedupe build.sh overlay staging
- page_scenario.go: onclick built via JSON.stringify() embedded raw double
  quotes inside a double-quoted HTML attribute, truncating the attribute so
  the click handler never compiled; pass the name through an escaped
  data-scenario-name attribute instead.
- build.sh: overlay staging rsyncs (OVERLAY_DIR->stage, stage->includes.chroot)
  ran without --delete, so a scenario removed from the repo (a9924b0) stayed
  baked into every ISO built from the persistent stage cache since — the
  "second script" in the Scenario page's list.
- blackbox: rewritten around a deterministic local zip + incremental
  patch-the-changed-suffix onto removable media, instead of walking/copying
  ~90 files through a synchronous ntfs-3g FUSE mount every cycle. journalctl
  captures are now "--since last sync" (were "--since boot", growing with
  uptime) and metrics.db is excluded (was copied whole every cycle).
- scenario: nvbandwidth-acs-ab now escalates GPU count (same-socket pair,
  other socket's pair, one cross-socket pair, all GPUs) under each ACS state
  instead of always running all 6 GPUs at once, using a new `bee
  gpu-bandwidth-groups` subcommand that discovers socket layout from
  `nvidia-smi topo -m` at runtime — gpu_indices is host-specific, so this
  can't be baked into the scenario file.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-07-29 18:11:24 +03:00

236 lines
7.2 KiB
Go

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)
}
}
}