Skip to content

Commit 7f1b76d

Browse files
committed
Add vector runner and shared vector fixtures for legacy calculator tests
1 parent 7cfda26 commit 7f1b76d

5 files changed

Lines changed: 202 additions & 18 deletions

File tree

README.md

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -913,17 +913,29 @@ The suite now includes proof-style parity tests that run the same function behav
913913

914914
Fixture sources:
915915
- `fixtures/java/simple/LegacyCalculator.java`
916+
- `fixtures/java/simple/LegacyCalculatorVectorRunner.java`
916917
- `fixtures/expected_python/legacy_calculator.py`
918+
- `fixtures/vectors/legacy_calculator_vectors.json`
919+
- `fixtures/vectors/legacy_calculator_vectors.csv`
920+
921+
### Shared Vector Baseline (Single Source Of Truth)
922+
923+
| Asset | Runtime Consumer | Purpose | Status |
924+
|---|---|---|---|
925+
| `legacy_calculator_vectors.json` | Python parity tests + Java vector runner | Canonical vector source (id, input, expected) | Implemented |
926+
| `legacy_calculator_vectors.csv` | Optional import/export interoperability | Spreadsheet-friendly mirror for manual review | Implemented |
927+
| `LegacyCalculatorVectorRunner.java` | Java runtime | Reads shared JSON vectors and evaluates legacy function | Implemented |
928+
| `test_legacy_java_python_equivalence.py` | pytest | Parameterized cross-runtime parity assertions | Implemented |
917929

918930
### Tools That Already Support This Testing Pattern
919931

920932
| Tool | How It Helps With Java-to-Python Parity | Typical Use |
921933
|---|---|---|
922-
| `pytest` parameterized tests | Reuse the same vectors for both runtimes | Core parity assertions |
923-
| JUnit 5 parameterized tests | Capture legacy Java oracle outputs | Legacy baseline generation |
924-
| ApprovalTests | Golden-master snapshot comparisons | Regression lock for legacy outputs |
925-
| JSON/CSV test vectors | Runtime-agnostic shared inputs/outputs | Single source of truth for parity data |
926-
| Testcontainers | Reproducible Java runtime for parity execution | Stable local/CI runtime (optional) |
934+
| `pytest` parameterized tests | Reuse the same vectors for both runtimes | Core parity assertions (implemented) |
935+
| JUnit 5 parameterized tests | Capture legacy Java oracle outputs | Legacy baseline generation (recommended next) |
936+
| ApprovalTests | Golden-master snapshot comparisons | Regression lock for legacy outputs (recommended next) |
937+
| JSON/CSV test vectors | Runtime-agnostic shared inputs/outputs | Single source of truth for parity data (implemented) |
938+
| Testcontainers | Reproducible Java runtime execution | Stable local runtime parity in isolated containers (recommended next) |
927939

