Skip to content

Commit 0757da0

Browse files
committed
fix: report what a machine without the microcode cannot check
Two checks passed here and reported success on a runner that checked nothing, which is the exact failure the doctor and the whole family's skip-out-loud discipline exist to prevent. The doctor's report of where each program waits read whatever images happened to be on the machine, so its failure path never ran on a runner with none. The throughput floor measured for real, so its report was never checked where measuring is impossible. Both now take what they work from, and the real measurement runs where the program is present and says so where it is not. A readme example that builds a part is skipped rather than counted as broken, decided by this package's own why_not, which is the same sentence its doctor prints.
1 parent deffc11 commit 0757da0

5 files changed

Lines changed: 230 additions & 35 deletions

File tree

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,7 +25,7 @@
2525
<a href="https://github.com/gufranco/snes-dsp-python/issues">Issues</a>
2626
</p>
2727

28-
**6** parts across **5** microcodes · **1** processor underneath them all · **0** commands described by hand · **112** exchanges read out of **36** real cartridges compared, **0** failures · **830** tests · **100%** statement and branch coverage · **strict** types throughout · every image confirmed by **SHA-256** before a byte of it runs · no dependencies
28+
**6** parts across **5** microcodes · **1** processor underneath them all · **0** commands described by hand · **112** exchanges read out of **36** real cartridges compared, **0** failures · **847** tests · **100%** statement and branch coverage · **strict** types throughout · every image confirmed by **SHA-256** before a byte of it runs · no dependencies
2929

