diff --git a/c/Makefile b/c/Makefile index 495b20171..10d250ec9 100644 --- a/c/Makefile +++ b/c/Makefile @@ -1354,8 +1354,8 @@ tests/test_ue8m0$(EXE): tests/test_ue8m0.c st.h json.h compat.h # Compiles the engine with -DCOLI_XDNA so the QT side pointer, its reset helper # and the expert-slot wiring are exercised as the engine actually builds them. -tests/test_xdna_qt_state$(EXE): tests/test_xdna_qt_state.c colibri.c backend_xdna.c backend_xdna.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h route_trace.h - $(CC) $(CFLAGS) -DCOLI_XDNA $< backend_xdna.c -o $@ $(LDFLAGS) +tests/test_xdna_qt_state$(EXE): tests/test_xdna_qt_state.c colibri.c backend_xdna.c backend_xdna.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) + $(CC) $(CFLAGS) -DCOLI_XDNA $< backend_xdna.c $(VK_OBJ) -o $@ $(LDFLAGS) tests/test_xdna_prepared_state$(EXE): tests/test_xdna_prepared_state.c backend_xdna.c backend_xdna.h compat.h $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) @@ -1381,8 +1381,8 @@ tests/test_xdna_execution$(EXE): tests/test_xdna_execution.c backend_xdna.c back # Failure/fallback owner. Needs BOTH the engine (matmul_qt is the fallback # target it verifies against) and the synthetic ABI-2 helpers, which is why it # is a separate owner from test_xdna_execution. -tests/test_xdna_failure$(EXE): tests/test_xdna_failure.c colibri.c backend_xdna.c backend_xdna.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h route_trace.h $(XDNA_FAKE_HELPERS) - $(CC) $(CFLAGS) -DCOLI_XDNA $< backend_xdna.c -o $@ $(LDFLAGS) +tests/test_xdna_failure$(EXE): tests/test_xdna_failure.c colibri.c backend_xdna.c backend_xdna.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h route_trace.h $(XDNA_FAKE_HELPERS) $(VK_OBJ) + $(CC) $(CFLAGS) -DCOLI_XDNA $< backend_xdna.c $(VK_OBJ) -o $@ $(LDFLAGS) # NOT a test gate -- intentionally absent from TEST_BINS (its name does not match # the tests/test_*$(EXE) pattern the gate list is derived from). It needs a real @@ -1393,8 +1393,8 @@ tests/test_xdna_failure$(EXE): tests/test_xdna_failure.c colibri.c backend_xdna. # # M-list defaults to 1,32,64 (the M64 bucket); pass 65,130,256 to qualify # the M256 bucket through the same owner. -tests/xdna_physical_probe$(EXE): tests/xdna_physical_probe.c colibri.c backend_xdna.c backend_xdna.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h route_trace.h - $(CC) $(CFLAGS) -DCOLI_XDNA $< backend_xdna.c -o $@ $(LDFLAGS) +tests/xdna_physical_probe$(EXE): tests/xdna_physical_probe.c colibri.c backend_xdna.c backend_xdna.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h sample.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) + $(CC) $(CFLAGS) -DCOLI_XDNA $< backend_xdna.c $(VK_OBJ) -o $@ $(LDFLAGS) tests/test_json$(EXE): tests/test_json.c json.h $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) @@ -1542,14 +1542,14 @@ tests/test_grammar_cache$(EXE): tests/test_grammar_cache.c sse41_kernels.h colib # agreement check against the plain logprob_target the sampling path uses # on rows a float subtraction cannot mis-round, and a double-precision # check against a second double computation on rows that do. -tests/test_logprob_status$(EXE): tests/test_logprob_status.c colibri.c sse41_kernels.h exact_dot.h oracle.h sample.h evidence_digest.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h kv_persist.h telemetry.h route_trace.h - $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +tests/test_logprob_status$(EXE): tests/test_logprob_status.c colibri.c sse41_kernels.h exact_dot.h oracle.h sample.h evidence_digest.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) + $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) # The ablation mode's manifest loader, evidence writer and dispatch contract. # The adapter build bypasses only the model computation, so the parser/writer # path is exercised for real with no model and no weights. -tests/test_ablate_mode$(EXE): tests/test_ablate_mode.c colibri.c sse41_kernels.h exact_dot.h oracle.h sample.h evidence_digest.h abl.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h kv_persist.h telemetry.h route_trace.h - $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +tests/test_ablate_mode$(EXE): tests/test_ablate_mode.c colibri.c sse41_kernels.h exact_dot.h oracle.h sample.h evidence_digest.h abl.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) + $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) # Standalone: drives a faithful miniature of moe()'s routing+accumulate and links # the SAME abl.h the engine links -- no model/weights needed (the ablation-logic gate). @@ -1650,8 +1650,8 @@ tests/test_topp$(EXE): tests/test_topp.c sse41_kernels.h colibri.c oracle.h st.h # bench_topp is a microbenchmark (old qsort vs new heap partial-select, #335), NOT a test # gate -- intentionally absent from TEST_BINS. Build on demand: make tests/bench_topp -tests/bench_topp$(EXE): tests/bench_topp.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h - $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +tests/bench_topp$(EXE): tests/bench_topp.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) + $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) tests/test_sample_nan$(EXE): tests/test_sample_nan.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) @@ -2170,13 +2170,13 @@ tests/test_ssd_probe$(EXE): tests/test_ssd_probe.c sse41_kernels.h colibri.c ora # bench_dsa_select is a microbenchmark (old qsort vs new quickselect partial-select, #356), # NOT a test gate -- intentionally absent from TEST_BINS. Build on demand: make tests/bench_dsa_select -tests/bench_dsa_select$(EXE): tests/bench_dsa_select.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h - $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +tests/bench_dsa_select$(EXE): tests/bench_dsa_select.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) + $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) # bench_router_select is a microbenchmark (duplicate-prefix scan vs marked-score scan), # not a test gate. Build on demand: make tests/bench_router_select. -tests/bench_router_select$(EXE): tests/bench_router_select.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h - $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +tests/bench_router_select$(EXE): tests/bench_router_select.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) + $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) # bench_indexer_allocations: microbenchmark (DeepSeek V4 indexer malloc vs persistent arena scratch), NOT a test gate. # Build on demand: make tests/bench_indexer_allocations @@ -2185,24 +2185,24 @@ tests/bench_indexer_allocations$(EXE): tests/bench_indexer_allocations.c # bench_idot: microbenchmark (single-acc vs independent-acc AVX-VNNI idot), NOT a test gate. # Build on demand on an AVX-VNNI CPU: make tests/bench_idot ARCH=native -tests/bench_idot$(EXE): tests/bench_idot.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h - $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +tests/bench_idot$(EXE): tests/bench_idot.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) + $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) # bench_i4p_gidot: microbenchmark (per-row vs multi-row/AMX K1b grouped planar IDOT), NOT a test gate. # Build on demand: make tests/bench_i4p_gidot ARCH=native -tests/bench_i4p_gidot$(EXE): tests/bench_i4p_gidot.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h - $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +tests/bench_i4p_gidot$(EXE): tests/bench_i4p_gidot.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) + $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) # bench_gemv_stream: microbenchmark (decode-regime GEMV bandwidth vs the read ceiling; # frozen-baseline + deinterleaved-x candidate A/B), NOT a test gate. # Build on demand: make tests/bench_gemv_stream ARCH=native -tests/bench_gemv_stream$(EXE): tests/bench_gemv_stream.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h - $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +tests/bench_gemv_stream$(EXE): tests/bench_gemv_stream.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) + $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) # bench_mla_simd: microbenchmark (scalar vs AVX2/NEON MLA-absorb reductions, #442), # NOT a test gate. Build on demand: make tests/bench_mla_simd -tests/bench_mla_simd$(EXE): tests/bench_mla_simd.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h - $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +tests/bench_mla_simd$(EXE): tests/bench_mla_simd.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) + $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) tests/test_uring$(EXE): tests/test_uring.c sse41_kernels.h colibri.c oracle.h st.h uring.h json.h tok.h tok_unicode.h tok_unicode_deepseek.h compat.h grammar.h tier.h quant.h fp8_format.h idot.h sample.h kv_persist.h telemetry.h route_trace.h $(VK_OBJ) $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) @@ -2272,8 +2272,8 @@ tests/test_shard_kvb_refuse$(EXE): tests/test_shard_kvb_refuse.c colibri.c st.h # exists so it also compiles under the suite's own $(CFLAGS) rather than only # under the driver's hand-copied flag list, and so a warning regression in it # fails the normal build. -tests/test_e8x4g64_loader$(EXE): tests/test_e8x4g64_loader.c st.h quant.h fp8_format.h compat.h - $(CC) $(CFLAGS) $< -o $@ $(LDFLAGS) +tests/test_e8x4g64_loader$(EXE): tests/test_e8x4g64_loader.c st.h quant.h fp8_format.h compat.h $(VK_OBJ) + $(CC) $(CFLAGS) $< $(VK_OBJ) -o $@ $(LDFLAGS) # Reachable build-only entry point for the harness above: `make check` never # calls this (it stays out of TEST_BINS/test-c, per TEST_EXCLUDE), so this adds diff --git a/c/tests/test_makefile_vk_obj.py b/c/tests/test_makefile_vk_obj.py new file mode 100644 index 000000000..c94714ba3 --- /dev/null +++ b/c/tests/test_makefile_vk_obj.py @@ -0,0 +1,238 @@ +"""A link that calls the Vulkan backend must link $(VK_OBJ). + +Under VK=1, CFLAGS carries -DCOLI_VULKAN. A translation unit that includes an +engine with Vulkan hooks (colibri.c, kimi_k3.c, ...) then calls coli_vk_*, and +the link fails unless the rule also links $(VK_OBJ). That variable is empty +unless VK=1, so the default build and every default CI job stay green while +`make test-c VK=1` is broken. #1728 fixed the 43 rules that +`make -k test-c VK=1` reported. Four more gates (test_xdna_qt_state, +test_xdna_failure, test_logprob_status, test_ablate_mode) arrived in pull +requests merged after it, and nine on-demand rules outside TEST_BINS -- the +benches, the XDNA physical probe, the e8x4g64 loader harness -- were never +built by test-c, so no such run could report them. + +Whether a link needs the backend is a preprocessor question, not a textual +one: code can pick CUDA over Vulkan with `#elif`, a hook can be an #ifdef that +only reads an environment variable, and a test can define the coli_vk_* +functions itself to fake the device. So nothing here reads #if lines. It asks +make for the expanded VK=1 link commands (`make -Bn`), preprocesses each .c on +a line with that line's own flags, and looks at what survives: a link needs +$(VK_OBJ) when the code calls a function backend_vulkan.h declares and does +not define it. The rule must then link $(VK_OBJ) and list it as a +prerequisite, or a clean build can reach the link before backend_vulkan.o +exists (#1728 found six rules like that). Nothing is hand-listed: the rules +come from the Makefile and the API from backend_vulkan.h. +""" +import os +import re +import shutil +import subprocess +import unittest +from concurrent.futures import ThreadPoolExecutor +from pathlib import Path + +C_DIR = Path(__file__).resolve().parent.parent +MAKE = shutil.which("make") + +ASSIGN_RE = re.compile( + r"^(?:override[ \t]+)?[A-Za-z0-9_]+[ \t]*(?:\+|\?|!|::?)?=") +RULE_RE = re.compile(r"^([^\t#][^=]*?)[ \t]*:(?![=:])[ \t]*(.*)$") +# Lines that are not rules even when they contain a colon, e.g. +# `$(warning mixed HIP_ARCH list: ...)`. A rule may still START with `$(`. +DIRECTIVE_RE = re.compile( + r"^(?:ifeq|ifneq|ifdef|ifndef|else|endif|(?:-|s)?include|export" + r"|unexport|vpath)\b|^\$\((?:error|warning|info|file|shell|eval|call)\b") +LINEMARKER_RE = re.compile(r'^#\s+\d+\s+"([^"]+)"') +STRING_RE = re.compile(r'"(?:\\.|[^"\\\n])*"|\'(?:\\.|[^\'\\\n])*\'') +NAME_RE = re.compile(r"\bcoli_vk_\w+") +DEF_RE = re.compile(r"\b(coli_vk_\w+)\s*\([^;{}()]*\)\s*\{") +# Link-only and dependency-file arguments; the rest of a link line is what +# the compile saw. -MMD/-MF would make `-E` write .d files beside the sources. +SKIP_WORD = re.compile(r"^(-l|-L|-Wl,|-static|-shared|-flto|-fuse-ld|-MMD$|-MD$|-MP$)") +SKIP_PAIR = {"-o", "-MF", "-MT", "-MQ", "-framework"} +INPUT_SUFFIXES = (".c", ".o", ".a", ".so", ".dll", ".lib", ".dylib") + + +def _link_rules(): + """{target: normal prerequisites} for every rule with a $(CC) link recipe. + + Prerequisites are the one thing `make -n` does not print. Continuations + are folded first, and define/endef bodies skipped. + """ + text = (C_DIR / "Makefile").read_text(encoding="utf-8") + text = re.sub(r"\\\n[ \t]*", " ", text) + rules, current, in_define = {}, None, False + for line in text.splitlines(): + if in_define: + in_define = not line.startswith("endef") + continue + if line.startswith("define "): + in_define, current = True, None + continue + if line.startswith("\t"): + words = line.split() + if current and "$(CC)" in words and "-c" not in words: + for target in current[0]: + rules[target] = current[1] + continue + stripped = line.split("#", 1)[0].strip() + if not stripped or DIRECTIVE_RE.match(stripped): + continue + if ASSIGN_RE.match(stripped): + current = None + continue + rule = RULE_RE.match(stripped) + if rule: + prereqs = rule.group(2).split(";", 1)[0].split("|", 1)[0].split() + current = (rule.group(1).split(), prereqs) + return rules + + +def _backend_api(): + text = (C_DIR / "backend_vulkan.h").read_text(encoding="utf-8") + return set(re.findall(r"\bcoli_vk_\w+", text)) + + +def _make(*args): + return subprocess.run([MAKE, "--no-print-directory", *args], cwd=C_DIR, + capture_output=True, text=True, errors="replace", + timeout=600) + + +def _unresolved(words, api): + """Backend functions the link line's sources call without defining them, + or None when this host cannot preprocess them (and so cannot build them + either; the platforms that can are where the line gets checked).""" + cc, flags, sources, i = words[0], [], [], 1 + while i < len(words): + w = words[i] + if w in SKIP_PAIR: + i += 2 + continue + if w.endswith(INPUT_SUFFIXES): + if w.endswith(".c"): + sources.append(w) + elif w != "-c" and not SKIP_WORD.match(w): + flags.append(w) + i += 1 + called, defined = set(), set() + for source in sources: + r = subprocess.run([cc, "-E", *flags, source], cwd=C_DIR, + capture_output=True, text=True, errors="replace", + timeout=300) + if r.returncode != 0: + return None + # Keep only this tree's own code: system headers cannot call the + # backend, and backend_vulkan.h only declares it. + kept, keep, own = [], True, {} + for line in r.stdout.splitlines(): + marker = LINEMARKER_RE.match(line) + if marker: + path = marker.group(1) + if path not in own: + resolved = (C_DIR / path).resolve() + own[path] = (not path.startswith("<") + and resolved.is_relative_to(C_DIR) + and resolved.name != "backend_vulkan.h") + keep = own[path] + elif keep: + kept.append(line) + code = STRING_RE.sub('""', "\n".join(kept)) + called |= set(NAME_RE.findall(code)) & api + defined |= set(DEF_RE.findall(code)) & api + return called - defined + + +@unittest.skipUnless(MAKE, "make is required") +class MakefileVkObjTest(unittest.TestCase): + @classmethod + def setUpClass(cls): + # A dry run still rewrites .build-config at parse time; put it back so + # running this test does not make the next real build relink. + stamp = C_DIR / ".build-config" + saved = (stamp.read_bytes(), stamp.stat()) if stamp.exists() else None + try: + # $(EXE) from make's own database. Not --eval: GNU Make 3.81, + # macOS's /usr/bin/make, predates it; -p is older than that. + database = _make("-pn", "VK=1", ".build-config").stdout + exe = re.search(r"(?m)^EXE :?= ?(\S*)", database) + if exe is None: + raise AssertionError("make -pn printed no EXE variable") + cls.rules = {t.replace("$(EXE)", exe.group(1)): p + for t, p in _link_rules().items() + if "$(" not in t.replace("$(EXE)", "") and "%" not in t} + dry = _make("-Bnk", "VK=1", *sorted(cls.rules)) + finally: + if saved is None: + stamp.unlink(missing_ok=True) + else: + stamp.write_bytes(saved[0]) + os.utime(stamp, ns=(saved[1].st_atime_ns, saved[1].st_mtime_ns)) + # A link line is any printed command that compiles a .c into an output + # without -c -- also for rules that write a differently named file + # (fuzz-rans) -- and the compiler is whatever make put first on it. + # `make -n` prints recipe continuations as written, so fold them first. + lines = {} + for line in re.sub(r"\\\n[ \t]*", " ", dry.stdout).splitlines(): + words = line.split() + if ("-o" in words and "-c" not in words + and words.index("-o") + 1 < len(words) + and any(w.endswith(".c") for w in words)): + lines[words[words.index("-o") + 1]] = words + compilers = {words[0] for words in lines.values()} + if lines and not any(shutil.which(c) for c in compilers): + raise unittest.SkipTest(f"{', '.join(sorted(compilers))} is required") + cls.links = {t: w for t, w in lines.items() if shutil.which(w[0])} + api = _backend_api() + with ThreadPoolExecutor(max_workers=os.cpu_count() or 4) as pool: + results = dict(zip(cls.links, pool.map( + lambda words: _unresolved(words, api), cls.links.values()))) + cls.needs = {t: names for t, names in results.items() if names} + cls.unchecked = sorted(t for t, names in results.items() if names is None) + + def test_the_scan_is_not_vacuous(self): + """A broken dry run or preprocess would pass the check below silently. + + Cross-check against the engines: a NAME.c that calls a backend function + and has a `NAME$(EXE):` rule compiles that call directly, so the scan + must report the rule as calling the backend. + """ + api = _backend_api() + self.assertTrue(api, "backend_vulkan.h declares no coli_vk_*") + self.assertTrue(self.links, "make -Bn VK=1 printed no link line") + call = re.compile(r"\b(%s)\s*\(" % "|".join(sorted(api))) + engines = sorted( + t for t in self.links + if "/" not in t and (C_DIR / (t.split(".")[0] + ".c")).is_file() + and call.search((C_DIR / (t.split(".")[0] + ".c")).read_text( + encoding="utf-8", errors="replace"))) + self.assertTrue(engines, "no engine source calls the Vulkan backend") + for target in engines: + self.assertNotIn(target, self.unchecked, + f"{target} could not be preprocessed here") + self.assertIn(target, self.needs, + f"{target} calls the backend in its source, yet the " + f"scan found no call -- the dry run or preprocess " + f"is broken") + self.assertTrue(any(t.startswith("tests/") for t in self.needs), + "the scan found no test that calls the backend") + + def test_every_link_that_calls_vulkan_links_vk_obj(self): + missing = [] + for target, names in sorted(self.needs.items()): + example = sorted(names)[0] + if "backend_vulkan.o" not in self.links[target]: + missing.append(f"{target}: not on the link line (calls {example})") + if target not in self.rules: + missing.append(f"{target}: its rule is not named after it; " + f"list $(VK_OBJ) as that rule's prerequisite") + elif "$(VK_OBJ)" not in self.rules[target]: + missing.append(f"{target}: $(VK_OBJ) not a prerequisite") + self.assertFalse( + missing, + "under VK=1 these links call coli_vk_* without defining it, so they " + "must link backend_vulkan.o:\n " + "\n ".join(missing)) + + +if __name__ == "__main__": + unittest.main()