jevals 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.
- jevals-0.1.0/.gitignore +26 -0
- jevals-0.1.0/PKG-INFO +61 -0
- jevals-0.1.0/README.md +39 -0
- jevals-0.1.0/pyproject.toml +36 -0
- jevals-0.1.0/src/jevals/__init__.py +6 -0
- jevals-0.1.0/src/jevals/__main__.py +4 -0
- jevals-0.1.0/src/jevals/cli.py +64 -0
- jevals-0.1.0/src/jevals/py.typed +0 -0
- jevals-0.1.0/src/jevals/suite.py +160 -0
- jevals-0.1.0/tests/test_jevals_cli.py +44 -0
- jevals-0.1.0/tests/test_suite.py +63 -0
jevals-0.1.0/.gitignore
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# Python
|
|
2
|
+
__pycache__/
|
|
3
|
+
*.py[cod]
|
|
4
|
+
*$py.class
|
|
5
|
+
*.so
|
|
6
|
+
.Python
|
|
7
|
+
*.egg-info/
|
|
8
|
+
.eggs/
|
|
9
|
+
dist/
|
|
10
|
+
build/
|
|
11
|
+
.pytest_cache/
|
|
12
|
+
.mypy_cache/
|
|
13
|
+
.ruff_cache/
|
|
14
|
+
.coverage
|
|
15
|
+
htmlcov/
|
|
16
|
+
.venv/
|
|
17
|
+
venv/
|
|
18
|
+
|
|
19
|
+
# Tools
|
|
20
|
+
*.log
|
|
21
|
+
|
|
22
|
+
# Editors
|
|
23
|
+
.idea/
|
|
24
|
+
.vscode/
|
|
25
|
+
*.swp
|
|
26
|
+
.DS_Store
|
jevals-0.1.0/PKG-INFO
ADDED
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: jevals
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Run suites of JSON evaluations and report a score.
|
|
5
|
+
Project-URL: Homepage, https://pypi.org/project/jevals/
|
|
6
|
+
Project-URL: Issues, https://pypi.org/project/jevals/
|
|
7
|
+
Author-email: Gabriel Tinoco <gabriel@openlayer.com>
|
|
8
|
+
License-Expression: MIT
|
|
9
|
+
Keywords: eval,evaluation,jeval,json,suite,testing
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
15
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
18
|
+
Classifier: Topic :: Software Development :: Testing
|
|
19
|
+
Requires-Python: >=3.10
|
|
20
|
+
Requires-Dist: jeval>=0.1.0
|
|
21
|
+
Description-Content-Type: text/markdown
|
|
22
|
+
|
|
23
|
+
# jevals
|
|
24
|
+
|
|
25
|
+
Run a suite of JSON evaluation cases and report a score.
|
|
26
|
+
|
|
27
|
+
```bash
|
|
28
|
+
pip install jevals
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`jevals` depends on [`jeval`](https://pypi.org/project/jeval/) for the per-case checks.
|
|
32
|
+
|
|
33
|
+
```bash
|
|
34
|
+
jevals run examples/suite.json
|
|
35
|
+
jevals run examples/cases
|
|
36
|
+
```
|
|
37
|
+
|
|
38
|
+
A suite is a JSON object with `cases`, a JSON list of cases, or a directory of `.json` files:
|
|
39
|
+
|
|
40
|
+
```json
|
|
41
|
+
{
|
|
42
|
+
"name": "checkout",
|
|
43
|
+
"cases": [
|
|
44
|
+
{
|
|
45
|
+
"id": "order-total",
|
|
46
|
+
"actual": {"total": 21, "currency": "USD"},
|
|
47
|
+
"checks": [
|
|
48
|
+
{"path": "total", "op": "eq", "value": 21},
|
|
49
|
+
{"path": "currency", "op": "eq", "value": "USD"}
|
|
50
|
+
]
|
|
51
|
+
}
|
|
52
|
+
]
|
|
53
|
+
}
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
Exit codes: `0` all passed, `1` some failed, `2` usage or file error.
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
jevals run examples/suite.json --json
|
|
60
|
+
jevals run examples/suite.json --fail-under 1.0
|
|
61
|
+
```
|
jevals-0.1.0/README.md
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# jevals
|
|
2
|
+
|
|
3
|
+
Run a suite of JSON evaluation cases and report a score.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install jevals
|
|
7
|
+
```
|
|
8
|
+
|
|
9
|
+
`jevals` depends on [`jeval`](https://pypi.org/project/jeval/) for the per-case checks.
|
|
10
|
+
|
|
11
|
+
```bash
|
|
12
|
+
jevals run examples/suite.json
|
|
13
|
+
jevals run examples/cases
|
|
14
|
+
```
|
|
15
|
+
|
|
16
|
+
A suite is a JSON object with `cases`, a JSON list of cases, or a directory of `.json` files:
|
|
17
|
+
|
|
18
|
+
```json
|
|
19
|
+
{
|
|
20
|
+
"name": "checkout",
|
|
21
|
+
"cases": [
|
|
22
|
+
{
|
|
23
|
+
"id": "order-total",
|
|
24
|
+
"actual": {"total": 21, "currency": "USD"},
|
|
25
|
+
"checks": [
|
|
26
|
+
{"path": "total", "op": "eq", "value": 21},
|
|
27
|
+
{"path": "currency", "op": "eq", "value": "USD"}
|
|
28
|
+
]
|
|
29
|
+
}
|
|
30
|
+
]
|
|
31
|
+
}
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
Exit codes: `0` all passed, `1` some failed, `2` usage or file error.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
jevals run examples/suite.json --json
|
|
38
|
+
jevals run examples/suite.json --fail-under 1.0
|
|
39
|
+
```
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "jevals"
|
|
7
|
+
version = "0.1.0"
|
|
8
|
+
description = "Run suites of JSON evaluations and report a score."
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.10"
|
|
12
|
+
authors = [
|
|
13
|
+
{ name = "Gabriel Tinoco", email = "gabriel@openlayer.com" },
|
|
14
|
+
]
|
|
15
|
+
keywords = ["eval", "evaluation", "json", "testing", "suite", "jeval"]
|
|
16
|
+
classifiers = [
|
|
17
|
+
"Development Status :: 3 - Alpha",
|
|
18
|
+
"Intended Audience :: Developers",
|
|
19
|
+
"License :: OSI Approved :: MIT License",
|
|
20
|
+
"Programming Language :: Python :: 3",
|
|
21
|
+
"Programming Language :: Python :: 3.10",
|
|
22
|
+
"Programming Language :: Python :: 3.11",
|
|
23
|
+
"Programming Language :: Python :: 3.12",
|
|
24
|
+
"Programming Language :: Python :: 3.13",
|
|
25
|
+
"Topic :: Software Development :: Testing",
|
|
26
|
+
]
|
|
27
|
+
dependencies = [
|
|
28
|
+
"jeval>=0.1.0",
|
|
29
|
+
]
|
|
30
|
+
|
|
31
|
+
[project.urls]
|
|
32
|
+
Homepage = "https://pypi.org/project/jevals/"
|
|
33
|
+
Issues = "https://pypi.org/project/jevals/"
|
|
34
|
+
|
|
35
|
+
[project.scripts]
|
|
36
|
+
jevals = "jevals.cli:main"
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import argparse
|
|
4
|
+
import json
|
|
5
|
+
import sys
|
|
6
|
+
from pathlib import Path
|
|
7
|
+
from typing import TextIO
|
|
8
|
+
|
|
9
|
+
from jeval import CaseError
|
|
10
|
+
|
|
11
|
+
from jevals import __version__
|
|
12
|
+
from jevals.suite import load_actuals, load_suite
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def main(argv: list[str] | None = None) -> int:
|
|
16
|
+
parser = argparse.ArgumentParser(
|
|
17
|
+
prog="jevals",
|
|
18
|
+
description="Run a suite of JSON evaluation cases and report a score.",
|
|
19
|
+
)
|
|
20
|
+
sub = parser.add_subparsers(dest="command")
|
|
21
|
+
|
|
22
|
+
run = sub.add_parser("run", help="Run a suite file or directory of cases")
|
|
23
|
+
run.add_argument("path", help="Suite JSON file or directory of .json cases")
|
|
24
|
+
run.add_argument(
|
|
25
|
+
"--actuals",
|
|
26
|
+
help="JSON file mapping case id to actual value (overrides case actuals).",
|
|
27
|
+
)
|
|
28
|
+
run.add_argument("--json", action="store_true", dest="as_json", help="Print JSON.")
|
|
29
|
+
run.add_argument(
|
|
30
|
+
"--fail-under",
|
|
31
|
+
type=float,
|
|
32
|
+
default=1.0,
|
|
33
|
+
help="Fail unless score (passed/total) is at least this value. Default: 1.0",
|
|
34
|
+
)
|
|
35
|
+
|
|
36
|
+
parser.add_argument("--version", action="version", version=f"jevals {__version__}")
|
|
37
|
+
args = parser.parse_args(argv)
|
|
38
|
+
|
|
39
|
+
if args.command != "run":
|
|
40
|
+
parser.print_help()
|
|
41
|
+
return 2
|
|
42
|
+
|
|
43
|
+
try:
|
|
44
|
+
suite = load_suite(Path(args.path))
|
|
45
|
+
actuals = load_actuals(Path(args.actuals)) if args.actuals else None
|
|
46
|
+
result = suite.run(actuals)
|
|
47
|
+
except (OSError, json.JSONDecodeError, CaseError, ValueError) as exc:
|
|
48
|
+
print(f"jevals: {exc}", file=sys.stderr)
|
|
49
|
+
return 2
|
|
50
|
+
|
|
51
|
+
_print(result, as_json=args.as_json, stream=sys.stdout)
|
|
52
|
+
if result.total == 0:
|
|
53
|
+
return 2
|
|
54
|
+
if result.score < args.fail_under:
|
|
55
|
+
return 1
|
|
56
|
+
return 0
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _print(result, as_json: bool, stream: TextIO) -> None:
|
|
60
|
+
if as_json:
|
|
61
|
+
json.dump(result.to_dict(), stream, indent=2)
|
|
62
|
+
stream.write("\n")
|
|
63
|
+
return
|
|
64
|
+
stream.write(result.summary() + "\n")
|
|
File without changes
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from dataclasses import dataclass, field
|
|
5
|
+
from pathlib import Path
|
|
6
|
+
from typing import Any
|
|
7
|
+
|
|
8
|
+
from jeval import Case, CaseError, Result, evaluate
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
@dataclass
|
|
12
|
+
class Suite:
|
|
13
|
+
name: str
|
|
14
|
+
cases: list[Case]
|
|
15
|
+
|
|
16
|
+
def run(self, actuals: dict[str, Any] | None = None) -> SuiteResult:
|
|
17
|
+
results: list[Result] = []
|
|
18
|
+
for case in self.cases:
|
|
19
|
+
if actuals is None or case.id not in actuals:
|
|
20
|
+
results.append(evaluate(case))
|
|
21
|
+
else:
|
|
22
|
+
results.append(evaluate(case, actuals[case.id]))
|
|
23
|
+
return SuiteResult(name=self.name, results=results)
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
@dataclass
|
|
27
|
+
class SuiteResult:
|
|
28
|
+
name: str
|
|
29
|
+
results: list[Result] = field(default_factory=list)
|
|
30
|
+
|
|
31
|
+
@property
|
|
32
|
+
def passed(self) -> int:
|
|
33
|
+
return sum(1 for item in self.results if item.ok)
|
|
34
|
+
|
|
35
|
+
@property
|
|
36
|
+
def failed(self) -> int:
|
|
37
|
+
return sum(1 for item in self.results if not item.ok)
|
|
38
|
+
|
|
39
|
+
@property
|
|
40
|
+
def total(self) -> int:
|
|
41
|
+
return len(self.results)
|
|
42
|
+
|
|
43
|
+
@property
|
|
44
|
+
def ok(self) -> bool:
|
|
45
|
+
return self.total > 0 and self.failed == 0
|
|
46
|
+
|
|
47
|
+
@property
|
|
48
|
+
def score(self) -> float:
|
|
49
|
+
if self.total == 0:
|
|
50
|
+
return 0.0
|
|
51
|
+
return self.passed / self.total
|
|
52
|
+
|
|
53
|
+
def to_dict(self) -> dict[str, Any]:
|
|
54
|
+
return {
|
|
55
|
+
"name": self.name,
|
|
56
|
+
"ok": self.ok,
|
|
57
|
+
"passed": self.passed,
|
|
58
|
+
"failed": self.failed,
|
|
59
|
+
"total": self.total,
|
|
60
|
+
"score": self.score,
|
|
61
|
+
"results": [item.to_dict() for item in self.results],
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
def summary(self) -> str:
|
|
65
|
+
if self.total == 0:
|
|
66
|
+
return f"{self.name}: no cases"
|
|
67
|
+
status = "PASS" if self.ok else "FAIL"
|
|
68
|
+
lines = [
|
|
69
|
+
f"{status} {self.name} {self.passed} passed, {self.failed} failed ({self.score:.0%})"
|
|
70
|
+
]
|
|
71
|
+
for item in self.results:
|
|
72
|
+
if item.ok:
|
|
73
|
+
lines.append(f" PASS {item.case_id}")
|
|
74
|
+
else:
|
|
75
|
+
lines.append(f" FAIL {item.case_id}")
|
|
76
|
+
for check in item.failed:
|
|
77
|
+
lines.append(f" {check.message}")
|
|
78
|
+
return "\n".join(lines)
|
|
79
|
+
|
|
80
|
+
|
|
81
|
+
def load_suite(path: Path) -> Suite:
|
|
82
|
+
path = path.resolve()
|
|
83
|
+
if path.is_dir():
|
|
84
|
+
cases = _load_dir(path)
|
|
85
|
+
return Suite(name=path.name, cases=cases)
|
|
86
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
87
|
+
return _suite_from_data(data, default_name=path.stem, source=path)
|
|
88
|
+
|
|
89
|
+
|
|
90
|
+
def _load_dir(path: Path) -> list[Case]:
|
|
91
|
+
files = sorted(item for item in path.glob("*.json") if item.is_file())
|
|
92
|
+
if not files:
|
|
93
|
+
raise CaseError(f"no .json cases in {path}")
|
|
94
|
+
cases: list[Case] = []
|
|
95
|
+
for file in files:
|
|
96
|
+
data = json.loads(file.read_text(encoding="utf-8"))
|
|
97
|
+
cases.extend(_cases_from_data(data, default_id=file.stem, source=file))
|
|
98
|
+
return cases
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
def _suite_from_data(data: Any, default_name: str, source: Path) -> Suite:
|
|
102
|
+
if isinstance(data, list):
|
|
103
|
+
return Suite(
|
|
104
|
+
name=default_name,
|
|
105
|
+
cases=_cases_from_data(data, default_id=default_name, source=source),
|
|
106
|
+
)
|
|
107
|
+
if not isinstance(data, dict):
|
|
108
|
+
raise CaseError(f"{source} must be a suite object or a list of cases")
|
|
109
|
+
if "cases" in data:
|
|
110
|
+
name = str(data.get("name") or data.get("id") or default_name)
|
|
111
|
+
raw_cases = data["cases"]
|
|
112
|
+
if not isinstance(raw_cases, list):
|
|
113
|
+
raise CaseError("cases must be a list")
|
|
114
|
+
cases = [
|
|
115
|
+
Case.from_dict(item, default_id=f"{name}-{index}")
|
|
116
|
+
if isinstance(item, dict)
|
|
117
|
+
else _reject_case(item)
|
|
118
|
+
for index, item in enumerate(raw_cases, start=1)
|
|
119
|
+
]
|
|
120
|
+
return Suite(name=name, cases=cases)
|
|
121
|
+
return Suite(name=default_name, cases=[Case.from_dict(data, default_id=default_name)])
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def _cases_from_data(data: Any, default_id: str, source: Path) -> list[Case]:
|
|
125
|
+
if isinstance(data, list):
|
|
126
|
+
return [
|
|
127
|
+
Case.from_dict(item, default_id=f"{default_id}-{index}")
|
|
128
|
+
if isinstance(item, dict)
|
|
129
|
+
else _reject_case(item)
|
|
130
|
+
for index, item in enumerate(data, start=1)
|
|
131
|
+
]
|
|
132
|
+
if isinstance(data, dict) and "cases" in data:
|
|
133
|
+
return _suite_from_data(data, default_name=default_id, source=source).cases
|
|
134
|
+
if isinstance(data, dict):
|
|
135
|
+
return [Case.from_dict(data, default_id=default_id)]
|
|
136
|
+
raise CaseError(f"{source} is not a valid case or suite")
|
|
137
|
+
|
|
138
|
+
|
|
139
|
+
def _reject_case(item: Any) -> Case:
|
|
140
|
+
raise CaseError(f"case must be an object, got {type(item).__name__}")
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def load_actuals(path: Path) -> dict[str, Any]:
|
|
144
|
+
data = json.loads(path.read_text(encoding="utf-8"))
|
|
145
|
+
if isinstance(data, dict):
|
|
146
|
+
return data
|
|
147
|
+
if isinstance(data, list):
|
|
148
|
+
actuals: dict[str, Any] = {}
|
|
149
|
+
for item in data:
|
|
150
|
+
if not isinstance(item, dict) or "id" not in item:
|
|
151
|
+
raise CaseError("actuals list items must be objects with an 'id'")
|
|
152
|
+
case_id = str(item["id"])
|
|
153
|
+
if "actual" in item:
|
|
154
|
+
actuals[case_id] = item["actual"]
|
|
155
|
+
elif "value" in item:
|
|
156
|
+
actuals[case_id] = item["value"]
|
|
157
|
+
else:
|
|
158
|
+
raise CaseError(f"actuals item {case_id!r} is missing 'actual'")
|
|
159
|
+
return actuals
|
|
160
|
+
raise CaseError("actuals must be an object mapping case id to value, or a list")
|
|
@@ -0,0 +1,44 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from jevals.cli import main
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def test_cli_run(tmp_path: Path) -> None:
|
|
10
|
+
suite = tmp_path / "suite.json"
|
|
11
|
+
suite.write_text(
|
|
12
|
+
json.dumps(
|
|
13
|
+
{
|
|
14
|
+
"name": "demo",
|
|
15
|
+
"cases": [
|
|
16
|
+
{
|
|
17
|
+
"id": "ok",
|
|
18
|
+
"actual": {"n": 1},
|
|
19
|
+
"checks": [{"path": "n", "op": "eq", "value": 1}],
|
|
20
|
+
}
|
|
21
|
+
],
|
|
22
|
+
}
|
|
23
|
+
),
|
|
24
|
+
encoding="utf-8",
|
|
25
|
+
)
|
|
26
|
+
assert main(["run", str(suite)]) == 0
|
|
27
|
+
assert main(["run", str(suite), "--json"]) == 0
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def test_cli_fail_under(tmp_path: Path) -> None:
|
|
31
|
+
suite = tmp_path / "suite.json"
|
|
32
|
+
suite.write_text(
|
|
33
|
+
json.dumps(
|
|
34
|
+
{
|
|
35
|
+
"cases": [
|
|
36
|
+
{"id": "a", "actual": 1, "expected": 1},
|
|
37
|
+
{"id": "b", "actual": 1, "expected": 2},
|
|
38
|
+
]
|
|
39
|
+
}
|
|
40
|
+
),
|
|
41
|
+
encoding="utf-8",
|
|
42
|
+
)
|
|
43
|
+
assert main(["run", str(suite)]) == 1
|
|
44
|
+
assert main(["run", str(suite), "--fail-under", "0.4"]) == 0
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
import json
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
6
|
+
from jevals.suite import Suite, load_actuals, load_suite
|
|
7
|
+
from jeval import Case, Check
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
def test_suite_score(tmp_path: Path) -> None:
|
|
11
|
+
suite_path = tmp_path / "suite.json"
|
|
12
|
+
suite_path.write_text(
|
|
13
|
+
json.dumps(
|
|
14
|
+
{
|
|
15
|
+
"name": "demo",
|
|
16
|
+
"cases": [
|
|
17
|
+
{
|
|
18
|
+
"id": "ok",
|
|
19
|
+
"actual": {"n": 1},
|
|
20
|
+
"checks": [{"path": "n", "op": "eq", "value": 1}],
|
|
21
|
+
},
|
|
22
|
+
{
|
|
23
|
+
"id": "bad",
|
|
24
|
+
"actual": {"n": 2},
|
|
25
|
+
"checks": [{"path": "n", "op": "eq", "value": 1}],
|
|
26
|
+
},
|
|
27
|
+
],
|
|
28
|
+
}
|
|
29
|
+
),
|
|
30
|
+
encoding="utf-8",
|
|
31
|
+
)
|
|
32
|
+
result = load_suite(suite_path).run()
|
|
33
|
+
assert result.passed == 1
|
|
34
|
+
assert result.failed == 1
|
|
35
|
+
assert result.score == 0.5
|
|
36
|
+
assert not result.ok
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
def test_directory_suite(tmp_path: Path) -> None:
|
|
40
|
+
cases = tmp_path / "cases"
|
|
41
|
+
cases.mkdir()
|
|
42
|
+
(cases / "one.json").write_text(
|
|
43
|
+
json.dumps({"id": "one", "actual": {"ok": True}, "expected": {"ok": True}}),
|
|
44
|
+
encoding="utf-8",
|
|
45
|
+
)
|
|
46
|
+
result = load_suite(cases).run()
|
|
47
|
+
assert result.ok
|
|
48
|
+
assert result.total == 1
|
|
49
|
+
|
|
50
|
+
|
|
51
|
+
def test_actuals_override() -> None:
|
|
52
|
+
suite = Suite(
|
|
53
|
+
name="override",
|
|
54
|
+
cases=[Case(id="n", checks=[Check(path="$", op="eq", value=3)], actual=1)],
|
|
55
|
+
)
|
|
56
|
+
assert not suite.run().ok
|
|
57
|
+
assert suite.run({"n": 3}).ok
|
|
58
|
+
|
|
59
|
+
|
|
60
|
+
def test_load_actuals_list(tmp_path: Path) -> None:
|
|
61
|
+
path = tmp_path / "actuals.json"
|
|
62
|
+
path.write_text(json.dumps([{"id": "n", "actual": 3}]), encoding="utf-8")
|
|
63
|
+
assert load_actuals(path) == {"n": 3}
|