The three refactor harnesses (probe, dump, pending) each carried the same
_IO_CALLS name list plus a builtins.open spy. Measured at bb4a85e in a clean
clone: Path(__file__).read_text(), os.open(__file__, O_RDONLY), io.open(__file__)
and with Path(__file__).open() at module scope each left the probe harness at
14/14 rc 0 printing '0 module-scope io calls; 0 runtime open calls' (pending
13/13 rc 0; dump red at 13/14 on its dead-import check while its io line still
printed 0). The name-list walk resolves Path(p).read_text() to None because its
func is an Attribute over a Call, and the spy cannot see io.open/pathlib.
Now: wizard-work/forge/import_io_detector.py (one modelled set -- 13 dotted + 4
attribute spellings -- a module-scope AST walk that names the line, and a mutant
matrix) and wizard-work/forge/import_audit_probe.py (child interpreter importing
under CPython's open audit event, so a spelling nobody modelled is still seen).
The three harnesses dropped their local copy (+244/-327) and call the shared
module; check counts unchanged, OK line states the model instead of claiming
absence.
Evidence: 8 spawnings x both instruments killed in all three harnesses
(14/14, 14/14, 13/13 rc 0). Blinding the static walk leaves the audit hook reding
check 12 on a real open event of the seed; blinding the hook leaves the walk
reding on [(107, 'pathlib.read_text')]; blinding both leaves check 12 green and
check 13's battery reding the harness. _F = open / _F(DEFAULT_PATH) -- a spelling
no list names -- is caught by the hook alone.
Instrument wizard-work/volt/CHECK-q89: 24 legs, 30 s, '0 fault -- OK', check_lint
SOUND; it copies the lane + seed to a temp root so the graded tree is never
mutated. Fourth copy (verify_dump_seed_refactor.py) untouched at 16/16 and used
as an independent oracle: agrees 12/12. Q90's beam guard untouched.
verify.py --fast: 10 legs, 9 pass, 0 fault, 1 skipped, gate OK.
113 lines
3.7 KiB
Python
113 lines
3.7 KiB
Python
#!/usr/bin/env python3
|
|
"""Child interpreter for ``import_io_detector.audit_reads``.
|
|
|
|
Import ``MODULE_PATH`` (argv[1]) with an audit hook on CPython's ``open`` event
|
|
and print ONE json object on stdout::
|
|
|
|
{"target": "<realpath>", "events": [{"path": ..., "mode": ..., "flags": ...}],
|
|
"import_error": null | "Type: message"}
|
|
|
|
Exit is 0 whenever that json was printed -- an import that raised is *reported*
|
|
in ``import_error``, not treated as a probe failure, because a mutant may raise
|
|
after the read and the read is what is being graded.
|
|
|
|
Why a child process: ``sys.addaudithook`` cannot be uninstalled, so an
|
|
in-process hook would keep watching the harness for the rest of its life.
|
|
|
|
What is filtered, and why (each rule is a claim, stated so it can be argued
|
|
with):
|
|
|
|
* the target module's own source and bytecode -- the import itself
|
|
* ``.py``/``.pyc``/``.pyo``/``.pyd``/``.so`` open from a directory on
|
|
``sys.path`` or under the interpreter's prefix -- the import
|
|
machinery loading code, not the module reading data
|
|
* opens whose mode/flags are write-only -- this grades reads;
|
|
``sys.dont_write_bytecode`` is set so a ``__pycache__`` write would not
|
|
even be attempted
|
|
|
|
Everything else is reported: a data file, a config, a seed, whatever spelling
|
|
of ``open`` produced it.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import importlib.util
|
|
import json
|
|
import os
|
|
import sys
|
|
|
|
sys.dont_write_bytecode = True
|
|
|
|
TARGET = os.path.realpath(sys.argv[1])
|
|
CODE_EXT = (".py", ".pyc", ".pyo", ".pyd", ".so")
|
|
|
|
_DIRS = sorted({
|
|
os.path.realpath(d)
|
|
for d in [*sys.path, sys.base_prefix, sys.prefix]
|
|
if d and os.path.isdir(d)
|
|
})
|
|
|
|
|
|
def _is_code(path: str) -> bool:
|
|
"""True when this open is the import machinery loading code."""
|
|
real = os.path.realpath(path)
|
|
if real in (TARGET, TARGET + "c"):
|
|
return True
|
|
if real.endswith(CODE_EXT):
|
|
return any(real == d or real.startswith(d + os.sep) for d in _DIRS)
|
|
return False
|
|
|
|
|
|
def _is_write(mode, flags) -> bool:
|
|
"""True when the open cannot be a read (so it is out of this probe's scope)."""
|
|
if isinstance(mode, str) and mode:
|
|
return any(c in mode for c in "wax")
|
|
try:
|
|
value = int(flags)
|
|
except (TypeError, ValueError):
|
|
return False
|
|
return bool(value & (os.O_WRONLY | os.O_CREAT | os.O_TRUNC | os.O_APPEND))
|
|
|
|
|
|
def main() -> int:
|
|
events: list = []
|
|
|
|
def hook(event, args):
|
|
if event != "open":
|
|
return
|
|
path = args[0] if args else None
|
|
mode = args[1] if len(args) > 1 else None
|
|
flags = args[2] if len(args) > 2 else None
|
|
if path is None or _is_write(mode, flags):
|
|
return
|
|
try:
|
|
spelling = os.fspath(path)
|
|
except TypeError:
|
|
spelling = str(path)
|
|
if isinstance(spelling, bytes):
|
|
spelling = spelling.decode("utf-8", "replace")
|
|
if _is_code(spelling):
|
|
return
|
|
events.append({
|
|
"path": spelling,
|
|
"mode": mode if isinstance(mode, str) else None,
|
|
"flags": flags if isinstance(flags, int) else None,
|
|
})
|
|
|
|
sys.addaudithook(hook)
|
|
error = None
|
|
try:
|
|
spec = importlib.util.spec_from_file_location("_audited_import", TARGET)
|
|
module = importlib.util.module_from_spec(spec)
|
|
sys.modules["_audited_import"] = module
|
|
spec.loader.exec_module(module)
|
|
except BaseException as exc: # report, do not grade, the error
|
|
error = "%s: %s" % (type(exc).__name__, exc)
|
|
json.dump({"target": TARGET, "events": events, "import_error": error},
|
|
sys.stdout)
|
|
sys.stdout.write("\n")
|
|
return 0
|
|
|
|
|
|
if __name__ == "__main__":
|
|
sys.exit(main())
|