Skip to content

fix: capture stderr when tests fail#3995

Merged
johanneskoester merged 7 commits intosnakemake:mainfrom
fgvieira:unit_tests_print_stdout
Mar 11, 2026
Merged

fix: capture stderr when tests fail#3995
johanneskoester merged 7 commits intosnakemake:mainfrom
fgvieira:unit_tests_print_stdout

Conversation

@fgvieira
Copy link
Copy Markdown
Contributor

@fgvieira fgvieira commented Mar 8, 2026

QC

  • The PR contains a test case for the changes or the changes are already covered by an existing test case.
  • The documentation (docs/) is updated to reflect the changes or this is not necessary (e.g. if the change does neither modify the language nor the behavior or functionalities of Snakemake).

Summary by CodeRabbit

  • Refactor

    • Improved test templates to capture subprocess errors more reliably (now capturing stderr), ignore internal/config folders during file comparisons, and add clearer logging of input and workdir file lists.
  • Style

    • Cleaned up templates by removing extraneous whitespace and fixing comment formatting for clearer maintenance.

@coderabbitai
Copy link
Copy Markdown
Contributor

coderabbitai bot commented Mar 8, 2026

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: afc132ab-8702-4234-a44a-e7a04b6dd565

📥 Commits

Reviewing files that changed from the base of the PR and between 335eab4 and e39f083.

📒 Files selected for processing (1)
  • src/snakemake/unit_tests/templates/common.py.jinja2
🚧 Files skipped from review as they are similar to previous changes (1)
  • src/snakemake/unit_tests/templates/common.py.jinja2

📝 Walkthrough

Walkthrough

Added PIPE to subprocess imports in a unit-test Jinja2 template, passed stderr=PIPE to a check_output call, renamed several local variables and debug prints, adjusted workdir filtering to ignore /.snakemake and /config, and removed a stray blank line plus a comment punctuation fix in another template.

Changes

Cohort / File(s) Summary
Subprocess stderr & variable renames
src/snakemake/unit_tests/templates/common.py.jinja2
Added PIPE to imports, passed stderr=PIPE to check_output, introduced/renamed variables (input_files, workdir_files, expected_files), updated debug prints, and changed workdir filtering to exclude paths containing /.snakemake and /config.
Formatting/comment cleanup
src/snakemake/unit_tests/templates/ruletest.py.jinja2
Removed an extra blank line after the module docstring and fixed a trailing-comma/space in a comment.

Estimated code review effort

🎯 2 (Simple) | ⏱️ ~10 minutes

Suggested reviewers

  • johanneskoester
🚥 Pre-merge checks | ✅ 2 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Description check ⚠️ Warning The PR description contains only the unchecked template with no additional context, explanation, or details about the changes and their rationale. Add a brief description explaining why stderr capture is important, what was changed, and which tests validate these changes. Check at least one of the QC items.
✅ Passed checks (2 passed)
Check name Status Explanation
Title check ✅ Passed The title directly relates to the main change: adding stderr=PIPE argument to capture stderr output when tests fail, which is a specific and meaningful improvement.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.

✏️ Tip: You can configure your own custom pre-merge checks in the settings.

✨ Finishing Touches
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Post copyable unit tests in a comment

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands and usage tips.

@fgvieira fgvieira changed the title fix : capture stderr fix : capture stderr when tests fail Mar 8, 2026
@fgvieira fgvieira changed the title fix : capture stderr when tests fail fix: capture stderr when tests fail Mar 8, 2026
Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

🧹 Nitpick comments (1)
src/snakemake/unit_tests/templates/ruletest.py.jinja2 (1)

10-10: Consider updating the snakemake invocation to also capture stderr.

For consistency with the changes in common.py.jinja2, this check_output call could also be updated to run(..., check=True, capture_output=True). This would ensure stderr is captured when the snakemake test run fails, aiding in debugging.

This is optional and could be addressed in a follow-up if desired.

♻️ Suggested change
-from subprocess import check_output
+from subprocess import run
         # Run the test job.
