"""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 json import shutil import subprocess 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, forge_tarball 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")]), # The rest of #2990's verify list, which this scan satisfies as written: # a comment holding a WHOLE rule defines nothing, and an unterminated # comment does not swallow the file. The phantom it was filed for — # `.editor-body` out of a wrapped comment in TaskEditorView.vue, a # persisted row with a `used_by` count of zero inflating unused_css — is # among the twenty-two this removes. ("comment-containing-a-whole-rule", "/* .ghost {\n color: red;\n } */\n.real { color: blue; }\n", [("css", "real")]), ("unterminated-block-comment", "/* never closed\n.after { color: red; }\n", [("css", "after")]), # 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 ----------------------------- _tarball = forge_tarball # shared with tests/test_platforms.py 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", '