"""Pattern-library coverage (#2692) — the extractor that mirrors the hook, the shape/record matcher, and the end-to-end measurement on real Postgres. The extractor here and the hook's awk program (scribe_prior_art.sh ARM 1) must agree on what counts as "a definition" — the metric and the write-path backstop are two views of the same doctrine. The EXTRACTION_VECTORS below deliberately reuse the definitions test_write_path_trigger stages for the hook; extending one detector means extending both, and this comment is the tripwire. """ import io import json import shutil import subprocess import tarfile from pathlib import Path import pytest import pytest_asyncio from scribe.services.coverage import ( ArchiveScan, class_references, coverage_line, scan_archive, extract_shapes, largest_gaps, scannable, shapes_from_archive, ) from scribe.services.shape_ledger import location_covers from tests.helpers import ensure_user PLUGIN = Path(__file__).resolve().parents[1] / "plugin" # --- unit: the definition extractor (shared vectors with the hook) ----------- EXTRACTION_VECTORS = [ # (id, source text, expected (kind, name) list) ("python", "def make_app():\n pass\nclass Config:\n pass\n", [("sym", "make_app"), ("sym", "Config")]), ("python-dunder-skip", "class C:\n def __init__(self):\n pass\n", [("sym", "C")]), ("go-func", "func Resolve(x int) error {\n\treturn nil\n}\n", [("sym", "Resolve")]), ("go-method", "func (s *Scanner) Resolve(x int) error {\n\treturn nil\n}\n", [("sym", "Resolve")]), ("kotlin-fun", "suspend fun refreshQueue(id: Long) {\n}\n", [("sym", "refreshQueue")]), ("rust-fn", "pub async fn fetch_all() -> u32 {\n 0\n}\n", [("sym", "fetch_all")]), ("go-type", "type ForgeAdapter struct {\n\tname string\n}\n", [("sym", "ForgeAdapter")]), ("rust-pub-crate", "pub(crate) struct Widget {}\n", [("sym", "Widget")]), ("js-export-default", "export default function App() {}\n", [("sym", "App")]), ("js-arrow", "const useThing = (id) => id;\nlet fetcher = async () => 0;\n", [("sym", "useThing"), ("sym", "fetcher")]), # Every selector line in a group counts — .btn-ghost, and .btn-text { } # both announce a class, exactly as the hook's awk sees them. ("css", ".btn-primary {\n color: red;\n}\n.btn-ghost,\n.btn-text { }\n", [("css", "btn-primary"), ("css", "btn-ghost"), ("css", "btn-text")]), # Call sites, imports, and impl blocks are NOT definitions — matching # them would drown the metric exactly as it would drown the hook. ("non-definitions", "make_app()\nimpl Widget {\nreturn fetch_all\nimport os\nx = 1\n", []), ("dedup-within-file", "def f():\n pass\ndef f():\n pass\n", [("sym", "f")]), # --- comment and string spans (#4222) ------------------------------------ # Prose is not code. A wrapped docstring line beginning "class AND the" # announced a shape called `AND` to a live session; `with` and `nobody` # out of one module docstring in scripts/check_dangling_styles.py reached # persisted, judged `code_shapes` rows. ("docstring-prose", 'def real_one():\n """Its own text is about the\n' ' class AND the to_dict, and is a def bar():\n' ' class Foo: lives here too.\n """\n pass\n', [("sym", "real_one")]), # An opener with no closer blanks NOTHING: the scan rewinds past it, so a # stray marker costs one span rather than the rest of the file. ("unterminated-docstring", 'def before():\n """oops, never closed\n\ndef after():\n pass\n', [("sym", "before"), ("sym", "after")]), # The CSS half of the same defect (#2990): a wrapped comment line that # happens to begin with a dotted token reads as a selector. ("css-comment-selector", "/* A real base rule, not just descendants: the check reads a\n" " .ghost, class that only ever appears as an ancestor */\n.check { }\n", [("css", "check")]), # A string that HOLDS a comment marker is not a comment — the case that # made the first draft of the scan eat 70 lines of live code. ("string-holds-a-marker", 'SAMPLE = "red /* "\ndef after_the_string():\n pass\n', [("sym", "after_the_string")]), # `#` is the one marker whose meaning is the language\'s: a colour here, # a comment two lines down, and the extractor is handed no path. ("hash-is-a-colour-not-a-comment", ".a { color: #fff; } /* .ghost,\n class Phantom: */\n.b { }\n", [("css", "a"), ("css", "b")]), ("line-comment-mentioning-a-docstring", 'def kept():\n pass\n# a stray """ in a comment\n' 'def also_kept():\n pass\n', [("sym", "kept"), ("sym", "also_kept")]), ] @pytest.mark.parametrize( ("text", "expected"), [(t, e) for _i, t, e in EXTRACTION_VECTORS], ids=[i for i, _t, _e in EXTRACTION_VECTORS], ) def test_extractor_agrees_with_the_hook_on_what_defines(text, expected): assert extract_shapes(text) == expected @pytest.mark.parametrize( "text", [t for _i, t, _e in EXTRACTION_VECTORS], ids=[i for i, _t, _e in EXTRACTION_VECTORS], ) def test_the_hook_extractor_runs_and_agrees_line_for_line(text): """The comment at the top of this module has been the only thing holding the two extractors together, and a comment cannot fail. This RUNS the hook's awk program over the same vectors. The hook emits every definition in source order with no dedup — identity there is per payload, not per file — so the comparison de-dupes its output before matching, which is the one difference between the two that is by design. """ if shutil.which("awk") is None: # pragma: no cover - env guard pytest.skip("awk not available") lib = PLUGIN / "hooks" / "scribe_defs.sh" out = subprocess.run( ["bash", "-c", f'. "{lib}"; scribe_defs'], input=text, capture_output=True, text=True, ) assert out.returncode == 0, out.stderr seen: list[tuple[str, str]] = [] for line in out.stdout.splitlines(): kind, _, name = line.partition("\t") if name and (kind, name) not in seen: seen.append((kind, name)) assert seen == extract_shapes(text) def test_scannable_gates_prose_vendored_and_sourcemaps(): assert scannable("src/app.py") assert scannable("web/button.css") assert scannable(".gitea/workflows/ci.yml") # config IS worth recording assert not scannable("README.md") assert not scannable("dist/bundle.js.map") assert not scannable("node_modules/x/index.js") assert not scannable("web/node_modules/y/util.ts") # A FILE named like a skip-dir is not a directory hit. assert scannable("src/vendor.py") # --- unit: reading shapes out of a forge tarball ----------------------------- def _tarball(files: dict[str, bytes], top: str = "widget") -> bytes: buf = io.BytesIO() with tarfile.open(fileobj=buf, mode="w:gz") as tar: for path, data in files.items(): info = tarfile.TarInfo(f"{top}/{path}") info.size = len(data) tar.addfile(info, io.BytesIO(data)) return buf.getvalue() TREE = { "src/app.py": b"def make_app():\n pass\nclass Config:\n def __init__(self):\n pass\n", "src/util.py": b"def helper():\n pass\n", "web/button.css": b".btn {\n color: red;\n}\n", "README.md": b"def not_code(): pass\n", "node_modules/x/index.js": b"function vendored() {}\n", "data.bin": b"\xff\xfe\x00\x01", } # What TREE holds once the gates run: 4 shapes, none from the skipped files. TREE_SHAPES = [ ("src/app.py", "sym", "make_app"), ("src/app.py", "sym", "Config"), ("src/util.py", "sym", "helper"), ("web/button.css", "css", "btn"), ] def test_shapes_from_archive_strips_the_wrapper_and_gates_files(): assert shapes_from_archive(_tarball(TREE)) == TREE_SHAPES # --- unit: template class references — the CSS consumer map (milestone 302) -- def test_class_references_reads_vue_static_and_dynamic_forms_only(): """A template's class attributes name the classes it consumes: the static `class=`, the Vue dynamic object/array/ternary forms (string literals and bare object keys), never a selector in \n" ) assert class_references("a/B.vue", vue) == { "card": 2, "card--wide": 1, "active": 1, "is-error": 1, "pill": 1, "pill-on": 1, "pill-off": 1, "closed": 1, } def test_class_references_reads_framework_transition_names(): """A transition `name=` is a class reference: the framework applies `.toast-enter-active` and friends at runtime, so a stylesheet that defines them is consumed even though no template spells one out (#2970). Every spelling of the tag counts; a bound `:name` stays unknowable.""" refs = class_references( "a/T.vue", '
', ) for suffix in ("-enter-from", "-enter-active", "-leave-to", "-move"): assert refs["toast" + suffix] == 1, suffix assert refs["toast-item"] == 1 # the static attribute still counts assert "peek-enter-active" in class_references("a/T.vue", '') assert "g-move" in class_references("a/T.vue", '') # React's CSSTransition names the same idea with a different suffix set react = class_references("a/T.jsx", '') assert {"fade-enter-active", "fade-exit-active", "fade-exit-done"} <= set(react) # A bound name is a variable, not a name we can read assert class_references("a/T.vue", '') == {} def test_class_references_reads_a_concatenated_name_as_a_prefix(): """`status-${s}` cannot be resolved to one class, but its static head is real information: it is emitted as the prefix reference `status-*` so the rows it could have built are not reported unused (#2970). A head too short to mean anything, or a bare separator, says nothing.""" vue = ( '
' "" '' ) assert class_references("a/P.vue", vue) == {"status-*": 1, "pri-*": 1} # a server template interpolating into the middle of a name, same reading assert class_references("t/p.html", '') == {"card-*": 1} # an ordinary name never picks up the marker assert class_references("a/P.vue", '