-        check_output(
+        run(
             [
                 "python",
                 "-m",
                 "snakemake",
                 ...
             ]
-            + conda_prefix
+            + conda_prefix,
+            check=True,
+            capture_output=True,
         )

Also applies to: 30-65

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/snakemake/unit_tests/templates/ruletest.py.jinja2` at line 10, The test
currently imports and uses subprocess.check_output which only captures stdout;
update the import and invocation to use subprocess.run and call run(...,
check=True, capture_output=True) instead of check_output so stderr is captured
on failure. Replace usages of check_output(...) in the ruletest template with
run(..., check=True, capture_output=True) and adjust any places reading the
output to use the CompletedProcess attributes (stdout/stderr) as needed; ensure
the import line in src/snakemake/unit_tests/templates/ruletest.py.jinja2 brings
in run (or import subprocess and prefix calls) so the new call resolves.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Nitpick comments:
In `@src/snakemake/unit_tests/templates/ruletest.py.jinja2`:
- Line 10: The test currently imports and uses subprocess.check_output which
only captures stdout; update the import and invocation to use subprocess.run and
call run(..., check=True, capture_output=True) instead of check_output so stderr
is captured on failure. Replace usages of check_output(...) in the ruletest
template with run(..., check=True, capture_output=True) and adjust any places
reading the output to use the CompletedProcess attributes (stdout/stderr) as
needed; ensure the import line in
src/snakemake/unit_tests/templates/ruletest.py.jinja2 brings in run (or import
subprocess and prefix calls) so the new call resolves.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 72e54b6a-44bc-402a-9a59-8a188ed73ce3

📥 Commits

Reviewing files that changed from the base of the PR and between b88171c and 188c189.

📒 Files selected for processing (2)
  • src/snakemake/unit_tests/templates/common.py.jinja2
  • src/snakemake/unit_tests/templates/ruletest.py.jinja2

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/snakemake/unit_tests/templates/common.py.jinja2`:
- Around line 55-59: The current call to check_output(...) passes stderr=PIPE
but doesn't surface it when cmp fails; wrap the check_output invocation in a
try/except that catches CalledProcessError (from subprocess) and re-raises a
clearer error that includes e.stderr (decoded if needed) so ruletest.py.jinja2's
direct .check() will show cmp diagnostics; modify the block around check_output
(the call using cmp_cmds.get(expected_file.suffix, ["cmp"]) + [expected_file,
generated_file] and PIPE) to catch CalledProcessError and raise a RuntimeError
or enrich the existing exception with the captured stderr.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 0908fe90-a64c-4267-9d1a-d8104a120f9a

📥 Commits

Reviewing files that changed from the base of the PR and between 188c189 and ecd6589.

📒 Files selected for processing (1)
  • src/snakemake/unit_tests/templates/common.py.jinja2

Copy link
Copy Markdown
Contributor

@coderabbitai coderabbitai bot left a comment

Choose a reason for hiding this comment

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

Actionable comments posted: 1

♻️ Duplicate comments (1)
src/snakemake/unit_tests/templates/common.py.jinja2 (1)

7-7: ⚠️ Potential issue | 🟠 Major

Capturing the subprocess streams here still won't surface the failure details.

check_output() already stores stdout on CalledProcessError, and Line 59 now stores stderr too, but the default exception text stays generic. A failing cmp will therefore still bubble up without the captured diagnostics unless you catch CalledProcessError here and re-raise with the decoded stream contents.

🛠️ Proposed fix
-from subprocess import check_output, PIPE
+from subprocess import CalledProcessError, PIPE, check_output
...
     def compare_files(self, expected_file, generated_file, cmp_cmds):
-        check_output(
-            cmp_cmds.get(expected_file.suffix, ["cmp"])
-            + [expected_file, generated_file],
-            stderr=PIPE,
-        )
+        try:
+            check_output(
+                cmp_cmds.get(expected_file.suffix, ["cmp"])
+                + [expected_file, generated_file],
+                stderr=PIPE,
+            )
+        except CalledProcessError as e:
+            details = "\n".join(
+                part
+                for part in [
+                    e.output.decode(errors="replace").strip() if e.output else "",
+                    e.stderr.decode(errors="replace").strip() if e.stderr else "",
+                ]
+                if part
+            )
+            raise AssertionError(
+                f"File comparison failed for {generated_file} against {expected_file}"
+                + (f":\n{details}" if details else "")
+            ) from e

Run this read-only check to confirm that str(e) does not include the captured streams:

#!/bin/bash
python - <<'PY'
import subprocess, sys

try:
    subprocess.check_output(
        [
            sys.executable,
            "-c",
            "import sys; sys.stdout.write('stdout detail\\n'); sys.stderr.write('stderr detail\\n'); sys.exit(1)",
        ],
        stderr=subprocess.PIPE,
    )
except subprocess.CalledProcessError as e:
    print("str(e):", str(e))
    print("output:", e.output.decode().strip())
    print("stderr:", e.stderr.decode().strip())
PY

Expected: str(e) stays generic, while output and stderr contain the useful diagnostics.

Also applies to: 55-60

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/snakemake/unit_tests/templates/common.py.jinja2` at line 7, The call to
check_output() must be wrapped in a try/except to catch
subprocess.CalledProcessError and re-raise a clearer error including the decoded
e.output and e.stderr so failure diagnostics aren't lost; locate the
check_output invocation(s) (and the block around line ~59) and replace the bare
call with a try: ... except subprocess.CalledProcessError as e: raise
RuntimeError(f"cmp failed: stdout={e.output.decode().strip()},
stderr={e.stderr.decode().strip()}") from e (or similar message) ensuring you
reference e.output and e.stderr and decode them before including in the raised
exception.
🧹 Nitpick comments (1)
src/snakemake/unit_tests/templates/common.py.jinja2 (1)

30-45: Drop the temporary DEBUG prints before merge.

Lines 30, 38, and 45 unconditionally dump full file sets on every successful test run, which will make CI logs noisy and can bury the comparison failure you actually want to inspect. Please gate these behind an opt-in verbose flag or remove them.

🤖 Prompt for AI Agents
Verify each finding against the current code and only fix it if needed.

In `@src/snakemake/unit_tests/templates/common.py.jinja2` around lines 30 - 45,
Remove the unconditional debug prints that dump input_files, workdir_files, and
expected_files; either delete the print(...) calls around those variables or
gate them behind an opt-in verbose/debug flag (e.g., check self.verbose or use a
logger.debug) so normal test runs don’t flood CI logs—update the code around the
variables input_files, workdir_files, and expected_files to only emit these
details when verbose/debug is enabled.
🤖 Prompt for all review comments with AI agents
Verify each finding against the current code and only fix it if needed.

Inline comments:
In `@src/snakemake/unit_tests/templates/common.py.jinja2`:
- Around line 31-37: The comment beside the workdir_files computation is
misleading about the substring patterns; update the comment near workdir_files
(the set comprehension that iterates os.walk(self.workdir)) to state that the
code uses "/.snakemake" and "/config" (no trailing slash) which correctly
excludes top-level and nested Unix paths, and clarify that the observed mismatch
is a Windows-path issue (e.g., backslashes) if Windows support is required;
optionally suggest replacing the substring checks with explicit top-level
directory filtering using Path.parts to handle Windows paths consistently.

---

Duplicate comments:
In `@src/snakemake/unit_tests/templates/common.py.jinja2`:
- Line 7: The call to check_output() must be wrapped in a try/except to catch
subprocess.CalledProcessError and re-raise a clearer error including the decoded
e.output and e.stderr so failure diagnostics aren't lost; locate the
check_output invocation(s) (and the block around line ~59) and replace the bare
call with a try: ... except subprocess.CalledProcessError as e: raise
RuntimeError(f"cmp failed: stdout={e.output.decode().strip()},
stderr={e.stderr.decode().strip()}") from e (or similar message) ensuring you
reference e.output and e.stderr and decode them before including in the raised
exception.

---

Nitpick comments:
In `@src/snakemake/unit_tests/templates/common.py.jinja2`:
- Around line 30-45: Remove the unconditional debug prints that dump
input_files, workdir_files, and expected_files; either delete the print(...)
calls around those variables or gate them behind an opt-in verbose/debug flag
(e.g., check self.verbose or use a logger.debug) so normal test runs don’t flood
CI logs—update the code around the variables input_files, workdir_files, and
expected_files to only emit these details when verbose/debug is enabled.

ℹ️ Review info
⚙️ Run configuration

Configuration used: Organization UI

Review profile: CHILL

Plan: Pro

Run ID: 96fddaf8-ebfd-4447-9466-6bf80e09241e

📥 Commits

Reviewing files that changed from the base of the PR and between ecd6589 and 335eab4.

📒 Files selected for processing (1)
  • src/snakemake/unit_tests/templates/common.py.jinja2

Copy link
Copy Markdown
Contributor

@cademirch cademirch left a comment

Choose a reason for hiding this comment

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

looks good!

@fgvieira fgvieira moved this to In review in Snakemake Hackathon 2026 Mar 10, 2026
@fgvieira fgvieira self-assigned this Mar 10, 2026
@johanneskoester johanneskoester merged commit 97d74ba into snakemake:main Mar 11, 2026
60 checks passed
@github-project-automation github-project-automation bot moved this from In review to Done in Snakemake Hackathon 2026 Mar 11, 2026
@fgvieira fgvieira deleted the unit_tests_print_stdout branch March 11, 2026 13:37
johanneskoester pushed a commit that referenced this pull request Mar 13, 2026
🤖 I have created a release *beep* *boop*
---


##
[9.17.0](v9.16.3...v9.17.0)
(2026-03-13)


### Features

* Allow storing snakemake metadata in files or databases
([#4012](#4012))
([dd75f31](dd75f31))
* Allow to specify comparison command per-unit test
([#3956](#3956))
([b88171c](b88171c))
* job table orderd topological when run is started
([#4018](#4018))
([75cf506](75cf506))
* lambda functions for priority in rules
([#3253](#3253))
([d2aa226](d2aa226))
* Make on... directive of modules accessible
([#4050](#4050))
([e9f2e1c](e9f2e1c))


### Bug Fixes

* adjust conda tests to not fail on apple silicon; fix
[#4040](#4040)
([#4049](#4049))
([f5b0142](f5b0142))
* allow "--containerize apptainer" to output apptainer format instead of
dockerfile ([#4030](#4030))
([f5cac30](f5cac30))
* apptainer command not recognized when singularity is absent
([#4010](#4010))
([b8162e2](b8162e2))
* capture stderr when tests fail
([#3995](#3995))
([97d74ba](97d74ba))
* **docs:** make Data-dependent conditional execution a complete example
([#4043](#4043))
([3a1d7f2](3a1d7f2))
* don't build the DAG when running unlock. Fixes
[#4000](#4000) and
[#198](#198)
([#4007](#4007))
([acf79fd](acf79fd))
* Ensure pixi tasks may be run as advertised
([#4046](#4046))
([88253c2](88253c2))
* fix checkpoint handling corner cases
([#3870](#3870) and
[#3559](#3559))
([#4015](#4015))
([63f4257](63f4257))
* issue 3642
([#4054](#4054))
([76e6fc2](76e6fc2))
* issue 3815
([#4026](#4026))
([b0eec96](b0eec96))
* logging None in shellcmd context causes error
([#4064](#4064))
([d0652cd](d0652cd))
* lookup function returns default value for empty DataFrame queries
([#4056](#4056))
([f71de97](f71de97))
* make `cache: omit-software` a rule specific property
([#4085](#4085))
([034a9e7](034a9e7))
* reduce number of tests leaving temporary files behind
([#4033](#4033))
([a3a1c97](a3a1c97))
* regression in dynamic resource handling
([#4038](#4038))
([f2c554a](f2c554a))
* somewhat shorter announce message
([#4080](#4080))
([57efc71](57efc71))


### Performance Improvements

* switch reretry with tenacity; decouple container classes (with Python
3.7 compat for old scripts) from rest of the codebase (enabling moving
to newer python versions)
([#4032](#4032))
([ffb19e7](ffb19e7))


### Documentation

* Add AI-assisted contributions policy to contributing guidelines
([#4051](#4051))
([dd70526](dd70526))
* **codebase:** Update & simplify plugin architecture section
([#4052](#4052))
([176cf63](176cf63))
* Correct workflow.source_path() description in documentation
([#4036](#4036))
([45883c5](45883c5))
* fixed wrong code example for collect() function
([#4037](#4037))
([5c85ed8](5c85ed8))
* Minor docs improvements
([#4089](#4089))
([29ea226](29ea226))
* switch to sphinx_design for tabs
([#3976](#3976))
([9674614](9674614))
* typo in the migration table breaking a pip install command
([#4024](#4024))
([66f9dda](66f9dda))

---
This PR was generated with [Release
Please](https://github.com/googleapis/release-please). See
[documentation](https://github.com/googleapis/release-please#release-please).
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants