Skip to content

Reject a bad --summary-format before running the command - #454

Open
asmacdo wants to merge 4 commits into
con:mainfrom
asmacdo:summary-format-errors
Open

asmacdo wants to merge 4 commits into
con:mainfrom
asmacdo:summary-format-errors

Conversation

@asmacdo

@asmacdo asmacdo commented Sep 30, 2026 •

Copy link
Copy Markdown
Member

A malformed --summary-format is now rejected before the command runs (exit 2, one-line error, no traceback) instead of crashing after the run and replacing the command's exit code. The str fallback that silently discarded a bad format spec is removed.

Changes

  • Validate at parse time. --summary-format gets an argparse type= that renders the template against typed placeholder values (VALIDATE_SUMMARY_PLACEHOLDERS), once with colors off and once on. argparse applies type= to string defaults too, so a bad DUCT_SUMMARY_FORMAT (env or .env) is caught the same way; the error names all three sources.
  • Drop the fallback in SummaryFormatter.format_field: {peak_rss:.2d} used to print the raw 123456 plus a warning; it now fails validation.
  • Guard the post-run render in execute(): if rendering still fails (e.g. execute() called directly), log an error and return the command's exit code.
  • Hint for Python's {field!conv:spec} order: duct's conversions return colored strings, so {wall_clock_time!X:.3f} fails with colors; the error suggests {wall_clock_time:.3f!X}.
  • CLAUDE.md: fail fast before the run, loud fallbacks after it.

Example:

$ con-duct run --summary-format '{nonexistent}' sleep 5
con-duct run: error: argument --summary-format: '{nonexistent}': KeyError: 'nonexistent' (set via --summary-format, DUCT_SUMMARY_FORMAT, or a .env file)
$ echo $?
2

Behavior changes to note

  • Templates the fallback used to tolerate are now rejected: a spec that does not fit the value's type ({wall_clock_time:d}, {start_time:%Y}, {average_rss:,d}), and {field!X:spec} when it only works without colors.
  • test_execution_summary_formatted_wall_clock_time_nowvalid pinned the fallback for {wall_clock_time!X:.3f} with colors; it now expects the ValueError.
  • The default format is unaffected (a test asserts it validates).

Verification

  • tox -e py312: 497 passed; tox -e lint,typing clean.
  • New tests: parse-time rejection of 12 bad templates (unknown key, unmatched brace, unknown conversion, positional fields, type-mismatched specs, attribute/index access, conversion order); env-var rejection; the conversion-order hint; placeholder keys and types equal a real Report's execution_summary after two samples (mutation-checked: an int average_rss placeholder fails it); execute() with a bad template returns the command's exit code and keeps the logs.
Why placeholders, and why the type test

A spec error depends only on the value's type (None renders as - before the spec is applied), so rendering against one correctly typed value per key reproduces exactly what the post-run render would do, no stricter and no looser. Python accepts float specs on ints ({peak_rss:.2f}) but not d on floats, and the validator inherits that exactly. The type test keeps the placeholders honest as summary fields change.

except Exception in the argparse type: formats can also raise AttributeError ({peak_rss.foo}) or TypeError ({peak_rss[0]}); catching only KeyError/IndexError/ValueError left those as a traceback or argparse's generic "invalid value" message.

🤖 Generated with Claude Code

asmacdo and others added 4 commits September 30, 2026 12:38
Co-Authored-By: Claude Code 2.1.285 / Claude Opus 5.5 <noreply@anthropic.com>
The summary template was only rendered after the command finished, so a
typo cost the whole run and its traceback replaced the command's exit
code. Render it against typed placeholder values at parse time instead;
argparse reports the error and exits 2. Since argparse also applies
type= to string defaults, a bad DUCT_SUMMARY_FORMAT (env or .env) is
caught the same way.

