Skip to content

Commit 538ea3f

Browse files
committed
Implement Ticket 6: file output mode (--llm-report=file)
- Save test results to a Markdown file (default path: `test-results.md`) and confirm output in stdout. - Support custom output paths via `--llm-report-file` and `llm_report_file` ini option. - New behavior overwrites files on subsequent runs instead of appending. - File output mode does not suppress pytest default output. - Add integration tests for all acceptance criteria.
1 parent c52384e commit 538ea3f

2 files changed

Lines changed: 72 additions & 0 deletions

File tree

pytest_llm_report/plugin.py

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,13 +103,15 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
103103
Finalize the report at the end of the test session.
104104
105105
When ``term`` mode is active, renders the Markdown report and prints it to stdout.
106+
When ``file`` mode is active, writes the Markdown report to disk and prints a confirmation line.
106107
107108
Args:
108109
session: The pytest session object.
109110
exitstatus: The exit status code for the session.
110111
"""
111112
config = session.config
112113
modes = get_output_modes(config)
114+
113115
if "term" in modes:
114116
llm_plugin: LLMReportPlugin | None = config.pluginmanager.get_plugin(_PLUGIN_NAME)
115117
if llm_plugin is not None:
@@ -118,6 +120,17 @@ def pytest_sessionfinish(session: pytest.Session, exitstatus: int) -> None:
118120
report = render_report(llm_plugin.collector, verbose=verbose, tb_style=tb_style)
119121
print(report, end="")
120122

123+
if "file" in modes:
124+
llm_plugin = config.pluginmanager.get_plugin(_PLUGIN_NAME)
125+
if llm_plugin is not None:
126+
verbose = bool(getattr(config.option, "verbose", 0))
127+
tb_style = getattr(config.option, "tbstyle", "short")
128+
report = render_report(llm_plugin.collector, verbose=verbose, tb_style=tb_style)
129+
path = get_report_path(config)
130+
path.parent.mkdir(parents=True, exist_ok=True)
131+
path.write_text(report, encoding="utf-8")
132+
print(f"LLM report written to {path}")
133+
121134

122135
def get_output_modes(config: pytest.Config) -> set[str]:
123136
"""

tests/test_plugin_integration.py

Lines changed: 59 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -103,3 +103,62 @@ def test_term_mode_non_verbose_omits_passes_section(pytester: pytest.Pytester) -
103103
pytester.makepyfile("def test_passes(): pass")
104104
result = pytester.runpytest("--llm-report=term")
105105
assert "## Passes" not in result.stdout.str()
106+
107+
108+
# ---------------------------------------------------------------------------
109+
# Ticket 6 — File output mode
110+
# ---------------------------------------------------------------------------
111+
112+
113+
def test_file_mode_creates_default_report(pytester: pytest.Pytester) -> None: # type: ignore[name-defined]
114+
"""--llm-report=file creates test-results.md in cwd."""
115+
pytester.makepyfile("def test_passes(): pass")
116+
result = pytester.runpytest("--llm-report=file")
117+
assert result.ret == 0
118+
assert (pytester.path / "test-results.md").exists()
119+
120+
121+
def test_file_mode_content_matches_markdown(pytester: pytest.Pytester) -> None: # type: ignore[name-defined]
122+
"""File content is valid Markdown with expected summary."""
123+
pytester.makepyfile("def test_passes(): pass")
124+
pytester.runpytest("--llm-report=file")
125+
content = (pytester.path / "test-results.md").read_text()
126+
assert "1 passed" in content
127+
128+
129+
def test_file_mode_custom_path(pytester: pytest.Pytester) -> None: # type: ignore[name-defined]
130+
"""--llm-report-file=out/results.md writes to that path, creating parent dirs."""
131+
pytester.makepyfile("def test_passes(): pass")
132+
pytester.runpytest("--llm-report=file", "--llm-report-file=out/results.md")
133+
assert (pytester.path / "out" / "results.md").exists()
134+
135+
136+
def test_file_mode_overwrites_on_second_run(pytester: pytest.Pytester) -> None: # type: ignore[name-defined]
137+
"""Running twice overwrites, not appends."""
138+
pytester.makepyfile("def test_passes(): pass")
139+
pytester.runpytest("--llm-report=file")
140+
pytester.runpytest("--llm-report=file")
141+
content = (pytester.path / "test-results.md").read_text()
142+
assert content.count("1 passed") == 1 # not duplicated
143+
144+
145+
def test_file_mode_default_output_unchanged(pytester: pytest.Pytester) -> None: # type: ignore[name-defined]
146+
"""--llm-report=file alone does not suppress pytest's default output."""
147+
pytester.makepyfile("def test_passes(): pass")
148+
result = pytester.runpytest("--llm-report=file")
149+
assert "=== test session starts ===" in result.stdout.str()
150+
151+
152+
def test_file_mode_prints_confirmation_line(pytester: pytest.Pytester) -> None: # type: ignore[name-defined]
153+
"""--llm-report=file prints 'LLM report written to <path>' to stdout."""
154+
pytester.makepyfile("def test_passes(): pass")
155+
result = pytester.runpytest("--llm-report=file")
156+
assert "LLM report written to" in result.stdout.str()
157+
158+
159+
def test_file_mode_ini_option_respected(pytester: pytest.Pytester) -> None: # type: ignore[name-defined]
160+
"""llm_report_file ini option sets the default output path."""
161+
pytester.makeini("[pytest]\nllm_report_file = custom-report.md\n")
162+
pytester.makepyfile("def test_passes(): pass")
163+
pytester.runpytest("--llm-report=file")
164+
assert (pytester.path / "custom-report.md").exists()

0 commit comments

Comments
 (0)