chp-adapter-ci 0.8.0__tar.gz
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.
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
node_modules/
|
|
2
|
+
dist/
|
|
3
|
+
.yalc/
|
|
4
|
+
yalc.lock
|
|
5
|
+
*.tgz
|
|
6
|
+
.env
|
|
7
|
+
.env.*
|
|
8
|
+
coverage/
|
|
9
|
+
dashboard/
|
|
10
|
+
components/
|
|
11
|
+
.tech-hub-cache/
|
|
12
|
+
__pycache__/
|
|
13
|
+
*.py[cod]
|
|
14
|
+
.pytest_cache/
|
|
15
|
+
.chp/
|
|
16
|
+
.DS_Store
|
|
17
|
+
.coverage
|
|
18
|
+
.forge/
|
|
19
|
+
.hypothesis/
|
|
20
|
+
.claude/
|
|
21
|
+
|
|
22
|
+
# chp-agent evidence store
|
|
23
|
+
.chp-agent/sessions.sqlite
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: chp-adapter-ci
|
|
3
|
+
Version: 0.8.0
|
|
4
|
+
Summary: CHP adapter for running package test suites as governed evidence-producing invocations
|
|
5
|
+
Keywords: capability-host-protocol,chp,ci,pytest,testing
|
|
6
|
+
Requires-Python: >=3.11
|
|
7
|
+
Requires-Dist: chp-core
|
|
@@ -0,0 +1,227 @@
|
|
|
1
|
+
"""CIAdapter — run package test suites as governed CHP capability invocations.
|
|
2
|
+
|
|
3
|
+
Each package's pytest run becomes a ctx.ainvoke("chp.adapters.ci.run_suite")
|
|
4
|
+
call so it appears as a first-class record in the correlation chain. The
|
|
5
|
+
run_all capability orchestrates the full suite via the lego-block pattern:
|
|
6
|
+
run_all → ctx.ainvoke(run_suite) × N packages → evidence per package
|
|
7
|
+
|
|
8
|
+
Evidence policy:
|
|
9
|
+
Emitted: package name, path, pass/fail counts, duration, exit code, summary.
|
|
10
|
+
NOT emitted: test output, failure messages (may contain paths or secrets).
|
|
11
|
+
"""
|
|
12
|
+
|
|
13
|
+
from __future__ import annotations
|
|
14
|
+
|
|
15
|
+
import re
|
|
16
|
+
import subprocess
|
|
17
|
+
import sys
|
|
18
|
+
import time
|
|
19
|
+
from dataclasses import dataclass
|
|
20
|
+
from pathlib import Path
|
|
21
|
+
from typing import Any
|
|
22
|
+
|
|
23
|
+
from chp_core import BaseAdapter, capability
|
|
24
|
+
|
|
25
|
+
_EMITS = [
|
|
26
|
+
"ci_run_started",
|
|
27
|
+
"ci_run_completed",
|
|
28
|
+
"ci_suite_started",
|
|
29
|
+
"ci_suite_completed",
|
|
30
|
+
"ci_suite_failed",
|
|
31
|
+
]
|
|
32
|
+
|
|
33
|
+
_PASSED_RE = re.compile(r"(\d+) passed")
|
|
34
|
+
_FAILED_RE = re.compile(r"(\d+) failed")
|
|
35
|
+
_ERROR_RE = re.compile(r"(\d+) error")
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class CIConfig:
|
|
40
|
+
"""Config for CIAdapter.
|
|
41
|
+
|
|
42
|
+
``repo_root`` — root of the chp-dev repo; defaults to cwd() at call time.
|
|
43
|
+
``python`` — Python executable to use for subprocess pytest invocations.
|
|
44
|
+
"""
|
|
45
|
+
|
|
46
|
+
repo_root: str = ""
|
|
47
|
+
python: str = ""
|
|
48
|
+
|
|
49
|
+
def resolved_root(self) -> Path:
|
|
50
|
+
return Path(self.repo_root).expanduser().resolve() if self.repo_root else Path.cwd()
|
|
51
|
+
|
|
52
|
+
def resolved_python(self) -> str:
|
|
53
|
+
return self.python or sys.executable
|
|
54
|
+
|
|
55
|
+
|
|
56
|
+
class CIAdapter(BaseAdapter):
|
|
57
|
+
"""Run package test suites as governed CHP invocations with per-suite evidence."""
|
|
58
|
+
|
|
59
|
+
adapter_id = "chp.adapters.ci"
|
|
60
|
+
adapter_name = "CI"
|
|
61
|
+
adapter_description = "Run pytest across CHP packages as governed capability calls. Each suite is a separate evidence-producing invocation."
|
|
62
|
+
adapter_category = "developer_tooling"
|
|
63
|
+
adapter_tags = ["ci", "testing", "pytest", "quality", "evidence"]
|
|
64
|
+
|
|
65
|
+
def __init__(self, config: CIConfig | None = None) -> None:
|
|
66
|
+
self._config = config or CIConfig()
|
|
67
|
+
|
|
68
|
+
def _discover_packages(self) -> list[dict[str, str]]:
|
|
69
|
+
"""Find all packages under repo_root/packages/ that have a pyproject.toml."""
|
|
70
|
+
packages_dir = self._config.resolved_root() / "packages"
|
|
71
|
+
if not packages_dir.is_dir():
|
|
72
|
+
return []
|
|
73
|
+
return sorted(
|
|
74
|
+
[
|
|
75
|
+
{"name": p.name, "path": str(p)}
|
|
76
|
+
for p in packages_dir.iterdir()
|
|
77
|
+
if p.is_dir() and (p / "pyproject.toml").exists()
|
|
78
|
+
],
|
|
79
|
+
key=lambda x: x["name"],
|
|
80
|
+
)
|
|
81
|
+
|
|
82
|
+
# ------------------------------------------------------------------
|
|
83
|
+
# run_suite
|
|
84
|
+
# ------------------------------------------------------------------
|
|
85
|
+
|
|
86
|
+
@capability(
|
|
87
|
+
id="chp.adapters.ci.run_suite",
|
|
88
|
+
version="1.0.0",
|
|
89
|
+
description="Run pytest for a single package and emit pass/fail evidence. Output is not recorded — only counts and duration.",
|
|
90
|
+
category="developer_tooling",
|
|
91
|
+
risk="low",
|
|
92
|
+
emits=_EMITS,
|
|
93
|
+
input_schema={
|
|
94
|
+
"type": "object",
|
|
95
|
+
"properties": {
|
|
96
|
+
"package_name": {"type": "string", "description": "Human-readable package name (e.g. chp-adapter-messages)"},
|
|
97
|
+
"package_path": {"type": "string", "description": "Absolute path to the package directory"},
|
|
98
|
+
},
|
|
99
|
+
"required": ["package_name", "package_path"],
|
|
100
|
+
"additionalProperties": False,
|
|
101
|
+
},
|
|
102
|
+
)
|
|
103
|
+
async def run_suite(self, ctx: Any, payload: dict) -> dict:
|
|
104
|
+
package_name: str = payload["package_name"]
|
|
105
|
+
package_path: str = payload["package_path"]
|
|
106
|
+
|
|
107
|
+
ctx.emit("ci_suite_started", {
|
|
108
|
+
"package": package_name,
|
|
109
|
+
"path": package_path,
|
|
110
|
+
}, redacted=False)
|
|
111
|
+
|
|
112
|
+
t0 = time.monotonic()
|
|
113
|
+
result = subprocess.run(
|
|
114
|
+
[self._config.resolved_python(), "-m", "pytest", "-q", "-o", "addopts="],
|
|
115
|
+
cwd=package_path,
|
|
116
|
+
capture_output=True,
|
|
117
|
+
text=True,
|
|
118
|
+
)
|
|
119
|
+
duration_ms = round((time.monotonic() - t0) * 1000)
|
|
120
|
+
|
|
121
|
+
output = (result.stdout + result.stderr).strip()
|
|
122
|
+
summary_line = next(
|
|
123
|
+
(ln for ln in reversed(output.splitlines()) if ln.strip()),
|
|
124
|
+
"",
|
|
125
|
+
)
|
|
126
|
+
|
|
127
|
+
passed = int(m.group(1)) if (m := _PASSED_RE.search(summary_line)) else 0
|
|
128
|
+
failed = int(m.group(1)) if (m := _FAILED_RE.search(summary_line)) else 0
|
|
129
|
+
errors = int(m.group(1)) if (m := _ERROR_RE.search(summary_line)) else 0
|
|
130
|
+
ok = result.returncode == 0
|
|
131
|
+
|
|
132
|
+
evidence = {
|
|
133
|
+
"package": package_name,
|
|
134
|
+
"passed": passed,
|
|
135
|
+
"failed": failed,
|
|
136
|
+
"errors": errors,
|
|
137
|
+
"duration_ms": duration_ms,
|
|
138
|
+
"exit_code": result.returncode,
|
|
139
|
+
"summary": summary_line[:200],
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
if ok:
|
|
143
|
+
ctx.emit("ci_suite_completed", evidence, redacted=False)
|
|
144
|
+
else:
|
|
145
|
+
ctx.emit("ci_suite_failed", evidence, redacted=False)
|
|
146
|
+
|
|
147
|
+
return {"ok": ok, **evidence}
|
|
148
|
+
|
|
149
|
+
# ------------------------------------------------------------------
|
|
150
|
+
# run_all
|
|
151
|
+
# ------------------------------------------------------------------
|
|
152
|
+
|
|
153
|
+
@capability(
|
|
154
|
+
id="chp.adapters.ci.run_all",
|
|
155
|
+
version="1.0.0",
|
|
156
|
+
description="Run pytest across all packages in the repo. Each suite is a separate ctx.ainvoke(run_suite) call — every package gets its own evidence record.",
|
|
157
|
+
category="developer_tooling",
|
|
158
|
+
risk="low",
|
|
159
|
+
emits=_EMITS,
|
|
160
|
+
input_schema={
|
|
161
|
+
"type": "object",
|
|
162
|
+
"properties": {
|
|
163
|
+
"packages": {
|
|
164
|
+
"type": "array",
|
|
165
|
+
"items": {"type": "string"},
|
|
166
|
+
"description": "Package names to run (default: all discovered packages)",
|
|
167
|
+
},
|
|
168
|
+
},
|
|
169
|
+
"additionalProperties": False,
|
|
170
|
+
},
|
|
171
|
+
)
|
|
172
|
+
async def run_all(self, ctx: Any, payload: dict) -> dict:
|
|
173
|
+
all_packages = self._discover_packages()
|
|
174
|
+
filter_names: list[str] | None = payload.get("packages") or None
|
|
175
|
+
|
|
176
|
+
packages = (
|
|
177
|
+
[p for p in all_packages if p["name"] in filter_names]
|
|
178
|
+
if filter_names
|
|
179
|
+
else all_packages
|
|
180
|
+
)
|
|
181
|
+
|
|
182
|
+
ctx.emit("ci_run_started", {
|
|
183
|
+
"package_count": len(packages),
|
|
184
|
+
"packages": [p["name"] for p in packages],
|
|
185
|
+
}, redacted=False)
|
|
186
|
+
|
|
187
|
+
suite_results: list[dict] = []
|
|
188
|
+
for pkg in packages:
|
|
189
|
+
result = await ctx.ainvoke(
|
|
190
|
+
"chp.adapters.ci.run_suite",
|
|
191
|
+
{"package_name": pkg["name"], "package_path": pkg["path"]},
|
|
192
|
+
)
|
|
193
|
+
if result.success:
|
|
194
|
+
suite_results.append(result.data)
|
|
195
|
+
else:
|
|
196
|
+
suite_results.append({
|
|
197
|
+
"package": pkg["name"],
|
|
198
|
+
"ok": False,
|
|
199
|
+
"passed": 0,
|
|
200
|
+
"failed": 0,
|
|
201
|
+
"errors": 1,
|
|
202
|
+
"duration_ms": 0,
|
|
203
|
+
"exit_code": -1,
|
|
204
|
+
"summary": str(result.error),
|
|
205
|
+
})
|
|
206
|
+
|
|
207
|
+
total_passed = sum(r.get("passed", 0) for r in suite_results)
|
|
208
|
+
total_failed = sum(r.get("failed", 0) + r.get("errors", 0) for r in suite_results)
|
|
209
|
+
failed_suites = [r["package"] for r in suite_results if not r.get("ok")]
|
|
210
|
+
ok = len(failed_suites) == 0
|
|
211
|
+
|
|
212
|
+
ctx.emit("ci_run_completed", {
|
|
213
|
+
"ok": ok,
|
|
214
|
+
"total_packages": len(packages),
|
|
215
|
+
"total_passed": total_passed,
|
|
216
|
+
"total_failed": total_failed,
|
|
217
|
+
"failed_suites": failed_suites,
|
|
218
|
+
}, redacted=False)
|
|
219
|
+
|
|
220
|
+
return {
|
|
221
|
+
"ok": ok,
|
|
222
|
+
"total_packages": len(packages),
|
|
223
|
+
"total_passed": total_passed,
|
|
224
|
+
"total_failed": total_failed,
|
|
225
|
+
"failed_suites": failed_suites,
|
|
226
|
+
"suites": suite_results,
|
|
227
|
+
}
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.25"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "chp-adapter-ci"
|
|
7
|
+
version = "0.8.0"
|
|
8
|
+
description = "CHP adapter for running package test suites as governed evidence-producing invocations"
|
|
9
|
+
requires-python = ">=3.11"
|
|
10
|
+
dependencies = ["chp-core"]
|
|
11
|
+
keywords = ["chp", "capability-host-protocol", "ci", "testing", "pytest"]
|
|
12
|
+
|
|
13
|
+
[project.entry-points."chp.adapters"]
|
|
14
|
+
ci = "chp_adapter_ci:CIAdapter"
|
|
15
|
+
|
|
16
|
+
[tool.hatch.build.targets.wheel]
|
|
17
|
+
packages = ["chp_adapter_ci"]
|
|
18
|
+
|
|
19
|
+
[tool.pytest.ini_options]
|
|
20
|
+
asyncio_mode = "strict"
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
"""Tests for chp-adapter-ci."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import asyncio
|
|
6
|
+
import json
|
|
7
|
+
import textwrap
|
|
8
|
+
from pathlib import Path
|
|
9
|
+
from unittest.mock import MagicMock, patch
|
|
10
|
+
|
|
11
|
+
import pytest
|
|
12
|
+
|
|
13
|
+
from chp_adapter_ci import CIAdapter, CIConfig
|
|
14
|
+
from chp_core import LocalCapabilityHost, register_adapter
|
|
15
|
+
from chp_core.store import SQLiteEvidenceStore
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
# ---------------------------------------------------------------------------
|
|
19
|
+
# Helpers
|
|
20
|
+
# ---------------------------------------------------------------------------
|
|
21
|
+
|
|
22
|
+
def _make_host(repo_root: str = "") -> LocalCapabilityHost:
|
|
23
|
+
store = SQLiteEvidenceStore(":memory:")
|
|
24
|
+
host = LocalCapabilityHost(store=store)
|
|
25
|
+
config = CIConfig(repo_root=repo_root)
|
|
26
|
+
register_adapter(host, CIAdapter(config))
|
|
27
|
+
return host
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def _invoke(host: LocalCapabilityHost, cap_id: str, payload: dict | None = None):
|
|
31
|
+
return asyncio.get_event_loop().run_until_complete(
|
|
32
|
+
host.ainvoke(cap_id, payload or {})
|
|
33
|
+
)
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def _fake_run(returncode: int = 0, stdout: str = "1 passed in 0.1s", stderr: str = ""):
|
|
37
|
+
result = MagicMock()
|
|
38
|
+
result.returncode = returncode
|
|
39
|
+
result.stdout = stdout
|
|
40
|
+
result.stderr = stderr
|
|
41
|
+
return result
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
# ---------------------------------------------------------------------------
|
|
45
|
+
# CIConfig
|
|
46
|
+
# ---------------------------------------------------------------------------
|
|
47
|
+
|
|
48
|
+
class TestCIConfig:
|
|
49
|
+
def test_resolved_root_defaults_to_cwd(self):
|
|
50
|
+
config = CIConfig()
|
|
51
|
+
assert config.resolved_root() == Path.cwd()
|
|
52
|
+
|
|
53
|
+
def test_resolved_root_expands_path(self, tmp_path):
|
|
54
|
+
config = CIConfig(repo_root=str(tmp_path))
|
|
55
|
+
assert config.resolved_root() == tmp_path
|
|
56
|
+
|
|
57
|
+
def test_resolved_python_defaults_to_sys(self):
|
|
58
|
+
import sys
|
|
59
|
+
config = CIConfig()
|
|
60
|
+
assert config.resolved_python() == sys.executable
|
|
61
|
+
|
|
62
|
+
def test_resolved_python_uses_override(self):
|
|
63
|
+
config = CIConfig(python="/custom/python3")
|
|
64
|
+
assert config.resolved_python() == "/custom/python3"
|
|
65
|
+
|
|
66
|
+
|
|
67
|
+
# ---------------------------------------------------------------------------
|
|
68
|
+
# CIAdapter._discover_packages
|
|
69
|
+
# ---------------------------------------------------------------------------
|
|
70
|
+
|
|
71
|
+
class TestDiscoverPackages:
|
|
72
|
+
def test_finds_packages_with_pyproject(self, tmp_path):
|
|
73
|
+
pkg_a = tmp_path / "packages" / "pkg-a"
|
|
74
|
+
pkg_a.mkdir(parents=True)
|
|
75
|
+
(pkg_a / "pyproject.toml").write_text("[project]\nname = 'pkg-a'\n")
|
|
76
|
+
|
|
77
|
+
pkg_b = tmp_path / "packages" / "pkg-b"
|
|
78
|
+
pkg_b.mkdir(parents=True)
|
|
79
|
+
(pkg_b / "pyproject.toml").write_text("[project]\nname = 'pkg-b'\n")
|
|
80
|
+
|
|
81
|
+
no_toml = tmp_path / "packages" / "not-a-package"
|
|
82
|
+
no_toml.mkdir(parents=True)
|
|
83
|
+
|
|
84
|
+
adapter = CIAdapter(CIConfig(repo_root=str(tmp_path)))
|
|
85
|
+
packages = adapter._discover_packages()
|
|
86
|
+
|
|
87
|
+
names = [p["name"] for p in packages]
|
|
88
|
+
assert "pkg-a" in names
|
|
89
|
+
assert "pkg-b" in names
|
|
90
|
+
assert "not-a-package" not in names
|
|
91
|
+
|
|
92
|
+
def test_returns_empty_when_no_packages_dir(self, tmp_path):
|
|
93
|
+
adapter = CIAdapter(CIConfig(repo_root=str(tmp_path)))
|
|
94
|
+
assert adapter._discover_packages() == []
|
|
95
|
+
|
|
96
|
+
def test_sorted_alphabetically(self, tmp_path):
|
|
97
|
+
for name in ["pkg-z", "pkg-a", "pkg-m"]:
|
|
98
|
+
d = tmp_path / "packages" / name
|
|
99
|
+
d.mkdir(parents=True)
|
|
100
|
+
(d / "pyproject.toml").write_text("")
|
|
101
|
+
|
|
102
|
+
adapter = CIAdapter(CIConfig(repo_root=str(tmp_path)))
|
|
103
|
+
names = [p["name"] for p in adapter._discover_packages()]
|
|
104
|
+
assert names == sorted(names)
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
# ---------------------------------------------------------------------------
|
|
108
|
+
# run_suite capability
|
|
109
|
+
# ---------------------------------------------------------------------------
|
|
110
|
+
|
|
111
|
+
class TestRunSuite:
|
|
112
|
+
def test_passing_suite(self, tmp_path):
|
|
113
|
+
host = _make_host()
|
|
114
|
+
with patch("chp_adapter_ci.adapter.subprocess.run") as mock_run:
|
|
115
|
+
mock_run.return_value = _fake_run(0, "3 passed in 0.42s")
|
|
116
|
+
result = _invoke(host, "chp.adapters.ci.run_suite", {
|
|
117
|
+
"package_name": "my-pkg",
|
|
118
|
+
"package_path": str(tmp_path),
|
|
119
|
+
})
|
|
120
|
+
|
|
121
|
+
assert result.success
|
|
122
|
+
assert result.data["ok"] is True
|
|
123
|
+
assert result.data["passed"] == 3
|
|
124
|
+
assert result.data["failed"] == 0
|
|
125
|
+
assert result.data["exit_code"] == 0
|
|
126
|
+
assert result.data["package"] == "my-pkg"
|
|
127
|
+
|
|
128
|
+
def test_failing_suite(self, tmp_path):
|
|
129
|
+
host = _make_host()
|
|
130
|
+
with patch("chp_adapter_ci.adapter.subprocess.run") as mock_run:
|
|
131
|
+
mock_run.return_value = _fake_run(1, "2 passed, 1 failed in 0.5s")
|
|
132
|
+
result = _invoke(host, "chp.adapters.ci.run_suite", {
|
|
133
|
+
"package_name": "bad-pkg",
|
|
134
|
+
"package_path": str(tmp_path),
|
|
135
|
+
})
|
|
136
|
+
|
|
137
|
+
assert result.success
|
|
138
|
+
assert result.data["ok"] is False
|
|
139
|
+
assert result.data["passed"] == 2
|
|
140
|
+
assert result.data["failed"] == 1
|
|
141
|
+
assert result.data["exit_code"] == 1
|
|
142
|
+
|
|
143
|
+
def test_summary_truncated_at_200(self, tmp_path):
|
|
144
|
+
host = _make_host()
|
|
145
|
+
long_line = "x" * 300
|
|
146
|
+
with patch("chp_adapter_ci.adapter.subprocess.run") as mock_run:
|
|
147
|
+
mock_run.return_value = _fake_run(0, long_line)
|
|
148
|
+
result = _invoke(host, "chp.adapters.ci.run_suite", {
|
|
149
|
+
"package_name": "pkg",
|
|
150
|
+
"package_path": str(tmp_path),
|
|
151
|
+
})
|
|
152
|
+
|
|
153
|
+
assert len(result.data["summary"]) <= 200
|
|
154
|
+
|
|
155
|
+
def test_duration_ms_positive(self, tmp_path):
|
|
156
|
+
host = _make_host()
|
|
157
|
+
with patch("chp_adapter_ci.adapter.subprocess.run") as mock_run:
|
|
158
|
+
mock_run.return_value = _fake_run()
|
|
159
|
+
result = _invoke(host, "chp.adapters.ci.run_suite", {
|
|
160
|
+
"package_name": "pkg",
|
|
161
|
+
"package_path": str(tmp_path),
|
|
162
|
+
})
|
|
163
|
+
assert result.data["duration_ms"] >= 0
|
|
164
|
+
|
|
165
|
+
def test_subprocess_called_with_correct_args(self, tmp_path):
|
|
166
|
+
host = _make_host()
|
|
167
|
+
with patch("chp_adapter_ci.adapter.subprocess.run") as mock_run:
|
|
168
|
+
mock_run.return_value = _fake_run()
|
|
169
|
+
_invoke(host, "chp.adapters.ci.run_suite", {
|
|
170
|
+
"package_name": "pkg",
|
|
171
|
+
"package_path": str(tmp_path),
|
|
172
|
+
})
|
|
173
|
+
|
|
174
|
+
call_args = mock_run.call_args
|
|
175
|
+
cmd = call_args[0][0]
|
|
176
|
+
assert "-m" in cmd
|
|
177
|
+
assert "pytest" in cmd
|
|
178
|
+
assert call_args[1]["cwd"] == str(tmp_path)
|
|
179
|
+
assert call_args[1]["capture_output"] is True
|
|
180
|
+
|
|
181
|
+
|
|
182
|
+
# ---------------------------------------------------------------------------
|
|
183
|
+
# run_all capability
|
|
184
|
+
# ---------------------------------------------------------------------------
|
|
185
|
+
|
|
186
|
+
class TestRunAll:
|
|
187
|
+
def test_all_pass(self, tmp_path):
|
|
188
|
+
for name in ["pkg-a", "pkg-b"]:
|
|
189
|
+
d = tmp_path / "packages" / name
|
|
190
|
+
d.mkdir(parents=True)
|
|
191
|
+
(d / "pyproject.toml").write_text("")
|
|
192
|
+
|
|
193
|
+
host = _make_host(repo_root=str(tmp_path))
|
|
194
|
+
with patch("chp_adapter_ci.adapter.subprocess.run") as mock_run:
|
|
195
|
+
mock_run.return_value = _fake_run(0, "5 passed in 0.1s")
|
|
196
|
+
result = _invoke(host, "chp.adapters.ci.run_all", {})
|
|
197
|
+
|
|
198
|
+
assert result.success
|
|
199
|
+
assert result.data["ok"] is True
|
|
200
|
+
assert result.data["total_packages"] == 2
|
|
201
|
+
assert result.data["total_passed"] == 10 # 5 per package
|
|
202
|
+
assert result.data["failed_suites"] == []
|
|
203
|
+
|
|
204
|
+
def test_one_fails(self, tmp_path):
|
|
205
|
+
for name in ["pkg-ok", "pkg-broken"]:
|
|
206
|
+
d = tmp_path / "packages" / name
|
|
207
|
+
d.mkdir(parents=True)
|
|
208
|
+
(d / "pyproject.toml").write_text("")
|
|
209
|
+
|
|
210
|
+
host = _make_host(repo_root=str(tmp_path))
|
|
211
|
+
|
|
212
|
+
def side_effect(cmd, cwd=None, **kwargs):
|
|
213
|
+
if "pkg-broken" in str(cwd):
|
|
214
|
+
return _fake_run(1, "1 failed in 0.1s")
|
|
215
|
+
return _fake_run(0, "3 passed in 0.1s")
|
|
216
|
+
|
|
217
|
+
with patch("chp_adapter_ci.adapter.subprocess.run", side_effect=side_effect):
|
|
218
|
+
result = _invoke(host, "chp.adapters.ci.run_all", {})
|
|
219
|
+
|
|
220
|
+
assert result.success
|
|
221
|
+
assert result.data["ok"] is False
|
|
222
|
+
assert "pkg-broken" in result.data["failed_suites"]
|
|
223
|
+
assert "pkg-ok" not in result.data["failed_suites"]
|
|
224
|
+
|
|
225
|
+
def test_package_filter(self, tmp_path):
|
|
226
|
+
for name in ["pkg-a", "pkg-b", "pkg-c"]:
|
|
227
|
+
d = tmp_path / "packages" / name
|
|
228
|
+
d.mkdir(parents=True)
|
|
229
|
+
(d / "pyproject.toml").write_text("")
|
|
230
|
+
|
|
231
|
+
host = _make_host(repo_root=str(tmp_path))
|
|
232
|
+
with patch("chp_adapter_ci.adapter.subprocess.run") as mock_run:
|
|
233
|
+
mock_run.return_value = _fake_run()
|
|
234
|
+
result = _invoke(host, "chp.adapters.ci.run_all", {
|
|
235
|
+
"packages": ["pkg-a", "pkg-c"],
|
|
236
|
+
})
|
|
237
|
+
|
|
238
|
+
assert result.data["total_packages"] == 2
|
|
239
|
+
ran = {r["package"] for r in result.data["suites"]}
|
|
240
|
+
assert ran == {"pkg-a", "pkg-c"}
|
|
241
|
+
|
|
242
|
+
def test_empty_repo_no_packages(self, tmp_path):
|
|
243
|
+
host = _make_host(repo_root=str(tmp_path))
|
|
244
|
+
result = _invoke(host, "chp.adapters.ci.run_all", {})
|
|
245
|
+
|
|
246
|
+
assert result.success
|
|
247
|
+
assert result.data["total_packages"] == 0
|
|
248
|
+
assert result.data["ok"] is True
|
|
249
|
+
|
|
250
|
+
|
|
251
|
+
# ---------------------------------------------------------------------------
|
|
252
|
+
# Conformance: adapter.py has no violations
|
|
253
|
+
# ---------------------------------------------------------------------------
|
|
254
|
+
|
|
255
|
+
class TestCIAdapterConformance:
|
|
256
|
+
def test_no_conformance_violations(self):
|
|
257
|
+
from chp_adapter_conformance import check_source_file
|
|
258
|
+
import chp_adapter_ci.adapter as mod
|
|
259
|
+
import inspect
|
|
260
|
+
|
|
261
|
+
src_path = inspect.getfile(mod)
|
|
262
|
+
violations = check_source_file(src_path)
|
|
263
|
+
assert not violations, f"CIAdapter has conformance violations: {violations}"
|