spck-conformance 0.1.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.
- spck_conformance-0.1.0/PKG-INFO +96 -0
- spck_conformance-0.1.0/README.md +80 -0
- spck_conformance-0.1.0/pyproject.toml +38 -0
- spck_conformance-0.1.0/setup.cfg +4 -0
- spck_conformance-0.1.0/spck_conformance/__init__.py +2 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/checks/engine.py +157 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/checks/merchant.py +348 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/checks/merchant_checks.py +764 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/checkout-lifecycle.json +918 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/discount-consent-identity.json +627 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/discovery-negotiation.json +627 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/errors-validation-security.json +410 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/fulfillment.json +557 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/order.json +356 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-11/payment.json +626 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/checkout-lifecycle.json +75 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/discount-consent-identity.json +519 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/discovery-negotiation.json +50 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/errors-validation-security.json +322 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/fulfillment.json +47 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/order.json +280 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-01-23/payment.json +518 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/cart.json +47 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/catalog.json +54 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/checkout-lifecycle.json +75 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/discounts-consent.json +52 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/discovery.json +90 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/error-envelope.json +53 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/fulfillment.json +47 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/identity-linking.json +854 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/negotiation-errors.json +62 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/order.json +49 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/payment.json +51 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/signals-attribution-eligibility.json +35 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/signatures.json +547 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/requirements/2026-04-08/totals.json +23 -0
- spck_conformance-0.1.0/spck_conformance/_bundle/conformance/selfcheck/verdict_gate.py +160 -0
- spck_conformance-0.1.0/spck_conformance/cli.py +26 -0
- spck_conformance-0.1.0/spck_conformance.egg-info/PKG-INFO +96 -0
- spck_conformance-0.1.0/spck_conformance.egg-info/SOURCES.txt +42 -0
- spck_conformance-0.1.0/spck_conformance.egg-info/dependency_links.txt +1 -0
- spck_conformance-0.1.0/spck_conformance.egg-info/entry_points.txt +2 -0
- spck_conformance-0.1.0/spck_conformance.egg-info/requires.txt +1 -0
- spck_conformance-0.1.0/spck_conformance.egg-info/top_level.txt +1 -0
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: spck-conformance
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Unofficial, capability-adaptive conformance runner for the Universal Commerce Protocol (UCP)
|
|
5
|
+
Author: spck.dev
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://spck.dev
|
|
8
|
+
Project-URL: Source, https://github.com/vishkaty/ucp-conformance
|
|
9
|
+
Keywords: ucp,universal-commerce-protocol,conformance,agentic-commerce
|
|
10
|
+
Classifier: Development Status :: 4 - Beta
|
|
11
|
+
Classifier: Programming Language :: Python :: 3
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Requires-Python: >=3.9
|
|
14
|
+
Description-Content-Type: text/markdown
|
|
15
|
+
Requires-Dist: certifi>=2023.7.22
|
|
16
|
+
|
|
17
|
+
# spck-conformance
|
|
18
|
+
|
|
19
|
+
Unofficial, capability-adaptive conformance runner for the **Universal Commerce
|
|
20
|
+
Protocol (UCP)**. Point it at any UCP server and get an honest, capability-scoped
|
|
21
|
+
report — it runs only the checks that apply to what the server declares, and every
|
|
22
|
+
check is kill-rate-validated (proven to catch its own defects) before it ships.
|
|
23
|
+
|
|
24
|
+
> Independent project. Not affiliated with, endorsed by, or a substitute for the
|
|
25
|
+
> official UCP conformance suite. It reports only the checks it actually runs.
|
|
26
|
+
|
|
27
|
+
## Install
|
|
28
|
+
|
|
29
|
+
```bash
|
|
30
|
+
pip install spck-conformance
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
No third-party dependencies (Python ≥ 3.9, stdlib only).
|
|
34
|
+
|
|
35
|
+
## Use
|
|
36
|
+
|
|
37
|
+
```bash
|
|
38
|
+
spck-conformance --server https://api.example.com \
|
|
39
|
+
[--config merchant.json] [--json] [--junit report.xml]
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
### Quickstart (30 seconds)
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
# 1. point it at your server — no config needed for the discovery + structure checks
|
|
46
|
+
spck-conformance --server https://api.example.com
|
|
47
|
+
|
|
48
|
+
# 2. scaffold a config tailored to YOUR server's declared capabilities
|
|
49
|
+
spck-conformance --server https://api.example.com --init merchant.json
|
|
50
|
+
# -> fill in the FILL_ME placeholders (a product id, discount code, payment token…)
|
|
51
|
+
|
|
52
|
+
# 3. re-run with the config to unlock the data-dependent checks
|
|
53
|
+
spck-conformance --server https://api.example.com --config merchant.json
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
On a deviation the report shows **expected (the requirement) vs observed (your actual
|
|
57
|
+
response)** so you can fix it directly, and the footer's **Next steps** tells you how to
|
|
58
|
+
unlock any `not-tested` checks.
|
|
59
|
+
|
|
60
|
+
### Use in CI (GitHub Action)
|
|
61
|
+
|
|
62
|
+
```yaml
|
|
63
|
+
# .github/workflows/ucp.yml
|
|
64
|
+
jobs:
|
|
65
|
+
conformance:
|
|
66
|
+
runs-on: ubuntu-latest
|
|
67
|
+
steps:
|
|
68
|
+
- uses: vishkaty/ucp-conformance@main
|
|
69
|
+
with:
|
|
70
|
+
server: https://api.example.com
|
|
71
|
+
config: merchant.json # optional
|
|
72
|
+
# fail-on-deviation: false # report-only mode
|
|
73
|
+
```
|
|
74
|
+
|
|
75
|
+
The job fails on any MUST deviation and writes a JUnit report (`ucp-conformance.xml`)
|
|
76
|
+
your CI can display as a test run.
|
|
77
|
+
|
|
78
|
+
- **`--config`** — optional JSON supplying data-dependent inputs (product id, discount
|
|
79
|
+
codes, a succeeding/failing payment, an out-of-stock id). Without it, those checks
|
|
80
|
+
are honestly `not-tested` rather than silently passed.
|
|
81
|
+
- **`--json`** — full machine-readable report; each check cites its normative clause
|
|
82
|
+
(id, verbatim text, spec source).
|
|
83
|
+
- **`--junit FILE`** — JUnit XML for CI (deviation → `<failure>`, not-applicable /
|
|
84
|
+
not-tested → `<skipped>`).
|
|
85
|
+
- **Exit code** — `2` if any MUST deviates, else `0` (partial coverage is not a failure).
|
|
86
|
+
|
|
87
|
+
## What it checks
|
|
88
|
+
|
|
89
|
+
Discovery + profile structure, checkout lifecycle, idempotency, validation,
|
|
90
|
+
fulfillment, order completion, payment-credential handling, discounts, catalog
|
|
91
|
+
(search/lookup), and cart — scoped to the capabilities the target declares. The
|
|
92
|
+
profile-schema check requires the native `ucp-schema` validator (not shipped in the
|
|
93
|
+
wheel), so it reports `not-tested` here; run it from the source repo for full fidelity.
|
|
94
|
+
|
|
95
|
+
Source, methodology, and the self-validating CI harness:
|
|
96
|
+
<https://github.com/vishkaty/ucp-conformance>.
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
# spck-conformance
|
|
2
|
+
|
|
3
|
+
Unofficial, capability-adaptive conformance runner for the **Universal Commerce
|
|
4
|
+
Protocol (UCP)**. Point it at any UCP server and get an honest, capability-scoped
|
|
5
|
+
report — it runs only the checks that apply to what the server declares, and every
|
|
6
|
+
check is kill-rate-validated (proven to catch its own defects) before it ships.
|
|
7
|
+
|
|
8
|
+
> Independent project. Not affiliated with, endorsed by, or a substitute for the
|
|
9
|
+
> official UCP conformance suite. It reports only the checks it actually runs.
|
|
10
|
+
|
|
11
|
+
## Install
|
|
12
|
+
|
|
13
|
+
```bash
|
|
14
|
+
pip install spck-conformance
|
|
15
|
+
```
|
|
16
|
+
|
|
17
|
+
No third-party dependencies (Python ≥ 3.9, stdlib only).
|
|
18
|
+
|
|
19
|
+
## Use
|
|
20
|
+
|
|
21
|
+
```bash
|
|
22
|
+
spck-conformance --server https://api.example.com \
|
|
23
|
+
[--config merchant.json] [--json] [--junit report.xml]
|
|
24
|
+
```
|
|
25
|
+
|
|
26
|
+
### Quickstart (30 seconds)
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
# 1. point it at your server — no config needed for the discovery + structure checks
|
|
30
|
+
spck-conformance --server https://api.example.com
|
|
31
|
+
|
|
32
|
+
# 2. scaffold a config tailored to YOUR server's declared capabilities
|
|
33
|
+
spck-conformance --server https://api.example.com --init merchant.json
|
|
34
|
+
# -> fill in the FILL_ME placeholders (a product id, discount code, payment token…)
|
|
35
|
+
|
|
36
|
+
# 3. re-run with the config to unlock the data-dependent checks
|
|
37
|
+
spck-conformance --server https://api.example.com --config merchant.json
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
On a deviation the report shows **expected (the requirement) vs observed (your actual
|
|
41
|
+
response)** so you can fix it directly, and the footer's **Next steps** tells you how to
|
|
42
|
+
unlock any `not-tested` checks.
|
|
43
|
+
|
|
44
|
+
### Use in CI (GitHub Action)
|
|
45
|
+
|
|
46
|
+
```yaml
|
|
47
|
+
# .github/workflows/ucp.yml
|
|
48
|
+
jobs:
|
|
49
|
+
conformance:
|
|
50
|
+
runs-on: ubuntu-latest
|
|
51
|
+
steps:
|
|
52
|
+
- uses: vishkaty/ucp-conformance@main
|
|
53
|
+
with:
|
|
54
|
+
server: https://api.example.com
|
|
55
|
+
config: merchant.json # optional
|
|
56
|
+
# fail-on-deviation: false # report-only mode
|
|
57
|
+
```
|
|
58
|
+
|
|
59
|
+
The job fails on any MUST deviation and writes a JUnit report (`ucp-conformance.xml`)
|
|
60
|
+
your CI can display as a test run.
|
|
61
|
+
|
|
62
|
+
- **`--config`** — optional JSON supplying data-dependent inputs (product id, discount
|
|
63
|
+
codes, a succeeding/failing payment, an out-of-stock id). Without it, those checks
|
|
64
|
+
are honestly `not-tested` rather than silently passed.
|
|
65
|
+
- **`--json`** — full machine-readable report; each check cites its normative clause
|
|
66
|
+
(id, verbatim text, spec source).
|
|
67
|
+
- **`--junit FILE`** — JUnit XML for CI (deviation → `<failure>`, not-applicable /
|
|
68
|
+
not-tested → `<skipped>`).
|
|
69
|
+
- **Exit code** — `2` if any MUST deviates, else `0` (partial coverage is not a failure).
|
|
70
|
+
|
|
71
|
+
## What it checks
|
|
72
|
+
|
|
73
|
+
Discovery + profile structure, checkout lifecycle, idempotency, validation,
|
|
74
|
+
fulfillment, order completion, payment-credential handling, discounts, catalog
|
|
75
|
+
(search/lookup), and cart — scoped to the capabilities the target declares. The
|
|
76
|
+
profile-schema check requires the native `ucp-schema` validator (not shipped in the
|
|
77
|
+
wheel), so it reports `not-tested` here; run it from the source repo for full fidelity.
|
|
78
|
+
|
|
79
|
+
Source, methodology, and the self-validating CI harness:
|
|
80
|
+
<https://github.com/vishkaty/ucp-conformance>.
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["setuptools>=61"]
|
|
3
|
+
build-backend = "setuptools.build_meta"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "spck-conformance"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Unofficial, capability-adaptive conformance runner for the Universal Commerce Protocol (UCP)"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
requires-python = ">=3.9"
|
|
11
|
+
license = {text = "MIT"}
|
|
12
|
+
authors = [{name = "spck.dev"}]
|
|
13
|
+
keywords = ["ucp", "universal-commerce-protocol", "conformance", "agentic-commerce"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 4 - Beta",
|
|
16
|
+
"Programming Language :: Python :: 3",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
]
|
|
19
|
+
# certifi provides a CA bundle so TLS verification works on Pythons that ship none
|
|
20
|
+
# (common on macOS python.org builds). Everything else is stdlib.
|
|
21
|
+
dependencies = ["certifi>=2023.7.22"]
|
|
22
|
+
|
|
23
|
+
[project.urls]
|
|
24
|
+
Homepage = "https://spck.dev"
|
|
25
|
+
Source = "https://github.com/vishkaty/ucp-conformance"
|
|
26
|
+
|
|
27
|
+
[project.scripts]
|
|
28
|
+
spck-conformance = "spck_conformance.cli:main"
|
|
29
|
+
|
|
30
|
+
[tool.setuptools]
|
|
31
|
+
packages = ["spck_conformance"]
|
|
32
|
+
|
|
33
|
+
[tool.setuptools.package-data]
|
|
34
|
+
spck_conformance = [
|
|
35
|
+
"_bundle/conformance/checks/*.py",
|
|
36
|
+
"_bundle/conformance/selfcheck/*.py",
|
|
37
|
+
"_bundle/conformance/requirements/*/*.json",
|
|
38
|
+
]
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
#!/usr/bin/env python3
|
|
2
|
+
"""
|
|
3
|
+
engine.py — Phase 1 register-driven conformance check engine.
|
|
4
|
+
|
|
5
|
+
A check cites the register requirement(s) it verifies, evaluates a real server
|
|
6
|
+
response, and declares the mutations that MUST break it. Before any check counts
|
|
7
|
+
toward a verdict, the engine self-validates it: the check must pass on the clean
|
|
8
|
+
response AND fail on every declared mutant (kill-rate). A check that isn't
|
|
9
|
+
mutation-safe is reported kill_safe=False, which the verdict gate downgrades to
|
|
10
|
+
inconclusive (so it can never produce a green). Results feed verdict_gate for an
|
|
11
|
+
honest, coverage-gated report.
|
|
12
|
+
|
|
13
|
+
Mutations here operate on the CAPTURED response (deterministic golden+mutation),
|
|
14
|
+
which works for stateful checks too — we mutate the final response under test.
|
|
15
|
+
The live mutation_proxy remains for integration/timing/replay cases.
|
|
16
|
+
"""
|
|
17
|
+
import json, sys, ssl, urllib.request, urllib.error, pathlib, glob
|
|
18
|
+
sys.path.insert(0, str(pathlib.Path(__file__).resolve().parents[1] / "selfcheck"))
|
|
19
|
+
from verdict_gate import CheckResult, aggregate, CLEAN, DEVIATION, INCONCLUSIVE # noqa: E402
|
|
20
|
+
|
|
21
|
+
ROOT = pathlib.Path(__file__).resolve().parents[2]
|
|
22
|
+
REQ_DIR = ROOT / "conformance" / "requirements"
|
|
23
|
+
|
|
24
|
+
# ---- TLS: use certifi's CA bundle if available (many Pythons ship no bundle, so a
|
|
25
|
+
# plain urlopen against https fails with CERTIFICATE_VERIFY_FAILED). --insecure skips
|
|
26
|
+
# verification entirely (testing only). -------------------------------------------
|
|
27
|
+
def _make_ssl_context(insecure=False):
|
|
28
|
+
if insecure:
|
|
29
|
+
ctx = ssl.create_default_context()
|
|
30
|
+
ctx.check_hostname = False
|
|
31
|
+
ctx.verify_mode = ssl.CERT_NONE
|
|
32
|
+
return ctx
|
|
33
|
+
try:
|
|
34
|
+
import certifi
|
|
35
|
+
return ssl.create_default_context(cafile=certifi.where())
|
|
36
|
+
except Exception:
|
|
37
|
+
return ssl.create_default_context()
|
|
38
|
+
|
|
39
|
+
INSECURE = False
|
|
40
|
+
_SSL_CTX = _make_ssl_context()
|
|
41
|
+
|
|
42
|
+
def set_insecure(v):
|
|
43
|
+
"""Toggle TLS verification (used by the --insecure CLI flag)."""
|
|
44
|
+
global INSECURE, _SSL_CTX
|
|
45
|
+
INSECURE = bool(v)
|
|
46
|
+
_SSL_CTX = _make_ssl_context(INSECURE)
|
|
47
|
+
|
|
48
|
+
class TLSError(Exception):
|
|
49
|
+
"""Raised on a TLS certificate verification failure, with a friendly hint."""
|
|
50
|
+
|
|
51
|
+
# ---- response capture -------------------------------------------------------
|
|
52
|
+
class Resp:
|
|
53
|
+
def __init__(self, status, headers, body):
|
|
54
|
+
self.status, self.headers, self.body = status, dict(headers), body
|
|
55
|
+
try: self.json = json.loads(body)
|
|
56
|
+
except Exception: self.json = None
|
|
57
|
+
def clone(self):
|
|
58
|
+
return Resp(self.status, dict(self.headers), self.body)
|
|
59
|
+
|
|
60
|
+
def fetch(base, path, method="GET", body=None, headers=None):
|
|
61
|
+
data = json.dumps(body).encode() if body is not None else None
|
|
62
|
+
h = {"Content-Type": "application/json", **(headers or {})}
|
|
63
|
+
req = urllib.request.Request(base.rstrip("/") + path, data=data, method=method, headers=h)
|
|
64
|
+
try:
|
|
65
|
+
with urllib.request.urlopen(req, context=_SSL_CTX) as r:
|
|
66
|
+
return Resp(r.status, r.getheaders(), r.read())
|
|
67
|
+
except urllib.error.HTTPError as e:
|
|
68
|
+
return Resp(e.code, e.headers.items(), e.read())
|
|
69
|
+
except Exception as e:
|
|
70
|
+
return Resp(0, {}, str(e).encode())
|
|
71
|
+
|
|
72
|
+
# ---- mutations on a captured response (defect injection) --------------------
|
|
73
|
+
def _reparse(r):
|
|
74
|
+
r.body = json.dumps(r.json).encode() if r.json is not None else r.body
|
|
75
|
+
return r
|
|
76
|
+
def mutate(resp, mut):
|
|
77
|
+
r = resp.clone()
|
|
78
|
+
name, _, arg = mut.partition(":")
|
|
79
|
+
if name == "status": r.status = int(arg); return r
|
|
80
|
+
if name == "empty": r.body = b""; r.json = None; return r
|
|
81
|
+
if name == "corrupt-json": r.body = (r.body or b"{}")[:-1]; r.json = None; return r
|
|
82
|
+
if name == "drop" and r.json is not None:
|
|
83
|
+
cur = r.json; parts = arg.split(".")
|
|
84
|
+
for p in parts[:-1]:
|
|
85
|
+
if isinstance(cur, list) and p.isdigit() and int(p) < len(cur): cur = cur[int(p)]
|
|
86
|
+
elif isinstance(cur, dict): cur = cur.get(p)
|
|
87
|
+
else: cur = None
|
|
88
|
+
if cur is None: break
|
|
89
|
+
last = parts[-1]
|
|
90
|
+
if isinstance(cur, dict): cur.pop(last, None)
|
|
91
|
+
elif isinstance(cur, list) and last.isdigit() and int(last) < len(cur): cur.pop(int(last))
|
|
92
|
+
return _reparse(r)
|
|
93
|
+
if name == "set" and r.json is not None:
|
|
94
|
+
path, _, v = arg.partition("="); val = json.loads(v)
|
|
95
|
+
cur = r.json; parts = path.split(".")
|
|
96
|
+
for p in parts[:-1]:
|
|
97
|
+
if isinstance(cur, list) and p.isdigit() and int(p) < len(cur): cur = cur[int(p)]
|
|
98
|
+
elif isinstance(cur, dict): cur = cur.get(p)
|
|
99
|
+
else: cur = None
|
|
100
|
+
if cur is None: break
|
|
101
|
+
last = parts[-1]
|
|
102
|
+
if isinstance(cur, dict): cur[last] = val
|
|
103
|
+
elif isinstance(cur, list) and last.isdigit() and int(last) < len(cur): cur[int(last)] = val
|
|
104
|
+
return _reparse(r)
|
|
105
|
+
if name == "hset": # set/replace a response header (case-insensitive): hset:Content-Type=text/plain
|
|
106
|
+
k, _, v = arg.partition("=")
|
|
107
|
+
for hk in [h for h in r.headers if h.lower() == k.lower()]: r.headers.pop(hk)
|
|
108
|
+
r.headers[k] = v; return r
|
|
109
|
+
if name == "hdrop": # remove a response header (case-insensitive)
|
|
110
|
+
for hk in [h for h in r.headers if h.lower() == arg.lower()]: r.headers.pop(hk)
|
|
111
|
+
return r
|
|
112
|
+
return r # no-op (mutation not applicable)
|
|
113
|
+
|
|
114
|
+
# ---- check spec -------------------------------------------------------------
|
|
115
|
+
class Check:
|
|
116
|
+
def __init__(self, cid, req_ids, keyword, fetch_fn, predicate, mutations):
|
|
117
|
+
self.id, self.req_ids, self.keyword = cid, req_ids, keyword
|
|
118
|
+
self.fetch_fn, self.predicate, self.mutations = fetch_fn, predicate, mutations
|
|
119
|
+
|
|
120
|
+
def run_check(chk, base):
|
|
121
|
+
"""Returns (results: list[CheckResult], detail: dict). kill_safe is computed by
|
|
122
|
+
mutation: clean must pass, every declared mutant must deviate."""
|
|
123
|
+
try:
|
|
124
|
+
golden = chk.fetch_fn(base)
|
|
125
|
+
except Exception as e:
|
|
126
|
+
kill_safe = False
|
|
127
|
+
res = [CheckResult(rid, chk.keyword, INCONCLUSIVE, kill_safe) for rid in chk.req_ids]
|
|
128
|
+
return res, {"clean": "error:" + str(e), "kills": "", "kill_safe": kill_safe}
|
|
129
|
+
clean = chk.predicate(golden)
|
|
130
|
+
kills, survivors = 0, []
|
|
131
|
+
for m in chk.mutations:
|
|
132
|
+
if chk.predicate(mutate(golden, m)) == DEVIATION: kills += 1
|
|
133
|
+
else: survivors.append(m)
|
|
134
|
+
kill_safe = (clean == CLEAN and not survivors)
|
|
135
|
+
status = clean if kill_safe else (clean if clean == DEVIATION else INCONCLUSIVE)
|
|
136
|
+
res = [CheckResult(rid, chk.keyword, status, kill_safe) for rid in chk.req_ids]
|
|
137
|
+
return res, {"clean": clean, "kills": f"{kills}/{len(chk.mutations)}",
|
|
138
|
+
"kill_safe": kill_safe, "survivors": survivors}
|
|
139
|
+
|
|
140
|
+
# ---- inscope MUSTs from the register (coverage denominator) -----------------
|
|
141
|
+
def inscope_musts(version, transports=("rest", "any")):
|
|
142
|
+
ids = set()
|
|
143
|
+
for f in glob.glob(str(REQ_DIR / version / "*.json")):
|
|
144
|
+
for r in json.load(open(f)).get("rows", []):
|
|
145
|
+
if r["keyword"] in ("MUST", "MUST NOT") and r["testability"] == "testable" \
|
|
146
|
+
and any(t in transports for t in r.get("transport", [])):
|
|
147
|
+
ids.add(r["id"])
|
|
148
|
+
return ids
|
|
149
|
+
|
|
150
|
+
def run_report(checks, base, version, scope_stamp, disclaimer, transports=("rest", "any")):
|
|
151
|
+
all_results, details = [], []
|
|
152
|
+
for chk in checks:
|
|
153
|
+
res, det = run_check(chk, base)
|
|
154
|
+
all_results += res
|
|
155
|
+
details.append((chk, det))
|
|
156
|
+
rep = aggregate(all_results, inscope_musts(version, transports), scope_stamp, disclaimer)
|
|
157
|
+
return rep, details
|