Skip to content

Commit 3f1c744

Browse files
authored
Merge pull request #443 from Titas-Ghosh/add-protocol-conformance-fixtures-phase1
test: Add language-neutral protocol fixtures and Python runner (Phase 1)
2 parents de44e34 + cc3f74a commit 3f1c744

4 files changed

Lines changed: 294 additions & 0 deletions

File tree

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
# Protocol Conformance Fixtures (Phase 1)
2+
3+
This directory contains the phase-1 protocol conformance baseline for Python.
4+
5+
- `schema.phase1.json`: fixture document shape and supported case targets.
6+
- `python_phase1_cases.json`: initial baseline cases (report-only mode metadata).
7+
8+
Phase-1 scope:
9+
10+
- No runtime behavior changes.
11+
- Python-only execution through `tests/test_protocol_conformance.py`.
12+
- Fixture format is language-neutral to enable future cross-binding runners.
Lines changed: 105 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,105 @@
1+
{
2+
"schema_version": "1.0",
3+
"runtime": "python",
4+
"mode": "report_only",
5+
"cases": [
6+
{
7+
"id": "parse_params/simple_types_and_whitespace",
8+
"target": "parse_params",
9+
"description": "Semicolon-delimited params preserve string values and coerce literals.",
10+
"input": {
11+
"sparams": "delay=5; coeffs=[1,2,3]; label = hello world"
12+
},
13+
"expected": {
14+
"result": {
15+
"delay": 5,
16+
"coeffs": [
17+
1,
18+
2,
19+
3
20+
],
21+
"label": "hello world"
22+
}
23+
}
24+
},
25+
{
26+
"id": "parse_params/embedded_equals_not_split",
27+
"target": "parse_params",
28+
"description": "Only the first '=' is used as key/value separator.",
29+
"input": {
30+
"sparams": "url=https://example.com?a=1&b=2"
31+
},
32+
"expected": {
33+
"result": {
34+
"url": "https://example.com?a=1&b=2"
35+
}
36+
}
37+
},
38+
{
39+
"id": "initval/valid_list_sets_simtime",
40+
"target": "initval",
41+
"description": "initval sets simtime to first numeric entry and returns payload tail.",
42+
"input": {
43+
"initial_simtime": 0,
44+
"simtime_val_str": "[12.5, \"a\", 3]"
45+
},
46+
"expected": {
47+
"result": [
48+
"a",
49+
3
50+
],
51+
"simtime_after": 12.5
52+
}
53+
},
54+
{
55+
"id": "initval/invalid_input_returns_empty_and_preserves_simtime",
56+
"target": "initval",
57+
"description": "Invalid non-list input returns [] and leaves simtime unchanged.",
58+
"input": {
59+
"initial_simtime": 7,
60+
"simtime_val_str": "not_a_list"
61+
},
62+
"expected": {
63+
"result": [],
64+
"simtime_after": 7
65+
}
66+
},
67+
{
68+
"id": "write_zmq/list_payload_prepends_timestamp_without_mutation",
69+
"target": "write_zmq",
70+
"description": "write() prepends simtime+delta for list payloads but does not mutate global simtime.",
71+
"input": {
72+
"initial_simtime": 10,
73+
"delta": 2,
74+
"name": "data",
75+
"value": [
76+
1.5,
77+
2.5
78+
]
79+
},
80+
"expected": {
81+
"sent_payload": [
82+
12,
83+
1.5,
84+
2.5
85+
],
86+
"simtime_after": 10
87+
}
88+
},
89+
{
90+
"id": "write_zmq/non_list_payload_forwarded_as_is",
91+
"target": "write_zmq",
92+
"description": "Non-list payloads are forwarded as-is and simtime remains unchanged.",
93+
"input": {
94+
"initial_simtime": 10,
95+
"delta": 3,
96+
"name": "status",
97+
"value": "ok"
98+
},
99+
"expected": {
100+
"sent_payload": "ok",
101+
"simtime_after": 10
102+
}
103+
}
104+
]
105+
}
Lines changed: 62 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,62 @@
1+
{
2+
"$schema": "https://json-schema.org/draft/2020-12/schema",
3+
"title": "Concore Protocol Conformance Fixtures (Phase 1)",
4+
"description": "Language-neutral fixture format. Phase 1 executes Python-only baseline checks.",
5+
"type": "object",
6+
"required": [
7+
"schema_version",
8+
"runtime",
9+
"mode",
10+
"cases"
11+
],
12+
"properties": {
13+
"schema_version": {
14+
"type": "string"
15+
},
16+
"runtime": {
17+
"type": "string"
18+
},
19+
"mode": {
20+
"type": "string",
21+
"enum": [
22+
"report_only"
23+
]
24+
},
25+
"cases": {
26+
"type": "array",
27+
"items": {
28+
"type": "object",
29+
"required": [
30+
"id",
31+
"target",
32+
"input",
33+
"expected"
34+
],
35+
"properties": {
36+
"id": {
37+
"type": "string"
38+
},
39+
"target": {
40+
"type": "string",
41+
"enum": [
42+
"parse_params",
43+
"initval",
44+
"write_zmq"
45+
]
46+
},
47+
"description": {
48+
"type": "string"
49+
},
50+
"input": {
51+
"type": "object"
52+
},
53+
"expected": {
54+
"type": "object"
55+
}
56+
},
57+
"additionalProperties": true
58+
}
59+
}
60+
},
61+
"additionalProperties": false
62+
}

