|
22 | 22 | python scripts/run_tests.py |
23 | 23 | """ |
24 | 24 |
|
| 25 | +import re |
25 | 26 | import subprocess |
26 | 27 | import sys |
27 | 28 | from pathlib import Path |
@@ -74,45 +75,115 @@ def get_integration_dirs(args: list[str]) -> list[Path]: |
74 | 75 | ) |
75 | 76 |
|
76 | 77 |
|
77 | | -def install_integration_deps(integration_dir: Path) -> bool: |
78 | | - """Install an integration's requirements.txt. Returns True on success.""" |
| 78 | +def install_integration_deps(integration_dir: Path) -> tuple[bool, str]: |
| 79 | + """Install an integration's requirements.txt. Returns (success, error_output).""" |
79 | 80 | req_file = integration_dir / "requirements.txt" |
80 | 81 | if not req_file.is_file(): |
81 | | - return True |
| 82 | + return True, "" |
82 | 83 |
|
83 | 84 | result = subprocess.run( |
84 | 85 | [sys.executable, "-m", "pip", "install", "-r", str(req_file), "-q"], |
85 | 86 | capture_output=True, |
86 | 87 | text=True, |
87 | 88 | ) |
88 | 89 | if result.returncode != 0: |
89 | | - print(f" ❌ Failed to install dependencies for {integration_dir.name}") |
90 | | - if result.stderr.strip(): |
91 | | - for line in result.stderr.strip().splitlines(): |
92 | | - print(f" {line}") |
93 | | - return False |
94 | | - return True |
| 90 | + return False, result.stderr.strip() |
| 91 | + return True, "" |
95 | 92 |
|
96 | 93 |
|
97 | | -def run_integration_tests(integration_dir: Path, test_files: list[Path]) -> int: |
98 | | - """Run pytest for a single integration. Returns the pytest exit code.""" |
| 94 | +def run_integration_tests(integration_dir: Path, test_files: list[Path]) -> tuple[int, str]: |
| 95 | + """Run pytest for a single integration. Returns (exit_code, output).""" |
99 | 96 | cmd = [ |
100 | 97 | sys.executable, |
101 | 98 | "-m", |
102 | 99 | "pytest", |
103 | 100 | "--import-mode=importlib", |
104 | 101 | "-m", |
105 | 102 | "unit", |
106 | | - "-v", |
107 | 103 | "--tb=short", |
108 | 104 | "--no-header", |
| 105 | + "-q", |
109 | 106 | "--cov", |
110 | 107 | str(integration_dir), |
111 | 108 | "--cov-report=term-missing:skip-covered", |
112 | 109 | *[str(f) for f in test_files], |
113 | 110 | ] |
114 | 111 |
|
115 | | - return subprocess.run(cmd).returncode |
| 112 | + result = subprocess.run(cmd, capture_output=True, text=True) |
| 113 | + return result.returncode, result.stdout + result.stderr |
| 114 | + |
| 115 | + |
| 116 | +def parse_results(output: str) -> tuple[int, int, str]: |
| 117 | + """Parse pytest output for passed count, failed count, and coverage.""" |
| 118 | + passed = failed = 0 |
| 119 | + coverage = "n/a" |
| 120 | + |
| 121 | + # Match summary line e.g. "37 passed" or "2 failed, 35 passed" |
| 122 | + summary = re.search(r"(\d+) passed", output) |
| 123 | + if summary: |
| 124 | + passed = int(summary.group(1)) |
| 125 | + fail_match = re.search(r"(\d+) failed", output) |
| 126 | + if fail_match: |
| 127 | + failed = int(fail_match.group(1)) |
| 128 | + |
| 129 | + # Match coverage total e.g. "TOTAL 166 13 92%" |
| 130 | + cov_match = re.search(r"^TOTAL\s+\d+\s+\d+\s+(\d+%)", output, re.MULTILINE) |
| 131 | + if cov_match: |
| 132 | + coverage = cov_match.group(1) |
| 133 | + |
| 134 | + return passed, failed, coverage |
| 135 | + |
| 136 | + |
| 137 | +def print_table(rows: list[tuple[str, str, str, str, str]], failed_outputs: dict[str, str]) -> None: |
| 138 | + """Print a formatted results table.""" |
| 139 | + col_widths = [ |
| 140 | + max(len(r[0]) for r in rows) + 2, |
| 141 | + 8, # Tests |
| 142 | + 10, # Coverage |
| 143 | + 14, # Status |
| 144 | + ] |
| 145 | + |
| 146 | + header = ( |
| 147 | + f"{'Integration':<{col_widths[0]}}" |
| 148 | + f"{'Tests':>{col_widths[1]}}" |
| 149 | + f"{'Coverage':>{col_widths[2]}}" |
| 150 | + f"{'Status':>{col_widths[3]}}" |
| 151 | + ) |
| 152 | + divider = "-" * len(header) |
| 153 | + |
| 154 | + print() |
| 155 | + print(header) |
| 156 | + print(divider) |
| 157 | + for name, tests, coverage, status, _ in rows: |
| 158 | + print( |
| 159 | + f"{name:<{col_widths[0]}}" |
| 160 | + f"{tests:>{col_widths[1]}}" |
| 161 | + f"{coverage:>{col_widths[2]}}" |
| 162 | + f"{status:>{col_widths[3]}}" |
| 163 | + ) |
| 164 | + print(divider) |
| 165 | + |
| 166 | + # Total row |
| 167 | + total_passed = sum(int(r[1].split("/")[0]) for r in rows if "/" in r[1]) |
| 168 | + total_total = sum(int(r[1].split("/")[1]) for r in rows if "/" in r[1]) |
| 169 | + all_passed = all(r[4] == "pass" for r in rows) |
| 170 | + total_status = "✅ All passed" if all_passed else "❌ Some failed" |
| 171 | + print( |
| 172 | + f"{'Total':<{col_widths[0]}}" |
| 173 | + f"{f'{total_passed}/{total_total}':>{col_widths[1]}}" |
| 174 | + f"{'':>{col_widths[2]}}" |
| 175 | + f"{total_status:>{col_widths[3]}}" |
| 176 | + ) |
| 177 | + print() |
| 178 | + |
| 179 | + # Print full output only for failures |
| 180 | + for name, _, _, _, outcome in rows: |
| 181 | + if outcome == "fail" and name in failed_outputs: |
| 182 | + print(f"{'=' * 60}") |
| 183 | + print(f" {name} — failure detail") |
| 184 | + print(f"{'=' * 60}") |
| 185 | + print(failed_outputs[name]) |
| 186 | + print() |
116 | 187 |
|
117 | 188 |
|
118 | 189 | def main() -> int: |
@@ -140,37 +211,37 @@ def main() -> int: |
140 | 211 | print("⚠️ No unit tests to run") |
141 | 212 | return 0 |
142 | 213 |
|
143 | | - print(f"🧪 Running unit tests for: {', '.join(d.name for d, _ in testable)}") |
144 | | - print() |
145 | | - |
146 | 214 | # Run each integration separately with its own dependencies |
147 | | - failed = [] |
148 | | - passed = [] |
149 | | - for d, test_files in testable: |
150 | | - print(f"{'=' * 60}") |
151 | | - print(f" {d.name}") |
152 | | - print(f"{'=' * 60}") |
| 215 | + rows = [] |
| 216 | + failed_outputs = {} |
153 | 217 |
|
154 | | - if not install_integration_deps(d): |
155 | | - failed.append(d.name) |
| 218 | + for d, test_files in testable: |
| 219 | + ok, err = install_integration_deps(d) |
| 220 | + if not ok: |
| 221 | + rows.append((d.name, "n/a", "n/a", "❌ Dep install failed", "fail")) |
| 222 | + failed_outputs[d.name] = err |
156 | 223 | continue |
157 | 224 |
|
158 | | - exit_code = run_integration_tests(d, test_files) |
| 225 | + exit_code, output = run_integration_tests(d, test_files) |
| 226 | + passed, failed, coverage = parse_results(output) |
| 227 | + total = passed + failed |
| 228 | + tests_str = f"{passed}/{total}" |
| 229 | + |
159 | 230 | if exit_code == 0: |
160 | | - passed.append(d.name) |
| 231 | + rows.append((d.name, tests_str, coverage, "✅ Passed", "pass")) |
161 | 232 | else: |
162 | | - failed.append(d.name) |
163 | | - print() |
164 | | - |
165 | | - # Summary |
166 | | - print("=" * 60) |
167 | | - if passed: |
168 | | - print(f"✅ Tests passed: {', '.join(passed)}") |
169 | | - if failed: |
170 | | - print(f"❌ Tests failed: {', '.join(failed)}") |
171 | | - print("=" * 60) |
172 | | - |
173 | | - return 1 if failed else 0 |
| 233 | + rows.append((d.name, tests_str, coverage, "❌ Failed", "fail")) |
| 234 | + failed_outputs[d.name] = output |
| 235 | + |
| 236 | + print_table(rows, failed_outputs) |
| 237 | + |
| 238 | + any_failed = any(r[4] == "fail" for r in rows) |
| 239 | + if any_failed: |
| 240 | + print(f"❌ Tests failed: {', '.join(r[0] for r in rows if r[4] == 'fail')}") |
| 241 | + else: |
| 242 | + print(f"✅ Tests passed: {', '.join(r[0] for r in rows if r[4] == 'pass')}") |
| 243 | + |
| 244 | + return 1 if any_failed else 0 |
174 | 245 |
|
175 | 246 |
|
176 | 247 | if __name__ == "__main__": |
|
0 commit comments