Coverage for tests / scripts / test_extract_test_summary.py: 100%
93 statements
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
« prev ^ index » next coverage.py v7.13.0, created at 2026-04-03 18:53 +0000
1"""Tests for extract-test-summary.sh script.
3This module tests the extract-test-summary.sh script which parses pytest output
4and extracts test results into a JSON summary file for PR comments.
5"""
7from __future__ import annotations
9import json
10import os
11import subprocess
12import tempfile
13from pathlib import Path
15from assertpy import assert_that
17# Compute repo root from this test file location (tests/scripts/test_*.py -> repo root)
18_REPO_ROOT = Path(__file__).resolve().parent.parent.parent
19SCRIPT_PATH = _REPO_ROOT / "scripts/ci/testing/extract-test-summary.sh"
22def test_script_help_output() -> None:
23 """Script should display help and exit 0 with --help flag."""
24 result = subprocess.run(
25 [str(SCRIPT_PATH), "--help"],
26 capture_output=True,
27 text=True,
28 )
30 assert_that(result.returncode).is_equal_to(0)
31 assert_that(result.stdout).contains("Usage:")
32 assert_that(result.stdout).contains("extract-test-summary.sh")
33 assert_that(result.stdout).contains("test-output-file")
34 assert_that(result.stdout).contains("output-json-file")
37def test_script_syntax_check() -> None:
38 """Script should pass bash syntax check."""
39 result = subprocess.run(
40 ["bash", "-n", str(SCRIPT_PATH)],
41 capture_output=True,
42 text=True,
43 )
45 assert_that(result.returncode).is_equal_to(0)
46 assert_that(result.stderr).is_empty()
49def test_extract_from_standard_pytest_output() -> None:
50 """Extract test summary from standard pytest output format."""
51 pytest_output = """
52============================= test session starts ==============================
53platform linux -- Python 3.13.1, pytest-8.0.0, pluggy-1.4.0
54collected 100 items
56tests/test_foo.py ............................ [ 28%]
57tests/test_bar.py ............................ [ 56%]
58tests/test_baz.py ............................ [ 84%]
59tests/test_qux.py ................ [100%]
61=============================== warnings summary ===============================
621 warning
64============== 95 passed, 3 failed, 2 skipped in 12.34s ================
65"""
66 with tempfile.TemporaryDirectory() as tmpdir:
67 input_file = Path(tmpdir) / "test-output.log"
68 output_file = Path(tmpdir) / "test-summary.json"
69 input_file.write_text(pytest_output)
71 result = subprocess.run(
72 [str(SCRIPT_PATH), str(input_file), str(output_file)],
73 capture_output=True,
74 text=True,
75 cwd=tmpdir,
76 )
78 assert_that(result.returncode).is_equal_to(0)
79 assert_that(output_file.exists()).is_true()
81 summary = json.loads(output_file.read_text())
82 assert_that(summary["tests"]["passed"]).is_equal_to(95)
83 assert_that(summary["tests"]["failed"]).is_equal_to(3)
84 assert_that(summary["tests"]["skipped"]).is_equal_to(2)
85 assert_that(summary["tests"]["duration"]).is_equal_to(12.34)
88def test_extract_from_lintro_table_format() -> None:
89 """Extract test summary from lintro table format output.
91 Note: The pattern supports both emoji and non-emoji formats
92 (e.g., '| 🧪 pytest' or '| pytest').
93 """
94 lintro_output = """
95| Tool | Status | Passed | Failed | Skipped | Total | Duration |
96|------|--------|--------|--------|---------|-------|----------|
97| pytest | PASS | 150 | 0 | 5 | 155 | 45.67s |
98"""
99 with tempfile.TemporaryDirectory() as tmpdir:
100 input_file = Path(tmpdir) / "test-output.log"
101 output_file = Path(tmpdir) / "test-summary.json"
102 input_file.write_text(lintro_output)
104 result = subprocess.run(
105 [str(SCRIPT_PATH), str(input_file), str(output_file)],
106 capture_output=True,
107 text=True,
108 cwd=tmpdir,
109 )
111 assert_that(result.returncode).is_equal_to(0)
112 assert_that(output_file.exists()).is_true()
114 summary = json.loads(output_file.read_text())
115 assert_that(summary["tests"]["passed"]).is_equal_to(150)
116 assert_that(summary["tests"]["failed"]).is_equal_to(0)
117 assert_that(summary["tests"]["skipped"]).is_equal_to(5)
118 assert_that(summary["tests"]["total"]).is_equal_to(155)
121def test_extract_with_coverage_xml() -> None:
122 """Extract both test summary and coverage data when coverage.xml exists."""
123 pytest_output = "10 passed in 5.00s\n"
124 coverage_xml = """<?xml version="1.0" ?>
125<coverage version="7.0.0" timestamp="1234567890" lines-covered="800" lines-valid="1000" line-rate="0.8" branch-rate="0.0" complexity="0">
126 <packages>
127 <package name="lintro" line-rate="0.8" branch-rate="0.0" complexity="0">
128 <classes>
129 <class name="foo.py" filename="lintro/foo.py" line-rate="0.9" branch-rate="0.0" complexity="0">
130 <lines/>
131 </class>
132 <class name="bar.py" filename="lintro/bar.py" line-rate="0.7" branch-rate="0.0" complexity="0">
133 <lines/>
134 </class>
135 </classes>
136 </package>
137 </packages>
138</coverage>
139"""
140 with tempfile.TemporaryDirectory() as tmpdir:
141 input_file = Path(tmpdir) / "test-output.log"
142 output_file = Path(tmpdir) / "test-summary.json"
143 coverage_file = Path(tmpdir) / "coverage.xml"
145 input_file.write_text(pytest_output)
146 coverage_file.write_text(coverage_xml)
148 result = subprocess.run(
149 [str(SCRIPT_PATH), str(input_file), str(output_file)],
150 capture_output=True,
151 text=True,
152 cwd=tmpdir,
153 )
155 assert_that(result.returncode).is_equal_to(0)
157 summary = json.loads(output_file.read_text())
158 assert_that(summary["coverage"]["percentage"]).is_equal_to(80.0)
159 assert_that(summary["coverage"]["lines_covered"]).is_equal_to(800)
160 assert_that(summary["coverage"]["lines_total"]).is_equal_to(1000)
161 assert_that(summary["coverage"]["lines_missing"]).is_equal_to(200)
162 assert_that(summary["coverage"]["files"]).is_equal_to(2)
165def test_extract_from_environment_variables() -> None:
166 """Extract test summary from environment variables when no input file."""
167 env = os.environ.copy()
168 env["TEST_PASSED"] = "42"
169 env["TEST_FAILED"] = "1"
170 env["TEST_SKIPPED"] = "3"
171 env["TEST_ERRORS"] = "0"
172 env["TEST_TOTAL"] = "46"
173 env["TEST_DURATION"] = "8.5"
175 with tempfile.TemporaryDirectory() as tmpdir:
176 output_file = Path(tmpdir) / "test-summary.json"
178 result = subprocess.run(
179 [str(SCRIPT_PATH), "", str(output_file)],
180 capture_output=True,
181 text=True,
182 cwd=tmpdir,
183 env=env,
184 )
186 assert_that(result.returncode).is_equal_to(0)
188 summary = json.loads(output_file.read_text())
189 assert_that(summary["tests"]["passed"]).is_equal_to(42)
190 assert_that(summary["tests"]["failed"]).is_equal_to(1)
191 assert_that(summary["tests"]["skipped"]).is_equal_to(3)
192 assert_that(summary["tests"]["total"]).is_equal_to(46)
195def test_default_output_file() -> None:
196 """Script uses test-summary.json as default output file."""
197 pytest_output = "5 passed in 1.00s\n"
199 with tempfile.TemporaryDirectory() as tmpdir:
200 input_file = Path(tmpdir) / "test-output.log"
201 input_file.write_text(pytest_output)
203 # Run without specifying output file
204 result = subprocess.run(
205 [str(SCRIPT_PATH), str(input_file)],
206 capture_output=True,
207 text=True,
208 cwd=tmpdir,
209 )
211 assert_that(result.returncode).is_equal_to(0)
213 # Default output file should be created
214 default_output = Path(tmpdir) / "test-summary.json"
215 assert_that(default_output.exists()).is_true()
217 summary = json.loads(default_output.read_text())
218 assert_that(summary["tests"]["passed"]).is_equal_to(5)