tests/test_protocol_conformance.py

Lines changed: 115 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,115 @@
1+
import json
2+
from pathlib import Path
3+
4+
import pytest
5+
6+
import concore
7+
8+
9+
FIXTURE_DIR = Path(__file__).parent / "protocol_fixtures"
10+
SCHEMA_PATH = FIXTURE_DIR / "schema.phase1.json"
11+
CASES_PATH = FIXTURE_DIR / "python_phase1_cases.json"
12+
SUPPORTED_TARGETS = {"parse_params", "initval", "write_zmq"}
13+
14+
15+
def _load_json(path):
16+
with path.open("r", encoding="utf-8") as f:
17+
return json.load(f)
18+
19+
20+
def _validate_fixture_document_shape(doc):
21+
required_top = {"schema_version", "runtime", "mode", "cases"}
22+
missing = required_top - set(doc.keys())
23+
if missing:
24+
raise AssertionError(f"Fixture document missing required top-level keys: {sorted(missing)}")
25+
if doc["runtime"] != "python":
26+
raise AssertionError(f"Phase-1 fixture runtime must be 'python', found: {doc['runtime']}")
27+
if doc["mode"] != "report_only":
28+
raise AssertionError(f"Phase-1 fixture mode must be 'report_only', found: {doc['mode']}")
29+
if not isinstance(doc["cases"], list) or not doc["cases"]:
30+
raise AssertionError("Fixture document must contain a non-empty 'cases' list")
31+
32+
for idx, case in enumerate(doc["cases"]):
33+
for key in ("id", "target", "input", "expected"):
34+
if key not in case:
35+
raise AssertionError(f"Case index {idx} missing required key '{key}'")
36+
if case["target"] not in SUPPORTED_TARGETS:
37+
raise AssertionError(
38+
f"Case '{case['id']}' has unsupported target '{case['target']}'"
39+
)
40+
41+
42+
def _run_parse_params_case(case):
43+
result = concore.parse_params(case["input"]["sparams"])
44+
assert result == case["expected"]["result"]
45+
46+
47+
def _run_initval_case(case):
48+
old_simtime = concore.simtime
49+
try:
50+
concore.simtime = case["input"]["initial_simtime"]
51+
result = concore.initval(case["input"]["simtime_val_str"])
52+
assert result == case["expected"]["result"]
53+
assert concore.simtime == case["expected"]["simtime_after"]
54+
finally:
55+
concore.simtime = old_simtime
56+
57+
58+
def _run_write_zmq_case(case):
59+
class DummyPort:
60+
def __init__(self):
61+
self.sent_payload = None
62+
63+
def send_json_with_retry(self, message):
64+
self.sent_payload = message
65+
66+
old_simtime = concore.simtime
67+
port_name = f"fixture_{case['id'].replace('/', '_')}"
68+
existing_port = concore.zmq_ports.get(port_name)
69+
dummy_port = DummyPort()
70+
71+
try:
72+
concore.simtime = case["input"]["initial_simtime"]
73+
concore.zmq_ports[port_name] = dummy_port
74+
concore.write(
75+
port_name,
76+
case["input"]["name"],
77+
case["input"]["value"],
78+
delta=case["input"]["delta"],
79+
)
80+
assert dummy_port.sent_payload == case["expected"]["sent_payload"]
81+
assert concore.simtime == case["expected"]["simtime_after"]
82+
finally:
83+
concore.simtime = old_simtime
84+
if existing_port is None:
85+
concore.zmq_ports.pop(port_name, None)
86+
else:
87+
concore.zmq_ports[port_name] = existing_port
88+
89+
90+
def _run_case(case):
91+
if case["target"] == "parse_params":
92+
_run_parse_params_case(case)
93+
elif case["target"] == "initval":
94+
_run_initval_case(case)
95+
elif case["target"] == "write_zmq":
96+
_run_write_zmq_case(case)
97+
else:
98+
raise AssertionError(f"Unsupported target: {case['target']}")
99+
100+
101+
def _load_cases():
102+
doc = _load_json(CASES_PATH)
103+
_validate_fixture_document_shape(doc)
104+
return doc["cases"]
105+
106+
107+
def test_phase1_schema_file_present_and_basic_shape():
108+
schema = _load_json(SCHEMA_PATH)
109+
assert schema["title"] == "Concore Protocol Conformance Fixtures (Phase 1)"
110+
assert "cases" in schema["properties"]
111+
112+
113+
@pytest.mark.parametrize("case", _load_cases(), ids=lambda case: case["id"])
114+
def test_phase1_python_protocol_conformance(case):
115+
_run_case(case)

0 commit comments

Comments
 (0)