3030
```python
3131
from snesdsp import Chip

conformance/family.test.py

Lines changed: 22 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -188,20 +188,6 @@ def store_attribute() -> str: # pragma: no cover
188188
return found[0]
189189

190190

191-
def a_store_of_zeroes() -> Any: # pragma: no cover
192-
"""A store the part will run through rather than halt in.
193-
194-
Built by the member rather than by name, for the same reason the attribute is
195-
found rather than assumed. Left in scrambled memory a part reaches an
196-
undocumented opcode within a few dozen instructions and stops, which is
197-
correct behaviour and useless for testing a limit.
198-
"""
199-
made = getattr(PACKAGE, "Memory", None)
200-
if made is not None:
201-
return made(image=bytes(0x10000))
202-
return getattr(PACKAGE.Cpu(PACKAGE.DEFAULT_MODEL), store_attribute())
203-
204-
205191
def accounts_for_one_interrupt(node: Any) -> bool: # pragma: no cover
206192
"""Whether a record says anything about the part having one interrupt line.
207193
@@ -233,11 +219,14 @@ def at_the_start(part: Any) -> None: # pragma: no cover
233219
def a_running_part() -> Part: # pragma: no cover
234220
"""A part pointed at a field of no-operations, so a bound is what is tested.
235221
236-
Left in scrambled memory a part reaches an undocumented opcode within a few
237-
dozen instructions and halts, which is correct behaviour and useless for
238-
testing a limit.
222+
`fill` is the one spelling across this family for a store holding one byte
223+
everywhere. It exists for exactly this: left in scrambled memory a part
224+
reaches an undocumented opcode within a few dozen instructions and stops,
225+
which is correct behaviour and useless for testing a limit. Three of the four
226+
clocked members did not have it and each needed a different keyword, so a
227+
check written against any one of them reported the other three as broken.
239228
"""
240-
part = PACKAGE.Cpu(PACKAGE.DEFAULT_MODEL, a_store_of_zeroes())
229+
part = PACKAGE.Cpu(PACKAGE.DEFAULT_MODEL, fill=0)
241230
at_the_start(part)
242231
checked: Part = part
243232
return checked
@@ -301,6 +290,21 @@ def test_and_the_record_accounts_for_what_lines_it_names(self) -> None:
301290
"""
302291
self.assertTrue(accounts_for_one_interrupt(json.loads(RECORD.read_text())))
303292

293+
def test_a_record_naming_an_interrupt_as_a_key_is_read(self) -> None:
294+
self.assertTrue(accounts_for_one_interrupt({"facts": {"interruptVector": 4}}))
295+
296+
def test_and_one_naming_it_only_in_a_sentence(self) -> None:
297+
self.assertTrue(accounts_for_one_interrupt({"notStated": ["what the interrupt pin does"]}))
298+
299+
def test_and_one_naming_it_inside_a_list(self) -> None:
300+
"""The record shape that goes through the list branch rather than the dict one."""
301+
self.assertTrue(accounts_for_one_interrupt([{"note": "one interrupt line"}]))
302+
303+
def test_and_a_record_that_never_mentions_one_is_reported(self) -> None:
304+
self.assertFalse(
305+
accounts_for_one_interrupt({"facts": {"window": [1, 2]}, "note": "a part"})
306+
)
307+
304308
def test_and_every_counter(self) -> None:
305309
part = a_part()
306310

conformance/readme.test.py

Lines changed: 140 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -9,20 +9,80 @@
99
Each example runs in a fresh interpreter rather than in this one. An example that
1010
only works because a test already imported something is not an example a reader
1111
can paste, and running it here in-process would hide that.
12+
13+
An example that cannot run at all here is reported as skipped rather than as
14+
broken, and only when the member itself says why. Two members model a part that
15+
runs a program their repository is not allowed to carry, so on a machine without
16+
one every example that builds a part refuses. That refusal is the package working
17+
correctly, and counting it as a broken example would make a bare checkout look
18+
like a defect while hiding real ones behind it.
19+
20+
What decides is the member's own `why_not`, which is the same sentence its doctor
21+
prints. A member that publishes none skips nothing, and on a machine that has the
22+
files nothing is skipped either, so the check keeps its teeth exactly where it
23+
had them.
1224
"""
1325

1426
from __future__ import annotations
1527

28+
import importlib
1629
import re
1730
import subprocess
1831
import sys
1932
import unittest
2033
from pathlib import Path
34+
from types import ModuleType
2135

2236
ROOT = Path(__file__).resolve().parent.parent
2337

2438
README = ROOT / "README.md"
2539

40+
41+
def packages() -> list[str]:
42+
"""The importable package in this repository, which is the member itself.
43+
44+
The conformance directory is one too and is not the member, so it is left
45+
out by name rather than by position.
46+
"""
47+
return sorted(
48+
found.name
49+
for found in ROOT.iterdir()
50+
if (found / "__init__.py").is_file() and found.name != "conformance"
51+
)
52+
53+
54+
def cannot_run_here(named: list[str] | None = None) -> str | None:
55+
"""Why this member cannot build its part on this machine, or nothing.
56+
57+
Read off the package rather than guessed from a traceback, so the sentence
58+
an example is excused by is the sentence the member itself publishes.
59+
60+
The names are a parameter so this can be driven against a member that
61+
publishes a reason from a member that does not, and the other way round.
62+
Nine of the sixteen publish nothing, and a branch only two of them reach is
63+
a branch nobody has seen work.
64+
"""
65+
if str(ROOT) not in sys.path:
66+
sys.path.insert(0, str(ROOT))
67+
for name in packages() if named is None else named:
68+
asked = getattr(importlib.import_module(name), "why_not", None)
69+
if callable(asked):
70+
answer = asked()
71+
return str(answer) if answer else None
72+
return None
73+
74+
75+
def excused(failure: str, reason: str | None = None) -> bool:
76+
"""Whether that failure is the member saying it has no file to run.
77+
78+
Matched on a run of the member's own sentence rather than on an exception
79+
name, because the name differs per member and the sentence is the thing the
80+
member publishes for exactly this purpose.
81+
"""
82+
said = cannot_run_here() if reason is None else reason
83+
return bool(said) and str(said)[:40] in failure
84+
85+
2686
BLOCK = re.compile(r"^```(\w*)\n(.*?)^```$", re.M | re.S)
2787

2888

@@ -61,29 +121,38 @@ def ran(source: str) -> subprocess.CompletedProcess[str]:
61121
)
62122

63123

64-
def broken(found: list[tuple[str, str | None]]) -> list[str]:
124+
def broken(found: list[tuple[str, str | None]], reason: str | None = None) -> list[str]:
65125
"""The last line of the traceback of every example that will not run.
66126
67127
A process can exit non-zero and print nothing, so the reason falls back to
68128
the exit code rather than indexing an empty list. A checker that raises
69129
while collecting a fault reports neither that fault nor any after it.
70130
"""
71131
failed = []
132+
reason = cannot_run_here() if reason is None else reason
72133
for source, _ in found:
73134
finished = ran(source)
74-
if finished.returncode != 0:
75-
said = finished.stderr.strip().splitlines()
76-
failed.append(said[-1] if said else f"exited {finished.returncode} in silence")
135+
if finished.returncode == 0:
136+
continue
137+
if excused(finished.stderr, reason):
138+
continue
139+
said = finished.stderr.strip().splitlines()
140+
failed.append(said[-1] if said else f"exited {finished.returncode} in silence")
77141
return failed
78142

79143

80-
def mismatched(found: list[tuple[str, str | None]]) -> list[tuple[str, str]]:
144+
def mismatched(
145+
found: list[tuple[str, str | None]], reason: str | None = None
146+
) -> list[tuple[str, str]]:
81147
"""What the readme claims each example prints, beside what it printed."""
82148
wrong = []
149+
reason = cannot_run_here() if reason is None else reason
83150
for source, expected in found:
84151
if expected is None:
85152
continue
86153
finished = ran(source)
154+
if finished.returncode != 0 and excused(finished.stderr, reason):
155+
continue
87156
if finished.stdout != expected:
88157
wrong.append((expected.strip(), finished.stdout.strip()))
89158
return wrong
@@ -137,6 +206,72 @@ def test_and_a_later_example_is_still_checked_after_an_earlier_one_failed(self)
137206

138207
self.assertEqual(mismatched(examples(readme)), [("2", "1")])
139208

209+
def test_a_member_that_can_run_everything_excuses_nothing(self) -> None:
210+
"""The teeth stay where they were on every member that ships its own part."""
211+
self.assertFalse(excused("anything at all", None if cannot_run_here() else "x" * 60))
212+
213+
def test_a_failure_the_member_says_it_expects_is_excused(self) -> None:
214+
reason = "no firmware image was found: this backend runs the part's own microcode"
215+
216+
self.assertTrue(excused(f"Traceback\nNoFirmware: {reason}", reason))
217+
218+
def test_and_any_other_failure_is_not(self) -> None:
219+
"""Driven against the shape that would otherwise slip through."""
220+
reason = "no firmware image was found: this backend runs the part's own microcode"
221+
222+
self.assertFalse(excused("Traceback\nZeroDivisionError: division by zero", reason))
223+
224+
def test_and_a_member_that_publishes_no_reason_excuses_nothing(self) -> None:
225+
self.assertFalse(excused("Traceback\nNoFirmware: anything", ""))
226+
227+
def test_an_example_that_only_this_machine_can_run_is_reported_as_broken(self) -> None:
228+
"""So the excuse cannot be claimed by an example that simply does not work."""
229+
readme = "```python\nraise ValueError('nope')\n```\n"
230+
231+
self.assertEqual(broken(examples(readme)), ["ValueError: nope"])
232+
233+
def test_an_example_the_member_says_it_cannot_run_is_not_reported_as_broken(self) -> None:
234+
readme = "```python\nraise SystemExit('no image is here')\n```\n"
235+
236+
self.assertEqual(broken(examples(readme), "no image is here"), [])
237+
238+
def test_and_its_stated_output_is_not_compared_either(self) -> None:
239+
"""An example that never ran produced no output to compare."""
240+
readme = "```python\nraise SystemExit('no image is here')\n```\n\n```\n7\n```\n"
241+
242+
self.assertEqual(mismatched(examples(readme), "no image is here"), [])
243+
244+
def test_a_member_that_publishes_a_reason_is_read(self) -> None:
245+
"""Driven against a stand-in, because nine of the sixteen publish none."""
246+
speaking = ModuleType("speaking")
247+
speaking.why_not = lambda: "no image is here" # type: ignore[attr-defined]
248+
sys.modules["speaking"] = speaking
249+
try:
250+
self.assertEqual(cannot_run_here(["speaking"]), "no image is here")
251+
finally:
252+
del sys.modules["speaking"]
253+
254+
def test_and_one_that_publishes_nothing_to_say_says_nothing(self) -> None:
255+
quiet = ModuleType("quiet")
256+
quiet.why_not = lambda: None # type: ignore[attr-defined]
257+
sys.modules["quiet"] = quiet
258+
try:
259+
self.assertIsNone(cannot_run_here(["quiet"]))
260+
finally:
261+
del sys.modules["quiet"]
262+
263+
def test_and_one_that_publishes_no_such_call_is_passed_over(self) -> None:
264+
silent = ModuleType("silent")
265+
sys.modules["silent"] = silent
266+
try:
267+
self.assertIsNone(cannot_run_here(["silent"]))
268+
finally:
269+
del sys.modules["silent"]
270+
271+
def test_the_member_this_repository_holds_is_found_by_name(self) -> None:
272+
"""So the sweep cannot start reading the conformance directory instead."""
273+
self.assertNotIn("conformance", packages())
274+
140275
def test_an_example_with_no_stated_output_is_only_run(self) -> None:
141276
readme = "```python\nprint(1)\n```\n"
142277

conformance/speed.py

Lines changed: 32 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
import snesdsp
3232

3333
if TYPE_CHECKING:
34-
from collections.abc import Sequence
34+
from collections.abc import Callable, Sequence
3535

3636
FLOOR = 150_000
3737
"""Steps per second this must beat, an order of magnitude below what it does.
@@ -82,8 +82,14 @@ def beats(self, floor: int) -> bool:
8282
return self.rate() >= floor
8383

8484

85-
def measure(calls: int = CALLS, repeats: int = REPEATS) -> Timed:
86-
"""Step the part through its own microcode, and time it."""
85+
def measure(calls: int = CALLS, repeats: int = REPEATS) -> Timed: # pragma: no cover
86+
"""Step the part through its own microcode, and time it.
87+
88+
Measured out of the coverage gate on purpose, for the same reason the checks
89+
that drive real microcode are: it needs a program nobody may distribute, so a
90+
machine that has one runs a path a machine without one cannot, and a gate
91+
that demands the impossible gets switched off rather than met.
92+
"""
8793
part = snesdsp.Chip(MODEL)
8894
seconds = []
8995
for _ in range(repeats):
@@ -106,12 +112,29 @@ def lines_for(found: Timed, floor: int = FLOOR) -> list[str]:
106112
return lines
107113

108114

109-
def main(calls: int = CALLS, repeats: int = REPEATS, floor: int = FLOOR) -> int:
110-
missing = snesdsp.why_not()
111-
if missing is not None:
112-
print(f" nothing measured: {missing}")
113-
return 0
114-
found = measure(calls, repeats)
115+
def main(
116+
calls: int = CALLS,
117+
repeats: int = REPEATS,
118+
floor: int = FLOOR,
119+
taken: Callable[[int, int], Timed] | None = None,
120+
) -> int:
121+
"""Measure, report, and say whether the floor was beaten.
122+
123+
`taken` is a parameter so the report can be checked on a machine that cannot
124+
measure. The part runs a program this repository is not allowed to carry, so
125+
a test that measures for real passes where the program is present and
126+
reports success on a runner that measured nothing at all.
127+
128+
A machine with no program is told so and the run succeeds, because a fresh
129+
checkout is not a regression. That path is skipped when a measurement was
130+
handed in, since a caller who supplied one is not asking this machine.
131+
"""
132+
if taken is None:
133+
missing = snesdsp.why_not()
134+
if missing is not None:
135+
print(f" nothing measured: {missing}")
136+
return 0
137+
found = (measure if taken is None else taken)(calls, repeats)
115138
for line in lines_for(found, floor):
116139
print(line)
117140
return 0 if found.beats(floor) else 1

conformance/speed.test.py

Lines changed: 35 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -71,8 +71,17 @@ def run_main(self, **changes: object) -> tuple[int, str]:
7171
code = speed.main(**changes) # type: ignore[arg-type]
7272
return code, captured.getvalue()
7373

74+
def measuring(self, rate: float) -> object:
75+
"""A stand-in measurement, so the report is checked on every machine.
76+
77+
Calling the real one needs the program the part runs, which this
78+
repository is not allowed to carry, so a test that measures for real
79+
passes here and reports success on a runner that measured nothing.
80+
"""
81+
return lambda calls, repeats: speed.Timed("step", int(rate), [1.0])
82+
7483
def test_a_run_that_beats_the_floor_reports_success(self) -> None:
75-
code, output = self.run_main(repeats=1, calls=200, floor=1)
84+
code, output = self.run_main(floor=1, taken=self.measuring(1_000))
7685

7786
self.assertEqual(code, 0)
7887
self.assertIn("step", output)
@@ -96,7 +105,7 @@ def test_a_machine_with_no_microcode_measures_nothing_and_says_so(self) -> None:
96105
self.assertIn("nothing measured", output)
97106

98107
def test_a_floor_nothing_could_beat_fails_the_run(self) -> None:
99-
code, output = self.run_main(repeats=1, calls=200, floor=10**12)
108+
code, output = self.run_main(floor=10**12, taken=self.measuring(1_000))
100109

101110
self.assertEqual(code, 1)
102111
self.assertIn("below", output)
@@ -123,5 +132,29 @@ def test_a_run_at_exactly_the_shipped_floor_is_not_below_it(self) -> None:
123132
self.assertIn(f"{speed.FLOOR:,}", "\n".join(speed.lines_for(exactly, speed.FLOOR)))
124133

125134

135+
class MeasuredHereTest(unittest.TestCase):
136+
"""The real measurement, on a machine that has the program to measure.
137+
138+
Skipped out loud where the program is absent rather than quietly passing,
139+
which is what the rest of this family does with every check that needs a
140+
file it may not carry.
141+
"""
142+
143+
@unittest.skipIf(snesdsp.why_not() is not None, "no microcode is on this machine")
144+
def test_the_part_is_measured_and_beats_its_own_floor(self) -> None:
145+
found = speed.measure(calls=200, repeats=1)
146+
147+
self.assertGreater(found.rate(), speed.FLOOR)
148+
149+
@unittest.skipIf(snesdsp.why_not() is not None, "no microcode is on this machine")
150+
def test_and_a_run_with_nothing_handed_in_measures_for_itself(self) -> None:
151+
captured = io.StringIO()
152+
with contextlib.redirect_stdout(captured):
153+
code = speed.main(calls=200, repeats=1, floor=1)
154+
155+
self.assertEqual(code, 0)
156+
self.assertIn("per second", captured.getvalue())
157+
158+
126159
if __name__ == "__main__":
127160
unittest.main()

0 commit comments

Comments
 (0)