From 75412e89cc49b84ef73d3bc421316f8a89f54463 Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Sun, 19 Jul 2026 17:44:54 -0700 Subject: [PATCH 1/4] chore: development v0.6.28 - comprehensive testing complete [auto-commit] --- CHANGELOG.md | 13 ++++++++- Cargo.lock | 56 +++++++++++++++++++------------------- Cargo.toml | 30 ++++++++++---------- crates/uffs-cli/Cargo.toml | 4 +-- 4 files changed, 57 insertions(+), 46 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a6b078b32..0210ae4ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -14,6 +14,16 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ## [Unreleased] +## [0.6.28] - 2026-07-19 + +### Added + +- content: VSS-snapshot content-ingest pipeline for Docenta (UFI.0-UFI.6) (#563) + +### Fixed + +- test: deflake ensure_warm parallel-promote assertion (#562) + ## [0.6.27] - 2026-07-14 ### Added @@ -2697,7 +2707,8 @@ thin clients over a unified `uffsd` process. ### Fixed - Various MFT parsing edge cases -[Unreleased]: https://github.com/skyllc-ai/UltraFastFileSearch/compare/v0.6.27...HEAD +[Unreleased]: https://github.com/skyllc-ai/UltraFastFileSearch/compare/v0.6.28...HEAD +[0.6.28]: https://github.com/skyllc-ai/UltraFastFileSearch/compare/v0.6.27...v0.6.28 [0.6.27]: https://github.com/skyllc-ai/UltraFastFileSearch/compare/v0.6.26...v0.6.27 [0.6.26]: https://github.com/skyllc-ai/UltraFastFileSearch/compare/v0.6.25...v0.6.26 [0.6.25]: https://github.com/skyllc-ai/UltraFastFileSearch/compare/v0.6.24...v0.6.25 diff --git a/Cargo.lock b/Cargo.lock index 9fba791be..a8dc02235 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4379,7 +4379,7 @@ checksum = "b6f5e870be6c3b371b77fe0ee0bafb859fa4964b4404c27de1d380043c4dda20" [[package]] name = "uffs-bench" -version = "0.6.27" +version = "0.6.28" dependencies = [ "chrono", "clap", @@ -4396,7 +4396,7 @@ dependencies = [ [[package]] name = "uffs-broker" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "serde", @@ -4414,14 +4414,14 @@ dependencies = [ [[package]] name = "uffs-broker-protocol" -version = "0.6.27" +version = "0.6.28" dependencies = [ "thiserror 2.0.18", ] [[package]] name = "uffs-ci-pipeline" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "chrono", @@ -4440,7 +4440,7 @@ dependencies = [ [[package]] name = "uffs-cli" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "assert_cmd", @@ -4462,7 +4462,7 @@ dependencies = [ [[package]] name = "uffs-client" -version = "0.6.27" +version = "0.6.28" dependencies = [ "dirs-next", "libc", @@ -4482,7 +4482,7 @@ dependencies = [ [[package]] name = "uffs-content" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "blake3", @@ -4506,7 +4506,7 @@ dependencies = [ [[package]] name = "uffs-content-protocol" -version = "0.6.27" +version = "0.6.28" dependencies = [ "bitflags", "blake3", @@ -4516,7 +4516,7 @@ dependencies = [ [[package]] name = "uffs-content-reader" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "proptest", @@ -4533,7 +4533,7 @@ dependencies = [ [[package]] name = "uffs-content-reader-protocol" -version = "0.6.27" +version = "0.6.28" dependencies = [ "proptest", "thiserror 2.0.18", @@ -4541,7 +4541,7 @@ dependencies = [ [[package]] name = "uffs-core" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "bytemuck", @@ -4572,7 +4572,7 @@ dependencies = [ [[package]] name = "uffs-daemon" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "clap", @@ -4605,7 +4605,7 @@ dependencies = [ [[package]] name = "uffs-diag" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "chrono", @@ -4620,7 +4620,7 @@ dependencies = [ [[package]] name = "uffs-format" -version = "0.6.27" +version = "0.6.28" dependencies = [ "chrono", "itoa", @@ -4631,7 +4631,7 @@ dependencies = [ [[package]] name = "uffs-gen-hooks" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "clap", @@ -4643,7 +4643,7 @@ dependencies = [ [[package]] name = "uffs-gen-workflow" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "clap", @@ -4656,7 +4656,7 @@ dependencies = [ [[package]] name = "uffs-manifest-audit" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "clap", @@ -4668,7 +4668,7 @@ dependencies = [ [[package]] name = "uffs-mcp" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "axum", @@ -4692,7 +4692,7 @@ dependencies = [ [[package]] name = "uffs-mft" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "bitflags", @@ -4733,14 +4733,14 @@ dependencies = [ [[package]] name = "uffs-polars" -version = "0.6.27" +version = "0.6.28" dependencies = [ "polars", ] [[package]] name = "uffs-security" -version = "0.6.27" +version = "0.6.28" dependencies = [ "aes-gcm", "dirs-next", @@ -4755,22 +4755,22 @@ dependencies = [ [[package]] name = "uffs-statusfmt" -version = "0.6.27" +version = "0.6.28" [[package]] name = "uffs-text" -version = "0.6.27" +version = "0.6.28" dependencies = [ "bytemuck", ] [[package]] name = "uffs-time" -version = "0.6.27" +version = "0.6.28" [[package]] name = "uffs-update" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "dirs-next", @@ -4789,11 +4789,11 @@ dependencies = [ [[package]] name = "uffs-version" -version = "0.6.27" +version = "0.6.28" [[package]] name = "uffs-vss-requestor" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "cc", @@ -4805,7 +4805,7 @@ dependencies = [ [[package]] name = "uffs-winsvc" -version = "0.6.27" +version = "0.6.28" dependencies = [ "anyhow", "windows 0.62.2", diff --git a/Cargo.toml b/Cargo.toml index 60f0c791e..23a1c4c1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -70,7 +70,7 @@ members = [ # Workspace Package Metadata (inherited by all crates) # ───────────────────────────────────────────────────────────────────────────── [workspace.package] -version = "0.6.27" +version = "0.6.28" edition = "2024" # No `rust-version` claim: the workspace is structurally nightly-only. # `crates/uffs-polars` enables `polars/nightly` unconditionally, which @@ -137,23 +137,23 @@ publish = false # proposed-plan output for 12 days because `release-plz update` # failed at `cargo package` with this very error. See # `release-automation-baseline.md` §10 for the diagnostic trail. -uffs-polars = { path = "crates/uffs-polars", version = "0.6.27" } -uffs-security = { path = "crates/uffs-security", version = "0.6.27" } -uffs-text = { path = "crates/uffs-text", version = "0.6.27" } -uffs-time = { path = "crates/uffs-time", version = "0.6.27" } -uffs-version = { path = "crates/uffs-version", version = "0.6.27" } -uffs-statusfmt = { path = "crates/uffs-statusfmt", version = "0.6.27" } -uffs-mft = { path = "crates/uffs-mft", version = "0.6.27" } -uffs-format = { path = "crates/uffs-format", version = "0.6.27" } -uffs-core = { path = "crates/uffs-core", version = "0.6.27" } -uffs-client = { path = "crates/uffs-client", version = "0.6.27" } +uffs-polars = { path = "crates/uffs-polars", version = "0.6.28" } +uffs-security = { path = "crates/uffs-security", version = "0.6.28" } +uffs-text = { path = "crates/uffs-text", version = "0.6.28" } +uffs-time = { path = "crates/uffs-time", version = "0.6.28" } +uffs-version = { path = "crates/uffs-version", version = "0.6.28" } +uffs-statusfmt = { path = "crates/uffs-statusfmt", version = "0.6.28" } +uffs-mft = { path = "crates/uffs-mft", version = "0.6.28" } +uffs-format = { path = "crates/uffs-format", version = "0.6.28" } +uffs-core = { path = "crates/uffs-core", version = "0.6.28" } +uffs-client = { path = "crates/uffs-client", version = "0.6.28" } # `uffs-broker-protocol` carries the wire-protocol types shared between # `uffs-broker` (the elevated handle vendor, Windows-only binary) and # `uffs-daemon::broker_client` (the handle consumer). Pure-logic # Layer-0 lib — cross-platform tests run on every CI lane. Added in # F5 (issue #205) so neither side duplicates `BROKER_PIPE_NAME` / # wire-format byte literals. -uffs-broker-protocol = { path = "crates/uffs-broker-protocol", version = "0.6.27" } +uffs-broker-protocol = { path = "crates/uffs-broker-protocol", version = "0.6.28" } # `uffs-content-protocol` carries the wire-protocol types shared between # `uffs-content` (the unprivileged content-coordinator producer, # Windows-only binary) and any downstream consumer (e.g. Docenta). @@ -163,21 +163,21 @@ uffs-broker-protocol = { path = "crates/uffs-broker-protocol", version = "0.6.27 # `content-stream-tool-design.md` sketch, its # `uffs-content-stream-enterprise-design-review.md` replacement-design # review, and Docenta's `uffs-ingest-protocol-v2-vss.md`. -uffs-content-protocol = { path = "crates/uffs-content-protocol", version = "0.6.27" } +uffs-content-protocol = { path = "crates/uffs-content-protocol", version = "0.6.28" } # `uffs-content-reader-protocol` — the private wire format between # `uffs-content` (Coordinator) and the privileged Snapshot Reader process # (addendum §2.1-§2.4). Deliberately does NOT depend on # `uffs-content-protocol` — both are Layer 0, and Layer-0-to-Layer-0 # internal deps are disallowed (see crate-graph.md); see this crate's # Cargo.toml for the full rationale. -uffs-content-reader-protocol = { path = "crates/uffs-content-reader-protocol", version = "0.6.27" } +uffs-content-reader-protocol = { path = "crates/uffs-content-reader-protocol", version = "0.6.28" } # `uffs-winsvc` — native Windows service control (SCM query/start/stop) + # the non-connecting broker-pipe readiness probe. Layer-0 leaf: its only # dependency is the `windows` crate (windows-target), with non-Windows # stubs so cross-platform consumers (uffs-update, uffs-cli) compile. # Single source of truth for the `sc`/SCM mechanics previously duplicated # across uffs-broker, uffs-update, and uffs-cli. -uffs-winsvc = { path = "crates/uffs-winsvc", version = "0.6.27" } +uffs-winsvc = { path = "crates/uffs-winsvc", version = "0.6.28" } # NOTE: no `uffs-broker` workspace dependency alias on purpose — # `uffs-broker` is a binary-only crate (the only `[lib]` it carries is # this protocol module's now-extracted sibling); no other workspace diff --git a/crates/uffs-cli/Cargo.toml b/crates/uffs-cli/Cargo.toml index 946e54eea..28b9b8874 100644 --- a/crates/uffs-cli/Cargo.toml +++ b/crates/uffs-cli/Cargo.toml @@ -69,7 +69,7 @@ path = "src/main.rs" # `version = "0.5.90"` is required for `cargo package` validation — # see root `Cargo.toml`'s [workspace.dependencies] note for the full # rationale (R6 of `release-automation-plan.md`). -uffs-client = { path = "../uffs-client", version = "0.6.27", default-features = false } +uffs-client = { path = "../uffs-client", version = "0.6.28", default-features = false } # Canonical CSV / parity / legacy-footer writer. Direct dep (not a # re-export chain through `uffs-client`) so the CLI and the daemon @@ -77,7 +77,7 @@ uffs-client = { path = "../uffs-client", version = "0.6.27", default-features = # `version = "0.5.90"` is required for `cargo package` validation — # see root `Cargo.toml`'s [workspace.dependencies] note for the full # rationale (R6 of `release-automation-plan.md`). -uffs-format = { path = "../uffs-format", version = "0.6.27" } +uffs-format = { path = "../uffs-format", version = "0.6.28" } # Typed drive-letter newtype. Direct dep so the CLI command signatures # (`daemon_load`, `daemon_tiering`, etc.) name `DriveLetter` natively From b2c7d6147792657271838e86cd1a52cf2f281152 Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:14:33 -0700 Subject: [PATCH 2/4] ci: retrigger checks (previous run hit a transient GitHub API outage) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The prior CodeQL run's Initialize step failed on "Encountered an error while trying to determine feature enablement: HttpError: No server is currently available" — GitHub's own API was degraded at that moment, unrelated to this branch's content. Empty commit to get a clean CI run against the same tree. From 3a8634de953f53ce7662e9838c9aaaf16d597d8c Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Sun, 19 Jul 2026 18:24:08 -0700 Subject: [PATCH 3/4] fix(ci): don't let a transient GitHub API error fail the commitlint gate gh api's stdout on a 5xx error is its JSON error body, but the command still exits non-zero; the existing `|| true` swallowed the exit code while EXISTING_ID still captured that JSON text as if it were a real comment id. The subsequent DELETE/PATCH call then crashed on the garbage URL, failing this *required* gate for a reason unrelated to title conformance (release PR #564 hit this: title conformed, the stale-comment-cleanup lookup 503'd, and the whole job died). Validate EXISTING_ID is purely numeric before trusting it; anything else (including a JSON error blob) is treated as "no existing comment" so the conformance result stands on its own. --- .github/workflows/commitlint.yml | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/.github/workflows/commitlint.yml b/.github/workflows/commitlint.yml index 6af627355..7b5dd253a 100644 --- a/.github/workflows/commitlint.yml +++ b/.github/workflows/commitlint.yml @@ -196,11 +196,24 @@ jobs: # comments endpoint (PR comments live there) and jq-filter # by marker presence. `head -1` defends against the # (impossible-but-cheap-to-guard) double-managed-comment case. + # + # A transient GitHub API error (e.g. a 503) makes `gh api` + # print its JSON error body to stdout while still exiting + # non-zero; the `|| true` swallows the exit code but not + # that body, so EXISTING_ID could otherwise become garbage + # text instead of a real comment id — which then crashes the + # DELETE/PATCH call below and fails this *required* gate for + # a reason unrelated to title conformance. Validate the + # result is purely numeric (a real id always is) and treat + # anything else as "no existing comment found". EXISTING_ID=$( gh api --paginate "repos/${GITHUB_REPOSITORY}/issues/${PR_NUMBER}/comments" \ --jq ".[] | select(.body | contains(\"${COMMENT_MARKER}\")) | .id" \ | head -1 || true ) + if ! [[ "${EXISTING_ID}" =~ ^[0-9]+$ ]]; then + EXISTING_ID="" + fi if echo "$TITLE" | grep -qE "$PATTERN"; then # ── Conforming title ─────────────────────────────────── From 2f0b864cb5fe6c397bbc907b7251aa6fc3136b11 Mon Sep 17 00:00:00 2001 From: Robert M1 <50460704+githubrobbi@users.noreply.github.com> Date: Sun, 19 Jul 2026 19:12:17 -0700 Subject: [PATCH 4/4] fix(test): stop asserting concurrency via a wall-clock threshold Two tests inferred concurrency from `elapsed < sequential_estimate * 3 / 4` (100ms-per-operation fixtures). That's only ever true relative to how fast the machine happens to be that run: on a loaded/throttled GitHub-hosted Windows runner, thread::sleep(100ms) itself can take several hundred milliseconds of wall-clock time, which pushes *both* the sequential and concurrent paths past any fixed absolute threshold. Hit for real in release PR #564's merge-queue run: elapsed 889ms vs. a 600ms "fully sequential" estimate -- worse than fully sequential, which is only possible under uniform scheduling overhead, not an actual concurrency regression (the dispatch code was independently re-verified: `emit::read_and_emit_all_candidates` and `workflow::enumerate_all_roots_concurrently` both still spawn one thread per lease/root via `std::thread::scope`). Replace the wall-clock-ratio assertion with direct proof: record every operation's (start, end) Instant and assert at least two intervals overlap. Two things overlapping in time is true or false independent of how slow the machine is -- it only asks whether they ran at the same time, which is what "concurrent" actually means. Verified 15/15 consecutive local runs; xwin cross-compile clean; lint-tests (pedantic + nursery) clean. --- crates/uffs-content/src/job/tests.rs | 139 ++++++++++++++++++--------- 1 file changed, 94 insertions(+), 45 deletions(-) diff --git a/crates/uffs-content/src/job/tests.rs b/crates/uffs-content/src/job/tests.rs index e0dd35023..e6dceb0c7 100644 --- a/crates/uffs-content/src/job/tests.rs +++ b/crates/uffs-content/src/job/tests.rs @@ -7,9 +7,11 @@ //! `crates/uffs-content/tests/e2e_dir_walk_parity_fake_reader.rs` — these //! tests instead cover this module's own internals in isolation. +use alloc::sync::Arc; use core::time::Duration; use std::fs; use std::path::{Path, PathBuf}; +use std::sync::Mutex; use std::time::Instant; use uffs_content_protocol::codec::Reader; @@ -358,14 +360,53 @@ impl CandidateSource for MultiLeaseCandidateSource { } } +/// Asserts that at least two of `intervals` overlap in wall-clock time — +/// direct proof that two of the recorded operations actually ran at the +/// same time, rather than inferring concurrency from a coarse +/// elapsed-time-vs-threshold check. A fixed millisecond threshold is +/// only ever true *relative to how fast the test machine happens to be +/// that run*; on a loaded/throttled CI runner, `thread::sleep(100ms)` +/// itself can take several hundred milliseconds of wall-clock time +/// (real GitHub-hosted-Windows-runner behavior, not hypothetical — see +/// the `concurrent_lease_runs_...` test's git history), which pushes +/// *both* the sequential and concurrent paths past any fixed absolute +/// threshold and produces a false failure. Two intervals overlapping is +/// true or false independent of how slow the machine is: it only asks +/// whether two things happened during the same stretch of time. +fn assert_any_two_intervals_overlap(intervals: &[(Instant, Instant)], what: &str) { + for (i, &(a_start, a_end)) in intervals.iter().enumerate() { + for &(b_start, b_end) in intervals.get(i + 1..).unwrap_or_default() { + if a_start < b_end && b_start < a_end { + return; + } + } + } + panic!("no two {what} intervals overlap in wall-clock time: {intervals:?}"); +} + /// Test-only [`ContentSource`] whose every candidate read sleeps /// `per_candidate_delay` before returning a fixed 4-byte payload — /// simulates real per-candidate I/O latency without touching a real -/// disk, so a test can assert on wall-clock time to prove concurrent -/// lease runs actually overlap (rather than merely not crashing). +/// disk. Records each read's (start, end) so a test can prove two +/// lease runs' reads genuinely overlapped in wall-clock time (see +/// [`assert_any_two_intervals_overlap`]) rather than inferring it from +/// an elapsed-time-vs-threshold check. struct SlowContentSource { /// How long each candidate's one real read takes. per_candidate_delay: Duration, + /// (start, end) of every `read_at` call across every thread. `Arc` + /// (not a borrow of `&self`) because `ContentSource::begin_read` + /// returns a `'static` `Box`. + intervals: Arc>>, +} + +impl SlowContentSource { + fn new(per_candidate_delay: Duration) -> Self { + Self { + per_candidate_delay, + intervals: Arc::new(Mutex::new(Vec::new())), + } + } } impl ContentSource for SlowContentSource { @@ -377,6 +418,7 @@ impl ContentSource for SlowContentSource { Ok(Box::new(SlowReadSession { delay: self.per_candidate_delay, served: false, + intervals: Arc::clone(&self.intervals), })) } } @@ -388,11 +430,19 @@ struct SlowReadSession { delay: Duration, /// Whether the 4-byte payload has already been served. served: bool, + /// Shared back-reference to record this read's (start, end) into. + intervals: Arc>>, } impl ReadSession for SlowReadSession { fn read_at(&mut self, _offset: u64, _max_len: u32) -> std::io::Result> { + let start = Instant::now(); std::thread::sleep(self.delay); + let end = Instant::now(); + self.intervals + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push((start, end)); if self.served { return Ok(Vec::new()); } @@ -428,12 +478,9 @@ fn concurrent_lease_runs_actually_overlap_and_never_interleave_a_candidates_fram let candidate_source = MultiLeaseCandidateSource { per_lease: CANDIDATES_PER_LEASE, }; - let content_source = SlowContentSource { - per_candidate_delay: PER_CANDIDATE_DELAY, - }; + let content_source = SlowContentSource::new(PER_CANDIDATE_DELAY); let mut frames = Vec::new(); - let started_at = Instant::now(); let outcome = run_job( &request, &candidate_source, @@ -448,28 +495,22 @@ fn concurrent_lease_runs_actually_overlap_and_never_interleave_a_candidates_fram }, ) .expect("run_job must succeed"); - let elapsed = started_at.elapsed(); let total_candidates = 2 * CANDIDATES_PER_LEASE; assert_eq!(outcome.run_summary.candidate_count, total_candidates as u64); assert_eq!(outcome.run_summary.succeeded_count, total_candidates as u64); assert_eq!(outcome.run_summary.failed_retryable_count, 0); - // Sequential-lease processing would cost roughly - // 2 * CANDIDATES_PER_LEASE * PER_CANDIDATE_DELAY (~600ms); concurrent - // lease runs should cost roughly CANDIDATES_PER_LEASE * - // PER_CANDIDATE_DELAY (~300ms), since both leases' single-connection - // (concurrency = 1) reads proceed at the same time. The threshold - // sits comfortably between the two, with slack for scheduling - // jitter on a loaded CI machine. - let sequential_estimate = - PER_CANDIDATE_DELAY * u32::try_from(total_candidates).unwrap_or(u32::MAX); - assert!( - elapsed < sequential_estimate * 3 / 4, - "elapsed {elapsed:?} should be well under the fully-sequential estimate \ - {sequential_estimate:?} -- lease runs (drives) do not appear to be running \ - concurrently" - ); + // Direct proof of concurrency: two different leases' reads must + // genuinely overlap in wall-clock time (each lease runs its + // CANDIDATES_PER_LEASE reads sequentially within itself, at + // concurrency = 1, so an overlap can only come from two *different* + // leases' single-connection reads proceeding at the same time). + let intervals = content_source + .intervals + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + assert_any_two_intervals_overlap(&intervals, "lease-run read_at"); // Correctness: decode every frame in emission order and confirm no // candidate's frame group (FILE_BEGIN..FILE_END) is ever split apart @@ -533,16 +574,36 @@ fn concurrent_lease_runs_actually_overlap_and_never_interleave_a_candidates_fram /// Test-only [`CandidateSource`] whose every `enumerate` call sleeps /// `per_root_delay` before returning one fixed candidate for `root` — /// simulates real per-root search latency (a synchronous round trip to -/// the daemon) without touching a real daemon, so a test can assert on -/// wall-clock time to prove root enumeration actually overlaps. +/// the daemon) without touching a real daemon. Records each call's +/// (start, end) so a test can prove two roots' enumeration genuinely +/// overlapped in wall-clock time (see +/// [`assert_any_two_intervals_overlap`]) rather than inferring it from +/// an elapsed-time-vs-threshold check. struct SlowEnumerateCandidateSource { /// How long each root's `enumerate` call takes. per_root_delay: Duration, + /// (start, end) of every `enumerate` call across every thread. + intervals: Mutex>, +} + +impl SlowEnumerateCandidateSource { + fn new(per_root_delay: Duration) -> Self { + Self { + per_root_delay, + intervals: Mutex::new(Vec::new()), + } + } } impl CandidateSource for SlowEnumerateCandidateSource { fn enumerate(&self, root: &Path) -> std::io::Result> { + let start = Instant::now(); std::thread::sleep(self.per_root_delay); + let end = Instant::now(); + self.intervals + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner) + .push((start, end)); let root_str = root.to_string_lossy(); let root_index: u64 = root_str .strip_prefix("root:") @@ -583,14 +644,9 @@ fn root_enumeration_actually_overlaps_across_roots() { ..Default::default() }; - let candidate_source = SlowEnumerateCandidateSource { - per_root_delay: PER_ROOT_DELAY, - }; - let content_source = SlowContentSource { - per_candidate_delay: Duration::ZERO, - }; + let candidate_source = SlowEnumerateCandidateSource::new(PER_ROOT_DELAY); + let content_source = SlowContentSource::new(Duration::ZERO); - let started_at = Instant::now(); let outcome = run_job( &request, &candidate_source, @@ -602,23 +658,16 @@ fn root_enumeration_actually_overlaps_across_roots() { |_frame| Ok(()), ) .expect("run_job must succeed"); - let elapsed = started_at.elapsed(); assert_eq!(outcome.run_summary.candidate_count, ROOT_COUNT as u64); assert_eq!(outcome.run_summary.succeeded_count, ROOT_COUNT as u64); assert_eq!(outcome.run_summary.failed_retryable_count, 0); - // Sequential enumeration would cost roughly - // ROOT_COUNT * PER_ROOT_DELAY (~400ms); concurrent enumeration - // should cost roughly PER_ROOT_DELAY (~100ms), since every root's - // `enumerate` call runs on its own thread at the same time. The - // threshold sits comfortably between the two, with slack for - // scheduling jitter on a loaded CI machine. - let sequential_estimate = PER_ROOT_DELAY * u32::try_from(ROOT_COUNT).unwrap_or(u32::MAX); - assert!( - elapsed < sequential_estimate * 3 / 4, - "elapsed {elapsed:?} should be well under the fully-sequential estimate \ - {sequential_estimate:?} -- root enumeration does not appear to be running \ - concurrently" - ); + // Direct proof of concurrency: two different roots' `enumerate` + // calls must genuinely overlap in wall-clock time. + let intervals = candidate_source + .intervals + .lock() + .unwrap_or_else(std::sync::PoisonError::into_inner); + assert_any_two_intervals_overlap(&intervals, "root enumeration"); }