Repository navigation
(fix) --json keeps stdout to one JSON document (closes #16, #40, #41) - #99
Merged
Merged
Conversation
The flag promised a singular document, but broke in the following functions/files 1. confirm() 2. src/utils/fs.rs 3. help <command> 4. version Now outputs to stderr and the 4 sources of the bug no longer ask for that document. Instead throwing a new error: BallError::ConfirmationRequired Tested on Windows and Linux
`baller --json roster <name>` printed "Searching '<name>' not found locally, searching registries..." on stdout ahead of the search document whenever the name was not on the roster, so the output failed jq. The notice is progress output and now goes through tracing: on stderr, and hidden by -q and --json. The same match fell back to a registry search on any lookup error; only PackageNotFound does now, so a roster that cannot be read is reported rather than searched. Child processes inherit stdout too: hook scripts, sudo apt-get/dnf/ pacman and cargo install wrote ahead of the JSON document. Under --json their stdout now goes to stderr (utils::output::child_stdout, with the mode set once in main); text mode is unchanged. --quiet keeps printing `version`: it hides progress, never a command's result. Documented, and pinned by a test. Tested with three new tests in tests/json_output.rs, each failing when its fix is reverted, on Linux and on a Windows 11 VM. The hook test fails under Wine only, whose powershell is a stub. Closes #40 Closes #41
Installing a system package printed "System installing via apt... quiet=false" on stderr. The tracing migration (#32) kept the quiet argument of the old output::info(quiet, msg) helper on one call in draft.rs, and tracing reads it as a field to print. The tracer already drops info lines under -q and --json, so the argument is removed; the leftover test_info_is_quiet_safe, which still exercised tracing::info!(bool, ...), goes with it. Two Referee integration tests failed intermittently on the Windows CI runner. MockServer polls a non-blocking listener, and on Windows an accepted socket inherits that mode, voiding read_request's read timeout: a request still in flight at accept time read as WouldBlock and was dropped unrecorded. read_request now restores blocking mode. The new test_the_mock_server_waits_for_a_request_that_arrives_late sends its request 200 ms after connecting. Built without the fix it failed 3 of 3 runs on a Windows 11 VM and under Wine; with the fix it passed 3 of 3. Linux does not inherit the mode, so the test passes there either way.
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Description
--jsonpromised a single parseable document on stdout but broke on four paths (#16). The first commit (07c7931) fixes those four, so prompts,help,version,-h/-Vand errors each leave exactly one JSON document on stdout. The second commit (ed3d488) closes the remaining leaks:roster <name>(#40) and child processes. It also settlesversionunder--quiet(#41). See the second-commit section below. The third commit (528ea86) is two small cleanups found along the way: a strayquiet=falseindraft's output, and a race in Referee's mock OSV server that made two tests flaky on the Windows CI runner.Confirmation prompts.
confirm()(src/utils/fs.rs) printed its prompt to stdout, soballer --json ejecton a terminal putAre you sure…? [y/N]into the JSON stream. Prompts now go to stderr. Under--jsonnothing is prompted at all:confirm(prompt, json)returns the newBallError::ConfirmationRequired, which carries the question.eject,sweep,substituteandinjecttherefore need--yesunder--json, and change nothing without it. The check lives inconfirm()itself rather than in each command, so a future caller can't forget it.helpandversion. Both ignored--jsonand printed text.execute_command_helpandexecute_command_versionnow takeGlobalFlags:commands,injectedandglobal_options.arguments/subcommandsrows (name,description), with wrapped lines folded in, plusnotesparagraphs..ballfield.versionprints{"command": "version", "version": …}.-h/--help/-V/--version. clap answers these itself, in text, before baller's flags are read. When--jsonis on the command line,BallerCommand::parse_commandnow re-parses the request ashelp [<command>]orversion, keeping--configso injected commands still load.baller --json eject -hisballer --json help eject. It follows clap's ordering: a top-level-his answered before any subcommand after it, anddraft --version <V>is still draft's pin flag.Errors. Every error went to stderr as
[Error]: …, leaving a JSON consumer an empty stdout. Under--json,mainnow prints{"command", "error", "code"}on stdout and still exits 1.codeis the newBallError::code()(the variant name), which scripts can branch on.commandcomes from the parsed command, or fromcli::parse::scan_argsreading the raw arguments when clap fails first; it isnullif no subcommand was named.referee --fail-onkeeps one document.referee audit/check/scan --fail-onprint their report and then fail. A JSON error after that would be a second document, soprint_jsonand referee'sdeliverrecord that a document went out, andmainsends that failure to stderr as text instead.Also on these paths:
inject --jsonreports{"command": "inject", "name", "binary"}instead of text.substitute's rollback warning moved from a stdoutprintln!totracing::warn!.--json, so no escape codes reach JSON strings.Behaviour changes worth a reviewer's eye
baller eject x > out.txtno longer writes the prompt into the file.baller --json sweepwithout--yesnow fails withConfirmationRequiredinstead of prompting. Its"status": "aborted"document could no longer be reached and is removed.BallerCommand::parse_commandnow takes the argument list (&[OsString]).Second commit (
ed3d488): #40, #41 and child processesroster <name>(#40). When the name was not on the roster,execute_rosterprintedSearching '<name>' not found locally, searching registries...withprintln!beforesearch_remoteprinted its JSON, so the output failedjq.tracing::info!: on stderr, and hidden by-qand--json. In text mode it moves from stdout to stderr, which only shows when stdout is piped.get_packageerror, so a roster that could not be read was reported as "not installed". OnlyPackageNotFoundfalls back now (local_lookup); any other error is returned. This is the patternis_frozenswallows DB errors and reportsfalse— a failing query marks packages as unfrozen and allows modification #78 fixed foris_frozen.Child processes. An audit of every non-test
println!found no other baller print ahead of a--jsondocument. Child processes inherit stdout too, though: a hook that printed,sudo apt-get/dnf/pacman installfor a system package, orcargo install, wrote into the stream ahead of the JSON.--jsontheir stdout now goes to stderr viautils::output::child_stdout(), incore/hooks.rs,http/system.rsandhttp/cargo.rs. The output is still visible, just not on stdout.mainsets the mode once withset_json_mode, next tocolored's override, sorun_hook's 23 callers are untouched.versionunder--quiet(#41). The--jsonhalf of #41 is the first commit. The issue also asked forballer --quiet versionto print nothing; that is deliberately not adopted.--quiethides progress, and every command still prints its result under it (-q rosterstill prints the table). A silent-q versionwould be the one command whose result disappears, and would do nothing at all. The--quietrow and theversionsection ofdocs/commands.mdnow say so, and a test pins it.Third commit (
528ea86):quiet=falseand the flaky Windows mockdraftprintedSystem installing via apt... quiet=false. The tracing migration (#32) left thequietargument of the oldoutput::info(quiet, msg)helper on one call insrc/commands/draft.rs, and tracing reads it as a field to print. The argument is removed; the tracer already dropsinfolines under-q/--json, so the line is still hidden there. The leftovertest_info_is_quiet_safe, which still exercisedtracing::info!(bool, …), is removed with it.Two Referee integration tests were flaky on Windows CI.
test_a_cached_verdict_does_not_cover_another_versionandtest_refresh_re_queries_a_cached_identityfailed this PR's first run ofed3d488and passed on a re-run.MockServer(src/security/integration.rs) polls a non-blocking listener, and on Windows an accepted socket inherits that mode. That voidedread_request's read timeout, so a request still in flight at accept time read asWouldBlockand was dropped unrecorded.read_requestnow restores blocking mode first. Linux sockets don't inherit the mode, which is why the bug only showed on Windows.Docs:
docs/commands.md(the--jsoncontract, help JSON, confirmations, the roster notice,--quiet, plus a newversionsection),docs/error-handling.md(the JSON error shape, the new variants),docs/architecture.md(a JSON Output section, includingchild_stdout),docs/hooks.md(hook output under--json) anddocs/inject.md.CHANGELOG.mdhas five entries under Unreleased → Fixed: one each for #16, #40 and #41, plus one per third-commit fix.Type of Change
Platform
The change is platform-agnostic. It was tested natively on Linux, cross-compiled for
x86_64-pc-windows-gnuand run under Wine, and run on a real Windows 11 Pro VM.Testing
New tests.
tests/json_output.rs(17 tests) runs the real binary in an isolated--configdirectory per case and requires stdout to parse as one JSON document. It covers:-h/--help/-V/--versioncombination, including--configsurviving the rewritecommand: nullwhen no subcommand)--jsonafter an injected command belonging to that commandConfirmationRequiredforeject,sweepandinject, and nothing changed without--yesinject --yessuccess JSONreferee audit/check --fail-onagainst a mock OSV serverroster <uninstalled>under--json(one document; every registry source disabled, so no network), the roster notice on stderr in text mode and absent under-q, apre_ejecthook's output on stderr under--jsonand on stdout without it, and-q versionstill printingEvery guard was mutation-checked. Removing the
document_printed()guard makes the--fail-ontest fail with "trailing characters". Restoring the rosterprintln!, or droppingchild_stdout()from hooks, fails the three second-commit tests. The third commit addstest_the_mock_server_waits_for_a_request_that_arrives_late, which sends its request 200 ms after connecting. A Windows build without the fix fails it 3 of 3 times on the Windows 11 VM and under Wine; with the fix it passes 3 of 3. It passes on Linux either way.BALLER_TEST_BINpoints the test at another binary, so the Windows build can be run on a Windows host. Unit tests were added forscan_args, the display-request rewrite,CommandTypes::name(), help-row splitting and the JSON help shapes,version,error_json,BallError::code()and theConfirmationRequireddisplay text.cargo build --all-targets,cargo test(802 + 832 unit, 17 integration; unchanged by the third commit, which removes one stale test and adds one),cargo clippy --all-targets -- -D warnings,cargo fmt --check: all pass. #16's five reproduction steps re-run; on a real tty (script), the prompt is on stderr only,naborts,yejects. #40's reproduction now gives one document.--json draft jq --source system, with a fakesudoonPATHthat only echoes: theapt-getlines go to stderr and stdout is one document; in text mode they stay on stdout. After the third commit the same run printsSystem installing via apt...withoutquiet=false, and-qstill hides itcargo clippy --all-targets --target x86_64-pc-windows-gnu -- -D warningspasses; no Windows-only warningscore::hooksunit tests andtest_utils_symlink_integration, fails under Wine only (Wine'spowershellis a stub); all of them pass on the VM belowRestrictedpolicy)--jsoninvocations, includingroster not-installed: each stdout one valid document, stderr empty. 17/17 integration tests (the hook test running real PowerShell); 795 + 825 unit tests pass. Text-V,version,-q versionandhelpunchanged; the roster notice is on stderrInteractive check on the Windows 11 console (transcript of the screenshot):
Checklist
07c7931,ed3d488and528ea86do;3fb77ba trigger CIis an empty commit without a type prefix, and squash-merge drops itRelated Issues
Closes #16
Closes #40
Closes #41 (the
--jsonhalf is fixed; the--quiethalf is deliberately not adopted, see Description)Screenshots/Videos
None attached: this is a CLI-only change. The Windows console transcript above is taken from a screenshot of the VM run.