"""Exercise 5 - write the test that catches the bug (CS extension). Optional. Unit 8, CS path. Non-programmers can skip this without missing any core content. YOUR TASK --------- So far the checker has done the judging. Now you write the judgement. Complete ``catches_the_bug`` below. It receives some implementation of ``bernoulli`` and must return ``True`` if that implementation is faulty and ``False`` if it is sound. You may call it with any indices you like. This is harder than it sounds, and the difficulty is the lesson. Your test has to separate the four supplied faults from one correct implementation: * the other B1 convention * Note G-style non-zero-only renumbering * floating point instead of exact rationals * an off-by-one index shift (plus, as an extra target, whatever your own Exercise 4 code did). A test that only checks ``B4 == -1/30`` will let one of these straight through: the other B1 convention, which agrees with this course at B4. RUN IT ------ python3 exercises/ex05_write_a_test.py The exercise grades itself against the real examples in ``examples/``. It reports which implementations you caught, which you missed, and -- just as important -- whether you wrongly failed the correct one. """ from __future__ import annotations import sys from fractions import Fraction from pathlib import Path LAB_DIR = Path(__file__).resolve().parent.parent sys.path.insert(0, str(LAB_DIR)) def catches_the_bug(candidate) -> bool: """Return True if ``candidate`` is a faulty Bernoulli implementation. ``candidate`` is a function. Call it as ``candidate(n)`` for any ``n >= 0``. Guard against exceptions: a faulty implementation is allowed to raise. """ # TODO: replace this with your test. # # Starting point -- deliberately inadequate, it misses one of the four faults: # # try: # return candidate(4) != Fraction(-1, 30) # except Exception: # return True # raise NotImplementedError("Write your test here, then run this file.") # --------------------------------------------------------------------------- # Self-grading. You do not need to change anything below this line. # --------------------------------------------------------------------------- FAULTY = [ ("wrong_b1_sign", "the other B1 convention"), ("ada_indexing", "Note G non-zero-only indexing"), ("float_output", "floating point, not exact"), ("off_by_one_range", "index shifted by one"), ] SOUND = [("correct_example", "a correct implementation")] def _load(name: str): import importlib.util path = LAB_DIR / "examples" / f"{name}.py" spec = importlib.util.spec_from_file_location(f"ex_{name}", path) module = importlib.util.module_from_spec(spec) spec.loader.exec_module(module) return module.bernoulli def _main() -> int: try: catches_the_bug(_load("correct_example")) except NotImplementedError as exc: print(exc) return 1 print("Running your test against every example implementation.") print("-" * 66) missed, false_alarms = [], [] for name, description in FAULTY: try: verdict = bool(catches_the_bug(_load(name))) except Exception as exc: # noqa: BLE001 - the learner's test may throw print(f" ERROR {name:<20} your test raised {type(exc).__name__}: {exc}") missed.append(name) continue if verdict: print(f" caught {name:<20} ({description})") else: print(f" MISSED {name:<20} ({description})") missed.append(name) for name, description in SOUND: try: verdict = bool(catches_the_bug(_load(name))) except Exception as exc: # noqa: BLE001 print(f" ERROR {name:<20} your test raised {type(exc).__name__}: {exc}") false_alarms.append(name) continue if verdict: print(f" WRONG {name:<20} ({description}) - your test rejected a good one") false_alarms.append(name) else: print(f" passed {name:<20} ({description})") print("-" * 66) if not missed and not false_alarms: print("PASS - your test separates all four faults from the correct version.") print() print("Now notice what you had to know in order to write it. Every clause in") print("your test corresponds to a clause the specification should have had.") print("That is the same list you build in Unit 10.") return 0 print("Not yet.") if missed: print(f" Slipped through: {', '.join(missed)}") print(" Ask what property each of those violates that a correct one does not.") if false_alarms: print(f" Wrongly rejected: {', '.join(false_alarms)}") print(" A test that fails good code is worse than no test -- you will learn") print(" to ignore it. Check your comparison is exact rather than approximate.") return 1 if __name__ == "__main__": raise SystemExit(_main())