tests: make temp dir names unique per thread

366d2f8gl0w

all commits

Message

tests: make temp dir names unique per thread

Two test threads could read the same SystemTime tick and share one
temp dir (seen as a repo.bundle.lock race in responsive.rs under CI
load). Add a per-process atomic counter to the nanos suffix.

Diff

.forge/prs/68873a40-ecae-4935-91f3-b4a34305e427/patch.diff+390 −0
diff --git a/.forge/prs/68873a40-ecae-4935-91f3-b4a34305e427/patch.diff b/.forge/prs/68873a40-ecae-4935-91f3-b4a34305e427/patch.diffnew file mode 100644index 0000000..3da249a--- /dev/null+++ b/.forge/prs/68873a40-ecae-4935-91f3-b4a34305e427/patch.diff@@ -0,0 +1,390 @@1+From 260dbd29cba59b9bda1ac2cfbed54b704ae258bd Mon Sep 17 00:00:00 20012+From: hu13-test <[email protected]>3+Date: Tue, 27 Oct 2026 15:38:21 +00004+Subject: [PATCH] stream oversized blobs to raw/ instead of buffering (#66)5+6+A 400 MB blob made the build abort in the allocator ('memory7+allocation of 400000000 bytes failed'). Now blob_size() decides:8+blobs over MAX_INLINE_BLOB (8 MiB) go through show_to_file(),9+which copies git show stdout to raw/ in chunks, never enters the10+in-memory blob map, and renders a placeholder page. README lookup11+also skips candidates over 1 MiB. Partial files are removed when12+git fails or times out.13+---14+ src/build.rs        | 104 +++++++++++++++++++++++++------------15+ src/gitx.rs         | 124 +++++++++++++++++++++++++++++++++++++++++++-16+ tests/large_blob.rs |  71 +++++++++++++++++++++++++17+ 3 files changed, 264 insertions(+), 35 deletions(-)18+ create mode 100644 tests/large_blob.rs19+20+diff --git a/src/build.rs b/src/build.rs21+index 90e6651..e3ba54a 10064422+--- a/src/build.rs23++++ b/src/build.rs24+@@ -196,8 +196,13 @@ pub fn find_readme(repo: &Path, branch: &str) -> Option<(String, gitx::Blob)> {25+         ".forge/wiki/README",26+         ".forge/wiki/readme",27+     ] {28+-        if let Ok(blob) = gitx::show(repo, branch, Path::new(candidate)) {29+-            return Some((candidate.to_string(), blob));30++        let fits = gitx::blob_size(repo, branch, Path::new(candidate))31++            .map(|size| size <= README_MAX_BYTES)32++            .unwrap_or(false);33++        if fits {34++            if let Ok(blob) = gitx::show(repo, branch, Path::new(candidate)) {35++                return Some((candidate.to_string(), blob));36++            }37+         }38+     }39+     None40+@@ -209,6 +214,17 @@ pub fn is_placeholder(bytes: &[u8]) -> bool {41+     bytes.len() >= PREVIEW_MAX_BYTES || bytes.contains(&0)42+ }43+ 44++/// Blobs above this size are streamed straight to `raw/` instead of45++/// being held in memory (f0rge #66). Reading every blob fully used to46++/// abort the process in the allocator on a few-hundred-MB file; the47++/// page for such a blob is a placeholder anyway, so only the raw copy48++/// needs the bytes.49++pub const MAX_INLINE_BLOB: u64 = 8 * 1024 * 1024;50++51++/// READMEs above this size are ignored (next candidate in the chain is52++/// tried). Prevents a huge README from bloating every page's preview.53++pub const README_MAX_BYTES: u64 = 1024 * 1024;54++55+ /// Bytes sniffed for the language-math binary check (first 8KiB).56+ pub const BINARY_SNIFF_BYTES: usize = 8 * 1024;57+ 58+@@ -955,16 +971,27 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result<BuildSu59+         wiki::page_count(repo) > 0,60+     );61+ 62+-    // Raw files first: byte-identical copies.63++    // Raw files first: byte-identical copies. Blobs over MAX_INLINE_BLOB64++    // are streamed to disk and never enter the in-memory map; their file65++    // pages become placeholders (f0rge #66).66+     let mut blobs: BTreeMap<String, Vec<u8>> = BTreeMap::new();67++    let mut oversize: BTreeMap<String, u64> = BTreeMap::new();68+     for f in &files {69+-        let blob = gitx::show(repo, &branch, Path::new(f)).map_err(|e| format!("hu13: {e}"))?;70+         let dest = out.join("raw").join(f);71+         if let Some(parent) = dest.parent() {72+             std::fs::create_dir_all(parent).map_err(|e| format!("hu13: create raw parent: {e}"))?;73+         }74+-        std::fs::write(&dest, &blob.bytes).map_err(|e| format!("hu13: write raw {f}: {e}"))?;75+-        blobs.insert(f.clone(), blob.bytes);76++        let size =77++            gitx::blob_size(repo, &branch, Path::new(f)).map_err(|e| format!("hu13: {e}"))?;78++        if size > MAX_INLINE_BLOB {79++            gitx::show_to_file(repo, &branch, Path::new(f), &dest)80++                .map_err(|e| format!("hu13: {e}"))?;81++            oversize.insert(f.clone(), size);82++        } else {83++            let blob = gitx::show(repo, &branch, Path::new(f)).map_err(|e| format!("hu13: {e}"))?;84++            std::fs::write(&dest, &blob.bytes).map_err(|e| format!("hu13: write raw {f}: {e}"))?;85++            blobs.insert(f.clone(), blob.bytes);86++        }87+     }88+ 89+     // Per-path pages (m6 code-gitea, f0rge #33): Gitea-style file90+@@ -1038,41 +1065,50 @@ pub fn run_build(repo: &Path, out: &Path, opts: &BuildOptions) -> Result<BuildSu91+     }92+ 93+     for f in &files {94+-        let bytes = blobs.get(f).cloned().unwrap_or_default();95+         let mut body = breadcrumb(f);96+         body.push_str(&format!("<h1>{}</h1>\n", esc(f)));97+-        if is_placeholder(&bytes) {98+-            body.push_str(&file_meta_bar(f, bytes.len() as u64, None));99+-            let kind = if bytes.len() >= PREVIEW_MAX_BYTES {100+-                format!("too large to preview ({} bytes)", bytes.len())101+-            } else {102+-                "binary file".to_string()103+-            };104++        if let Some(&size) = oversize.get(f) {105++            body.push_str(&file_meta_bar(f, size, None));106+             body.push_str(&format!(107+-                "<p class=\"notice\">placeholder: {kind} — preview omitted. \108++                "<p class=\"notice\">placeholder: too large to preview ({size} bytes) — \109+                  <a href=\"/raw/{}\">download the raw file</a> instead.</p>\n",110+                 href_esc(f)111+             ));112+-        } else if is_markdown(f) {113+-            let display = String::from_utf8_lossy(&bytes).into_owned();114+-            body.push_str(&file_meta_bar(115+-                f,116+-                bytes.len() as u64,117+-                Some(display.lines().count()),118+-            ));119+-            body.push_str(&markdown_to_html(&display));120+-            body.push_str(&format!(121+-                "<p class=\"muted\"><a href=\"/raw/{}\">view raw</a></p>\n",122+-                href_esc(f)123+-            ));124+         } else {125+-            let display = String::from_utf8_lossy(&bytes).into_owned();126+-            body.push_str(&file_meta_bar(127+-                f,128+-                bytes.len() as u64,129+-                Some(display.lines().count()),130+-            ));131+-            body.push_str(&render_text_pre(&display));132++            let bytes = blobs.get(f).cloned().unwrap_or_default();133++            if is_placeholder(&bytes) {134++                body.push_str(&file_meta_bar(f, bytes.len() as u64, None));135++                let kind = if bytes.len() >= PREVIEW_MAX_BYTES {136++                    format!("too large to preview ({} bytes)", bytes.len())137++                } else {138++                    "binary file".to_string()139++                };140++                body.push_str(&format!(141++                    "<p class=\"notice\">placeholder: {kind} — preview omitted. \142++                 <a href=\"/raw/{}\">download the raw file</a> instead.</p>\n",143++                    href_esc(f)144++                ));145++            } else if is_markdown(f) {146++                let display = String::from_utf8_lossy(&bytes).into_owned();147++                body.push_str(&file_meta_bar(148++                    f,149++                    bytes.len() as u64,150++                    Some(display.lines().count()),151++                ));152++                body.push_str(&markdown_to_html(&display));153++                body.push_str(&format!(154++                    "<p class=\"muted\"><a href=\"/raw/{}\">view raw</a></p>\n",155++                    href_esc(f)156++                ));157++            } else {158++                let display = String::from_utf8_lossy(&bytes).into_owned();159++                body.push_str(&file_meta_bar(160++                    f,161++                    bytes.len() as u64,162++                    Some(display.lines().count()),163++                ));164++                body.push_str(&render_text_pre(&display));165++            }166+         }167+         let html = crate::layout::page_site_with_provenance(168+             &format!("{f} — {name}"),169+diff --git a/src/gitx.rs b/src/gitx.rs170+index ac68f8f..8d09364 100644171+--- a/src/gitx.rs172++++ b/src/gitx.rs173+@@ -7,7 +7,7 @@174+ #![allow(dead_code)]175+ 176+ use std::ffi::{OsStr, OsString};177+-use std::io::Read;178++use std::io::{Read, Write};179+ use std::path::{Path, PathBuf};180+ use std::process::{Command, Stdio};181+ use std::sync::mpsc;182+@@ -669,6 +669,128 @@ pub fn show(repo: &Path, branch: &str, path: &Path) -> Result<Blob, GitError> {183+     })184+ }185+ 186++/// Size in bytes of `branch:path` without reading it (`git cat-file -s`).187++///188++/// Callers use this to avoid loading very large blobs into memory189++/// (f0rge #66): a 400 MB blob used to abort the process in the190++/// allocator.191++pub fn blob_size(repo: &Path, branch: &str, path: &Path) -> Result<u64, GitError> {192++    let mut spec = OsString::from(branch);193++    spec.push(":");194++    spec.push(path.as_os_str());195++    let a_cat: &OsStr = OsStr::new("cat-file");196++    let a_size: &OsStr = OsStr::new("-s");197++    let args: Vec<&OsStr> = vec![a_cat, a_size, spec.as_os_str()];198++    let out = run_git(repo, &args)?;199++    if !out.status_ok {200++        let stderr = String::from_utf8_lossy(&out.stderr).trim().to_string();201++        return Err(GitError::NotFound(format!(202++            "`{branch}:{}` not found: {stderr}",203++            path.to_string_lossy()204++        )));205++    }206++    String::from_utf8_lossy(&out.stdout)207++        .trim()208++        .parse::<u64>()209++        .map_err(|e| GitError::Failed {210++            cmd: cmd_string(repo, &args),211++            status: "parse".to_string(),212++            stderr: format!("cat-file -s: {e}"),213++        })214++}215++216++/// Stream `branch:path` to `dest` without buffering it in the process.217++///218++/// Used for blobs above the in-memory cap (f0rge #66). The destination219++/// is removed again if git fails or the timeout fires, so a partial220++/// file never survives a failed build.221++pub fn show_to_file(repo: &Path, branch: &str, path: &Path, dest: &Path) -> Result<(), GitError> {222++    let mut spec = OsString::from(branch);223++    spec.push(":");224++    spec.push(path.as_os_str());225++    let a_show: &OsStr = OsStr::new("show");226++    let args: Vec<&OsStr> = vec![a_show, spec.as_os_str()];227++228++    let mut cmd = Command::new("git");229++    cmd.arg("-C")230++        .arg(repo)231++        .args(&args)232++        .stdout(Stdio::piped())233++        .stderr(Stdio::piped());234++    let mut child = cmd.spawn().map_err(|e| GitError::Spawn(e.to_string()))?;235++    let stdout = child.stdout.take();236++    let stderr = child.stderr.take();237++238++    let (tx_err, rx_err) = mpsc::channel::<Vec<u8>>();239++    if let Some(mut err) = stderr {240++        thread::spawn(move || {241++            let mut buf = Vec::new();242++            let _ = err.read_to_end(&mut buf);243++            let _ = tx_err.send(buf);244++        });245++    } else {246++        let _ = tx_err.send(Vec::new());247++    }248++249++    let dest_owned = dest.to_path_buf();250++    let (tx_copy, rx_copy) = mpsc::channel::<std::io::Result<u64>>();251++    if let Some(mut out) = stdout {252++        thread::spawn(move || {253++            let file = match std::fs::File::create(&dest_owned) {254++                Ok(f) => f,255++                Err(e) => {256++                    let _ = tx_copy.send(Err(e));257++                    return;258++                }259++            };260++            let mut w = std::io::BufWriter::new(file);261++            let res = std::io::copy(&mut out, &mut w).and_then(|n| w.flush().map(|()| n));262++            let _ = tx_copy.send(res);263++        });264++    } else {265++        let _ = tx_copy.send(Err(std::io::Error::other("git show produced no stdout")));266++    }267++268++    let start = Instant::now();269++    let status = loop {270++        match child.try_wait() {271++            Err(e) => {272++                let _ = std::fs::remove_file(dest);273++                return Err(GitError::Failed {274++                    cmd: cmd_string(repo, &args),275++                    status: "wait-error".to_string(),276++                    stderr: e.to_string(),277++                });278++            }279++            Ok(Some(status)) => break status,280++            Ok(None) => {281++                if start.elapsed() > GIT_TIMEOUT {282++                    let _ = child.kill();283++                    let _ = child.wait();284++                    let _ = std::fs::remove_file(dest);285++                    return Err(GitError::Timeout(cmd_string(repo, &args)));286++                }287++                thread::sleep(Duration::from_millis(10));288++            }289++        }290++    };291++    let written = rx_copy292++        .recv_timeout(Duration::from_secs(5))293++        .unwrap_or_else(|_| Err(std::io::Error::other("copy thread hung")));294++    let stderr = rx_err295++        .recv_timeout(Duration::from_secs(5))296++        .unwrap_or_default();297++    if !status.success() || written.is_err() {298++        let _ = std::fs::remove_file(dest);299++        return Err(GitError::Failed {300++            cmd: cmd_string(repo, &args),301++            status: status.to_string(),302++            stderr: String::from_utf8_lossy(&stderr).trim().to_string(),303++        });304++    }305++    Ok(())306++}307++308+ /// Unified diff text for one commit `sha`.309+ ///310+ /// Binary diffs keep git's `Binary files ... differ` note (or gain a311+diff --git a/tests/large_blob.rs b/tests/large_blob.rs312+new file mode 100644313+index 0000000..00414c7314+--- /dev/null315++++ b/tests/large_blob.rs316+@@ -0,0 +1,71 @@317++//! f0rge #66: build must not read every blob fully into memory. A blob318++//! over the inline cap is streamed to `raw/`; its page is a placeholder319++//! and the build still succeeds.320++321++use std::path::{Path, PathBuf};322++use std::process::Command;323++324++fn git(repo: &Path, args: &[&str]) {325++    let st = Command::new("git")326++        .arg("-C")327++        .arg(repo)328++        .args(args)329++        .status()330++        .expect("spawn git");331++    assert!(st.success(), "git {args:?} failed");332++}333++334++fn fresh_dir(prefix: &str) -> PathBuf {335++    let dir = std::env::temp_dir().join(format!(336++        "hu13-large-{prefix}-{}-{}",337++        std::process::id(),338++        std::time::SystemTime::now()339++            .duration_since(std::time::UNIX_EPOCH)340++            .map(|d| d.as_nanos())341++            .unwrap_or(0)342++    ));343++    std::fs::create_dir_all(&dir).unwrap();344++    dir345++}346++347++#[test]348++fn oversized_blob_is_streamed_and_never_aborts() {349++    let repo = fresh_dir("repo");350++    let out = fresh_dir("out");351++    git(&repo, &["init", "-b", "main"]);352++    git(&repo, &["config", "user.name", "hu13-test"]);353++    git(&repo, &["config", "user.email", "[email protected]"]);354++    git(&repo, &["config", "commit.gpgsign", "false"]);355++    std::fs::write(repo.join("README.md"), "# large blob\n").unwrap();356++    // 12 MiB of text: above MAX_INLINE_BLOB (8 MiB), compressible enough357++    // for the test to stay fast.358++    let big = vec![b'a'; 12 * 1024 * 1024];359++    std::fs::write(repo.join("big.txt"), &big).unwrap();360++    git(&repo, &["add", "-A"]);361++    git(&repo, &["commit", "-m", "big"]);362++363++    let produced = Command::new(env!("CARGO_BIN_EXE_hu13"))364++        .arg("build")365++        .arg(&repo)366++        .arg("-o")367++        .arg(&out)368++        .output()369++        .expect("spawn hu13 build");370++    let stderr = String::from_utf8_lossy(&produced.stderr);371++    assert!(produced.status.success(), "build failed: {stderr}");372++373++    // Raw copy is byte-identical.374++    let raw = std::fs::read(out.join("raw/big.txt")).expect("raw big.txt");375++    assert_eq!(raw.len(), big.len());376++    assert!(raw.iter().all(|b| *b == b'a'));377++378++    // Page is a placeholder, not 12 MiB of spans.379++    let page = std::fs::read_to_string(out.join("files/big.txt.html")).expect("page");380++    assert!(page.contains("too large to preview"), "placeholder shown");381++    assert!(page.contains("download the raw file"));382++    assert!(page.len() < 200_000, "page stayed small: {}", page.len());383++384++    // A normal file still gets a real preview.385++    let readme = std::fs::read_to_string(out.join("files/README.md.html")).expect("readme");386++    assert!(readme.contains("<h1>README.md</h1>") || readme.contains("README.md"));387++}388+-- 389+2.56.0390+
.forge/prs/68873a40-ecae-4935-91f3-b4a34305e427/pr.md+13 −0
diff --git a/.forge/prs/68873a40-ecae-4935-91f3-b4a34305e427/pr.md b/.forge/prs/68873a40-ecae-4935-91f3-b4a34305e427/pr.mdnew file mode 100644index 0000000..4932e82--- /dev/null+++ b/.forge/prs/68873a40-ecae-4935-91f3-b4a34305e427/pr.md@@ -0,0 +1,13 @@1+---2+id: 68873a40-ecae-4935-91f3-b4a34305e4273+title: Stream oversized blobs to raw/ instead of buffering (#66)4+state: open5+base: master6+head: issue-66-large-blobs7+draft: false8+issues:9+- f06329e9-7e81-41f8-acf2-a48baf70cdcd10+ci: null11+created: 2026-10-27T15:38:21.865609109Z12+updated: null13+---
tests/build_cli.rs+6 −0
diff --git a/tests/build_cli.rs b/tests/build_cli.rsindex 652b59c..5fdc205 100644--- a/tests/build_cli.rs+++ b/tests/build_cli.rs@@ -1,5 +1,9 @@11 //! CLI-level test: `hu13 build` on a repo without `.forge/` exits 0.22 3+use std::sync::atomic::{AtomicU64, Ordering};4+5+static SEQ: AtomicU64 = AtomicU64::new(0);6+37 use std::path::{Path, PathBuf};48 use std::process::Command;59 @@ -22,6 +26,7 @@ fn build_no_forge_repo_exits_zero() {2226             .duration_since(std::time::UNIX_EPOCH)2327             .map(|d| d.as_nanos())2428             .unwrap_or(0)29+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2530     ));2631     std::fs::create_dir_all(&dir).unwrap();2732     git(&dir, &["init", "-b", "main"]);@@ -41,6 +46,7 @@ fn build_no_forge_repo_exits_zero() {4146             .duration_since(std::time::UNIX_EPOCH)4247             .map(|d| d.as_nanos())4348             .unwrap_or(1)49+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))4450     ));4551     let bin = env!("CARGO_BIN_EXE_hu13");4652     let st = Command::new(bin)
tests/clean_out.rs+5 −0
diff --git a/tests/clean_out.rs b/tests/clean_out.rsindex 5288f56..fdc1c03 100644--- a/tests/clean_out.rs+++ b/tests/clean_out.rs@@ -2,6 +2,10 @@22 //! (issue, file, release) was deleted. Every build starts from an empty33 //! output dir, and the guard refuses to delete the repo or its ancestors.44 5+use std::sync::atomic::{AtomicU64, Ordering};6+7+static SEQ: AtomicU64 = AtomicU64::new(0);8+59 use std::path::{Path, PathBuf};610 use std::process::Command;711 @@ -23,6 +27,7 @@ fn fresh_dir(prefix: &str) -> PathBuf {2327             .duration_since(std::time::UNIX_EPOCH)2428             .map(|d| d.as_nanos())2529             .unwrap_or(0)30+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2631     ));2732     std::fs::create_dir_all(&dir).unwrap();2833     dir
tests/clone_origin.rs+5 −0
diff --git a/tests/clone_origin.rs b/tests/clone_origin.rsindex 0d514a3..653247b 100644--- a/tests/clone_origin.rs+++ b/tests/clone_origin.rs@@ -4,6 +4,10 @@44 //! live `git clone http://host:port/repo.bundle` (smart, serve-only) +55 //! `git clone <origin>/repo.git` (dumb, any static server), entrypoint passthrough.66 7+use std::sync::atomic::{AtomicU64, Ordering};8+9+static SEQ: AtomicU64 = AtomicU64::new(0);10+711 use std::io::{BufRead, BufReader, Read, Write};812 use std::net::TcpStream;913 use std::path::{Path, PathBuf};@@ -25,6 +29,7 @@ fn fresh(prefix: &str) -> PathBuf {2529             .duration_since(std::time::UNIX_EPOCH)2630             .map(|d| d.as_nanos())2731             .unwrap_or(0)32+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2833     ));2934     std::fs::create_dir_all(&dir).unwrap();3035     dir
tests/code_cols.rs+5 −0
diff --git a/tests/code_cols.rs b/tests/code_cols.rsindex a21ebf3..73a3ccb 100644--- a/tests/code_cols.rs+++ b/tests/code_cols.rs@@ -5,6 +5,10 @@55 //! Builds a small fixture repo with the real binary and string-asserts on66 //! the emitted HTML + `static/style.css` (no rendering engine needed).77 8+use std::sync::atomic::{AtomicU64, Ordering};9+10+static SEQ: AtomicU64 = AtomicU64::new(0);11+812 use std::path::{Path, PathBuf};913 use std::process::Command;1014 @@ -24,6 +28,7 @@ fn fresh(prefix: &str) -> PathBuf {2428             .duration_since(std::time::UNIX_EPOCH)2529             .map(|d| d.as_nanos())2630             .unwrap_or(0)31+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2732     ));2833     std::fs::create_dir_all(&dir).unwrap();2934     dir
tests/code_space.rs+5 −0
diff --git a/tests/code_space.rs b/tests/code_space.rsindex 9e01e8c..02e1aab 100644--- a/tests/code_space.rs+++ b/tests/code_space.rs@@ -5,6 +5,10 @@55 //! Builds a small fixture repo with the real binary and string-asserts on66 //! the emitted HTML + `static/style.css` (no rendering engine needed).77 8+use std::sync::atomic::{AtomicU64, Ordering};9+10+static SEQ: AtomicU64 = AtomicU64::new(0);11+812 use std::path::{Path, PathBuf};913 use std::process::Command;1014 @@ -24,6 +28,7 @@ fn fresh(prefix: &str) -> PathBuf {2428             .duration_since(std::time::UNIX_EPOCH)2529             .map(|d| d.as_nanos())2630             .unwrap_or(0)31+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2732     ));2833     std::fs::create_dir_all(&dir).unwrap();2934     dir
tests/favicon.rs+5 −0
diff --git a/tests/favicon.rs b/tests/favicon.rsindex 3064d9f..61c916e 100644--- a/tests/favicon.rs+++ b/tests/favicon.rs@@ -4,6 +4,10 @@44 //! og tags, manifest link) + `manifest.webmanifest` valid JSON in the55 //! output + correct serve mime types over HTTP.66 7+use std::sync::atomic::{AtomicU64, Ordering};8+9+static SEQ: AtomicU64 = AtomicU64::new(0);10+711 use std::io::{BufRead, BufReader, Read, Write};812 use std::net::TcpStream;913 use std::path::{Path, PathBuf};@@ -26,6 +30,7 @@ fn fresh(prefix: &str) -> PathBuf {2630             .duration_since(std::time::UNIX_EPOCH)2731             .map(|d| d.as_nanos())2832             .unwrap_or(0)33+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2934     ));3035     std::fs::create_dir_all(&dir).unwrap();3136     dir
tests/forge_readers.rs+5 −0
diff --git a/tests/forge_readers.rs b/tests/forge_readers.rsindex 27762ef..f0b7baa 100644--- a/tests/forge_readers.rs+++ b/tests/forge_readers.rs@@ -11,6 +11,10 @@1111 //! - a failing CI run fixture -> `fail` badge + steps + log1212 //! - rendered counts match a direct file-count of the fixture1313 14+use std::sync::atomic::{AtomicU64, Ordering};15+16+static SEQ: AtomicU64 = AtomicU64::new(0);17+1418 use std::path::{Path, PathBuf};1519 use std::process::Command;1620 @@ -39,6 +43,7 @@ fn fresh_dir(prefix: &str) -> PathBuf {3943             .duration_since(std::time::UNIX_EPOCH)4044             .map(|d| d.as_nanos())4145             .unwrap_or(0)46+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))4247     ));4348     std::fs::create_dir_all(&dir).unwrap();4449     dir
tests/header_config.rs+5 −0
diff --git a/tests/header_config.rs b/tests/header_config.rsindex 640c2c4..a21c169 100644--- a/tests/header_config.rs+++ b/tests/header_config.rs@@ -5,6 +5,10 @@55 //! repo-head — never into the minimal header (logo • nav only).66 //! Hostile `<script>` input renders as escaped text everywhere.77 8+use std::sync::atomic::{AtomicU64, Ordering};9+10+static SEQ: AtomicU64 = AtomicU64::new(0);11+812 use std::path::{Path, PathBuf};913 use std::process::Command;1014 use std::sync::Mutex;@@ -29,6 +33,7 @@ fn fresh(prefix: &str) -> PathBuf {2933             .duration_since(std::time::UNIX_EPOCH)3034             .map(|d| d.as_nanos())3135             .unwrap_or(0)36+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))3237     ));3338     std::fs::create_dir_all(&dir).unwrap();3439     dir
tests/hide_empty.rs+5 −0
diff --git a/tests/hide_empty.rs b/tests/hide_empty.rsindex 5b19f01..e39026d 100644--- a/tests/hide_empty.rs+++ b/tests/hide_empty.rs@@ -4,6 +4,10 @@44 //! Hidden sections still build (deep links keep working with their55 //! "No X yet" empty states); only the nav omits the tab.66 7+use std::sync::atomic::{AtomicU64, Ordering};8+9+static SEQ: AtomicU64 = AtomicU64::new(0);10+711 use std::path::{Path, PathBuf};812 use std::process::Command;913 @@ -19,6 +23,7 @@ fn fresh(prefix: &str) -> PathBuf {1923             .duration_since(std::time::UNIX_EPOCH)2024             .map(|d| d.as_nanos())2125             .unwrap_or(0)26+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2227     ));2328     std::fs::create_dir_all(&dir).unwrap();2429     dir
tests/large_blob.rs+5 −0
diff --git a/tests/large_blob.rs b/tests/large_blob.rsindex 00414c7..a7e8dbf 100644--- a/tests/large_blob.rs+++ b/tests/large_blob.rs@@ -2,6 +2,10 @@22 //! over the inline cap is streamed to `raw/`; its page is a placeholder33 //! and the build still succeeds.44 5+use std::sync::atomic::{AtomicU64, Ordering};6+7+static SEQ: AtomicU64 = AtomicU64::new(0);8+59 use std::path::{Path, PathBuf};610 use std::process::Command;711 @@ -23,6 +27,7 @@ fn fresh_dir(prefix: &str) -> PathBuf {2327             .duration_since(std::time::UNIX_EPOCH)2428             .map(|d| d.as_nanos())2529             .unwrap_or(0)30+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2631     ));2732     std::fs::create_dir_all(&dir).unwrap();2833     dir
tests/navbar.rs+5 −0
diff --git a/tests/navbar.rs b/tests/navbar.rsindex bba644c..02cab35 100644--- a/tests/navbar.rs+++ b/tests/navbar.rs@@ -6,6 +6,10 @@66 //! Builds a small fixture repo with the real binary and string-asserts on77 //! the emitted HTML + `static/style.css` (no rendering engine needed).88 9+use std::sync::atomic::{AtomicU64, Ordering};10+11+static SEQ: AtomicU64 = AtomicU64::new(0);12+913 use std::path::{Path, PathBuf};1014 use std::process::Command;1115 @@ -25,6 +29,7 @@ fn fresh(prefix: &str) -> PathBuf {2529             .duration_since(std::time::UNIX_EPOCH)2630             .map(|d| d.as_nanos())2731             .unwrap_or(0)32+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2833     ));2934     std::fs::create_dir_all(&dir).unwrap();3035     dir
tests/path_safety.rs+6 −0
diff --git a/tests/path_safety.rs b/tests/path_safety.rsindex d56023f..e07cef4 100644--- a/tests/path_safety.rs+++ b/tests/path_safety.rs@@ -3,6 +3,10 @@33 //! (`issues/<id>.html`, `releases/<version>.html`, `releases/<version>/`),44 //! so a `../../..` value used to write outside `-o`.55 6+use std::sync::atomic::{AtomicU64, Ordering};7+8+static SEQ: AtomicU64 = AtomicU64::new(0);9+610 use std::path::{Path, PathBuf};711 use std::process::Command;812 @@ -24,6 +28,7 @@ fn fresh_dir(prefix: &str) -> PathBuf {2428             .duration_since(std::time::UNIX_EPOCH)2529             .map(|d| d.as_nanos())2630             .unwrap_or(0)31+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2732     ));2833     std::fs::create_dir_all(&dir).unwrap();2934     dir@@ -46,6 +51,7 @@ fn hostile_frontmatter_cannot_write_outside_out() {4651             .duration_since(std::time::UNIX_EPOCH)4752             .map(|d| d.as_nanos())4853             .unwrap_or(0)54+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))4955     );5056     let escaped = |name: &str| PathBuf::from(format!("/tmp/{marker}-{name}.html"));5157 
tests/polish.rs+5 −0
diff --git a/tests/polish.rs b/tests/polish.rsindex 92f49f2..6e95324 100644--- a/tests/polish.rs+++ b/tests/polish.rs@@ -5,6 +5,10 @@55 //! (`hu13 build --branch <bad>` exits non-zero, naming the branch) plus a66 //! `bash -n` syntax check of `entrypoint.sh` itself.77 8+use std::sync::atomic::{AtomicU64, Ordering};9+10+static SEQ: AtomicU64 = AtomicU64::new(0);11+812 use std::io::{BufRead, BufReader, Read, Write};913 use std::net::TcpStream;1014 use std::path::{Path, PathBuf};@@ -27,6 +31,7 @@ fn fresh(prefix: &str) -> PathBuf {2731             .duration_since(std::time::UNIX_EPOCH)2832             .map(|d| d.as_nanos())2933             .unwrap_or(0)34+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))3035     ));3136     std::fs::create_dir_all(&dir).unwrap();3237     dir
tests/provenance.rs+5 −0
diff --git a/tests/provenance.rs b/tests/provenance.rsindex dff7e55..d6a59e9 100644--- a/tests/provenance.rs+++ b/tests/provenance.rs@@ -5,6 +5,10 @@55 //! Builds a small fixture repo with the real binary and string-asserts on66 //! the emitted HTML + `static/style.css` (no rendering engine needed).77 8+use std::sync::atomic::{AtomicU64, Ordering};9+10+static SEQ: AtomicU64 = AtomicU64::new(0);11+812 use std::path::{Path, PathBuf};913 use std::process::Command;1014 @@ -24,6 +28,7 @@ fn fresh(prefix: &str) -> PathBuf {2428             .duration_since(std::time::UNIX_EPOCH)2529             .map(|d| d.as_nanos())2630             .unwrap_or(0)31+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2732     ));2833     std::fs::create_dir_all(&dir).unwrap();2934     dir
tests/releases_api.rs+5 −0
diff --git a/tests/releases_api.rs b/tests/releases_api.rsindex 04e6cee..e65f785 100644--- a/tests/releases_api.rs+++ b/tests/releases_api.rs@@ -3,6 +3,10 @@33 //! api counts == html counts, agents.md curl examples hittable,44 //! feed includes the release.55 6+use std::sync::atomic::{AtomicU64, Ordering};7+8+static SEQ: AtomicU64 = AtomicU64::new(0);9+610 use std::path::{Path, PathBuf};711 use std::process::Command;812 @@ -24,6 +28,7 @@ fn fresh_dir(prefix: &str) -> PathBuf {2428             .duration_since(std::time::UNIX_EPOCH)2529             .map(|d| d.as_nanos())2630             .unwrap_or(0)31+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2732     ));2833     std::fs::create_dir_all(&dir).unwrap();2934     dir
tests/responsive.rs+5 −0
diff --git a/tests/responsive.rs b/tests/responsive.rsindex 9879f70..1cf554a 100644--- a/tests/responsive.rs+++ b/tests/responsive.rs@@ -4,6 +4,10 @@44 //! Builds a small fixture repo with the real binary and string-asserts on55 //! the emitted HTML + `static/style.css` (no rendering engine needed).66 7+use std::sync::atomic::{AtomicU64, Ordering};8+9+static SEQ: AtomicU64 = AtomicU64::new(0);10+711 use std::path::{Path, PathBuf};812 use std::process::Command;913 @@ -23,6 +27,7 @@ fn fresh(prefix: &str) -> PathBuf {2327             .duration_since(std::time::UNIX_EPOCH)2428             .map(|d| d.as_nanos())2529             .unwrap_or(0)30+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2631     ));2732     std::fs::create_dir_all(&dir).unwrap();2833     dir
tests/truncate.rs+5 −0
diff --git a/tests/truncate.rs b/tests/truncate.rsindex 2f08e2d..4897608 100644--- a/tests/truncate.rs+++ b/tests/truncate.rs@@ -1,6 +1,10 @@11 //! Truncate all table fields (m6/truncate, f0rge #42):22 //! every table cell carries `.trunc` + `title=`, desktop fits 1100px.33 4+use std::sync::atomic::{AtomicU64, Ordering};5+6+static SEQ: AtomicU64 = AtomicU64::new(0);7+48 use std::path::{Path, PathBuf};59 use std::process::Command;610 @@ -20,6 +24,7 @@ fn fresh(prefix: &str) -> PathBuf {2024             .duration_since(std::time::UNIX_EPOCH)2125             .map(|d| d.as_nanos())2226             .unwrap_or(0)27+            .wrapping_add(u128::from(SEQ.fetch_add(1, Ordering::Relaxed)))2328     ));2429     std::fs::create_dir_all(&dir).unwrap();2530     dir