pytest-agent-digest 0.2.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- pytest_agent_digest/__init__.py +23 -0
- pytest_agent_digest/collector.py +194 -0
- pytest_agent_digest/plugin.py +153 -0
- pytest_agent_digest/renderer.py +93 -0
- pytest_agent_digest-0.2.0.dist-info/METADATA +86 -0
- pytest_agent_digest-0.2.0.dist-info/RECORD +9 -0
- pytest_agent_digest-0.2.0.dist-info/WHEEL +4 -0
- pytest_agent_digest-0.2.0.dist-info/entry_points.txt +2 -0
- pytest_agent_digest-0.2.0.dist-info/licenses/LICENSE +29 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
"""Top-level package for pytest_agent_digest.
|
|
2
|
+
|
|
3
|
+
The pytest11 entry point points to this package, so pytest hook functions
|
|
4
|
+
are imported here from ``plugin`` to make them discoverable.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from pytest_agent_digest.plugin import (
|
|
8
|
+
AgentDigestPlugin,
|
|
9
|
+
get_output_modes,
|
|
10
|
+
get_report_path,
|
|
11
|
+
pytest_addoption,
|
|
12
|
+
pytest_configure,
|
|
13
|
+
)
|
|
14
|
+
|
|
15
|
+
__version__ = "0.2.0"
|
|
16
|
+
|
|
17
|
+
__all__ = [
|
|
18
|
+
"AgentDigestPlugin",
|
|
19
|
+
"get_output_modes",
|
|
20
|
+
"get_report_path",
|
|
21
|
+
"pytest_addoption",
|
|
22
|
+
"pytest_configure",
|
|
23
|
+
]
|
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
"""Test result collector for pytest-agent-digest."""
|
|
2
|
+
|
|
3
|
+
import re
|
|
4
|
+
from collections import Counter
|
|
5
|
+
from dataclasses import dataclass
|
|
6
|
+
|
|
7
|
+
import pytest
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def strip_ansi(text: str) -> str:
|
|
11
|
+
"""
|
|
12
|
+
Remove ANSI escape sequences from *text*.
|
|
13
|
+
|
|
14
|
+
Args:
|
|
15
|
+
text: The string that may contain ANSI escape sequences.
|
|
16
|
+
|
|
17
|
+
Returns:
|
|
18
|
+
The input string with all ANSI color/style codes removed.
|
|
19
|
+
"""
|
|
20
|
+
return re.sub(r"\x1b\[[0-9;]*m", "", text)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
@dataclass
|
|
24
|
+
class TestResult:
|
|
25
|
+
"""Represents a single test outcome captured during a pytest session.
|
|
26
|
+
|
|
27
|
+
Attributes:
|
|
28
|
+
node_id: The pytest node ID (e.g. `tests/test_foo.py::test_bar`).
|
|
29
|
+
outcome: One of `"passed"`, `"failed"`, `"skipped"`, `"xfailed"`, `"xpassed"`.
|
|
30
|
+
longrepr: Formatted traceback or reason string, stripped of ANSI codes.
|
|
31
|
+
`None` when not applicable.
|
|
32
|
+
duration: Test duration in seconds.
|
|
33
|
+
skip_reason: Human-readable skip reason for skipped tests; `None` otherwise.
|
|
34
|
+
"""
|
|
35
|
+
|
|
36
|
+
node_id: str
|
|
37
|
+
outcome: str
|
|
38
|
+
longrepr: str | None
|
|
39
|
+
duration: float
|
|
40
|
+
skip_reason: str | None
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class ReportCollector:
|
|
44
|
+
"""
|
|
45
|
+
Accumulates the `TestResult` objects while a pytest session runs.
|
|
46
|
+
|
|
47
|
+
Wire this into `pytest_configure` (instantiate) and `pytest_runtest_logreport` (call `add`) so the renderer can
|
|
48
|
+
consume a fully populated collector at the session end.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(self) -> None:
|
|
52
|
+
"""Initialize with an empty result list."""
|
|
53
|
+
self.results: list[TestResult] = []
|
|
54
|
+
|
|
55
|
+
def add(self, report: pytest.TestReport) -> None:
|
|
56
|
+
"""
|
|
57
|
+
Classify *report* and append a `TestResult` class if it is a call phase.
|
|
58
|
+
|
|
59
|
+
``report.when == "call"`` reports are stored for normal outcomes.
|
|
60
|
+
``report.when == "setup"`` reports are stored only when skipped, because
|
|
61
|
+
``@pytest.mark.skip`` raises a ``Skipped`` exception during the setup phase
|
|
62
|
+
and never produces a call-phase report.
|
|
63
|
+
|
|
64
|
+
Args:
|
|
65
|
+
report: The pytest test report to classify and store.
|
|
66
|
+
"""
|
|
67
|
+
if report.when == "setup" and report.skipped:
|
|
68
|
+
outcome = "skipped"
|
|
69
|
+
longrepr = self._extract_longrepr(report)
|
|
70
|
+
skip_reason = self._extract_skip_reason(report, outcome)
|
|
71
|
+
self.results.append(
|
|
72
|
+
TestResult(
|
|
73
|
+
node_id=report.nodeid,
|
|
74
|
+
outcome=outcome,
|
|
75
|
+
longrepr=longrepr,
|
|
76
|
+
duration=report.duration,
|
|
77
|
+
skip_reason=skip_reason,
|
|
78
|
+
)
|
|
79
|
+
)
|
|
80
|
+
return
|
|
81
|
+
|
|
82
|
+
if report.when != "call":
|
|
83
|
+
return
|
|
84
|
+
|
|
85
|
+
outcome = self._classify(report)
|
|
86
|
+
longrepr = self._extract_longrepr(report)
|
|
87
|
+
skip_reason = self._extract_skip_reason(report, outcome)
|
|
88
|
+
|
|
89
|
+
self.results.append(
|
|
90
|
+
TestResult(
|
|
91
|
+
node_id=report.nodeid,
|
|
92
|
+
outcome=outcome,
|
|
93
|
+
longrepr=longrepr,
|
|
94
|
+
duration=report.duration,
|
|
95
|
+
skip_reason=skip_reason,
|
|
96
|
+
)
|
|
97
|
+
)
|
|
98
|
+
|
|
99
|
+
# ------------------------------------------------------------------
|
|
100
|
+
# Properties
|
|
101
|
+
# ------------------------------------------------------------------
|
|
102
|
+
|
|
103
|
+
@property
|
|
104
|
+
def counts(self) -> dict[str, int]:
|
|
105
|
+
"""
|
|
106
|
+
Return a dict of `{outcome: count}` with zero-count keys omitted.
|
|
107
|
+
|
|
108
|
+
Returns:
|
|
109
|
+
Mapping from the outcome string to the number of results with that outcome.
|
|
110
|
+
"""
|
|
111
|
+
return dict(Counter(tr.outcome for tr in self.results))
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def has_failures(self) -> bool:
|
|
115
|
+
"""
|
|
116
|
+
Return `True` if any result is `"failed"` or `"xpassed"`.
|
|
117
|
+
|
|
118
|
+
Returns:
|
|
119
|
+
`True` when there is at least one failure or unexpected pass.
|
|
120
|
+
"""
|
|
121
|
+
return any(tr.outcome in {"failed", "xpassed"} for tr in self.results)
|
|
122
|
+
|
|
123
|
+
# ------------------------------------------------------------------
|
|
124
|
+
# Private helpers
|
|
125
|
+
# ------------------------------------------------------------------
|
|
126
|
+
|
|
127
|
+
@staticmethod
|
|
128
|
+
def _classify(report: pytest.TestReport) -> str:
|
|
129
|
+
"""
|
|
130
|
+
Return the outcome string for *report*.
|
|
131
|
+
|
|
132
|
+
Args:
|
|
133
|
+
report: The test report to classify.
|
|
134
|
+
|
|
135
|
+
Returns:
|
|
136
|
+
One of `"passed"`, `"failed"`, `"skipped"`, `"xfailed"`, `"xpassed"`.
|
|
137
|
+
"""
|
|
138
|
+
has_xfail = hasattr(report, "wasxfail")
|
|
139
|
+
if report.passed:
|
|
140
|
+
return "xpassed" if has_xfail else "passed"
|
|
141
|
+
if report.skipped:
|
|
142
|
+
return "xfailed" if has_xfail else "skipped"
|
|
143
|
+
return "failed"
|
|
144
|
+
|
|
145
|
+
@staticmethod
|
|
146
|
+
def _longrepr_reason(longrepr: object) -> str:
|
|
147
|
+
"""
|
|
148
|
+
Extract and ANSI-strip a reason string from a longrepr value.
|
|
149
|
+
|
|
150
|
+
Skipped reports store ``longrepr`` as a ``(filename, lineno, reason)`` tuple;
|
|
151
|
+
all other reports store it as a string-like object.
|
|
152
|
+
|
|
153
|
+
Args:
|
|
154
|
+
longrepr: The raw ``report.longrepr`` value (tuple or str-like).
|
|
155
|
+
|
|
156
|
+
Returns:
|
|
157
|
+
A plain-text reason string.
|
|
158
|
+
"""
|
|
159
|
+
if isinstance(longrepr, tuple):
|
|
160
|
+
return strip_ansi(str(longrepr[2]))
|
|
161
|
+
return strip_ansi(str(longrepr))
|
|
162
|
+
|
|
163
|
+
@staticmethod
|
|
164
|
+
def _extract_longrepr(report: pytest.TestReport) -> str | None:
|
|
165
|
+
"""
|
|
166
|
+
Extract and ANSI-strip the longrepr from *report*.
|
|
167
|
+
|
|
168
|
+
Args:
|
|
169
|
+
report: The test report whose longrepr should be extracted.
|
|
170
|
+
|
|
171
|
+
Returns:
|
|
172
|
+
A plain-text longrepr string, or `None` if absent.
|
|
173
|
+
"""
|
|
174
|
+
if report.longrepr is None:
|
|
175
|
+
return None
|
|
176
|
+
return ReportCollector._longrepr_reason(report.longrepr)
|
|
177
|
+
|
|
178
|
+
@staticmethod
|
|
179
|
+
def _extract_skip_reason(report: pytest.TestReport, outcome: str) -> str | None:
|
|
180
|
+
"""
|
|
181
|
+
Return the skip reason for skipped tests, else `None`.
|
|
182
|
+
|
|
183
|
+
Args:
|
|
184
|
+
report: The test report to inspect.
|
|
185
|
+
outcome: The already-classified outcome string.
|
|
186
|
+
|
|
187
|
+
Returns:
|
|
188
|
+
The skip reason string, or `None` for non-skipped outcomes.
|
|
189
|
+
"""
|
|
190
|
+
if outcome != "skipped":
|
|
191
|
+
return None
|
|
192
|
+
if report.longrepr is None:
|
|
193
|
+
return None
|
|
194
|
+
return ReportCollector._longrepr_reason(report.longrepr)
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
"""pytest plugin hooks for pytest-agent-digest."""
|
|
2
|
+
|
|
3
|
+
from pathlib import Path
|
|
4
|
+
|
|
5
|
+
import pytest
|
|
6
|
+
|
|
7
|
+
from pytest_agent_digest.collector import ReportCollector
|
|
8
|
+
from pytest_agent_digest.renderer import render_report
|
|
9
|
+
|
|
10
|
+
_PLUGIN_NAME = "agent_digest_plugin"
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class AgentDigestPlugin:
|
|
14
|
+
"""
|
|
15
|
+
Internal plugin class that holds the per-session state.
|
|
16
|
+
|
|
17
|
+
Registered by `pytest_configure` so that `pytest_runtest_logreport` and
|
|
18
|
+
`pytest_sessionfinish` share the same `~pytest_agent_digest.collector.ReportCollector`
|
|
19
|
+
without a global.
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
def __init__(self) -> None:
|
|
23
|
+
"""Initialize the plugin with a fresh collector."""
|
|
24
|
+
self.collector: ReportCollector = ReportCollector()
|
|
25
|
+
|
|
26
|
+
def pytest_runtest_logreport(self, report: pytest.TestReport) -> None:
|
|
27
|
+
"""
|
|
28
|
+
Forward each test report to the collector.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
report: The test report for the current phase.
|
|
32
|
+
"""
|
|
33
|
+
self.collector.add(report)
|
|
34
|
+
|
|
35
|
+
def pytest_sessionfinish(self, session: pytest.Session, exitstatus: int) -> None:
|
|
36
|
+
"""
|
|
37
|
+
Finalize the digest at the end of the test session.
|
|
38
|
+
|
|
39
|
+
When ``term`` mode is active, renders the Markdown digest and prints it to stdout.
|
|
40
|
+
When ``file`` mode is active, writes the Markdown digest to disk and prints a confirmation line.
|
|
41
|
+
Both modes may be active simultaneously.
|
|
42
|
+
|
|
43
|
+
Args:
|
|
44
|
+
session: The pytest session object.
|
|
45
|
+
exitstatus: The exit status code for the session.
|
|
46
|
+
"""
|
|
47
|
+
config = session.config
|
|
48
|
+
modes = get_output_modes(config)
|
|
49
|
+
if not modes:
|
|
50
|
+
return
|
|
51
|
+
|
|
52
|
+
report = render_report(self.collector, verbose=config.option.verbose, tb_style=config.option.tbstyle)
|
|
53
|
+
|
|
54
|
+
if "term" in modes:
|
|
55
|
+
print(report, end="")
|
|
56
|
+
|
|
57
|
+
if "file" in modes:
|
|
58
|
+
path = get_report_path(config)
|
|
59
|
+
path.parent.mkdir(parents=True, exist_ok=True)
|
|
60
|
+
path.write_text(report, encoding="utf-8")
|
|
61
|
+
print(f"Agent digest written to {path}")
|
|
62
|
+
|
|
63
|
+
|
|
64
|
+
def pytest_addoption(parser: pytest.Parser) -> None:
|
|
65
|
+
"""
|
|
66
|
+
Register pytest-agent-digest command-line options.
|
|
67
|
+
|
|
68
|
+
Args:
|
|
69
|
+
parser: The pytest argument parser.
|
|
70
|
+
"""
|
|
71
|
+
parser.addoption(
|
|
72
|
+
"--agent-digest",
|
|
73
|
+
action="append",
|
|
74
|
+
choices=["term", "file"],
|
|
75
|
+
default=None,
|
|
76
|
+
help=(
|
|
77
|
+
"Generate Agent-friendly Markdown digest. Use 'term' for stdout, "
|
|
78
|
+
"'file' for file output. Can be passed twice."
|
|
79
|
+
),
|
|
80
|
+
)
|
|
81
|
+
parser.addoption(
|
|
82
|
+
"--agent-digest-file",
|
|
83
|
+
action="store",
|
|
84
|
+
default=None,
|
|
85
|
+
help="Path for the Markdown digest file (default: test-results.md).",
|
|
86
|
+
)
|
|
87
|
+
parser.addini(
|
|
88
|
+
"agent_digest_file",
|
|
89
|
+
default="test-results.md",
|
|
90
|
+
help="Default path for the Agent digest file.",
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
|
|
94
|
+
@pytest.hookimpl(trylast=True)
|
|
95
|
+
def pytest_configure(config: pytest.Config) -> None:
|
|
96
|
+
"""
|
|
97
|
+
Configure the pytest-agent-digest plugin.
|
|
98
|
+
|
|
99
|
+
Instantiates the `AgentDigestPlugin` class and registers it so its hooks are active for the session.
|
|
100
|
+
When ``term`` mode is active, unregisters pytest's built-in terminal reporter so only the Markdown
|
|
101
|
+
digest appears on stdout.
|
|
102
|
+
|
|
103
|
+
Marked ``trylast=True`` so that the built-in terminal reporter is already registered by the time
|
|
104
|
+
this hook runs, making it safe to unregister.
|
|
105
|
+
|
|
106
|
+
Args:
|
|
107
|
+
config: The pytest configuration object.
|
|
108
|
+
"""
|
|
109
|
+
plugin = AgentDigestPlugin()
|
|
110
|
+
config.pluginmanager.register(plugin, _PLUGIN_NAME)
|
|
111
|
+
|
|
112
|
+
if "term" in get_output_modes(config):
|
|
113
|
+
tr = config.pluginmanager.get_plugin("terminalreporter")
|
|
114
|
+
if tr is not None:
|
|
115
|
+
config.pluginmanager.unregister(tr)
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
def get_output_modes(config: pytest.Config) -> set[str]:
|
|
119
|
+
"""
|
|
120
|
+
Return the set of output modes requested via --agent-digest.
|
|
121
|
+
|
|
122
|
+
Args:
|
|
123
|
+
config: The pytest configuration object.
|
|
124
|
+
|
|
125
|
+
Returns:
|
|
126
|
+
A set of mode strings (`"term"`, `"file"`), or an empty set if the flag was not passed.
|
|
127
|
+
"""
|
|
128
|
+
modes = config.getoption("--agent-digest", default=None)
|
|
129
|
+
if not modes:
|
|
130
|
+
return set()
|
|
131
|
+
return set(modes)
|
|
132
|
+
|
|
133
|
+
|
|
134
|
+
def get_report_path(config: pytest.Config) -> Path:
|
|
135
|
+
"""
|
|
136
|
+
Return the output path for the Markdown digest file.
|
|
137
|
+
|
|
138
|
+
Resolution order:
|
|
139
|
+
|
|
140
|
+
1. `--agent-digest-file` CLI flag
|
|
141
|
+
2. `agent_digest_file` ini option
|
|
142
|
+
3. Hard-coded default `test-results.md`
|
|
143
|
+
|
|
144
|
+
Args:
|
|
145
|
+
config: The pytest configuration object.
|
|
146
|
+
|
|
147
|
+
Returns:
|
|
148
|
+
A `pathlib.Path` pointing to the desired report file location.
|
|
149
|
+
"""
|
|
150
|
+
cli_value = config.getoption("--agent-digest-file", default=None)
|
|
151
|
+
if cli_value is not None:
|
|
152
|
+
return Path(cli_value)
|
|
153
|
+
return Path(config.getini("agent_digest_file"))
|
|
@@ -0,0 +1,93 @@
|
|
|
1
|
+
"""Markdown renderer for pytest-agent-digest."""
|
|
2
|
+
|
|
3
|
+
from pytest_agent_digest.collector import ReportCollector, TestResult
|
|
4
|
+
|
|
5
|
+
_OUTCOME_ORDER = ["passed", "failed", "skipped", "xfailed", "xpassed"]
|
|
6
|
+
|
|
7
|
+
_STATUS_LABEL: dict[str, str] = {
|
|
8
|
+
"failed": "FAILED",
|
|
9
|
+
"xpassed": "XPASSED",
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def render_report(collector: ReportCollector, verbose: int, tb_style: str) -> str:
|
|
14
|
+
"""
|
|
15
|
+
Render a Markdown report from a populated collector.
|
|
16
|
+
|
|
17
|
+
Args:
|
|
18
|
+
collector: The populated ``ReportCollector`` instance.
|
|
19
|
+
verbose: If ``True``, include a ``## Passes`` section listing each passed test.
|
|
20
|
+
tb_style: The pytest ``--tb`` style value. When ``"no"``, traceback
|
|
21
|
+
code blocks are omitted from failure entries.
|
|
22
|
+
|
|
23
|
+
Returns:
|
|
24
|
+
A Markdown string suitable for Agent consumption. The document always
|
|
25
|
+
ends with a newline and contains no ANSI escape sequences.
|
|
26
|
+
"""
|
|
27
|
+
sections: list[str] = []
|
|
28
|
+
|
|
29
|
+
# ------------------------------------------------------------------
|
|
30
|
+
# Summary line (always present, even when empty)
|
|
31
|
+
# ------------------------------------------------------------------
|
|
32
|
+
counts = collector.counts
|
|
33
|
+
summary_parts = [f"{counts[o]} {o}" for o in _OUTCOME_ORDER if o in counts]
|
|
34
|
+
sections.append(", ".join(summary_parts))
|
|
35
|
+
|
|
36
|
+
# ------------------------------------------------------------------
|
|
37
|
+
# Failures section (failed + xpassed)
|
|
38
|
+
# ------------------------------------------------------------------
|
|
39
|
+
failures = [r for r in collector.results if r.outcome in {"failed", "xpassed"}]
|
|
40
|
+
if failures:
|
|
41
|
+
failure_lines: list[str] = ["## Failures"]
|
|
42
|
+
for result in failures:
|
|
43
|
+
failure_lines.extend(_failure_entry_lines(result, tb_style))
|
|
44
|
+
sections.append("\n".join(failure_lines))
|
|
45
|
+
|
|
46
|
+
# ------------------------------------------------------------------
|
|
47
|
+
# Skipped section
|
|
48
|
+
# ------------------------------------------------------------------
|
|
49
|
+
skipped = [r for r in collector.results if r.outcome == "skipped"]
|
|
50
|
+
if skipped:
|
|
51
|
+
skipped_lines: list[str] = ["## Skipped", ""]
|
|
52
|
+
for result in skipped:
|
|
53
|
+
skipped_lines.append(f"- {result.node_id}: {result.skip_reason}")
|
|
54
|
+
sections.append("\n".join(skipped_lines))
|
|
55
|
+
|
|
56
|
+
# ------------------------------------------------------------------
|
|
57
|
+
# Passes section (verbose only)
|
|
58
|
+
# ------------------------------------------------------------------
|
|
59
|
+
if verbose:
|
|
60
|
+
passed = [r for r in collector.results if r.outcome == "passed"]
|
|
61
|
+
if passed:
|
|
62
|
+
passed_lines: list[str] = ["## Passes", ""]
|
|
63
|
+
for result in passed:
|
|
64
|
+
passed_lines.append(f"- {result.node_id}")
|
|
65
|
+
sections.append("\n".join(passed_lines))
|
|
66
|
+
|
|
67
|
+
return "\n\n".join(sections) + "\n"
|
|
68
|
+
|
|
69
|
+
|
|
70
|
+
def _failure_entry_lines(result: TestResult, tb_style: str) -> list[str]:
|
|
71
|
+
"""
|
|
72
|
+
Build the lines for a single failure entry.
|
|
73
|
+
|
|
74
|
+
Args:
|
|
75
|
+
result: The ``TestResult`` to render (must be ``failed`` or ``xpassed``).
|
|
76
|
+
tb_style: The pytest ``--tb`` style; ``"no"`` suppresses the traceback block.
|
|
77
|
+
|
|
78
|
+
Returns:
|
|
79
|
+
A list of strings (without a trailing blank line) representing the entry.
|
|
80
|
+
"""
|
|
81
|
+
lines = [
|
|
82
|
+
"",
|
|
83
|
+
f"### {result.node_id}",
|
|
84
|
+
"",
|
|
85
|
+
f"**Status:** {_STATUS_LABEL.get(result.outcome, result.outcome.upper())}",
|
|
86
|
+
f"**Duration:** {result.duration:.2f}s",
|
|
87
|
+
]
|
|
88
|
+
if tb_style != "no" and result.longrepr:
|
|
89
|
+
lines.append("")
|
|
90
|
+
lines.append("```")
|
|
91
|
+
lines.extend(tb_line.rstrip() for tb_line in result.longrepr.splitlines())
|
|
92
|
+
lines.append("```")
|
|
93
|
+
return lines
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: pytest-agent-digest
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: A Pytest plugin to generate a Markdown report for AI Agents
|
|
5
|
+
Project-URL: Homepage, https://github.com/callowayproject/pytest-agent-digest
|
|
6
|
+
Project-URL: Documentation, https://callowayproject.github.io/pytest-agent-digest
|
|
7
|
+
Project-URL: Repository, https://github.com/callowayproject/pytest-agent-digest
|
|
8
|
+
Project-URL: Changelog, https://github.com/callowayproject/pytest-agent-digest/CHANGELOG.md
|
|
9
|
+
Author-email: Corey Oordt <coreyoordt@gmail.com>
|
|
10
|
+
License: BSD License
|
|
11
|
+
|
|
12
|
+
Copyright (c) 2026, Corey Oordt
|
|
13
|
+
All rights reserved.
|
|
14
|
+
|
|
15
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
16
|
+
are permitted provided that the following conditions are met:
|
|
17
|
+
|
|
18
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
19
|
+
list of conditions and the following disclaimer.
|
|
20
|
+
|
|
21
|
+
* Redistributions in binary form must reproduce the above copyright notice, this
|
|
22
|
+
list of conditions and the following disclaimer in the documentation and/or
|
|
23
|
+
other materials provided with the distribution.
|
|
24
|
+
|
|
25
|
+
* Neither the name of pytest-agent-digest nor the names of its
|
|
26
|
+
contributors may be used to endorse or promote products derived from this
|
|
27
|
+
software without specific prior written permission.
|
|
28
|
+
|
|
29
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
30
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
31
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
32
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
33
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
34
|
+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
35
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
36
|
+
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
|
37
|
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
|
38
|
+
OF THE POSSIBILITY OF SUCH DAMAGE.
|
|
39
|
+
License-File: LICENSE
|
|
40
|
+
Keywords: AI,LLM,agent,pytest,report
|
|
41
|
+
Classifier: Development Status :: 3 - Alpha
|
|
42
|
+
Classifier: Environment :: Plugins
|
|
43
|
+
Classifier: Framework :: Pytest
|
|
44
|
+
Classifier: Intended Audience :: Developers
|
|
45
|
+
Classifier: License :: OSI Approved :: BSD License
|
|
46
|
+
Classifier: Natural Language :: English
|
|
47
|
+
Classifier: Programming Language :: Python :: 3
|
|
48
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
49
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
50
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
51
|
+
Classifier: Programming Language :: Python :: 3.14
|
|
52
|
+
Classifier: Topic :: Scientific/Engineering :: Artificial Intelligence
|
|
53
|
+
Classifier: Topic :: Software Development :: Testing
|
|
54
|
+
Requires-Python: >=3.11
|
|
55
|
+
Requires-Dist: pytest>=7.0
|
|
56
|
+
Description-Content-Type: text/markdown
|
|
57
|
+
|
|
58
|
+
# pytest-agent-digest
|
|
59
|
+
|
|
60
|
+
A pytest plugin that generates compact Markdown digests designed for consumption by AI agents and LLMs. Instead of the standard terminal output, `pytest-agent-digest` produces structured Markdown that is typically **≥20% smaller in token count** than raw pytest output — keeping your context window budget under control.
|
|
61
|
+
|
|
62
|
+
[](https://pypi.org/project/pytest-agent-digest/)
|
|
63
|
+
[](https://pypi.org/project/pytest-agent-digest/)
|
|
64
|
+
|
|
65
|
+
## Quick start
|
|
66
|
+
|
|
67
|
+
```bash
|
|
68
|
+
# Install
|
|
69
|
+
pip install pytest-agent-digest
|
|
70
|
+
|
|
71
|
+
# Print Markdown digest to stdout (replaces normal output)
|
|
72
|
+
pytest --agent-digest=term
|
|
73
|
+
|
|
74
|
+
# Write digest to test-results.md (normal output preserved)
|
|
75
|
+
pytest --agent-digest=file
|
|
76
|
+
|
|
77
|
+
# Both at once
|
|
78
|
+
pytest --agent-digest=term --agent-digest=file
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
## Documentation
|
|
82
|
+
|
|
83
|
+
Full documentation is available at **<https://callowayproject.github.io/pytest-agent-digest>**, including:
|
|
84
|
+
|
|
85
|
+
- [Quickstart guide](https://callowayproject.github.io/pytest-agent-digest/quickstart/)
|
|
86
|
+
- [Options reference](https://callowayproject.github.io/pytest-agent-digest/reference/options/)
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
pytest_agent_digest/__init__.py,sha256=Vw4ToGGT_PLIznVxIq1G0mnQUjVPQPcSqQno5FZ67Us,498
|
|
2
|
+
pytest_agent_digest/collector.py,sha256=zMvZxV3kcL0ZO6BZLUiWqk5EU2phmAuGJKcG4-yvyTE,6203
|
|
3
|
+
pytest_agent_digest/plugin.py,sha256=HwhCSHdNNiWY-EfLglPokpfAFJNMolI3golyKgp_fOg,4729
|
|
4
|
+
pytest_agent_digest/renderer.py,sha256=x139dj-Ro-o36mOB7NMUoopkT5rsy2lXstFE7VGByJY,3629
|
|
5
|
+
pytest_agent_digest-0.2.0.dist-info/METADATA,sha256=qlHNobAm-MMe_QQVrIwAUrepnb9s5O0va2Wwu9_WkUY,4216
|
|
6
|
+
pytest_agent_digest-0.2.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
|
|
7
|
+
pytest_agent_digest-0.2.0.dist-info/entry_points.txt,sha256=CrI63EMB63yVm0o2dXTSaPtWQUpeFaNuqnxAJgOb_Qw,46
|
|
8
|
+
pytest_agent_digest-0.2.0.dist-info/licenses/LICENSE,sha256=TVX9QAgAPr4aFy9DSBKeZMmySsYXDsFRMTOXOh3q7Xc,1501
|
|
9
|
+
pytest_agent_digest-0.2.0.dist-info/RECORD,,
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
BSD License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026, Corey Oordt
|
|
4
|
+
All rights reserved.
|
|
5
|
+
|
|
6
|
+
Redistribution and use in source and binary forms, with or without modification,
|
|
7
|
+
are permitted provided that the following conditions are met:
|
|
8
|
+
|
|
9
|
+
* Redistributions of source code must retain the above copyright notice, this
|
|
10
|
+
list of conditions and the following disclaimer.
|
|
11
|
+
|
|
12
|
+
* Redistributions in binary form must reproduce the above copyright notice, this
|
|
13
|
+
list of conditions and the following disclaimer in the documentation and/or
|
|
14
|
+
other materials provided with the distribution.
|
|
15
|
+
|
|
16
|
+
* Neither the name of pytest-agent-digest nor the names of its
|
|
17
|
+
contributors may be used to endorse or promote products derived from this
|
|
18
|
+
software without specific prior written permission.
|
|
19
|
+
|
|
20
|
+
THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND
|
|
21
|
+
ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED
|
|
22
|
+
WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED.
|
|
23
|
+
IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT,
|
|
24
|
+
INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING,
|
|
25
|
+
BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
|
|
26
|
+
DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY
|
|
27
|
+
OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE
|
|
28
|
+
OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
|
|
29
|
+
OF THE POSSIBILITY OF SUCH DAMAGE.
|