mirror of
https://github.com/DeusData/codebase-memory-mcp.git
synced 2026-11-04 10:48:20 +00:00
Add per-grammar regression suite across all 159 grammars
Index a fixture for every grammar through the full pipeline and assert graph-level invariants: golden node-label histograms, def/node breadth, CALLS and IMPORTS breadth, presence of all 26 pipeline edge types (structural, type-relationship, service/dataflow, and similarity), no-crash, and call resolution. Reproduced quality gaps (under-extraction, import-extraction, and CALLS-resolution) are kept as hard failures for the fix phase rather than skipped. Adds a real-repo scale tier (scale_contract.sh) and ignores local-only private/ scratch.
This commit is contained in:
1 parent
eed87fd372
commit
7a43b38ef7
9 files changed
+1937
-13
No files matched your search
@@ -41,6 +41,9 @@ Thumbs.db
|
||||
memory/
|
||||
reference/
|
||||
|
||||
# Local-only scratch / session notes (never pushed)
|
||||
private/
|
||||
|
||||
# Build artifacts
|
||||
build/
|
||||
node_modules/
|
||||
|
||||
+3
-1
@@ -276,6 +276,8 @@ TEST_FOUNDATION_SRCS = \
|
||||
TEST_EXTRACTION_SRCS = \
|
||||
tests/test_extraction.c \
|
||||
tests/test_grammar_regression.c \
|
||||
tests/test_grammar_labels.c \
|
||||
tests/test_grammar_imports.c \
|
||||
tests/test_ac.c
|
||||
|
||||
TEST_STORE_SRCS = \
|
||||
@@ -338,7 +340,7 @@ TEST_JAVA_LSP_SRCS = tests/test_java_lsp.c tests/test_java_lsp_coverage.c
|
||||
TEST_KOTLIN_LSP_SRCS = tests/test_kotlin_lsp.c
|
||||
TEST_RUST_LSP_SRCS = tests/test_rust_lsp.c
|
||||
|
||||
TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c
|
||||
TEST_INTEGRATION_SRCS = tests/test_integration.c tests/test_incremental.c tests/test_lang_contract.c
|
||||
|
||||
TEST_TRACES_SRCS = tests/test_traces.c
|
||||
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
/*
|
||||
* grammar_cases.h — Shared per-language fixture table.
|
||||
*
|
||||
* One minimal source fixture per supported grammar. Shared by:
|
||||
* - test_grammar_regression.c : EXTRACTION-level breadth (cbm_extract_file with
|
||||
* an explicit language → defs/labels). Bypasses discover, so it covers every
|
||||
* grammar including those whose file extension collides with another's.
|
||||
* - test_lang_contract.c : GRAPH-level breadth (full pipeline index → the
|
||||
* fixture's defs must reach the graph as nodes).
|
||||
*
|
||||
* Keep this the single source of truth for per-grammar fixtures.
|
||||
*/
|
||||
#ifndef CBM_TEST_GRAMMAR_CASES_H
|
||||
#define CBM_TEST_GRAMMAR_CASES_H
|
||||
|
||||
#include "cbm.h"
|
||||
#include <stddef.h>
|
||||
|
||||
typedef struct {
|
||||
const char *name; /* short grammar label (unique) */
|
||||
CBMLanguage lang; /* explicit language for extraction-level tests */
|
||||
const char *path; /* representative filename (drives discover by ext/name) */
|
||||
const char *src; /* fixture source */
|
||||
int min_defs; /* catastrophic-break floor (0 for data/config/markup) */
|
||||
const char *expect[4]; /* expected def names (NULL-terminated), optional */
|
||||
} GrammarCase;
|
||||
|
||||
extern const GrammarCase CBM_GRAMMAR_CASES[];
|
||||
extern const size_t CBM_GRAMMAR_CASES_COUNT;
|
||||
|
||||
#endif /* CBM_TEST_GRAMMAR_CASES_H */
|
||||
@@ -0,0 +1,132 @@
|
||||
#!/usr/bin/env bash
|
||||
# scale_contract.sh — Real-repo SCALE-TIER contracts for known-bug languages.
|
||||
#
|
||||
# Some behavioral bugs only manifest at real-repo scale and cannot be reproduced
|
||||
# by the small in-process fixtures in tests/test_lang_contract.c:
|
||||
# - Java / TypeScript : SIGBUS during parallel extraction on large repos.
|
||||
# - C : function-call attribution (calls land on the file's
|
||||
# Module node instead of the enclosing Function).
|
||||
# - Kotlin : 0 IMPORTS edges (package -> module resolution).
|
||||
#
|
||||
# This tier runs the COMPILED binary on the cbm-bench-validate repos and asserts
|
||||
# invariants at scale. It is SLOW and needs the local bench repos, so it is
|
||||
# OPT-IN — not part of the fast `scripts/test.sh` unit run.
|
||||
#
|
||||
# Usage:
|
||||
# bash tests/scale_contract.sh [lang ...] # default: kotlin java ts
|
||||
# CBM_SCALE_INCLUDE_C=1 bash tests/scale_contract.sh c # add the slow C tier
|
||||
# CBM_BENCH_DIR=/path bash tests/scale_contract.sh # override repo root
|
||||
# Exit 0 if all selected contracts pass, non-zero otherwise.
|
||||
|
||||
set -uo pipefail
|
||||
|
||||
ROOT="$(cd "$(dirname "$0")/.." && pwd)"
|
||||
BIN="$ROOT/build/c/codebase-memory-mcp"
|
||||
BENCH="${CBM_BENCH_DIR:-$HOME/cbm-bench-validate}"
|
||||
PY="${PYTHON:-python3.9}"
|
||||
FAILURES=0
|
||||
RAN=0
|
||||
|
||||
# ── repo map (lang -> bench subdir) ──────────────────────────────
|
||||
repo_for() {
|
||||
case "$1" in
|
||||
kotlin) echo "$BENCH/ktorio_ktor" ;;
|
||||
java) echo "$BENCH/spring-projects_spring-boot" ;;
|
||||
ts) echo "$BENCH/microsoft_TypeScript" ;;
|
||||
c) echo "$BENCH/linux_mono/copy1" ;;
|
||||
*) echo "" ;;
|
||||
esac
|
||||
}
|
||||
|
||||
# project name == cbm_project_name_from_path(): strip leading '/', map every
|
||||
# char outside [A-Za-z0-9._-] to '-'.
|
||||
project_name() { printf '%s' "$1" | sed 's#^/##; s#[^A-Za-z0-9._-]#-#g'; }
|
||||
|
||||
# Run query_graph and print the first integer in the JSON response (or -1).
|
||||
query_count() {
|
||||
local proj="$1" cypher="$2" resp
|
||||
resp="$("$BIN" cli query_graph "{\"project\":\"$proj\",\"query\":\"$cypher\"}" 2>/dev/null)"
|
||||
printf '%s' "$resp" | "$PY" -c 'import sys,re
|
||||
s=sys.stdin.read()
|
||||
m=re.findall(r"-?\d+", s)
|
||||
print(m[0] if m else -1)' 2>/dev/null || echo -1
|
||||
}
|
||||
|
||||
assert_lang() {
|
||||
local lang="$1" repo
|
||||
repo="$(repo_for "$lang")"
|
||||
if [ -z "$repo" ] || [ ! -d "$repo" ]; then
|
||||
echo "[scale] SKIP $lang — repo not found ($repo)"
|
||||
return 0
|
||||
fi
|
||||
RAN=$((RAN + 1))
|
||||
local proj
|
||||
proj="$(project_name "$repo")"
|
||||
rm -f "$HOME/.cache/codebase-memory-mcp/$proj.db"*
|
||||
|
||||
echo "[scale] $lang — indexing $repo ..."
|
||||
"$BIN" cli index_repository "{\"repo_path\":\"$repo\"}" >/dev/null 2>&1
|
||||
local rc=$?
|
||||
|
||||
# Contract 1 (all langs): indexing must not crash by signal (rc >= 128).
|
||||
if [ "$rc" -ge 128 ]; then
|
||||
echo "[scale] FAIL $lang — index crashed by signal (exit=$rc, signal=$((rc - 128)))"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
return 0
|
||||
fi
|
||||
echo "[scale] ok $lang — index did not crash (exit=$rc)"
|
||||
|
||||
# Contract 2 (per-lang invariant).
|
||||
case "$lang" in
|
||||
kotlin)
|
||||
local imports
|
||||
imports="$(query_count "$proj" "MATCH ()-[r:IMPORTS]->() RETURN count(r) AS n")"
|
||||
if [ "$imports" -ge 1 ] 2>/dev/null; then
|
||||
echo "[scale] PASS kotlin — IMPORTS edges present (n>=$imports)"
|
||||
else
|
||||
echo "[scale] FAIL kotlin — 0 IMPORTS edges (got '$imports')"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
;;
|
||||
c)
|
||||
# Function nodes that have >=1 outbound CALLS (attribution invariant).
|
||||
local fcalls
|
||||
fcalls="$(query_count "$proj" "MATCH (a:Function)-[:CALLS]->() RETURN count(DISTINCT a) AS n")"
|
||||
if [ "$fcalls" -ge 1 ] 2>/dev/null; then
|
||||
echo "[scale] PASS c — Function-sourced CALLS present (functions-with-calls>=$fcalls)"
|
||||
else
|
||||
echo "[scale] FAIL c — no Function node has outbound CALLS (got '$fcalls')"
|
||||
FAILURES=$((FAILURES + 1))
|
||||
fi
|
||||
;;
|
||||
java | ts)
|
||||
echo "[scale] PASS $lang — no-crash contract satisfied"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
# ── build the prod binary if missing ─────────────────────────────
|
||||
if [ ! -x "$BIN" ]; then
|
||||
echo "[scale] building prod binary ..."
|
||||
(cd "$ROOT" && ./scripts/build.sh) || { echo "[scale] FAIL — build failed"; exit 1; }
|
||||
fi
|
||||
[ -x "$BIN" ] || { echo "[scale] FAIL — binary not found at $BIN"; exit 1; }
|
||||
|
||||
# ── select languages ─────────────────────────────────────────────
|
||||
LANGS=("$@")
|
||||
if [ "${#LANGS[@]}" -eq 0 ]; then
|
||||
LANGS=(kotlin java ts)
|
||||
[ "${CBM_SCALE_INCLUDE_C:-0}" = "1" ] && LANGS+=(c)
|
||||
fi
|
||||
|
||||
for lang in "${LANGS[@]}"; do
|
||||
assert_lang "$lang"
|
||||
done
|
||||
|
||||
echo "────────────────────────────────────────────"
|
||||
if [ "$FAILURES" -eq 0 ]; then
|
||||
echo "[scale] All scale contracts passed ($RAN ran)"
|
||||
exit 0
|
||||
fi
|
||||
echo "[scale] $FAILURES scale contract(s) FAILED ($RAN ran)"
|
||||
exit 1
|
||||
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* test_grammar_imports.c — Per-grammar IMPORTS EXTRACTION breadth.
|
||||
*
|
||||
* For every grammar cbm_extract_imports() handles, assert cbm_extract_file()
|
||||
* captures its import/include/use/require statements (imports.count >= N).
|
||||
* This is the EXTRACTION-level import contract: it uses an explicit language
|
||||
* (no discover, no pkgmap), so it is reliable in-unit for ALL import-capable
|
||||
* grammars — the graph IMPORTS-edge (which needs module resolution) is covered
|
||||
* for relative imports by test_lang_contract.c and at real scale by the scale
|
||||
* tier. No skips: a grammar that fails to extract its imports is a hard FAILURE
|
||||
* reproducing the bug (gap_note documents the already-root-caused ones).
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "cbm.h"
|
||||
|
||||
#include <string.h>
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
CBMLanguage lang;
|
||||
const char *filename;
|
||||
const char *content;
|
||||
int expected; /* imports.count must be >= this */
|
||||
const char *gap_note;
|
||||
} ImportCase;
|
||||
|
||||
static const ImportCase IMPORT_CASES[] = {
|
||||
{"astro", CBM_LANG_ASTRO, "fixture.astro",
|
||||
"<script>\nimport confetti from \"canvas-confetti\";\nimport dayjs from \"dayjs\";\n\nconfetti();\nconsole.log(dayjs().format());\n</script>\n<div>hi</div>\n",
|
||||
2, NULL},
|
||||
{"bash", CBM_LANG_BASH, "a.sh", "#!/usr/bin/env bash\nsource ./lib/util.sh\n. ./lib/other.sh\n", 2,
|
||||
NULL},
|
||||
{"c", CBM_LANG_C, "imports_fixture.c",
|
||||
"#include <stdio.h>\n#include \"mymodule.h\"\n\nint main(void) {\n return 0;\n}\n", 2, NULL},
|
||||
{"cpp", CBM_LANG_CPP, "main.cpp",
|
||||
"#include <iostream>\n#include \"widget.h\"\n\nint main() {\n std::cout << \"hi\\n\";\n return 0;\n}\n",
|
||||
2, NULL},
|
||||
{"csharp", CBM_LANG_CSHARP, "Imports.cs",
|
||||
"using System;\nusing static System.Math;\nusing Json = System.Text.Json;\n\nnamespace Demo;\n\nclass Program\n{\n static void Main() => Console.WriteLine(Sqrt(4.0));\n}\n",
|
||||
3, NULL},
|
||||
{"css", CBM_LANG_CSS, "styles.css",
|
||||
"@import \"reset.css\";\n@import url(\"theme.css\");\n\nbody {\n margin: 0;\n}\n", 2, NULL},
|
||||
{"dart", CBM_LANG_DART, "imports.dart",
|
||||
"import 'dart:async';\nimport 'package:meta/meta.dart';\n\nvoid main() {}\n", 2,
|
||||
"extract_imports.c SWIFT/DART case matches 'import_declaration' but tree-sitter-dart emits 'import_or_export' -> 0 imports"},
|
||||
{"elixir", CBM_LANG_ELIXIR, "imports_fixture.ex",
|
||||
"import Enum\nalias MyApp.Accounts.User\nrequire Logger\n", 3, NULL},
|
||||
{"erlang", CBM_LANG_ERLANG, "greeter.erl",
|
||||
"-module(greeter).\n-import(lists, [foldl/3]).\n-include_lib(\"kernel/include/file.hrl\").\n\nsum(Ns) -> foldl(fun(X, A) -> X + A end, 0, Ns).\n",
|
||||
1, NULL},
|
||||
{"form", CBM_LANG_FORM, "imports.frm", "#include \"procedures.prc\"\n#include \"tables.h\"\n", 2, NULL},
|
||||
{"go", CBM_LANG_GO, "imports_fixture.go",
|
||||
"package main\n\nimport (\n\t\"fmt\"\n\t\"os\"\n\tmrand \"math/rand\"\n)\n\nfunc main() {\n\tfmt.Fprintln(os.Stdout, mrand.Int())\n}\n",
|
||||
3, NULL},
|
||||
{"groovy", CBM_LANG_GROOVY, "a.groovy",
|
||||
"import java.util.List\nimport groovy.transform.CompileStatic\n\nclass Greeter {\n String greet() { \"hi\" }\n}\n",
|
||||
2, NULL},
|
||||
{"haskell", CBM_LANG_HASKELL, "a.hs",
|
||||
"module Main where\n\nimport Data.List (sort)\nimport qualified Data.Map as Map\n\nmain :: IO ()\nmain = print (sort [3, 1, 2])\n",
|
||||
2,
|
||||
"imports nested under an 'imports' container node; parse_generic_imports scans only root children -> 0 (needs parse_haskell_imports, like Kotlin)"},
|
||||
{"html", CBM_LANG_HTML, "fixture_imports.html",
|
||||
"<!DOCTYPE html>\n<html lang=\"en\">\n <head>\n <meta charset=\"utf-8\" />\n <title>Import Fixture</title>\n <script type=\"module\">\n import App from \"./app.js\";\n import { render } from \"./render.js\";\n\n render(App);\n </script>\n </head>\n <body></body>\n</html>\n",
|
||||
2, NULL},
|
||||
{"java", CBM_LANG_JAVA, "Fixture.java",
|
||||
"import java.util.List;\nimport java.util.Map;\n\nclass Fixture {\n List<String> names;\n Map<String, Integer> counts;\n}\n",
|
||||
2, NULL},
|
||||
{"javascript", CBM_LANG_JAVASCRIPT, "imports_fixture.js",
|
||||
"import path from \"node:path\";\nimport { readFile, writeFile } from \"node:fs/promises\";\n\nexport async function copyConfig(src) {\n const dst = path.join(\"/tmp\", \"config.json\");\n const data = await readFile(src, \"utf8\");\n await writeFile(dst, data);\n return dst;\n}\n",
|
||||
3, NULL},
|
||||
{"kotlin", CBM_LANG_KOTLIN, "fixture.kt",
|
||||
"package com.example.app\n\nimport kotlin.collections.List\nimport java.util.UUID\nimport kotlinx.coroutines.runBlocking\n",
|
||||
3, NULL},
|
||||
{"lean", CBM_LANG_LEAN, "imports.lean",
|
||||
"import Std.Data.List.Basic\nimport Mathlib.Tactic\n\ndef hello : String := \"world\"\n", 2, NULL},
|
||||
{"lua", CBM_LANG_LUA, "a.lua",
|
||||
"local socket = require(\"socket\")\nlocal json = require(\"dkjson\")\n\nlocal function connect()\n return socket, json\nend\n\nreturn connect\n",
|
||||
2, NULL},
|
||||
{"magma", CBM_LANG_MAGMA, "a.magma",
|
||||
"load \"mylib.m\";\nload \"helpers.m\";\n\nfunction foo()\n return 1;\nend function;\n", 2, NULL},
|
||||
{"objc", CBM_LANG_OBJC, "fixture_imports.m",
|
||||
"#import <Foundation/Foundation.h>\n#import \"AppModel.h\"\n", 2, NULL},
|
||||
{"ocaml", CBM_LANG_OCAML, "a.ml",
|
||||
"open List\nopen String\n\nlet greet name = String.cat \"Hi \" name\n", 2, NULL},
|
||||
{"perl", CBM_LANG_PERL, "imports.pl",
|
||||
"use strict;\nuse warnings;\nuse Data::Dumper;\n\nsub run { print Dumper([1, 2, 3]); }\n", 3, NULL},
|
||||
{"php", CBM_LANG_PHP, "a.php",
|
||||
"<?php\nrequire_once 'vendor/autoload.php';\ninclude 'config.php';\n", 2, NULL},
|
||||
{"python", CBM_LANG_PYTHON, "fixture_imports.py",
|
||||
"import os\nfrom typing import List, Optional\n\n\ndef widths(items: List[str], pad: Optional[int]) -> List[int]:\n return [len(s) + (pad or 0) for s in items if os.path.exists(s)]\n",
|
||||
3, NULL},
|
||||
{"r", CBM_LANG_R, "imports_fixture.R", "library(jsonlite)\nrequire(stats)\nsource(\"utils.R\")\n", 3,
|
||||
NULL},
|
||||
{"ruby", CBM_LANG_RUBY, "a.rb", "require \"json\"\nrequire_relative \"helper\"\n", 2, NULL},
|
||||
{"rust", CBM_LANG_RUST, "imports.rs",
|
||||
"use std::collections::HashMap;\nuse std::fmt::{self, Display};\nuse serde::Serialize;\n\nfn main() {\n let _m: HashMap<String, i32> = HashMap::new();\n}\n",
|
||||
3, NULL},
|
||||
{"scala", CBM_LANG_SCALA, "a.scala",
|
||||
"package example\n\nimport scala.collection.mutable.ListBuffer\nimport java.time.Instant\n\nobject Main {\n def run(): Unit = {\n val buf = ListBuffer[Instant]()\n buf += Instant.now()\n }\n}\n",
|
||||
2, NULL},
|
||||
{"scss", CBM_LANG_SCSS, "imports.scss",
|
||||
"@import \"variables\";\n@import \"mixins\";\n\n.button {\n color: red;\n}\n", 2, NULL},
|
||||
{"svelte", CBM_LANG_SVELTE, "a.svelte",
|
||||
"<script>\n import { onMount } from 'svelte';\n import App from './App.svelte';\n\n onMount(() => {});\n</script>\n\n<App />\n",
|
||||
2, NULL},
|
||||
{"swift", CBM_LANG_SWIFT, "a.swift", "import Foundation\nimport SwiftUI\n", 2, NULL},
|
||||
{"tsx", CBM_LANG_TSX, "a.tsx",
|
||||
"import React from \"react\";\nimport { useState, useEffect } from \"react\";\nimport * as styles from \"./Button.module.css\";\n\nexport function Button(): React.ReactElement {\n const [count, setCount] = useState(0);\n useEffect(() => {}, []);\n return <button className={styles.root} onClick={() => setCount(count + 1)}>{count}</button>;\n}\n",
|
||||
4, NULL},
|
||||
{"typescript", CBM_LANG_TYPESCRIPT, "imports_fixture.ts",
|
||||
"import express from \"express\";\nimport * as path from \"path\";\nimport { useState, useEffect } from \"react\";\n\nconst app = express();\nconst dir: string = path.join(\".\");\nconsole.log(app, dir, useState, useEffect);\n",
|
||||
4, NULL},
|
||||
{"vue", CBM_LANG_VUE, "component.vue",
|
||||
"<script setup>\nimport { ref } from 'vue'\nimport HelloWorld from './HelloWorld.vue'\n\nconst msg = ref('hello')\n</script>\n\n<template>\n <HelloWorld :msg=\"msg\" />\n</template>\n",
|
||||
2, NULL},
|
||||
{"wolfram", CBM_LANG_WOLFRAM, "main.wl",
|
||||
"Needs[\"MyPackage`Utils`\"]\n<< \"helpers.wl\"\n\nmain[x_] := process[x] + 1\n", 2, NULL},
|
||||
{"zig", CBM_LANG_ZIG, "imports.zig",
|
||||
"const std = @import(\"std\");\nconst builtin = @import(\"builtin\");\n\npub fn main() void {}\n", 2,
|
||||
"@import is nested in variable_declaration; parse_generic_imports scans only root children for builtin_function -> 0"},
|
||||
};
|
||||
|
||||
TEST(grammar_imports_extracted) {
|
||||
int n = (int)(sizeof(IMPORT_CASES) / sizeof(IMPORT_CASES[0]));
|
||||
int failures = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const ImportCase *c = &IMPORT_CASES[i];
|
||||
CBMFileResult *r = cbm_extract_file(c->content, (int)strlen(c->content), c->lang, "imp",
|
||||
c->filename, 0, NULL, NULL);
|
||||
if (!r) {
|
||||
fprintf(stderr, " [IMPORTS] FAIL %-12s extract returned NULL\n", c->name);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
int got = r->imports.count;
|
||||
cbm_free_result(r);
|
||||
if (got < c->expected) {
|
||||
fprintf(stderr, " [IMPORTS] FAIL %-12s imports=%d expected>=%d%s%s\n", c->name, got,
|
||||
c->expected, c->gap_note ? " — " : "", c->gap_note ? c->gap_note : "");
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
fprintf(stderr, " [IMPORTS] %d import-capable grammars: %d FAILURES (each = a grammar whose "
|
||||
"imports are not extracted)\n",
|
||||
n, failures);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void suite_grammar_imports(void) {
|
||||
RUN_TEST(grammar_imports_extracted);
|
||||
}
|
||||
@@ -0,0 +1,353 @@
|
||||
/*
|
||||
* test_grammar_labels.c — Per-grammar node-type LABEL golden snapshot.
|
||||
*
|
||||
* For every supported grammar, snapshot the deterministic node-type-label
|
||||
* histogram that cbm_extract_file() produces for the shared fixture (e.g.
|
||||
* "Class:1,Function:2,Module:1"). Asserting the histogram stays fixed catches
|
||||
* ANY future change to how a grammar's constructs are labeled — e.g. a class
|
||||
* silently downgraded to a Function, a lost Method, or a new spurious node.
|
||||
*
|
||||
* Node counts are deterministic (unlike edge counts), so an exact histogram
|
||||
* golden is stable. Labels come from CBMDefinition.label assigned during
|
||||
* extraction (the same labels that become graph-node labels downstream).
|
||||
*
|
||||
* Capture workflow: a grammar with no golden row prints a `[LABEL-CAPTURE]`
|
||||
* line and fails; copy those lines into LABEL_GOLDENS, then the test asserts.
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "cbm.h"
|
||||
#include "grammar_cases.h"
|
||||
|
||||
#include <stdlib.h>
|
||||
#include <string.h>
|
||||
|
||||
/* Build a sorted "Label:count,Label:count" histogram of a result's def labels
|
||||
* (includes the always-emitted Module node). Deterministic + stable. */
|
||||
static void label_histogram(CBMFileResult *r, char *out, size_t out_sz) {
|
||||
/* Collect distinct labels + counts (small N — linear scan is fine). */
|
||||
enum { MAXL = 32 };
|
||||
const char *labels[MAXL];
|
||||
int counts[MAXL];
|
||||
int nl = 0;
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
const char *l = r->defs.items[i].label ? r->defs.items[i].label : "(null)";
|
||||
int j = 0;
|
||||
for (; j < nl; j++) {
|
||||
if (strcmp(labels[j], l) == 0) {
|
||||
counts[j]++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (j == nl && nl < MAXL) {
|
||||
labels[nl] = l;
|
||||
counts[nl] = 1;
|
||||
nl++;
|
||||
}
|
||||
}
|
||||
/* Insertion sort labels alphabetically for a canonical string. */
|
||||
for (int i = 1; i < nl; i++) {
|
||||
const char *lk = labels[i];
|
||||
int ck = counts[i];
|
||||
int j = i - 1;
|
||||
while (j >= 0 && strcmp(labels[j], lk) > 0) {
|
||||
labels[j + 1] = labels[j];
|
||||
counts[j + 1] = counts[j];
|
||||
j--;
|
||||
}
|
||||
labels[j + 1] = lk;
|
||||
counts[j + 1] = ck;
|
||||
}
|
||||
out[0] = '\0';
|
||||
size_t used = 0;
|
||||
for (int i = 0; i < nl; i++) {
|
||||
int w = snprintf(out + used, out_sz - used, "%s%s:%d", i ? "," : "", labels[i], counts[i]);
|
||||
if (w < 0 || (size_t)w >= out_sz - used) {
|
||||
break;
|
||||
}
|
||||
used += (size_t)w;
|
||||
}
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
const char *hist;
|
||||
} LabelGolden;
|
||||
|
||||
/* Golden node-type-label histograms — captured from the current (correct)
|
||||
* extractor output. A mismatch = a labeling regression for that grammar.
|
||||
* (Populated from [LABEL-CAPTURE] output; see header.) */
|
||||
static const LabelGolden LABEL_GOLDENS[] = {
|
||||
{"go", "Function:2,Module:1"},
|
||||
{"c", "Function:2,Module:1"},
|
||||
{"cpp", "Class:1,Function:1,Module:1"},
|
||||
{"cuda", "Function:2,Module:1"},
|
||||
{"python", "Class:1,Function:1,Module:1"},
|
||||
{"javascript", "Class:1,Function:1,Module:1"},
|
||||
{"typescript", "Class:1,Function:1,Module:1"},
|
||||
{"tsx", "Function:1,Module:1"},
|
||||
{"java", "Class:1,Method:1,Module:1"},
|
||||
{"kotlin", "Class:1,Function:1,Module:1"},
|
||||
{"rust", "Class:1,Function:1,Module:1"},
|
||||
{"ruby", "Class:1,Function:1,Module:1"},
|
||||
{"php", "Class:1,Function:1,Module:1"},
|
||||
{"c_sharp", "Class:1,Method:1,Module:1"},
|
||||
{"bash", "Function:2,Module:1"},
|
||||
{"zsh", "Function:2,Module:1"},
|
||||
{"lua", "Function:2,Module:1"},
|
||||
{"luau", "Function:2,Module:1"},
|
||||
{"perl", "Function:2,Module:1"},
|
||||
{"dart", "Class:1,Function:1,Module:1"},
|
||||
{"swift", "Class:1,Function:1,Module:1"},
|
||||
{"scala", "Class:1,Function:1,Method:1,Module:1"},
|
||||
{"gdscript", "Function:1,Module:1"},
|
||||
{"groovy", "Class:1,Method:1,Module:1"},
|
||||
{"zig", "Function:2,Module:1"},
|
||||
{"nim", "Function:2,Module:1"},
|
||||
{"solidity", "Class:1,Function:1,Method:1,Module:1"},
|
||||
{"tcl", "Function:2,Module:1"},
|
||||
{"powershell", "Function:2,Module:1"},
|
||||
{"r", "Function:2,Module:1"},
|
||||
{"julia", "Function:1,Module:1"},
|
||||
{"matlab", "Function:2,Module:1"},
|
||||
{"ada", "Function:1,Module:1"},
|
||||
{"agda", "Module:1"},
|
||||
{"apex", "Class:1,Method:1,Module:1"},
|
||||
{"awk", "Function:2,Module:1"},
|
||||
{"cairo", "Function:2,Module:1"},
|
||||
{"clojure", "Function:2,Module:1"},
|
||||
{"commonlisp", "Function:2,Module:1"},
|
||||
{"emacslisp", "Function:2,Module:1"},
|
||||
{"crystal", "Class:1,Function:1,Module:1"},
|
||||
{"d", "Function:2,Module:1"},
|
||||
{"elixir", "Class:1,Function:1,Module:1"},
|
||||
{"erlang", "Function:2,Module:1"},
|
||||
{"fennel", "Function:2,Module:1"},
|
||||
{"fish", "Function:1,Module:1"},
|
||||
{"fortran", "Function:1,Module:1"},
|
||||
{"fsharp", "Function:2,Module:1"},
|
||||
{"gleam", "Function:2,Module:1"},
|
||||
{"glsl", "Function:2,Module:1"},
|
||||
{"hare", "Function:2,Module:1"},
|
||||
{"haskell", "Function:1,Module:1"},
|
||||
{"hlsl", "Function:2,Module:1"},
|
||||
{"ispc", "Function:2,Module:1"},
|
||||
{"objc", "Class:1,Method:1,Module:1"},
|
||||
{"ocaml", "Function:2,Module:1"},
|
||||
{"odin", "Function:2,Module:1"},
|
||||
{"pascal", "Function:1,Module:1"},
|
||||
{"pony", "Module:1"},
|
||||
{"purescript", "Function:1,Module:1"},
|
||||
{"racket", "Function:2,Module:1"},
|
||||
{"rescript", "Function:2,Module:1"},
|
||||
{"scheme", "Function:2,Module:1"},
|
||||
{"slang", "Function:2,Module:1"},
|
||||
{"squirrel", "Function:2,Module:1"},
|
||||
{"starlark", "Function:2,Module:1"},
|
||||
{"sway", "Function:2,Module:1"},
|
||||
{"teal", "Function:2,Module:1"},
|
||||
{"vimscript", "Function:1,Module:1"},
|
||||
{"elm", "Class:1,Function:1,Module:1"},
|
||||
{"func", "Function:1,Module:1"},
|
||||
{"lean", "Function:2,Module:1"},
|
||||
{"move", "Module:1"},
|
||||
{"smali", "Module:1"},
|
||||
{"systemverilog", "Module:1"},
|
||||
{"verilog", "Module:1"},
|
||||
{"vhdl", "Module:1"},
|
||||
{"wgsl", "Function:2,Module:1"},
|
||||
{"tlaplus", "Function:1,Module:1"},
|
||||
{"llvm", "Function:1,Module:1"},
|
||||
{"tablegen", "Module:1"},
|
||||
{"puppet", "Module:1"},
|
||||
{"assembly", "Module:1"},
|
||||
{"nasm", "Module:1"},
|
||||
{"cfml", "Function:1,Module:1"},
|
||||
{"cfscript", "Function:1,Module:1"},
|
||||
{"cobol", "Module:1"},
|
||||
{"janet", "Module:1"},
|
||||
{"magma", "Function:1,Module:1"},
|
||||
{"qml", "Function:1,Module:1"},
|
||||
{"wolfram", "Function:1,Module:1"},
|
||||
{"pine", "Module:1"},
|
||||
{"form", "Module:1,Variable:1"},
|
||||
{"protobuf", "Module:1"},
|
||||
{"soql", "Module:1"},
|
||||
{"sosl", "Module:1"},
|
||||
{"dotenv", "Module:1"},
|
||||
{"json", "Module:1,Variable:2"},
|
||||
{"json5", "Module:1"},
|
||||
{"jsonnet", "Module:1"},
|
||||
{"jsdoc", "Module:1"},
|
||||
{"yaml", "Module:1,Variable:2"},
|
||||
{"k8s", "Module:1"},
|
||||
{"kustomize", "Module:1"},
|
||||
{"toml", "Class:1,Module:1,Variable:1"},
|
||||
{"ini", "Class:1,Module:1,Variable:1"},
|
||||
{"csv", "Module:1"},
|
||||
{"sql", "Module:1,Variable:1"},
|
||||
{"xml", "Class:2,Module:1"},
|
||||
{"html", "Module:1"},
|
||||
{"css", "Module:1"},
|
||||
{"scss", "Module:1,Variable:1"},
|
||||
{"markdown", "Module:1,Section:1"},
|
||||
{"rst", "Module:1"},
|
||||
{"dockerfile", "Module:1"},
|
||||
{"makefile", "Function:1,Module:1"},
|
||||
{"cmake", "Module:1"},
|
||||
{"meson", "Module:1"},
|
||||
{"gn", "Module:1"},
|
||||
{"just", "Module:1"},
|
||||
{"hcl", "Class:1,Module:1"},
|
||||
{"nix", "Module:1"},
|
||||
{"gomod", "Module:1"},
|
||||
{"gotemplate", "Module:1"},
|
||||
{"graphql", "Module:1"},
|
||||
{"prisma", "Module:1"},
|
||||
{"thrift", "Module:1"},
|
||||
{"capnp", "Module:1"},
|
||||
{"smithy", "Module:1"},
|
||||
{"wit", "Module:1"},
|
||||
{"kdl", "Module:1"},
|
||||
{"ron", "Module:1"},
|
||||
{"nickel", "Module:1"},
|
||||
{"pkl", "Module:1"},
|
||||
{"bicep", "Module:1"},
|
||||
{"bitbake", "Module:1"},
|
||||
{"beancount", "Module:1"},
|
||||
{"bibtex", "Module:1"},
|
||||
{"po", "Module:1"},
|
||||
{"diff", "Module:1"},
|
||||
{"regex", "Module:1"},
|
||||
{"requirements", "Module:1"},
|
||||
{"properties", "Module:1"},
|
||||
{"gitignore", "Module:1"},
|
||||
{"gitattributes", "Module:1"},
|
||||
{"sshconfig", "Module:1"},
|
||||
{"hyprlang", "Module:1"},
|
||||
{"kconfig", "Class:1,Module:1"},
|
||||
{"linkerscript", "Module:1"},
|
||||
{"devicetree", "Module:1"},
|
||||
{"jinja2", "Module:1"},
|
||||
{"liquid", "Module:1"},
|
||||
{"blade", "Module:1"},
|
||||
{"vue", "Module:1"},
|
||||
{"svelte", "Module:1"},
|
||||
{"astro", "Module:1"},
|
||||
{"templ", "Class:1,Module:1"},
|
||||
{"typst", "Module:1"},
|
||||
{"mermaid", "Module:1"},
|
||||
{NULL, NULL},
|
||||
};
|
||||
|
||||
static const char *golden_for(const char *name) {
|
||||
for (int i = 0; LABEL_GOLDENS[i].name; i++) {
|
||||
if (strcmp(LABEL_GOLDENS[i].name, name) == 0) {
|
||||
return LABEL_GOLDENS[i].hist;
|
||||
}
|
||||
}
|
||||
return NULL;
|
||||
}
|
||||
|
||||
TEST(grammar_label_goldens) {
|
||||
int n = (int)CBM_GRAMMAR_CASES_COUNT;
|
||||
int failures = 0;
|
||||
int missing = 0;
|
||||
for (int i = 0; i < n; i++) {
|
||||
const GrammarCase *c = &CBM_GRAMMAR_CASES[i];
|
||||
CBMFileResult *r =
|
||||
cbm_extract_file(c->src, (int)strlen(c->src), c->lang, "lbl", c->path, 0, NULL, NULL);
|
||||
if (!r) {
|
||||
fprintf(stderr, " [LABEL] %-14s extract returned NULL\n", c->name);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
char hist[256];
|
||||
label_histogram(r, hist, sizeof(hist));
|
||||
cbm_free_result(r);
|
||||
|
||||
const char *golden = golden_for(c->name);
|
||||
if (!golden) {
|
||||
fprintf(stderr, " [LABEL-CAPTURE] {\"%s\", \"%s\"},\n", c->name, hist);
|
||||
missing++;
|
||||
} else if (strcmp(golden, hist) != 0) {
|
||||
fprintf(stderr, " [LABEL] %-14s MISMATCH golden=[%s] actual=[%s]\n", c->name, golden,
|
||||
hist);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
if (missing > 0) {
|
||||
fprintf(stderr, " [LABEL] %d grammar(s) missing a golden — copy the [LABEL-CAPTURE] lines "
|
||||
"into LABEL_GOLDENS\n",
|
||||
missing);
|
||||
}
|
||||
ASSERT_EQ(failures, 0);
|
||||
ASSERT_EQ(missing, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
/* Count def nodes that are NOT the structural file/module wrappers. */
|
||||
static int non_module_defs(CBMFileResult *r) {
|
||||
int n = 0;
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
const char *l = r->defs.items[i].label;
|
||||
if (l && strcmp(l, "Module") != 0 && strcmp(l, "File") != 0 && strcmp(l, "Folder") != 0 &&
|
||||
strcmp(l, "Package") != 0 && strcmp(l, "Directory") != 0) {
|
||||
n++;
|
||||
}
|
||||
}
|
||||
return n;
|
||||
}
|
||||
|
||||
/* Code / IDL grammars whose fixture contains a real definition construct
|
||||
* (function / type / message / model / module / label) that MUST extract to at
|
||||
* least one non-Module node. These currently extract only a Module node — an
|
||||
* under-extraction gap reproduced here as a hard failure (fix phase). Data /
|
||||
* config / markup grammars (json/yaml/css/…) are intentionally excluded — a
|
||||
* lone Module node is correct for them. */
|
||||
static const char *MUST_EXTRACT_DEFS[] = {
|
||||
"agda", "pony", "move", "cobol", "janet", "pine", "smali", "verilog",
|
||||
"vhdl", "systemverilog", "protobuf", "graphql", "thrift", "capnp", "smithy",
|
||||
"wit", "prisma", "cmake", "puppet", "tablegen", "assembly", "nasm", NULL};
|
||||
|
||||
TEST(grammar_code_extracts_defs) {
|
||||
int failures = 0;
|
||||
for (int k = 0; MUST_EXTRACT_DEFS[k]; k++) {
|
||||
const char *name = MUST_EXTRACT_DEFS[k];
|
||||
const GrammarCase *c = NULL;
|
||||
for (int i = 0; i < (int)CBM_GRAMMAR_CASES_COUNT; i++) {
|
||||
if (strcmp(CBM_GRAMMAR_CASES[i].name, name) == 0) {
|
||||
c = &CBM_GRAMMAR_CASES[i];
|
||||
break;
|
||||
}
|
||||
}
|
||||
if (!c) {
|
||||
fprintf(stderr, " [CODE-DEFS] FAIL %-14s — no fixture in CBM_GRAMMAR_CASES\n", name);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
CBMFileResult *r =
|
||||
cbm_extract_file(c->src, (int)strlen(c->src), c->lang, "cd", c->path, 0, NULL, NULL);
|
||||
if (!r) {
|
||||
fprintf(stderr, " [CODE-DEFS] FAIL %-14s extract returned NULL\n", name);
|
||||
failures++;
|
||||
continue;
|
||||
}
|
||||
int nd = non_module_defs(r);
|
||||
cbm_free_result(r);
|
||||
if (nd < 1) {
|
||||
fprintf(stderr, " [CODE-DEFS] FAIL %-14s extracts 0 non-Module defs (under-extraction "
|
||||
"gap — only a Module node)\n",
|
||||
name);
|
||||
failures++;
|
||||
}
|
||||
}
|
||||
fprintf(stderr, " [CODE-DEFS] %d code/IDL grammars: %d under-extraction FAILURES\n",
|
||||
(int)(sizeof(MUST_EXTRACT_DEFS) / sizeof(MUST_EXTRACT_DEFS[0])) - 1, failures);
|
||||
ASSERT_EQ(failures, 0);
|
||||
PASS();
|
||||
}
|
||||
|
||||
void suite_grammar_labels(void) {
|
||||
RUN_TEST(grammar_label_goldens);
|
||||
RUN_TEST(grammar_code_extracts_defs);
|
||||
}
|
||||
@@ -15,6 +15,7 @@
|
||||
*/
|
||||
#include "test_framework.h"
|
||||
#include "cbm.h"
|
||||
#include "grammar_cases.h"
|
||||
|
||||
static int reg_has_def_any(CBMFileResult *r, const char *name) {
|
||||
for (int i = 0; i < r->defs.count; i++) {
|
||||
@@ -29,16 +30,7 @@ static CBMFileResult *extract(const char *src, CBMLanguage lang, const char *pro
|
||||
return cbm_extract_file(src, (int)strlen(src), lang, proj, path, 0, NULL, NULL);
|
||||
}
|
||||
|
||||
typedef struct {
|
||||
const char *name;
|
||||
CBMLanguage lang;
|
||||
const char *path;
|
||||
const char *src;
|
||||
int min_defs;
|
||||
const char *expect[4];
|
||||
} GrammarCase;
|
||||
|
||||
static const GrammarCase CASES[] = {
|
||||
const GrammarCase CBM_GRAMMAR_CASES[] = {
|
||||
/* ── LSP-backed / mainstream code languages (expect named defs) ── */
|
||||
{"go", CBM_LANG_GO, "a.go", "package m\nfunc Foo() {}\nfunc Bar() int { return 0 }\n", 2, {"Foo", "Bar", NULL}},
|
||||
{"c", CBM_LANG_C, "a.c", "int foo(void){return 0;}\nint bar(void){return 1;}\n", 2, {"foo", "bar", NULL}},
|
||||
@@ -207,11 +199,13 @@ static const GrammarCase CASES[] = {
|
||||
{"mermaid", CBM_LANG_MERMAID, "a.mmd", "graph TD\n A --> B\n", 0, {NULL}},
|
||||
};
|
||||
|
||||
const size_t CBM_GRAMMAR_CASES_COUNT = sizeof(CBM_GRAMMAR_CASES) / sizeof(CBM_GRAMMAR_CASES[0]);
|
||||
|
||||
TEST(grammar_regression_all) {
|
||||
int failures = 0;
|
||||
size_t n = sizeof(CASES) / sizeof(CASES[0]);
|
||||
size_t n = CBM_GRAMMAR_CASES_COUNT;
|
||||
for (size_t i = 0; i < n; i++) {
|
||||
const GrammarCase *c = &CASES[i];
|
||||
const GrammarCase *c = &CBM_GRAMMAR_CASES[i];
|
||||
CBMFileResult *r = extract(c->src, c->lang, "reg", c->path);
|
||||
if (!r) {
|
||||
fprintf(stderr, " [REG] %-14s extract returned NULL\n", c->name);
|
||||
|
||||
File diff suppressed because it is too large.
Load diff
@@ -21,6 +21,8 @@ extern void suite_str_util(void);
|
||||
extern void suite_platform(void);
|
||||
extern void suite_extraction(void);
|
||||
extern void suite_grammar_regression(void);
|
||||
extern void suite_grammar_labels(void);
|
||||
extern void suite_grammar_imports(void);
|
||||
extern void suite_ac(void);
|
||||
extern void suite_store_nodes(void);
|
||||
extern void suite_store_edges(void);
|
||||
@@ -73,6 +75,7 @@ extern void suite_ui(void);
|
||||
extern void suite_security(void);
|
||||
extern void suite_yaml(void);
|
||||
extern void suite_integration(void);
|
||||
extern void suite_lang_contract(void);
|
||||
extern void suite_incremental(void);
|
||||
extern void suite_simhash(void);
|
||||
extern void suite_stack_overflow(void);
|
||||
@@ -93,6 +96,8 @@ int main(void) {
|
||||
RUN_SUITE(ac);
|
||||
RUN_SUITE(extraction);
|
||||
RUN_SUITE(grammar_regression);
|
||||
RUN_SUITE(grammar_labels);
|
||||
RUN_SUITE(grammar_imports);
|
||||
|
||||
/* Store (M5) */
|
||||
RUN_SUITE(store_nodes);
|
||||
@@ -196,6 +201,10 @@ int main(void) {
|
||||
|
||||
/* Integration (end-to-end) */
|
||||
RUN_SUITE(integration);
|
||||
|
||||
/* Per-language graph contracts (node/edge types, attribution, no-crash) */
|
||||
RUN_SUITE(lang_contract);
|
||||
|
||||
RUN_SUITE(incremental);
|
||||
|
||||
/* Release sqlite3 internal caches so ASan doesn't report them as leaks */
|
||||
|
||||
Reference in new issue
Block a user