928940
Practical recommendation: keep a shared vector file and run both Java and Python against it, treating Java output as the initial oracle during migration.
929941

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
import java.nio.file.Files;
2+
import java.nio.file.Path;
3+
import java.util.regex.Matcher;
4+
import java.util.regex.Pattern;
5+
6+
public class LegacyCalculatorVectorRunner {
7+
private static final Pattern VECTOR_PATTERN = Pattern.compile(
8+
"\\{\\s*\"id\"\\s*:\\s*\"([^\"]+)\"\\s*,\\s*\"input\"\\s*:\\s*\\{\\s*\"base\"\\s*:\\s*(-?\\d+)\\s*,\\s*\"multiplier\"\\s*:\\s*(-?\\d+)\\s*,\\s*\"premium\"\\s*:\\s*(true|false)\\s*\\}\\s*,\\s*\"expected\"\\s*:\\s*(-?\\d+)\\s*\\}",
9+
Pattern.DOTALL
10+
);
11+
12+
public static void main(String[] args) throws Exception {
13+
if (args.length != 1) {
14+
System.err.println("Usage: java LegacyCalculatorVectorRunner <vectors.json>");
15+
System.exit(1);
16+
}
17+
18+
String content = Files.readString(Path.of(args[0]));
19+
Matcher matcher = VECTOR_PATTERN.matcher(content);
20+
21+
while (matcher.find()) {
22+
String id = matcher.group(1);
23+
int base = Integer.parseInt(matcher.group(2));
24+
int multiplier = Integer.parseInt(matcher.group(3));
25+
boolean premium = Boolean.parseBoolean(matcher.group(4));
26+
int expected = Integer.parseInt(matcher.group(5));
27+
int actual = LegacyCalculator.calculateScore(base, multiplier, premium);
28+
29+
// Output format: id,actual,expected
30+
System.out.println(id + "," + actual + "," + expected);
31+
}
32+
}
33+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
id,base,multiplier,premium,expected
2+
v1,5,10,false,50
3+
v2,5,10,true,75
4+
v3,8,12,true,100
5+
v4,1,1,false,1
6+
v5,-4,10,false,0
7+
v6,0,999,true,25
Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
[
2+
{
3+
"id": "v1",
4+
"input": {
5+
"base": 5,
6+
"multiplier": 10,
7+
"premium": false
8+
},
9+
"expected": 50
10+
},
11+
{
12+
"id": "v2",
13+
"input": {
14+
"base": 5,
15+
"multiplier": 10,
16+
"premium": true
17+
},
18+
"expected": 75
19+
},
20+
{
21+
"id": "v3",
22+
"input": {
23+
"base": 8,
24+
"multiplier": 12,
25+
"premium": true
26+
},
27+
"expected": 100
28+
},
29+
{
30+
"id": "v4",
31+
"input": {
32+
"base": 1,
33+
"multiplier": 1,
34+
"premium": false
35+
},
36+
"expected": 1
37+
},
38+
{
39+
"id": "v5",
40+
"input": {
41+
"base": -4,
42+
"multiplier": 10,
43+
"premium": false
44+
},
45+
"expected": 0
46+
},
47+
{
48+
"id": "v6",
49+
"input": {
50+
"base": 0,
51+
"multiplier": 999,
52+
"premium": true
53+
},
54+
"expected": 25
55+
}
56+
]

tests/correctness/test_legacy_java_python_equivalence.py

Lines changed: 89 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import importlib.util
2+
import json
23
from pathlib import Path
34
import shutil
45
import subprocess
@@ -9,20 +10,21 @@
910
pytestmark = pytest.mark.correctness
1011

1112

12-
TEST_VECTORS: list[tuple[int, int, bool, int]] = [
13-
(5, 10, False, 50),
14-
(5, 10, True, 75),
15-
(8, 12, True, 100),
16-
(1, 1, False, 1),
17-
(-4, 10, False, 0),
18-
(0, 999, True, 25),
19-
]
13+
def _load_shared_vectors() -> list[dict]:
14+
vectors_path = _repo_root() / "fixtures" / "vectors" / "legacy_calculator_vectors.json"
15+
with open(vectors_path, encoding="utf-8") as handle:
16+
payload = json.load(handle)
17+
assert isinstance(payload, list)
18+
return payload
2019

2120

2221
def _repo_root() -> Path:
2322
return Path(__file__).resolve().parents[2]
2423

2524

25+
SHARED_VECTORS = _load_shared_vectors()
26+
27+
2628
def _read_fixture(relative_path: str) -> str:
2729
return (_repo_root() / relative_path).read_text(encoding="utf-8")
2830

@@ -43,11 +45,14 @@ def java_legacy_runner(tmp_path_factory):
4345

4446
workdir = tmp_path_factory.mktemp("legacy_java_calc")
4547
java_source = _read_fixture("fixtures/java/simple/LegacyCalculator.java")
48+
java_vector_runner = _read_fixture("fixtures/java/simple/LegacyCalculatorVectorRunner.java")
4649
java_path = workdir / "LegacyCalculator.java"
50+
java_runner_path = workdir / "LegacyCalculatorVectorRunner.java"
4751
java_path.write_text(java_source, encoding="utf-8")
52+
java_runner_path.write_text(java_vector_runner, encoding="utf-8")
4853

4954
compile_proc = subprocess.run(
50-
["javac", str(java_path)],
55+
["javac", str(java_path), str(java_runner_path)],
5156
cwd=workdir,
5257
check=False,
5358
capture_output=True,
@@ -71,20 +76,91 @@ def _run(base: int, multiplier: int, premium: bool) -> int:
7176
return _run
7277

7378

79+
@pytest.fixture(scope="module")
80+
def java_vector_batch_runner(tmp_path_factory):
81+
if not shutil.which("javac") or not shutil.which("java"):
82+
pytest.skip("Java toolchain not available (requires javac and java)")
83+
84+
workdir = tmp_path_factory.mktemp("legacy_java_vector_batch")
85+
java_source = _read_fixture("fixtures/java/simple/LegacyCalculator.java")
86+
java_vector_runner = _read_fixture("fixtures/java/simple/LegacyCalculatorVectorRunner.java")
87+
java_path = workdir / "LegacyCalculator.java"
88+
java_runner_path = workdir / "LegacyCalculatorVectorRunner.java"
89+
java_path.write_text(java_source, encoding="utf-8")
90+
java_runner_path.write_text(java_vector_runner, encoding="utf-8")
91+
92+
compile_proc = subprocess.run(
93+
["javac", str(java_path), str(java_runner_path)],
94+
cwd=workdir,
95+
check=False,
96+
capture_output=True,
97+
text=True,
98+
)
99+
if compile_proc.returncode != 0:
100+
pytest.fail(f"Failed compiling Java vector runner fixture: {compile_proc.stderr}")
101+
102+
def _run(vectors_path: Path) -> dict[str, tuple[int, int]]:
103+
run_proc = subprocess.run(
104+
["java", "-cp", str(workdir), "LegacyCalculatorVectorRunner", str(vectors_path)],
105+
cwd=workdir,
106+
check=False,
107+
capture_output=True,
108+
text=True,
109+
)
110+
if run_proc.returncode != 0:
111+
pytest.fail(f"Legacy Java vector batch execution failed: {run_proc.stderr}")
112+
113+
outputs: dict[str, tuple[int, int]] = {}
114+
for line in run_proc.stdout.splitlines():
115+
line = line.strip()
116+
if not line:
117+
continue
118+
case_id, actual, expected = line.split(",", 2)
119+
outputs[case_id] = (int(actual), int(expected))
120+
return outputs
121+
122+
return _run
123+
124+
74125
def test_legacy_java_fixture_expected_values(java_legacy_runner):
75-
for base, multiplier, premium, expected in TEST_VECTORS:
126+
for vector in SHARED_VECTORS:
127+
base = int(vector["input"]["base"])
128+
multiplier = int(vector["input"]["multiplier"])
129+
premium = bool(vector["input"]["premium"])
130+
expected = int(vector["expected"])
76131
assert java_legacy_runner(base, multiplier, premium) == expected
77132

78133

79134
def test_python_fixture_expected_values():
80135
calculate_score = _load_python_calculator()
81-
for base, multiplier, premium, expected in TEST_VECTORS:
136+
for vector in SHARED_VECTORS:
137+
base = int(vector["input"]["base"])
138+
multiplier = int(vector["input"]["multiplier"])
139+
premium = bool(vector["input"]["premium"])
140+
expected = int(vector["expected"])
82141
assert calculate_score(base, multiplier, premium) == expected
83142

84143

85144
def test_python_matches_legacy_java_outputs(java_legacy_runner):
86145
calculate_score = _load_python_calculator()
87-
for base, multiplier, premium, _expected in TEST_VECTORS:
146+
for vector in SHARED_VECTORS:
147+
base = int(vector["input"]["base"])
148+
multiplier = int(vector["input"]["multiplier"])
149+
premium = bool(vector["input"]["premium"])
88150
legacy_output = java_legacy_runner(base, multiplier, premium)
89151
python_output = calculate_score(base, multiplier, premium)
90-
assert python_output == legacy_output
152+
assert python_output == legacy_output
153+
154+
155+
def test_java_batch_runner_reads_shared_json_vectors(java_vector_batch_runner):
156+
vectors_path = _repo_root() / "fixtures" / "vectors" / "legacy_calculator_vectors.json"
157+
outputs = java_vector_batch_runner(vectors_path)
158+
assert outputs, "Expected java batch runner to produce vector outputs"
159+
assert len(outputs) == len(SHARED_VECTORS)
160+
for vector in SHARED_VECTORS:
161+
case_id = str(vector["id"])
162+
expected = int(vector["expected"])
163+
assert case_id in outputs
164+
actual, runner_expected = outputs[case_id]
165+
assert runner_expected == expected
166+
assert actual == expected

0 commit comments

Comments
 (0)