nativegate 0.1.0__py3-none-any.whl
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.
- nativegate/__init__.py +1 -0
- nativegate/__main__.py +4 -0
- nativegate/buildinfo.py +344 -0
- nativegate/cli.py +2007 -0
- nativegate/config.py +991 -0
- nativegate/declared_invariants.py +565 -0
- nativegate/discovery.py +167 -0
- nativegate/driverbuild.py +626 -0
- nativegate/drivers/__init__.py +5 -0
- nativegate/drivers/cpp.py +616 -0
- nativegate/drivers/fortran.py +507 -0
- nativegate/generators/__init__.py +0 -0
- nativegate/generators/cmake_gen.py +101 -0
- nativegate/generators/docker_gen.py +614 -0
- nativegate/generators/error_gen.py +104 -0
- nativegate/generators/f2py_gen.py +91 -0
- nativegate/generators/gateway_gen.py +110 -0
- nativegate/generators/golden_gen.py +50 -0
- nativegate/generators/k8s_gen.py +212 -0
- nativegate/generators/mcp_gen.py +281 -0
- nativegate/generators/middleware_gen.py +717 -0
- nativegate/generators/pybind_gen.py +406 -0
- nativegate/generators/pyproject_gen.py +61 -0
- nativegate/generators/python_pkg_gen.py +1164 -0
- nativegate/generators/test_gen.py +160 -0
- nativegate/golden.py +747 -0
- nativegate/invariants.py +532 -0
- nativegate/ir.py +789 -0
- nativegate/lattice.py +350 -0
- nativegate/locking.py +216 -0
- nativegate/oracle.py +904 -0
- nativegate/parsers/__init__.py +0 -0
- nativegate/parsers/cpp.py +105 -0
- nativegate/parsers/cpp_ast.py +1652 -0
- nativegate/parsers/cpp_regex.py +812 -0
- nativegate/parsers/fixed_form.py +868 -0
- nativegate/parsers/fortran.py +157 -0
- nativegate/parsers/fortran_fparser.py +1116 -0
- nativegate/parsers/fortran_regex.py +686 -0
- nativegate/preprocess.py +335 -0
- nativegate/structural_invariants.py +762 -0
- nativegate/suggest.py +208 -0
- nativegate/templates/__init__.py +20 -0
- nativegate/templates/golden_test_template.py +248 -0
- nativegate/wire.py +438 -0
- nativegate-0.1.0.dist-info/METADATA +547 -0
- nativegate-0.1.0.dist-info/RECORD +50 -0
- nativegate-0.1.0.dist-info/WHEEL +5 -0
- nativegate-0.1.0.dist-info/entry_points.txt +3 -0
- nativegate-0.1.0.dist-info/top_level.txt +1 -0
nativegate/suggest.py
ADDED
|
@@ -0,0 +1,208 @@
|
|
|
1
|
+
"""Rank the native sources in a tree by how cleanly they would bind.
|
|
2
|
+
|
|
3
|
+
The information needed to pick a starting file already exists — `inspect`
|
|
4
|
+
prints it — but only one file at a time, for someone who already knows which
|
|
5
|
+
files are candidates. Someone meeting a C++ codebase for the first time does
|
|
6
|
+
not, and the cost of guessing wrong is a service that half-binds and fails
|
|
7
|
+
later. This module runs the parse across a whole tree and sorts the results,
|
|
8
|
+
so the answer to "which file do I point at" is the first row of a table.
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
from __future__ import annotations
|
|
12
|
+
|
|
13
|
+
import re
|
|
14
|
+
from dataclasses import dataclass, field
|
|
15
|
+
from pathlib import Path
|
|
16
|
+
|
|
17
|
+
from .config import ExposeConfig
|
|
18
|
+
from .discovery import find_implementation_files, find_native_sources
|
|
19
|
+
from .ir import ModuleIR
|
|
20
|
+
from .parsers import cpp as cpp_parser
|
|
21
|
+
from .parsers.cpp_ast import ClangOptions
|
|
22
|
+
from .parsers import fortran as fortran_parser
|
|
23
|
+
|
|
24
|
+
# Verdicts, in ranking order. READY files bind everything they declare;
|
|
25
|
+
# WORKABLE ones bind more than they skip; POOR ones bind nothing useful.
|
|
26
|
+
READY = "ready"
|
|
27
|
+
WORKABLE = "workable"
|
|
28
|
+
POOR = "poor"
|
|
29
|
+
|
|
30
|
+
_QUOTED_INCLUDE = re.compile(r'^\s*#\s*include\s*"([^"]+)"', re.MULTILINE)
|
|
31
|
+
|
|
32
|
+
# Headers-only extensions: these are what you point nativegate at. A bare .cpp
|
|
33
|
+
# is an implementation file, not an interface, so it is not offered as a
|
|
34
|
+
# candidate unless nothing else exists.
|
|
35
|
+
_HEADER_SUFFIXES = {".hpp", ".hh", ".h"}
|
|
36
|
+
|
|
37
|
+
|
|
38
|
+
@dataclass
|
|
39
|
+
class Candidate:
|
|
40
|
+
"""One native source, and how well it would bind."""
|
|
41
|
+
|
|
42
|
+
path: Path
|
|
43
|
+
language: str
|
|
44
|
+
verdict: str
|
|
45
|
+
bound: int = 0
|
|
46
|
+
skipped: int = 0
|
|
47
|
+
classes: int = 0
|
|
48
|
+
structs: int = 0
|
|
49
|
+
functions: int = 0
|
|
50
|
+
methods: int = 0
|
|
51
|
+
impl_files: list[Path] = field(default_factory=list)
|
|
52
|
+
local_includes: list[str] = field(default_factory=list)
|
|
53
|
+
error: str | None = None
|
|
54
|
+
|
|
55
|
+
@property
|
|
56
|
+
def sort_key(self) -> tuple:
|
|
57
|
+
# Dependencies dominate, ahead of skip count: one skipped array method
|
|
58
|
+
# out of twenty costs you that method, while one #include of another
|
|
59
|
+
# subsystem's header costs you that whole subsystem's build. Only
|
|
60
|
+
# outright unusable files (POOR) sort below a dependency-heavy one.
|
|
61
|
+
return (
|
|
62
|
+
1 if self.verdict == POOR else 0,
|
|
63
|
+
len(self.local_includes),
|
|
64
|
+
self.skipped,
|
|
65
|
+
-self.bound,
|
|
66
|
+
str(self.path),
|
|
67
|
+
)
|
|
68
|
+
|
|
69
|
+
def binds_summary(self) -> str:
|
|
70
|
+
parts = []
|
|
71
|
+
if self.functions:
|
|
72
|
+
parts.append(f"{self.functions} fn")
|
|
73
|
+
if self.classes:
|
|
74
|
+
parts.append(f"{self.classes} class")
|
|
75
|
+
if self.methods:
|
|
76
|
+
parts.append(f"{self.methods} method")
|
|
77
|
+
if self.structs:
|
|
78
|
+
parts.append(f"{self.structs} struct")
|
|
79
|
+
return ", ".join(parts) or "nothing"
|
|
80
|
+
|
|
81
|
+
def notes(self) -> str:
|
|
82
|
+
if self.error:
|
|
83
|
+
return self.error
|
|
84
|
+
notes = []
|
|
85
|
+
if self.impl_files:
|
|
86
|
+
notes.append(", ".join(f.name for f in self.impl_files))
|
|
87
|
+
elif self.language == "cpp" and (self.methods or self.functions):
|
|
88
|
+
notes.append("no .cpp found")
|
|
89
|
+
if self.local_includes:
|
|
90
|
+
shown = sorted(self.local_includes)
|
|
91
|
+
extra = ""
|
|
92
|
+
if len(shown) > 2:
|
|
93
|
+
shown, extra = shown[:2], f" +{len(self.local_includes) - 2}"
|
|
94
|
+
notes.append("needs " + ", ".join(shown) + extra)
|
|
95
|
+
return "; ".join(notes)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def _local_includes(source: Path, impl_files: list[Path]) -> list[str]:
|
|
99
|
+
"""Quoted #includes that resolve to a real neighbouring header.
|
|
100
|
+
|
|
101
|
+
Scans the implementation files too: a header can look self-contained
|
|
102
|
+
while its .cpp pulls in a bridge to another subsystem, and that
|
|
103
|
+
dependency is exactly what makes a file a poor first choice.
|
|
104
|
+
"""
|
|
105
|
+
found: set[str] = set()
|
|
106
|
+
for path in [source, *impl_files]:
|
|
107
|
+
try:
|
|
108
|
+
text = path.read_text(errors="replace")
|
|
109
|
+
except OSError:
|
|
110
|
+
continue
|
|
111
|
+
for name in _QUOTED_INCLUDE.findall(text):
|
|
112
|
+
included = Path(name).name
|
|
113
|
+
if Path(included).stem == source.stem:
|
|
114
|
+
continue # the .cpp including its own header
|
|
115
|
+
# Only count it if it actually exists nearby — a missing include
|
|
116
|
+
# is a build problem, not a dependency worth ranking on.
|
|
117
|
+
if (path.parent / name).exists() or (source.parent / included).exists():
|
|
118
|
+
found.add(included)
|
|
119
|
+
return sorted(found)
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
def _classify(module: ModuleIR, local_includes: list[str]) -> tuple[str, dict]:
|
|
123
|
+
methods = sum(len(cls.methods) for cls in module.classes)
|
|
124
|
+
counts = {
|
|
125
|
+
"classes": len(module.classes),
|
|
126
|
+
"structs": len(module.structs),
|
|
127
|
+
"functions": len(module.functions),
|
|
128
|
+
"methods": methods,
|
|
129
|
+
}
|
|
130
|
+
bound = methods + len(module.functions) + len(module.structs)
|
|
131
|
+
skipped = len(module.skipped)
|
|
132
|
+
|
|
133
|
+
if bound == 0 or skipped >= bound:
|
|
134
|
+
verdict = POOR
|
|
135
|
+
elif skipped == 0 and not local_includes:
|
|
136
|
+
# Turnkey: everything it declares binds, and it drags nothing else in.
|
|
137
|
+
verdict = READY
|
|
138
|
+
else:
|
|
139
|
+
verdict = WORKABLE
|
|
140
|
+
return verdict, {"bound": bound, "skipped": skipped, **counts}
|
|
141
|
+
|
|
142
|
+
|
|
143
|
+
def _analyse_cpp(source: Path, backend: str, options: ClangOptions) -> Candidate:
|
|
144
|
+
impl_files = find_implementation_files(source)
|
|
145
|
+
try:
|
|
146
|
+
module = cpp_parser.parse_header(
|
|
147
|
+
source, ExposeConfig(), backend=backend, options=options
|
|
148
|
+
)
|
|
149
|
+
except Exception as exc: # a header that will not parse is a real answer
|
|
150
|
+
return Candidate(
|
|
151
|
+
path=source,
|
|
152
|
+
language="cpp",
|
|
153
|
+
verdict=POOR,
|
|
154
|
+
impl_files=impl_files,
|
|
155
|
+
error=f"parse failed: {type(exc).__name__}",
|
|
156
|
+
)
|
|
157
|
+
|
|
158
|
+
local_includes = _local_includes(source, impl_files)
|
|
159
|
+
verdict, counts = _classify(module, local_includes)
|
|
160
|
+
return Candidate(
|
|
161
|
+
path=source,
|
|
162
|
+
language="cpp",
|
|
163
|
+
verdict=verdict,
|
|
164
|
+
impl_files=impl_files,
|
|
165
|
+
local_includes=local_includes,
|
|
166
|
+
**counts,
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
|
|
170
|
+
def _analyse_fortran(source: Path) -> Candidate:
|
|
171
|
+
try:
|
|
172
|
+
routines = fortran_parser.list_routine_names(source)
|
|
173
|
+
except Exception as exc:
|
|
174
|
+
return Candidate(
|
|
175
|
+
path=source,
|
|
176
|
+
language="fortran",
|
|
177
|
+
verdict=POOR,
|
|
178
|
+
error=f"parse failed: {type(exc).__name__}",
|
|
179
|
+
)
|
|
180
|
+
return Candidate(
|
|
181
|
+
path=source,
|
|
182
|
+
language="fortran",
|
|
183
|
+
verdict=READY if routines else POOR,
|
|
184
|
+
bound=len(routines),
|
|
185
|
+
functions=len(routines),
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
|
|
189
|
+
def analyse_tree(
|
|
190
|
+
root: Path,
|
|
191
|
+
backend: str = "auto",
|
|
192
|
+
options: ClangOptions | None = None,
|
|
193
|
+
) -> list[Candidate]:
|
|
194
|
+
"""Parse every native source under `root`, best starting point first."""
|
|
195
|
+
options = options or ClangOptions()
|
|
196
|
+
candidates: list[Candidate] = []
|
|
197
|
+
|
|
198
|
+
cpp_sources = find_native_sources(root, "cpp")
|
|
199
|
+
headers = [p for p in cpp_sources if p.suffix.lower() in _HEADER_SUFFIXES]
|
|
200
|
+
# Prefer headers; fall back to .cpp only for projects that have no headers
|
|
201
|
+
# at all, where the .cpp really is the interface.
|
|
202
|
+
for source in headers or cpp_sources:
|
|
203
|
+
candidates.append(_analyse_cpp(source, backend, options))
|
|
204
|
+
|
|
205
|
+
for source in find_native_sources(root, "fortran"):
|
|
206
|
+
candidates.append(_analyse_fortran(source))
|
|
207
|
+
|
|
208
|
+
return sorted(candidates, key=lambda c: c.sort_key)
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
"""Files that are shipped verbatim and rendered into a generated service.
|
|
2
|
+
|
|
3
|
+
A template here is a REAL `.py` file, not a string literal inside a
|
|
4
|
+
generator. That is the whole point: as a string it was invisible to this
|
|
5
|
+
project's own linting, type checking and test suite, so a syntax error or a
|
|
6
|
+
stale reference inside it only surfaced when somebody ran the generated
|
|
7
|
+
service. As a module it is parsed, compiled and checked like any other file.
|
|
8
|
+
|
|
9
|
+
Rendering is plain textual substitution of the sentinel names below — not
|
|
10
|
+
`str.format`, which would require every literal brace in the template to be
|
|
11
|
+
doubled and so make the file invalid Python again.
|
|
12
|
+
|
|
13
|
+
The sentinels are chosen to be syntactically valid where they appear
|
|
14
|
+
(`NATIVEGATE_PACKAGE` stands in for a module name, the other two live inside
|
|
15
|
+
string literals), which is why the file compiles on its own. It is not
|
|
16
|
+
*runnable* on its own: `import NATIVEGATE_PACKAGE` resolves only once the
|
|
17
|
+
name has been substituted for a real service package.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
from __future__ import annotations
|
|
@@ -0,0 +1,248 @@
|
|
|
1
|
+
# Generated by nativegate. Do not edit by hand — re-run `ngate generate`.
|
|
2
|
+
"""Numerical regression: the answers must not change.
|
|
3
|
+
|
|
4
|
+
`golden.json` records what every bound entry point returned for a fixed set
|
|
5
|
+
of inputs, the tolerance each entry is held to, and the toolchain the values
|
|
6
|
+
were recorded on. This replays those calls against the currently built
|
|
7
|
+
extension and compares.
|
|
8
|
+
|
|
9
|
+
A failure here does not necessarily mean a bug — it means the numbers moved.
|
|
10
|
+
Two things move them: the code changed, or the toolchain did. The failure
|
|
11
|
+
message reports the recorded toolchain against this one so you can tell which
|
|
12
|
+
before investigating. Then re-record deliberately with:
|
|
13
|
+
|
|
14
|
+
ngate golden record NATIVEGATE_SERVICE_NAME
|
|
15
|
+
|
|
16
|
+
and review the diff in golden.json like any other change to behaviour.
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
import json
|
|
20
|
+
import math
|
|
21
|
+
import platform
|
|
22
|
+
import shutil
|
|
23
|
+
import subprocess
|
|
24
|
+
from pathlib import Path
|
|
25
|
+
|
|
26
|
+
import pytest
|
|
27
|
+
|
|
28
|
+
import NATIVEGATE_PACKAGE
|
|
29
|
+
|
|
30
|
+
GOLDEN_PATH = Path(__file__).resolve().parent.parent / "NATIVEGATE_GOLDEN_FILENAME"
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
def _numpy():
|
|
34
|
+
try:
|
|
35
|
+
import numpy
|
|
36
|
+
except ImportError:
|
|
37
|
+
return None
|
|
38
|
+
return numpy
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
def _is_number(value):
|
|
42
|
+
return isinstance(value, (int, float)) and not isinstance(value, bool)
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def _materialise(value):
|
|
46
|
+
if isinstance(value, dict) and "__struct__" in value:
|
|
47
|
+
native = getattr(NATIVEGATE_PACKAGE, value["__struct__"])()
|
|
48
|
+
for name, field in value["fields"].items():
|
|
49
|
+
setattr(native, name, _materialise(field))
|
|
50
|
+
return native
|
|
51
|
+
if isinstance(value, list) and value and all(_is_number(v) for v in value):
|
|
52
|
+
numpy = _numpy()
|
|
53
|
+
if numpy is not None:
|
|
54
|
+
dtype = "int64" if all(isinstance(v, int) for v in value) else "float64"
|
|
55
|
+
return numpy.array(value, dtype=dtype)
|
|
56
|
+
return value
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def _jsonable(value):
|
|
60
|
+
if value is None or isinstance(value, (bool, int, float, str)):
|
|
61
|
+
return value
|
|
62
|
+
if isinstance(value, (list, tuple)):
|
|
63
|
+
return [_jsonable(v) for v in value]
|
|
64
|
+
if hasattr(value, "tolist"):
|
|
65
|
+
return _jsonable(value.tolist())
|
|
66
|
+
return {
|
|
67
|
+
name: _jsonable(getattr(value, name))
|
|
68
|
+
for name in dir(value)
|
|
69
|
+
if not name.startswith("_") and not callable(getattr(value, name))
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def _invoke(entry):
|
|
74
|
+
"""Return (result, {argument index: value after the call}).
|
|
75
|
+
|
|
76
|
+
A Fortran routine with an `intent(inout)` array argument returns None and
|
|
77
|
+
writes its answer into the caller's array; comparing only the return value
|
|
78
|
+
would compare None against None and prove nothing.
|
|
79
|
+
"""
|
|
80
|
+
arguments = [_materialise(a) for a in entry["arguments"]]
|
|
81
|
+
if entry["kind"] == "function":
|
|
82
|
+
target = getattr(NATIVEGATE_PACKAGE, entry["name"])
|
|
83
|
+
elif entry["kind"] == "static":
|
|
84
|
+
target = getattr(getattr(NATIVEGATE_PACKAGE, entry["class"]), entry["name"])
|
|
85
|
+
else:
|
|
86
|
+
cls = getattr(NATIVEGATE_PACKAGE, entry["class"])
|
|
87
|
+
instance = cls(*[_materialise(a) for a in entry["constructor_arguments"]])
|
|
88
|
+
target = getattr(instance, entry["name"])
|
|
89
|
+
result = _jsonable(target(*arguments))
|
|
90
|
+
effects = {}
|
|
91
|
+
for index, (before, after) in enumerate(zip(entry["arguments"], arguments)):
|
|
92
|
+
if isinstance(before, list):
|
|
93
|
+
now = _jsonable(after)
|
|
94
|
+
if now != before:
|
|
95
|
+
effects[str(index)] = now
|
|
96
|
+
return result, effects
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
def _tool_version(command):
|
|
100
|
+
executable = shutil.which(command)
|
|
101
|
+
if executable is None:
|
|
102
|
+
return None
|
|
103
|
+
try:
|
|
104
|
+
done = subprocess.run(
|
|
105
|
+
[executable, "--version"], capture_output=True, text=True,
|
|
106
|
+
timeout=30, check=False,
|
|
107
|
+
)
|
|
108
|
+
except OSError:
|
|
109
|
+
return None
|
|
110
|
+
lines = (done.stdout or done.stderr or "").strip().splitlines()
|
|
111
|
+
return lines[0].strip() if lines else None
|
|
112
|
+
|
|
113
|
+
|
|
114
|
+
def _environment():
|
|
115
|
+
"""This machine, in the same shape golden.json recorded.
|
|
116
|
+
|
|
117
|
+
Source digests are deliberately absent: a deployed service does not ship
|
|
118
|
+
the native sources, so re-hashing them here is not possible. The recorded
|
|
119
|
+
digests still say which sources the values came from.
|
|
120
|
+
"""
|
|
121
|
+
numpy = _numpy()
|
|
122
|
+
return {
|
|
123
|
+
"platform": "%s %s" % (platform.system(), platform.release()),
|
|
124
|
+
"machine": platform.machine(),
|
|
125
|
+
"python": platform.python_version(),
|
|
126
|
+
"python_implementation": platform.python_implementation(),
|
|
127
|
+
"numpy": None if numpy is None else numpy.__version__,
|
|
128
|
+
"fortran_compiler": _tool_version("gfortran"),
|
|
129
|
+
"cxx_compiler": _tool_version("clang++") or _tool_version("g++"),
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
|
|
133
|
+
def _toolchain_report(recorded):
|
|
134
|
+
if not recorded:
|
|
135
|
+
return "golden.json records no provenance, so a toolchain change cannot be ruled out."
|
|
136
|
+
current = _environment()
|
|
137
|
+
moved = [
|
|
138
|
+
" %s: recorded %r, now %r" % (key, recorded.get(key), value)
|
|
139
|
+
for key, value in current.items()
|
|
140
|
+
if key in recorded and recorded[key] != value
|
|
141
|
+
]
|
|
142
|
+
if not moved:
|
|
143
|
+
return (
|
|
144
|
+
"The toolchain matches what golden.json recorded, so this is a change "
|
|
145
|
+
"in behaviour, not a platform difference."
|
|
146
|
+
)
|
|
147
|
+
return (
|
|
148
|
+
"The toolchain ALSO changed since these values were recorded. A platform "
|
|
149
|
+
"difference looks exactly like a regression — rule it out first:\n"
|
|
150
|
+
+ "\n".join(moved)
|
|
151
|
+
)
|
|
152
|
+
|
|
153
|
+
|
|
154
|
+
def _close(expected, actual, rtol, atol):
|
|
155
|
+
if isinstance(expected, bool) or isinstance(actual, bool):
|
|
156
|
+
return expected == actual
|
|
157
|
+
if isinstance(expected, (int, float)) and isinstance(actual, (int, float)):
|
|
158
|
+
if math.isnan(expected) and math.isnan(actual):
|
|
159
|
+
return True
|
|
160
|
+
return math.isclose(expected, actual, rel_tol=rtol, abs_tol=atol)
|
|
161
|
+
if isinstance(expected, list) and isinstance(actual, list):
|
|
162
|
+
return len(expected) == len(actual) and all(
|
|
163
|
+
_close(e, a, rtol, atol) for e, a in zip(expected, actual)
|
|
164
|
+
)
|
|
165
|
+
if isinstance(expected, dict) and isinstance(actual, dict):
|
|
166
|
+
return expected.keys() == actual.keys() and all(
|
|
167
|
+
_close(expected[k], actual[k], rtol, atol) for k in expected
|
|
168
|
+
)
|
|
169
|
+
return expected == actual
|
|
170
|
+
|
|
171
|
+
|
|
172
|
+
def _document():
|
|
173
|
+
if not GOLDEN_PATH.exists():
|
|
174
|
+
pytest.skip(
|
|
175
|
+
f"no {GOLDEN_PATH.name} recorded yet — run `ngate golden record NATIVEGATE_SERVICE_NAME`"
|
|
176
|
+
)
|
|
177
|
+
return json.loads(GOLDEN_PATH.read_text())
|
|
178
|
+
|
|
179
|
+
|
|
180
|
+
def test_golden_values_are_unchanged():
|
|
181
|
+
document = _document()
|
|
182
|
+
fallback = document.get("tolerance") or {}
|
|
183
|
+
|
|
184
|
+
differences = []
|
|
185
|
+
for key, entry in (document.get("entries") or {}).items():
|
|
186
|
+
# Per entry, not one hidden global: a routine that terminates on its
|
|
187
|
+
# own convergence tolerance cannot be held to the same number as a
|
|
188
|
+
# closed-form correlation, and saying so in the file is honest where
|
|
189
|
+
# loosening every entry to match the worst one is not.
|
|
190
|
+
own = entry.get("tolerance") or {}
|
|
191
|
+
rtol = own.get("rtol", fallback.get("rtol", 1e-9))
|
|
192
|
+
atol = own.get("atol", fallback.get("atol", 1e-12))
|
|
193
|
+
try:
|
|
194
|
+
actual, effects = _invoke(entry)
|
|
195
|
+
except Exception as exc: # noqa: BLE001
|
|
196
|
+
differences.append(f"{key}: recorded, but this build cannot call it — {exc}")
|
|
197
|
+
continue
|
|
198
|
+
expected = entry.get("result")
|
|
199
|
+
if not _close(expected, actual, rtol, atol):
|
|
200
|
+
differences.append(
|
|
201
|
+
f"{key}: expected {expected!r}, got {actual!r} (rtol={rtol}, atol={atol})"
|
|
202
|
+
)
|
|
203
|
+
for index, was in (entry.get("argument_effects") or {}).items():
|
|
204
|
+
if index not in effects:
|
|
205
|
+
differences.append(
|
|
206
|
+
f"{key}: argument {index} was written in place when recorded, "
|
|
207
|
+
"and this build does not write it"
|
|
208
|
+
)
|
|
209
|
+
elif not _close(was, effects[index], rtol, atol):
|
|
210
|
+
differences.append(
|
|
211
|
+
f"{key}: argument {index} after the call: expected {was!r}, "
|
|
212
|
+
f"got {effects[index]!r}"
|
|
213
|
+
)
|
|
214
|
+
|
|
215
|
+
assert not differences, (
|
|
216
|
+
"numerical regression:\n "
|
|
217
|
+
+ "\n ".join(differences)
|
|
218
|
+
+ "\n\n"
|
|
219
|
+
+ _toolchain_report(document.get("provenance"))
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
|
|
223
|
+
def test_golden_file_covers_the_api():
|
|
224
|
+
# A golden file that records two of forty entry points passes every run
|
|
225
|
+
# and proves almost nothing. This asserts only that *something* is
|
|
226
|
+
# covered; the skip reasons in the file say what is not.
|
|
227
|
+
document = _document()
|
|
228
|
+
assert document.get("entries"), (
|
|
229
|
+
"golden.json records no entry points at all — see its 'skipped' block "
|
|
230
|
+
"for why, then re-record with `ngate golden record NATIVEGATE_SERVICE_NAME`"
|
|
231
|
+
)
|
|
232
|
+
|
|
233
|
+
|
|
234
|
+
def test_golden_file_records_the_toolchain_it_was_recorded_on():
|
|
235
|
+
# Without this, a failure above cannot distinguish a regression from a
|
|
236
|
+
# different compiler — and the documented remedy (re-record) would erase
|
|
237
|
+
# the only evidence of which it was.
|
|
238
|
+
document = _document()
|
|
239
|
+
recorded = document.get("provenance") or {}
|
|
240
|
+
missing = [
|
|
241
|
+
field
|
|
242
|
+
for field in ("platform", "machine", "python", "sources")
|
|
243
|
+
if not recorded.get(field)
|
|
244
|
+
]
|
|
245
|
+
assert not missing, (
|
|
246
|
+
"golden.json is missing provenance %r — re-record with "
|
|
247
|
+
"`ngate golden record NATIVEGATE_SERVICE_NAME` to capture it" % missing
|
|
248
|
+
)
|