Skip to content

fix(cli): exit 1 when check cannot read its input - #1099

Merged
dekobon merged 5 commits into
mainfrom
fix/1060-check-read-failures
Jul 29, 2026
Merged

fix(cli): exit 1 when check cannot read its input#1099
dekobon merged 5 commits into
mainfrom
fix/1060-check-read-failures

Conversation

@dekobon

@dekobon dekobon commented Jul 29, 2026

Copy link
Copy Markdown
Owner

Fixes #1060.

The reported bug

validate_and_resolve_file bumped files_dispatched before
attempting the read, so the "no input files matched" guard in
run_check saw a non-zero count on a run that read nothing:

$ chmod 000 violator.c
$ bca check --no-config -p . --threshold cyclomatic=2
error processing ./violator.c: Permission denied (os error 13)
$ echo $?
0

The counter now moves only for files that were actually read, and read
failures are tallied separately in Config::read_failures. Any
unreadable input exits 1 (tool error), not 2 (gate breach) — a gate
that analysed less than its input has no verdict to report.

The half that wasn't reported

Reordering the counter alone would have left the bug live for files of
three bytes or fewer. read_file_with_eol decided "too small to
parse" from a bare fs::metadata and returned Ok(None) before
File::open — and stat succeeds on a file the process cannot read, so
a tiny unreadable file was counted as empty:

$ printf 'a;\n' > small.c && chmod 000 small.c
$ bca check --no-config -p . --threshold cyclomatic=1 ; echo $?
0

Note this produced no diagnostic at all — not even the
error processing … line from the original report — so it was strictly
worse than the reported symptom, and it contradicted the function's own
documented contract ("Returns any std::io::Error surfaced by
File::open … lacks read permission"). The shortcut now confirms
readability by opening the file first; the open is skipped for anything
that is not a regular file, so the function still never blocks on a
FIFO. Behaviour is unchanged for readable files of any size.

Scope notes

  • bca init shares the guard. scaffold_baseline produces its
    baseline by driving run_check, so an unreadable file aborts init
    rather than pinning a baseline that silently under-records the debt in
    the file it could not read. Both guards' messages are now prefixed
    bca: instead of bca check:, which misattributed an init failure
    to a subcommand the user never ran.
  • Not suppressed by --no-fail, which suppresses threshold
    failures, not broken input — consistent with the pre-existing
    empty-input guard. Both guards run before the gate is evaluated and
    before --write-baseline, so a partial run never records a baseline.
  • This can turn a currently-green CI red. A repo with any unreadable
    regular file under --paths (a stale docker-built target/, a
    node_modules owned by another uid) now fails until the path is
    excluded. That is the intended contract, and it matches what
    commands/README.md and man/bca.1 already documented ("unreadable
    input" → 1).
  • Other subcommands are unchangedbca metrics on an unreadable
    file still exits 0. Tracked in fix(cli): read failures exit 0 for every subcommand except check #1098, where bca diff --since is the
    sharper case (a dropped file reads as an added/removed function rather
    than an I/O error).

Tests

Every test was verified by perturbing the production line and
confirming it was the only failure in its suite:

  • dispatch.rs — an unreadable file bumps read_failures and not
    files_dispatched; a readable one bumps the opposite; an empty
    file still counts as dispatched, so the reorder did not narrow the
    documented semantics.
  • check_thresholds.rs — all-unreadable, partially-unreadable (two
    files, pinning the plural summary), tiny-unreadable, and
    --no-fail-does-not-mask.
  • init.rsinit refuses to baseline a partially-readable tree.
  • tools_tests.rs — a tiny readable file is still Ok(None); a tiny
    unreadable one returns PermissionDenied.

The unix-only tests each probe the real capability (fs::read on a
mode-000 file) rather than the uid, so a privileged runner skips instead
of failing, and #[cfg(unix)] sits on the fn rather than an inner
block (lesson #40).

make pre-commit is green. .bca-baseline.toml was refreshed in the
same change for the one metric that moved
(read_file_with_eol.nexits 6 → 7).

dekobon added 5 commits July 29, 2026 09:26
`files_dispatched` was bumped before the read was attempted, so the
zero-files-matched guard in `run_check` passed on a run whose every
file failed to read: `bca check` printed `error processing <path>:
Permission denied` and then exited 0 — a CI gate reporting success on
a tree it never analysed.

The counter now moves only for files that were actually read, and read
failures are tallied separately in `Config::read_failures`. Any
unreadable input exits 1 (tool error, distinct from 2 = gate breach),
because a partially analysed gate is not a passing gate. Like the
pre-existing empty-input guard, the check runs before the gate is
evaluated and before `--write-baseline`, and is not suppressed by
`--no-fail`, which suppresses threshold failures rather than broken
input. Both post-walk guards now live in `enforce_usable_input`,
mirroring `enforce_explicit_unrecognized` on the analyze side.

Fixes #1060
The three #1060 tests each staged one unreadable file, leaving the
plural arm of the summary line uncovered.

Refs #1060
Both helpers took or returned two `Arc<AtomicUsize>` values a call
site could transpose, inverting the distinction the tests pin.
`read_file_with_eol` decided "too small to parse" from a bare `stat`
and returned `Ok(None)` before opening the file. `stat` succeeds on a
file the process cannot read, so an unreadable tiny file was
indistinguishable from an empty one and the permission error the
function documents never surfaced: `bca check` on a 3-byte mode-000
file exited 0 with no diagnostic at all, not even the per-file
`error processing` line the larger-file path emits.

The shortcut now confirms readability by opening the file first. The
open is skipped for anything that is not a regular file, so the
function still never blocks on a FIFO, and behaviour is unchanged for
readable files of any size.

Fixes #1060
`bca init` scaffolds its baseline through `run_check`, so both
post-walk guards fire for it too — and named a subcommand the user
never invoked. Prefix the tool instead, and pin `init`'s behaviour:
an unreadable file aborts rather than pinning a baseline that would
silently under-record the debt in the file it could not read.

Also corrects the `read_file_with_eol` rustdoc, which still promised
`Ok(None)` for any too-small file after the readability probe landed.

Refs #1060
@codecov

codecov Bot commented Jul 29, 2026

Copy link
Copy Markdown

Codecov Report

❌ Patch coverage is 80.00000% with 1 line in your changes missing coverage. Please review.
✅ Project coverage is 98.08%. Comparing base (9f1d99d) to head (67f0eb7).

Files with missing lines Patch % Lines
src/tools.rs 80.00% 1 Missing ⚠️
Additional details and impacted files

Impacted file tree graph

@@            Coverage Diff             @@
##             main    #1099      +/-   ##
==========================================
- Coverage   98.08%   98.08%   -0.01%     
==========================================
  Files         272      272              
  Lines       67791    67795       +4     
  Branches    67361    67365       +4     
==========================================
+ Hits        66493    66496       +3     
- Misses        887      888       +1     
  Partials      411      411              
Flag Coverage Δ
rust 98.07% <80.00%> (-0.01%) ⬇️

Flags with carried forward coverage won't be shown. Click here to find out more.

Files with missing lines Coverage Δ
src/tools.rs 93.66% <80.00%> (-0.26%) ⬇️
🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.
  • 📦 JS Bundle Analysis: Save yourself from yourself by tracking and limiting bundle sizes in JS merges.

@dekobon
dekobon merged commit 6a06fd0 into main Jul 29, 2026
44 checks passed
@dekobon
dekobon deleted the fix/1060-check-read-failures branch July 29, 2026 20:34
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

fix(cli/dispatch): check exits 0 when every input file fails to read

1 participant