Skip to content

Commit 6bb45df

Browse files
pmaxhoganclaude
andauthored
fix(chaos): expect a permission-denied skip on unix in noaccess-file (#229)
Closes #218. The v2.5.0 tag's Chaos run failed on `noaccess-file` on BOTH ubuntu and macOS with: ``` scenario errored: exactly one local.io_error (the unreadable file); got 0 ``` ## Why #195 deliberately reclassified unix permission-denied opens: EACCES/EPERM now produce a graceful SKIP carrying `local.permission_denied` (a WARN, not counted in the cycle's error total) instead of failing as `local.io_error`. That was the point - on macOS a TCC denial is the common case, and "Driven hit a disk error" sent users to check a healthy disk. Windows keeps ERROR_ACCESS_DENIED as `local.io_error`, because elevation can genuinely read around some ACL denials. `posix-mode-000` was updated in #195. This sibling row was missed. ## Why it went unnoticed CI runs the chaos jobs on Windows, where the row still passes. So the suite was green while the row was broken for every unix contributor - and it only surfaced when the `v*` tag fired the full matrix. That is exactly the failure mode #192 fixed for the append-only-log flake: a row nobody can get green locally is a row everyone learns to ignore. ## The fix Both the run-time assertion and the declared `ExpectedOutcome` are now per-platform. They had to move together: the harness compares the declared outcome against the codes the run actually observed, so fixing only the assertion left it failing with `expected graceful failure with local.io_error but observed codes: [LocalPermissionDenied]`. ## Evidence - The row passes 5/5 consecutive runs on macOS. - Full hermetic suite locally: **63 PASS / 31 SKIP / 0 FAIL** - the first clean local chaos run on this machine. - **Negative control:** restoring the old `io_errors == 1` expectation makes it fail again, so the new assertion is load-bearing rather than self-confirming. Note this does not change any product behaviour - only what the harness expects. The skip-and-report path it now pins is the one #195 shipped and that the GUI release gate confirmed end to end (a chmod 000 file produced `reason=Denied code=local.permission_denied` and raised the Full Disk Access banner). --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent 6dd0cf9 commit 6bb45df

1 file changed

Lines changed: 58 additions & 11 deletions

File tree

crates/driven-chaos/src/scenarios/storage.rs

Lines changed: 58 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -52,7 +52,7 @@ use std::sync::Arc;
5252
use async_trait::async_trait;
5353
use tokio::io::AsyncReadExt;
5454

55-
use driven_core::executor::{DefaultExecutor, Executor, ExecutorDeps, OpOutcome};
55+
use driven_core::executor::{DefaultExecutor, Executor, ExecutorDeps, OpOutcome, SkipReason};
5656
use driven_core::pacer::{Pacer, PacerCeilings, ResponseClass};
5757
use driven_core::planner;
5858
use driven_core::scanner;
@@ -539,9 +539,12 @@ impl Scenario for ReadonlyFile {
539539

540540
/// `noaccess-file` (STRESS_HARNESS s3.1): a file that stats but is unreadable
541541
/// for the current user (POSIX `chmod 000`; Windows ACL Deny:READ). The scan
542-
/// includes it (it stats fine), the executor fails to open it -> per-file
543-
/// `local.io_error`; the scan continues and NO other file is trashed as a
544-
/// cascade.
542+
/// includes it (it stats fine) and the executor cannot open it. Since #195 the
543+
/// outcome is per-platform: on Unix an EACCES/EPERM open is a graceful SKIP
544+
/// carrying `local.permission_denied` (so a macOS TCC denial stops being
545+
/// misreported as a disk error), while Windows keeps ERROR_ACCESS_DENIED as
546+
/// `local.io_error`. Either way the scan continues and NO other file is
547+
/// trashed as a cascade.
545548
struct NoaccessFile;
546549

547550
#[async_trait]
@@ -551,7 +554,7 @@ impl Scenario for NoaccessFile {
551554
}
552555

553556
fn description(&self) -> &'static str {
554-
"a stat-able but unreadable file; local.io_error logged per file, scan continues, no cascade delete"
557+
"a stat-able but unreadable file; skipped as local.permission_denied on Unix / local.io_error on Windows, scan continues, no cascade delete"
555558
}
556559

557560
fn requires(&self) -> CapabilityRequirements {
@@ -604,6 +607,7 @@ impl Scenario for NoaccessFile {
604607
// Exactly the unreadable file fails with local.io_error; the two
605608
// readable ones complete.
606609
let mut io_errors = 0u32;
610+
let mut denied_skips = 0u32;
607611
let mut other_failures: Vec<ErrorCode> = Vec::new();
608612
let mut done = 0u32;
609613
for o in &outcomes {
@@ -615,6 +619,15 @@ impl Scenario for NoaccessFile {
615619
io_errors += 1
616620
}
617621
OpOutcome::Failed { code, .. } => other_failures.push(*code),
622+
// Since #195 a Unix EACCES/EPERM open is a graceful SKIP
623+
// carrying `local.permission_denied`, not a failure - the
624+
// whole point of that change was to stop reporting a TCC
625+
// denial as a disk error. Windows keeps ERROR_ACCESS_DENIED
626+
// as `local.io_error` (elevation can genuinely read around
627+
// some ACL denials), so the expectation is per-platform.
628+
OpOutcome::Skipped { reason, .. } if *reason == SkipReason::Denied => {
629+
denied_skips += 1
630+
}
618631
OpOutcome::Skipped { .. } => {}
619632
}
620633
}
@@ -623,11 +636,17 @@ impl Scenario for NoaccessFile {
623636

624637
anyhow::ensure!(
625638
other_failures.is_empty(),
626-
"only the unreadable file may fail, and only with local.io_error: saw {other_failures:?}"
639+
"no other failure code is allowed here: saw {other_failures:?}"
627640
);
641+
#[cfg(windows)]
628642
anyhow::ensure!(
629-
io_errors == 1,
630-
"exactly one local.io_error (the unreadable file); got {io_errors}"
643+
io_errors == 1 && denied_skips == 0,
644+
"Windows: exactly one local.io_error (the unreadable file); got {io_errors} errors / {denied_skips} denied skips"
645+
);
646+
#[cfg(not(windows))]
647+
anyhow::ensure!(
648+
denied_skips == 1 && io_errors == 0,
649+
"Unix: the unreadable file must be SKIPPED as local.permission_denied (#195), not failed as a disk error; got {denied_skips} denied skips / {io_errors} local.io_error"
631650
);
632651
anyhow::ensure!(
633652
done == 2,
@@ -657,12 +676,17 @@ impl Scenario for NoaccessFile {
657676
let inv_report = reporting::assert_invariants(handle, &remote, src.id, &folder).await?;
658677

659678
Ok(Outcome {
660-
error_codes_seen: vec![ErrorCode::LocalIoError],
679+
error_codes_seen: vec![if cfg!(windows) {
680+
ErrorCode::LocalIoError
681+
} else {
682+
ErrorCode::LocalPermissionDenied
683+
}],
661684
final_drive_object_count: live,
662685
final_hash_matches_local: intact,
663686
invariants: Some(inv_report.to_invariant_outcome(true)),
664687
notes: vec![format!(
665-
"{done} readable files uploaded, 1 local.io_error, 0 trashed (no cascade)"
688+
"{done} readable files uploaded, 1 unreadable file reported per platform, \
689+
0 trashed (no cascade)"
666690
)],
667691
})
668692
}
@@ -674,8 +698,18 @@ impl Scenario for NoaccessFile {
674698
}
675699

676700
fn expected_outcome(&self) -> ExpectedOutcome {
701+
// Per-platform since #195: a Unix EACCES/EPERM open is a graceful skip
702+
// carrying `local.permission_denied` (a WARN, not an ERROR, and not
703+
// counted in the cycle's error total), while Windows keeps
704+
// ERROR_ACCESS_DENIED as `local.io_error`. The harness compares this
705+
// against the codes the run actually observed, so it must name the
706+
// code this platform really produces.
677707
ExpectedOutcome::GracefulFailureWith {
678-
code: ErrorCode::LocalIoError,
708+
code: if cfg!(windows) {
709+
ErrorCode::LocalIoError
710+
} else {
711+
ErrorCode::LocalPermissionDenied
712+
},
679713
}
680714
}
681715
}
@@ -1115,12 +1149,25 @@ mod tests {
11151149
assert!(f.required.iter().any(|c| matches!(c, Capability::Unix)));
11161150
assert!(d.required.iter().any(|c| matches!(c, Capability::Unix)));
11171151
}
1152+
// Per-platform since #195: unix EACCES/EPERM is a graceful skip
1153+
// carrying local.permission_denied; Windows keeps ERROR_ACCESS_DENIED
1154+
// as local.io_error. Assert the exact code for THIS platform rather
1155+
// than accepting either, so a regression on one platform cannot hide
1156+
// behind the other's expectation.
1157+
#[cfg(windows)]
11181158
assert!(matches!(
11191159
NoaccessFile.expected_outcome(),
11201160
ExpectedOutcome::GracefulFailureWith {
11211161
code: ErrorCode::LocalIoError
11221162
}
11231163
));
1164+
#[cfg(not(windows))]
1165+
assert!(matches!(
1166+
NoaccessFile.expected_outcome(),
1167+
ExpectedOutcome::GracefulFailureWith {
1168+
code: ErrorCode::LocalPermissionDenied
1169+
}
1170+
));
11241171
assert!(matches!(
11251172
NoaccessFolder.expected_outcome(),
11261173
ExpectedOutcome::DocumentedBehaviour

0 commit comments

Comments
 (0)