Co-Authored-By: Claude Code 2.1.285 / Claude Opus 5.5 <noreply@anthropic.com>
SummaryFormatter.format_field caught ValueError and printed the raw value
with a warning, silently discarding the user's spec (e.g. {peak_rss:.2d}
printed 123456). With the template now validated before the command
runs, a bad spec fails there for free, so fail instead of guessing
(con#192).

Validation renders with colors both off and on, since the custom
conversions return colored strings: {wall_clock_time!X:.3f} (Python's
conversion-first order) works without colors but not with them. The
error suggests duct's {field:spec!conv} order when it spots the other.

Co-Authored-By: Claude Code 2.1.285 / Claude Opus 5.5 <noreply@anthropic.com>
execute() renders --summary-format after the command finished; an
exception there replaced the command's exit code with 1. Log an error
(pointing at info.json when it was written) and return the command's
exit code. The CLI now validates the template up front, but execute()
can be called directly, and after the run a duct failure must not cost
the user the run.

Co-Authored-By: Claude Code 2.1.285 / Claude Opus 5.5 <noreply@anthropic.com>
@asmacdo asmacdo added the semver-minor Increment the minor version when merged label Sep 30, 2026
@codecov

codecov Bot commented Sep 30, 2026 •

Copy link
Copy Markdown

Codecov Report

✅ All modified and coverable lines are covered by tests.
✅ Project coverage is 92.63%. Comparing base (7a25f42) to head (508b6ad).
⚠️ Report is 5 commits behind head on main.

Additional details and impacted files
@@            Coverage Diff             @@
##             main     #454      +/-   ##
==========================================
+ Coverage   92.03%   92.63%   +0.59%     
==========================================
  Files          15       15              
  Lines        1344     1358      +14     
  Branches      182      184       +2     
==========================================
+ Hits         1237     1258      +21     
+ Misses         71       69       -2     
+ Partials       36       31       -5     

☔ View full report in Codecov by Harness.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@asmacdo
asmacdo marked this pull request as ready for review September 30, 2026 19:47
Copilot AI balanced review requested due to automatic review settings September 30, 2026 19:47

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot review overview

🔵 Needs a closer look

It is a user-facing behavior change to CLI argument handling and exit-code semantics (previously-tolerated formats and env defaults now hard-fail at parse time), which warrants a human maintainer's compatibility sign-off despite being well-tested.

Review effort: Balanced
Findings: None

What changed in this PR

This PR makes con-duct run reject a malformed --summary-format at argparse parse time (exit 2, one-line error, no traceback) instead of crashing after the command runs and clobbering its exit code. It removes the silent str fallback in SummaryFormatter.format_field that used to mask bad specs, and adds a defensive guard around the post-run render so a render failure never costs the user a completed (potentially expensive) run. This implements the "fail fast, but never punish a finished run" philosophy now codified in CLAUDE.md, closing #192.

Changes:

  • Add validate_summary_format + VALIDATE_SUMMARY_PLACEHOLDERS and wire them into a new argparse type=_summary_format_type, which also validates the DUCT_SUMMARY_FORMAT/.env default and emits a conversion-order hint for {field!conv:spec}.
  • Drop the ValueError→str fallback in format_field; guard the post-run render in execute() to log an error and preserve the command's exit code and logs.
  • Add tests for parse-time rejection, env-var rejection, the conversion-order hint, placeholder/type parity with a real run, and exit-code preservation on post-run render failure.
File Description
src/​con_duct/​_formatter.py Removes silent str fallback; adds VALIDATE_SUMMARY_PLACEHOLDERS and validate_summary_format to render a template against typed placeholders.
src/​con_duct/​cli.py Adds _summary_format_type argparse type (with conversion-order hint) and applies it to --summary-format.
src/​con_duct/​_duct_main.py Wraps the post-run summary render in try/except, logging an error while preserving the command's exit code and logs.
test/​test_formatter.py Adds spec-mismatch, default-validation, and placeholder-parity tests; updates the conversion-order expectation to ValueError.
test/​test_cli.py Adds parse-time rejection, env-var rejection, pass-through, and conversion-order-hint tests.
test/​duct_main/​test_execution.py Adds a test that a post-run render failure keeps the command's exit code and logs.
CLAUDE.md Documents the fail-fast-before-run vs. loud-fallback-after-run convention.

💡 Add a code-review agent skill or configure MCP servers for context-aware, tailored reviews. Learn more in the docs.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

semver-minor Increment the minor version when merged

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Error out if user provides errorneous format specification

2 participants