/* * test_grammar_probe_f.c — IaC/build node/edge probe. * * SCOPE * ───── * Probes 19 IaC/config-as-code and build grammars: * IaC/config: hcl, k8s, kustomize, nix, nickel, pkl, ron, jsonnet, kdl, * hyprlang, devicetree * Build: cmake, makefile, meson, gn, just, bitbake, kconfig, gomod * * DIMENSIONS PER GRAMMAR * ────────────────────── * • NODE creation — resource/target/recipe/config-entry blocks reach the graph * • IMPORTS edges — include/import/`use` where the pipeline models it * • INFRA_MAPS — HCL module.source → Route edge (parallel path only; probed * in the single-path here as an expected RED) * • DEPENDS_ON — gomod require → dependency node * * COLOUR LEGEND * ───────────── * green = guard: the pipeline already produces the result; a failure is a regression. * red = bug reproduction: pipeline does NOT yet produce the expected node/edge; * the test FAILS until fixed. Brief inline comment names the root cause. * * Not registered in test_main.c — sibling agent owns that file. * * SUITE(grammar_probe_f) */ #include "../src/foundation/compat.h" #include "test_framework.h" #include "test_helpers.h" #include "cbm.h" #include #include #include #include #include #include #include #include #include /* ══════════════════════════════════════════════════════════════════ * Harness — mirrors test_grammar_probe_a.c (prefix "gpf_"). * ══════════════════════════════════════════════════════════════════ */ typedef struct { char tmpdir[256]; char dbpath[512]; char *project; cbm_mcp_server_t *srv; } GpfProj; typedef struct { const char *name; const char *content; } GpfFile; static void gpf_to_fwd_slashes(char *p) { for (; *p; p++) { if (*p == '\\') *p = '/'; } } static cbm_store_t *gpf_open_indexed(GpfProj *lp) { lp->project = cbm_project_name_from_path(lp->tmpdir); if (!lp->project) return NULL; const char *home = getenv("HOME"); if (!home) home = "/tmp"; char cache_dir[512]; snprintf(cache_dir, sizeof(cache_dir), "%s/.cache/codebase-memory-mcp", home); cbm_mkdir(cache_dir); snprintf(lp->dbpath, sizeof(lp->dbpath), "%s/%s.db", cache_dir, lp->project); unlink(lp->dbpath); lp->srv = cbm_mcp_server_new(NULL); if (!lp->srv) return NULL; char args[700]; snprintf(args, sizeof(args), "{\"repo_path\":\"%s\"}", lp->tmpdir); char *resp = cbm_mcp_handle_tool(lp->srv, "index_repository", args); if (resp) free(resp); return cbm_store_open_path(lp->dbpath); } static cbm_store_t *gpf_index_files(GpfProj *lp, const GpfFile *files, int nfiles) { memset(lp, 0, sizeof(*lp)); snprintf(lp->tmpdir, sizeof(lp->tmpdir), "/tmp/cbm_gpf_XXXXXX"); if (!cbm_mkdtemp(lp->tmpdir)) return NULL; gpf_to_fwd_slashes(lp->tmpdir); for (int i = 0; i < nfiles; i++) { char path[700]; snprintf(path, sizeof(path), "%s/%s", lp->tmpdir, files[i].name); char *slash = strrchr(path, '/'); if (slash && slash > path + strlen(lp->tmpdir)) { *slash = '\0'; cbm_mkdir_p(path, 0755); *slash = '/'; } FILE *f = fopen(path, "wb"); if (!f) return NULL; fputs(files[i].content, f); fclose(f); } return gpf_open_indexed(lp); } static void gpf_cleanup(GpfProj *lp, cbm_store_t *store) { if (store) cbm_store_close(store); if (lp->srv) { cbm_mcp_server_free(lp->srv); lp->srv = NULL; } free(lp->project); lp->project = NULL; th_rmtree(lp->tmpdir); unlink(lp->dbpath); char wal[600], shm[600]; snprintf(wal, sizeof(wal), "%s-wal", lp->dbpath); unlink(wal); snprintf(shm, sizeof(shm), "%s-shm", lp->dbpath); unlink(shm); } /* ── Node-count helpers ──────────────────────────────────────────── */ static int gpf_count_label(cbm_store_t *store, const char *project, const char *label) { cbm_node_t *nodes = NULL; int count = 0; if (cbm_store_find_nodes_by_label(store, project, label, &nodes, &count) != CBM_STORE_OK) return -1; cbm_store_free_nodes(nodes, count); return count; } /* Metrics bundled per index pass. */ typedef struct { int ok; int total_nodes; int functions; /* Function label */ int modules; /* Module label */ int classes; /* Class label */ int resources; /* Resource label (k8s) */ int imports; /* IMPORTS edges */ int depends_on; /* DEPENDS_ON edges */ int infra_maps; /* INFRA_MAPS edges */ } GpfMetrics; static GpfMetrics gpf_metrics_files(const GpfFile *files, int nfiles) { GpfProj lp; cbm_store_t *store = gpf_index_files(&lp, files, nfiles); GpfMetrics m = {0}; if (store) { m.ok = 1; m.total_nodes = cbm_store_count_nodes(store, lp.project); m.functions = gpf_count_label(store, lp.project, "Function"); m.modules = gpf_count_label(store, lp.project, "Module"); m.classes = gpf_count_label(store, lp.project, "Class"); m.resources = gpf_count_label(store, lp.project, "Resource"); m.imports = cbm_store_count_edges_by_type(store, lp.project, "IMPORTS"); m.depends_on = cbm_store_count_edges_by_type(store, lp.project, "DEPENDS_ON"); m.infra_maps = cbm_store_count_edges_by_type(store, lp.project, "INFRA_MAPS"); } gpf_cleanup(&lp, store); return m; } static GpfMetrics gpf_metrics(const char *filename, const char *content) { GpfFile f = {filename, content}; return gpf_metrics_files(&f, 1); } /* ══════════════════════════════════════════════════════════════════ * GROUP 1 — HCL / Terraform (.hcl, .tf) * * Labels histogram: Class:1, Module:1 * The extractor models HCL as a single Module node plus a Class node for * the file-level scope. Individual `resource` / `module` blocks are NOT * yet extracted as separate nodes (grammar-only path). * INFRA_MAPS is produced only by the parallel-path pipeline (>50 files); * single-file fixture tests it as RED. * ══════════════════════════════════════════════════════════════════ */ /* HCL: minimal resource block → at least 1 node (Module) and 1 Class node. */ TEST(probe_hcl_resource_nodes) { GpfMetrics m = gpf_metrics("main.tf", "resource \"aws_s3_bucket\" \"my_bucket\" {\n" " bucket = \"my-tf-test-bucket\"\n" " acl = \"private\"\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: at minimum a Module node is created for an HCL file. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* HCL: Class node present alongside Module (label histogram: Class:1,Module:1). */ TEST(probe_hcl_class_node) { GpfMetrics m = gpf_metrics("infra.hcl", "resource \"aws_instance\" \"web\" {\n" " ami = \"ami-0c55b159cbfafe1f0\"\n" " instance_type = \"t2.micro\"\n" "}\n" "\n" "resource \"aws_instance\" \"db\" {\n" " ami = \"ami-0c55b159cbfafe1f0\"\n" " instance_type = \"t2.small\"\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: histogram confirms Class:1,Module:1 for any non-empty HCL file. */ ASSERT_TRUE(m.classes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* HCL: module block with `source` → separate resource-like node. * RED: grammar-only HCL extractor produces only Class+Module (one per file); * individual resource/module blocks are not broken out as distinct nodes. */ TEST(probe_hcl_module_block_node) { GpfMetrics m = gpf_metrics("modules.tf", "module \"vpc\" {\n" " source = \"terraform-aws-modules/vpc/aws\"\n" " version = \"3.14.0\"\n" " cidr = \"10.0.0.0/16\"\n" "}\n" "\n" "module \"eks\" {\n" " source = \"terraform-aws-modules/eks/aws\"\n" " version = \"18.26.6\"\n" " cluster_name = \"my-eks-cluster\"\n" "}\n"); ASSERT_TRUE(m.ok); /* RED: individual module blocks not extracted as separate Resource/Module nodes; * only the file-level Class+Module pair is produced. */ ASSERT_TRUE(m.total_nodes >= 3); /* expected RED — no per-block node extraction */ PASS(); } /* HCL: INFRA_MAPS edge (module.source URL → Route node). * RED: INFRA_MAPS is produced only by the parallel-path pipeline (>50 files); * single-file fixture never crosses the threshold. */ TEST(probe_hcl_infra_maps_edge) { GpfMetrics m = gpf_metrics("sched.tf", "resource \"google_cloud_scheduler_job\" \"cron\" {\n" " name = \"cron-job\"\n" " schedule = \"0 * * * *\"\n" " http_target {\n" " uri = \"https://my-service.example.com/run\"\n" " }\n" "}\n"); ASSERT_TRUE(m.ok); /* RED: INFRA_MAPS requires parallel-path (>50 files) — not triggered here. */ ASSERT_TRUE(m.infra_maps >= 1); /* expected RED — parallel path only */ PASS(); } /* HCL: two-file fixture (.tf + .hcl) → still no cross-file IMPORTS edge * because HCL/Terraform uses a directory-level evaluation model, not imports. */ TEST(probe_hcl_no_spurious_imports) { static const GpfFile files[] = { {"vars.hcl", "variable \"region\" {\n" " default = \"us-east-1\"\n" "}\n"}, {"main.tf", "resource \"aws_s3_bucket\" \"b\" {\n" " bucket = \"test\"\n" "}\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* GREEN: HCL has no import syntax — pipeline should emit 0 IMPORTS edges. */ ASSERT_TRUE(m.imports == 0); PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 2 — Kubernetes manifests (.yaml with apiVersion:) * * Labels histogram: Module:1 (generic YAML pass); Resource node is emitted * by pass_k8s.c (handle_k8s_manifest → cbm_extract_file CBM_LANG_K8S). * ══════════════════════════════════════════════════════════════════ */ /* K8s: Pod manifest → Resource node (kind=Pod, name=nginx-pod). */ TEST(probe_k8s_pod_resource_node) { GpfMetrics m = gpf_metrics("pod.yaml", "apiVersion: v1\n" "kind: Pod\n" "metadata:\n" " name: nginx-pod\n" " namespace: default\n" "spec:\n" " containers:\n" " - name: nginx\n" " image: nginx:1.21\n"); ASSERT_TRUE(m.ok); /* GREEN: pass_k8s emits a Resource node for any k8s manifest with apiVersion. */ ASSERT_TRUE(m.resources >= 1); PASS(); } /* K8s: Deployment manifest → Resource node. */ TEST(probe_k8s_deployment_resource_node) { GpfMetrics m = gpf_metrics("deploy.yaml", "apiVersion: apps/v1\n" "kind: Deployment\n" "metadata:\n" " name: my-app\n" "spec:\n" " replicas: 3\n" " selector:\n" " matchLabels:\n" " app: my-app\n" " template:\n" " metadata:\n" " labels:\n" " app: my-app\n" " spec:\n" " containers:\n" " - name: app\n" " image: my-app:latest\n"); ASSERT_TRUE(m.ok); /* GREEN: Deployment kind → Resource node via pass_k8s. */ ASSERT_TRUE(m.resources >= 1); PASS(); } /* K8s: two manifests in one directory → 2 Resource nodes (one per file). */ TEST(probe_k8s_two_manifests) { static const GpfFile files[] = { {"service.yaml", "apiVersion: v1\n" "kind: Service\n" "metadata:\n" " name: my-svc\n" "spec:\n" " selector:\n" " app: my-app\n" " ports:\n" " - port: 80\n"}, {"configmap.yaml", "apiVersion: v1\n" "kind: ConfigMap\n" "metadata:\n" " name: app-config\n" "data:\n" " key: value\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* GREEN: each manifest file produces its own Resource node. */ ASSERT_TRUE(m.resources >= 2); PASS(); } /* K8s: cross-manifest reference via INFRA_MAPS. * RED: the single-path pipeline does not emit INFRA_MAPS edges for k8s refs; * pass_k8s only emits Resource nodes (no ref-resolution between manifests). */ TEST(probe_k8s_infra_maps_edge) { static const GpfFile files[] = { {"deploy.yaml", "apiVersion: apps/v1\n" "kind: Deployment\n" "metadata:\n" " name: frontend\n" "spec:\n" " template:\n" " spec:\n" " containers:\n" " - name: frontend\n" " image: frontend:latest\n"}, {"service.yaml", "apiVersion: v1\n" "kind: Service\n" "metadata:\n" " name: frontend-svc\n" "spec:\n" " selector:\n" " app: frontend\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: inter-manifest INFRA_MAPS edges not modeled by the current k8s pass. */ ASSERT_TRUE(m.infra_maps >= 1); /* expected RED — no k8s selector → INFRA_MAPS */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 3 — Kustomize (kustomization.yaml) * * Labels histogram: Module:1 * pass_k8s.c handle_kustomize: emits a Module node + IMPORTS edges for each * resources/bases/patches entry that resolves to an existing graph node. * ══════════════════════════════════════════════════════════════════ */ /* Kustomize: overlay file → Module node. */ TEST(probe_kustomize_module_node) { GpfMetrics m = gpf_metrics("kustomization.yaml", "apiVersion: kustomize.config.k8s.io/v1beta1\n" "kind: Kustomization\n" "resources:\n" " - deploy.yaml\n" " - service.yaml\n"); ASSERT_TRUE(m.ok); /* GREEN: handle_kustomize always emits a Module node for the overlay file. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* Kustomize: overlay + two resource manifests → IMPORTS edges. * The resolver looks for nodes whose QN matches the resource paths. * Each resolved entry produces an IMPORTS edge (Module → Resource node). * RED: resources are YAML files that pass_k8s processes as k8s manifests only * when apiVersion is present; without it they are plain YAML with no node * in the graph, so the IMPORTS edge can't be created. */ TEST(probe_kustomize_imports_edges) { static const GpfFile files[] = { {"kustomization.yaml", "apiVersion: kustomize.config.k8s.io/v1beta1\n" "kind: Kustomization\n" "resources:\n" " - deploy.yaml\n" " - service.yaml\n"}, {"deploy.yaml", "apiVersion: apps/v1\n" "kind: Deployment\n" "metadata:\n" " name: myapp\n" "spec:\n" " replicas: 1\n" " template:\n" " spec:\n" " containers:\n" " - name: app\n" " image: myapp:v1\n"}, {"service.yaml", "apiVersion: v1\n" "kind: Service\n" "metadata:\n" " name: myapp-svc\n" "spec:\n" " selector:\n" " app: myapp\n" " ports:\n" " - port: 80\n"} }; GpfMetrics m = gpf_metrics_files(files, 3); ASSERT_TRUE(m.ok); /* Kustomize Module node is always emitted. */ ASSERT_TRUE(m.modules >= 1); /* RED: IMPORTS edge from kustomize Module → Resource nodes requires the * resource file to already be in the graph as a QN-matched node; * the ordering of pipeline passes may leave resource nodes absent * when the kustomize pass runs, preventing edge creation. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — resource QN resolution fails */ PASS(); } /* Kustomize: base overlay with patches entry → Module node (no crash). */ TEST(probe_kustomize_with_patches) { GpfMetrics m = gpf_metrics("kustomization.yaml", "apiVersion: kustomize.config.k8s.io/v1beta1\n" "kind: Kustomization\n" "bases:\n" " - ../base\n" "patches:\n" " - path: replica-patch.yaml\n"); ASSERT_TRUE(m.ok); /* GREEN: handle_kustomize emits Module node regardless of patches content. */ ASSERT_TRUE(m.modules >= 1); ASSERT_TRUE(m.total_nodes >= 1); PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 4 — Nix (.nix) * * Labels histogram: Module:1 * Nix is a purely functional language; the grammar-only extractor produces * a single Module node per file. Nix `import` expressions (e.g. * `import ./other.nix`) are textual — the pipeline does not yet resolve * them into IMPORTS graph edges. * ══════════════════════════════════════════════════════════════════ */ /* Nix: attribute set with derivation-like attrs → at least 1 node. */ TEST(probe_nix_attrset_node) { GpfMetrics m = gpf_metrics("default.nix", "{ pkgs ? import {} }:\n" "\n" "pkgs.stdenv.mkDerivation {\n" " pname = \"mypackage\";\n" " version = \"1.0.0\";\n" " src = ./src;\n" " buildInputs = [ pkgs.gcc pkgs.make ];\n" " buildPhase = \"make\";\n" " installPhase = \"make install\";\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: any non-empty Nix file produces at least a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* Nix: let-binding expression — multiple top-level attrs. */ TEST(probe_nix_let_binding) { GpfMetrics m = gpf_metrics("lib.nix", "let\n" " double = x: x * 2;\n" " square = x: x * x;\n" " cube = x: x * x * x;\n" "in {\n" " inherit double square cube;\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: file-level Module node created. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* Nix: `import` in two-file fixture → IMPORTS edge. * RED: Nix import expressions are not resolved into IMPORTS graph edges. */ TEST(probe_nix_import_edge) { static const GpfFile files[] = { {"utils.nix", "{ double = x: x * 2; }\n"}, {"default.nix", "let utils = import ./utils.nix;\n" "in { result = utils.double 21; }\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: Nix `import` not resolved into IMPORTS edges by the grammar-only pipeline. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no Nix import resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 5 — Nickel (.ncl) * * Labels histogram: Module:1 * Nickel is a configuration language; the extractor produces a single Module * node per file. Import with `import "path"` is syntactically supported but * not yet resolved into IMPORTS edges. * ══════════════════════════════════════════════════════════════════ */ /* Nickel: record literal → Module node (no crash). */ TEST(probe_nickel_record_node) { GpfMetrics m = gpf_metrics("config.ncl", "{\n" " server = {\n" " host = \"localhost\",\n" " port = 8080,\n" " },\n" " database = {\n" " name = \"mydb\",\n" " max_connections = 10,\n" " },\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: any non-empty Nickel file produces a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* Nickel: let-binding with function value. */ TEST(probe_nickel_let_fn) { GpfMetrics m = gpf_metrics("lib.ncl", "let double = fun x => x * 2 in\n" "let square = fun x => x * x in\n" "{ double, square }\n"); ASSERT_TRUE(m.ok); /* GREEN: file-level Module node. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* Nickel: `import` in two-file fixture → IMPORTS edge. * RED: Nickel import expressions not resolved by the pipeline. */ TEST(probe_nickel_import_edge) { static const GpfFile files[] = { {"utils.ncl", "{ double = fun x => x * 2 }\n"}, {"main.ncl", "let utils = import \"utils.ncl\" in\n" "{ result = utils.double 21 }\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: Nickel `import` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no Nickel import resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 6 — Pkl (.pkl) * * Labels histogram: Module:1 * Pkl is Apple's configuration language. `amends`/`import` for cross-file * references — not yet resolved by the pipeline. * ══════════════════════════════════════════════════════════════════ */ /* Pkl: property assignments → Module node. */ TEST(probe_pkl_properties_node) { GpfMetrics m = gpf_metrics("config.pkl", "host = \"localhost\"\n" "port = 8080\n" "debug = false\n" "maxConnections = 10\n"); ASSERT_TRUE(m.ok); /* GREEN: any non-empty Pkl file produces at least a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* Pkl: class-like object definition. */ TEST(probe_pkl_object_block) { GpfMetrics m = gpf_metrics("server.pkl", "server {\n" " host = \"0.0.0.0\"\n" " port = 443\n" " tls {\n" " enabled = true\n" " cert = \"/etc/ssl/cert.pem\"\n" " }\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: Module node. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* Pkl: `import` in two-file fixture → IMPORTS edge. * RED: Pkl import/amends not resolved into IMPORTS edges. */ TEST(probe_pkl_import_edge) { static const GpfFile files[] = { {"base.pkl", "host = \"localhost\"\n" "port = 8080\n"}, {"prod.pkl", "amends \"base.pkl\"\n" "\n" "port = 443\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: Pkl `amends`/`import` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no Pkl import resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 7 — RON (.ron) * * Labels histogram: Module:1 * RON (Rusty Object Notation) is a data serialization format; the extractor * produces a Module node per file. No import mechanism exists in RON. * ══════════════════════════════════════════════════════════════════ */ /* RON: struct-like record → Module node. */ TEST(probe_ron_record_node) { GpfMetrics m = gpf_metrics("config.ron", "(\n" " host: \"localhost\",\n" " port: 8080,\n" " debug: false,\n" " tags: [\"api\", \"v2\"],\n" ")\n"); ASSERT_TRUE(m.ok); /* GREEN: RON file produces at least 1 node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* RON: named struct. */ TEST(probe_ron_named_struct) { GpfMetrics m = gpf_metrics("scene.ron", "Scene(\n" " entities: [\n" " Entity(id: 1, name: \"player\"),\n" " Entity(id: 2, name: \"enemy\"),\n" " ],\n" ")\n"); ASSERT_TRUE(m.ok); /* GREEN: Module node for the RON file. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* RON: no import mechanism → 0 IMPORTS edges expected. */ TEST(probe_ron_no_imports) { GpfMetrics m = gpf_metrics("data.ron", "[\n" " (key: \"a\", value: 1),\n" " (key: \"b\", value: 2),\n" "]\n"); ASSERT_TRUE(m.ok); /* GREEN: RON has no import syntax; pipeline emits 0 IMPORTS edges. */ ASSERT_TRUE(m.imports == 0); PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 8 — Jsonnet (.jsonnet, .libsonnet) * * Labels histogram: Module:1 * Jsonnet is a data templating language. `import`/`importstr` for cross-file * references — not yet resolved by the pipeline. * ══════════════════════════════════════════════════════════════════ */ /* Jsonnet: object literal → Module node. */ TEST(probe_jsonnet_object_node) { GpfMetrics m = gpf_metrics("config.jsonnet", "{\n" " local env = \"prod\",\n" " host: \"api.example.com\",\n" " port: 443,\n" " debug: env == \"dev\",\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: any non-empty Jsonnet file produces at least a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* Jsonnet: function definition in object. */ TEST(probe_jsonnet_function_in_object) { GpfMetrics m = gpf_metrics("lib.jsonnet", "{\n" " double(x):: x * 2,\n" " square(x):: x * x,\n" " greet(name):: \"Hello, \" + name,\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: Module node. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* Jsonnet: `import` in two-file fixture → IMPORTS edge. * RED: Jsonnet import not resolved into IMPORTS edges. */ TEST(probe_jsonnet_import_edge) { static const GpfFile files[] = { {"utils.libsonnet", "{\n" " double(x):: x * 2,\n" "}\n"}, {"main.jsonnet", "local utils = import 'utils.libsonnet';\n" "{\n" " result: utils.double(21),\n" "}\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: Jsonnet `import` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no Jsonnet import resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 9 — KDL (.kdl) * * Labels histogram: Module:1 * KDL is a document language. No import mechanism; single Module per file. * ══════════════════════════════════════════════════════════════════ */ /* KDL: node with children → Module node. */ TEST(probe_kdl_node_block) { GpfMetrics m = gpf_metrics("config.kdl", "server {\n" " host \"localhost\"\n" " port 8080\n" "}\n" "\n" "database {\n" " host \"db.internal\"\n" " port 5432\n" " name \"mydb\"\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: KDL file produces at least a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* KDL: flat node list (key-value style). */ TEST(probe_kdl_flat_nodes) { GpfMetrics m = gpf_metrics("manifest.kdl", "package \"my-package\" version=\"1.0.0\"\n" "description \"A test package\"\n" "author \"Test Author\" email=\"test@example.com\"\n"); ASSERT_TRUE(m.ok); /* GREEN: Module node. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* KDL: no import mechanism → 0 IMPORTS edges. */ TEST(probe_kdl_no_imports) { GpfMetrics m = gpf_metrics("data.kdl", "items {\n" " item id=1 name=\"alpha\"\n" " item id=2 name=\"beta\"\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: KDL has no import syntax; 0 IMPORTS edges expected. */ ASSERT_TRUE(m.imports == 0); PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 10 — Hyprlang (.conf, .hl, hyprland.conf) * * Labels histogram: Module:1 * Hyprlang is the Hyprland compositor config language. `source` directive * for including other files — not resolved by the pipeline. * ══════════════════════════════════════════════════════════════════ */ /* Hyprlang: section block → Module node. */ TEST(probe_hyprlang_section_node) { GpfMetrics m = gpf_metrics("hyprland.conf", "general {\n" " gaps_in = 5\n" " gaps_out = 10\n" " border_size = 2\n" " col.active_border = rgba(33ccffee)\n" "}\n" "\n" "decoration {\n" " rounding = 10\n" " blur {\n" " enabled = true\n" " size = 3\n" " }\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: any non-empty Hyprlang file produces at least a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* Hyprlang: keybind and monitor directives. */ TEST(probe_hyprlang_directives) { GpfMetrics m = gpf_metrics("binds.hl", "bind = $mainMod, Q, killactive\n" "bind = $mainMod, M, exit\n" "bind = $mainMod, F, fullscreen\n" "monitor = DP-1, 1920x1080@144, 0x0, 1\n"); ASSERT_TRUE(m.ok); /* GREEN: Module node for any non-empty .hl file. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* Hyprlang: `source` in two-file fixture → IMPORTS edge. * RED: Hyprlang `source` directive not resolved into IMPORTS edges. */ TEST(probe_hyprlang_source_import_edge) { static const GpfFile files[] = { {"colors.conf", "col.active_border = rgba(33ccffee)\n" "col.inactive_border = rgba(595959aa)\n"}, {"hyprland.conf", "source = ~/.config/hypr/colors.conf\n" "\n" "general {\n" " gaps_in = 5\n" "}\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: Hyprlang `source` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no Hyprlang source resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 11 — DeviceTree (.dts, .dtsi, .overlay) * * Labels histogram: Module:1 * DeviceTree Source describes hardware topology; node blocks like `/ { ... }` * or named nodes like `cpu@0 { ... }`. `#include` is preprocessor-level; * not modeled as IMPORTS in the pipeline. * ══════════════════════════════════════════════════════════════════ */ /* DeviceTree: minimal DTS with root node → Module node. */ TEST(probe_devicetree_root_node) { GpfMetrics m = gpf_metrics("board.dts", "/dts-v1/;\n" "\n" "/ {\n" " compatible = \"vendor,board\";\n" " model = \"Vendor Board v1\";\n" " #address-cells = <1>;\n" " #size-cells = <1>;\n" "};\n"); ASSERT_TRUE(m.ok); /* GREEN: any non-empty DTS file produces at least a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* DeviceTree: named child nodes (cpu, memory). */ TEST(probe_devicetree_child_nodes) { GpfMetrics m = gpf_metrics("soc.dts", "/dts-v1/;\n" "\n" "/ {\n" " cpus {\n" " #address-cells = <1>;\n" " #size-cells = <0>;\n" " cpu@0 {\n" " compatible = \"arm,cortex-a53\";\n" " reg = <0>;\n" " };\n" " cpu@1 {\n" " compatible = \"arm,cortex-a53\";\n" " reg = <1>;\n" " };\n" " };\n" " memory@80000000 {\n" " reg = <0x80000000 0x40000000>;\n" " };\n" "};\n"); ASSERT_TRUE(m.ok); /* GREEN: Module node (child nodes not individually extracted in grammar-only path). */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* DeviceTree: .dtsi include fragment → IMPORTS edge. * RED: DeviceTree `#include` is handled at preprocessor level, not by the pipeline * as an IMPORTS graph edge. */ TEST(probe_devicetree_include_edge) { static const GpfFile files[] = { {"soc.dtsi", "/dts-v1/;\n" "/ {\n" " soc: soc {\n" " compatible = \"simple-bus\";\n" " };\n" "};\n"}, {"board.dts", "/dts-v1/;\n" "#include \"soc.dtsi\"\n" "\n" "/ {\n" " model = \"My Board\";\n" "};\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: DTS `#include` not resolved into IMPORTS edges (preprocessor-level). */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no DTS include resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 12 — CMake (CMakeLists.txt, .cmake) * * Labels histogram: Function:1, Module:1 * CMake's grammar extractor produces a Function node per defined function/macro * plus a Module for the file. `include()` and `add_subdirectory()` are * candidates for IMPORTS edges but not yet resolved. * ══════════════════════════════════════════════════════════════════ */ /* CMake: function definition → Function node. */ TEST(probe_cmake_function_node) { GpfMetrics m = gpf_metrics("CMakeLists.txt", "cmake_minimum_required(VERSION 3.16)\n" "project(MyProject)\n" "\n" "function(add_test_target name)\n" " add_executable(${name} ${name}.cpp)\n" " target_link_libraries(${name} PRIVATE gtest)\n" "endfunction()\n"); ASSERT_TRUE(m.ok); /* GREEN: CMake function() definition must produce a Function node. */ ASSERT_TRUE(m.functions >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* CMake: macro definition → Function node (macros map to Function label). */ TEST(probe_cmake_macro_node) { GpfMetrics m = gpf_metrics("helpers.cmake", "macro(print_var varname)\n" " message(STATUS \"${varname} = ${${varname}}\")\n" "endmacro()\n" "\n" "function(setup_target name)\n" " add_library(${name} STATIC)\n" "endfunction()\n"); ASSERT_TRUE(m.ok); /* GREEN: both function() and macro() produce Function nodes. */ ASSERT_TRUE(m.functions >= 1); PASS(); } /* CMake: `include()` in two-file fixture → IMPORTS edge. * RED: CMake include() not resolved into IMPORTS edges. */ TEST(probe_cmake_include_edge) { static const GpfFile files[] = { {"helpers.cmake", "function(check_compiler)\n" " message(STATUS \"Compiler: ${CMAKE_CXX_COMPILER_ID}\")\n" "endfunction()\n"}, {"CMakeLists.txt", "cmake_minimum_required(VERSION 3.16)\n" "project(Foo)\n" "include(helpers.cmake)\n" "check_compiler()\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: CMake `include()` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no CMake include resolver */ PASS(); } /* CMake: add_subdirectory → child CMakeLists.txt; IMPORTS edge expected. * RED: add_subdirectory not resolved into IMPORTS edges. */ TEST(probe_cmake_subdirectory_edge) { static const GpfFile files[] = { {"CMakeLists.txt", "cmake_minimum_required(VERSION 3.16)\n" "project(Root)\n" "add_subdirectory(lib)\n"}, {"lib/CMakeLists.txt", "add_library(mylib STATIC mylib.cpp)\n" "\n" "function(mylib_configure target)\n" " target_link_libraries(${target} PRIVATE mylib)\n" "endfunction()\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: CMake add_subdirectory() not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no add_subdirectory resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 13 — Makefile (Makefile, .mk, GNUmakefile) * * Labels histogram: Function:1, Module:1 * The Makefile extractor produces a Function node for targets and a Module for * the file. `include` directive is a candidate for IMPORTS but not resolved. * ══════════════════════════════════════════════════════════════════ */ /* Makefile: phony targets → Function nodes. */ TEST(probe_makefile_target_nodes) { GpfMetrics m = gpf_metrics("Makefile", ".PHONY: all build test clean\n" "\n" "all: build\n" "\n" "build:\n" "\tgcc -o myapp main.c utils.c\n" "\n" "test:\n" "\t./run_tests.sh\n" "\n" "clean:\n" "\trm -f myapp *.o\n"); ASSERT_TRUE(m.ok); /* GREEN: at least the first target must produce a Function node. */ ASSERT_TRUE(m.functions >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* Makefile: pattern rules and variables. */ TEST(probe_makefile_pattern_rule) { GpfMetrics m = gpf_metrics("rules.mk", "CC = gcc\n" "CFLAGS = -Wall -Wextra -O2\n" "\n" "%.o: %.c\n" "\t$(CC) $(CFLAGS) -c $< -o $@\n" "\n" "install:\n" "\tcp myapp /usr/local/bin/\n"); ASSERT_TRUE(m.ok); /* GREEN: Module node for any non-empty Makefile. */ ASSERT_TRUE(m.total_nodes >= 1); PASS(); } /* Makefile: `include` in two-file fixture → IMPORTS edge. * RED: Makefile `include` directive not resolved into IMPORTS edges. */ TEST(probe_makefile_include_edge) { static const GpfFile files[] = { {"common.mk", "CC = gcc\n" "CFLAGS = -Wall -O2\n" "\n" "clean:\n" "\trm -f *.o\n"}, {"Makefile", "include common.mk\n" "\n" "all:\n" "\t$(CC) $(CFLAGS) -o myapp main.c\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: Makefile `include` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no Makefile include resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 14 — Meson (meson.build, meson.options) * * Labels histogram: Module:1 * Meson's grammar-only extractor produces a Module node per file. Individual * build targets (executable(), library()) are not extracted as distinct nodes. * `subdir()` maps to IMPORTS conceptually but is not resolved. * ══════════════════════════════════════════════════════════════════ */ /* Meson: project + executable → Module node. */ TEST(probe_meson_project_node) { GpfMetrics m = gpf_metrics("meson.build", "project('myapp', 'c',\n" " version: '1.0.0',\n" " default_options: ['warning_level=2'])\n" "\n" "executable('myapp',\n" " sources: ['main.c', 'utils.c'],\n" " install: true)\n"); ASSERT_TRUE(m.ok); /* GREEN: any non-empty meson.build produces at least a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* Meson: options file → Module node. */ TEST(probe_meson_options_node) { GpfMetrics m = gpf_metrics("meson.options", "option('enable_tests', type: 'boolean', value: true,\n" " description: 'Build and run tests')\n" "option('build_type', type: 'combo', choices: ['debug', 'release'],\n" " value: 'release')\n"); ASSERT_TRUE(m.ok); /* GREEN: meson.options also produces a Module node. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* Meson: subdir() across two files → IMPORTS edge. * RED: Meson subdir() not resolved into IMPORTS edges. */ TEST(probe_meson_subdir_edge) { static const GpfFile files[] = { {"meson.build", "project('root', 'c')\n" "subdir('lib')\n"}, {"lib/meson.build", "static_library('mylib', 'mylib.c')\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: Meson subdir() not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no Meson subdir resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 15 — GN (.gn, .gni) * * Labels histogram: Module:1 * GN (Generate Ninja) build system for Chromium-like projects. The extractor * produces a Module node per file. `import()` for shared .gni files is a * candidate for IMPORTS but not yet resolved. * ══════════════════════════════════════════════════════════════════ */ /* GN: executable target → Module node (individual targets not extracted). */ TEST(probe_gn_executable_node) { GpfMetrics m = gpf_metrics("BUILD.gn", "executable(\"myapp\") {\n" " sources = [\n" " \"main.cc\",\n" " \"utils.cc\",\n" " ]\n" " deps = [ \"//third_party/base\" ]\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: any non-empty .gn file produces at least a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* GN: shared_library and static_library targets. */ TEST(probe_gn_library_targets) { GpfMetrics m = gpf_metrics("lib.gn", "static_library(\"crypto\") {\n" " sources = [ \"crypto.cc\", \"sha256.cc\" ]\n" "}\n" "\n" "shared_library(\"net\") {\n" " sources = [ \"net.cc\", \"socket.cc\" ]\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: Module node. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* GN: import() of a .gni file → IMPORTS edge. * RED: GN import() not resolved into IMPORTS edges. */ TEST(probe_gn_import_edge) { static const GpfFile files[] = { {"//build/common.gni", "template(\"cc_binary\") {\n" " executable(target_name) {\n" " forward_variables_from(invoker, \"*\")\n" " }\n" "}\n"}, {"BUILD.gn", "import(\"//build/common.gni\")\n" "\n" "cc_binary(\"myapp\") {\n" " sources = [ \"main.cc\" ]\n" "}\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: GN import() not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no GN import resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 16 — Just (justfile, Justfile, .justfile) * * Labels histogram: Module:1 * Just is a command runner. Recipes are the build units; the grammar-only * extractor currently produces only a Module node per file (recipes not * individually extracted as Function nodes per the histogram). * ══════════════════════════════════════════════════════════════════ */ /* Just: recipe definitions → at least Module node. */ TEST(probe_just_recipe_nodes) { GpfMetrics m = gpf_metrics("justfile", "build:\n" " cargo build --release\n" "\n" "test:\n" " cargo test\n" "\n" "clean:\n" " cargo clean\n" "\n" "fmt:\n" " cargo fmt\n"); ASSERT_TRUE(m.ok); /* GREEN: any non-empty justfile produces at least a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* Just: recipe nodes as Function nodes. * RED: histogram shows Module:1 only — individual recipes are not extracted * as Function nodes by the grammar-only Just extractor. */ TEST(probe_just_recipe_function_nodes) { GpfMetrics m = gpf_metrics("Justfile", "deploy env:\n" " ./scripts/deploy.sh {{env}}\n" "\n" "lint:\n" " golangci-lint run ./...\n" "\n" "generate:\n" " go generate ./...\n"); ASSERT_TRUE(m.ok); /* RED: Just recipes not extracted as Function nodes (only Module:1 in histogram). */ ASSERT_TRUE(m.functions >= 1); /* expected RED — no Just recipe → Function extraction */ PASS(); } /* Just: `import` directive (Just >=1.5) → IMPORTS edge. * RED: Just `import` not resolved into IMPORTS edges. */ TEST(probe_just_import_edge) { static const GpfFile files[] = { {"common.just", "fmt:\n" " cargo fmt\n" "\n" "lint:\n" " cargo clippy\n"}, {"justfile", "import 'common.just'\n" "\n" "build:\n" " cargo build\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: Just `import` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no Just import resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 17 — BitBake (.bb, .bbappend, .bbclass) * * Labels histogram: Module:1 * BitBake is the build system used by Yocto/OpenEmbedded. Recipes define * tasks (do_compile, do_install, etc.) and variables. The extractor produces * a Module node per file; individual tasks not extracted as Function nodes. * `inherit` and `require`/`include` are cross-file mechanisms. * ══════════════════════════════════════════════════════════════════ */ /* BitBake: recipe with task and variable → Module node. */ TEST(probe_bitbake_recipe_node) { GpfMetrics m = gpf_metrics("mypackage_1.0.bb", "DESCRIPTION = \"A simple test package\"\n" "HOMEPAGE = \"https://example.com\"\n" "LICENSE = \"MIT\"\n" "LIC_FILES_CHKSUM = \"file://COPYING;md5=abc123\"\n" "\n" "SRC_URI = \"https://example.com/mypackage-1.0.tar.gz\"\n" "\n" "do_compile() {\n" " make\n" "}\n" "\n" "do_install() {\n" " install -d ${D}${bindir}\n" " install -m 0755 myapp ${D}${bindir}/\n" "}\n"); ASSERT_TRUE(m.ok); /* GREEN: any non-empty .bb file produces at least a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* BitBake: bbclass file → Module node. */ TEST(probe_bitbake_bbclass_node) { GpfMetrics m = gpf_metrics("cmake.bbclass", "# CMake class for BitBake\n" "EXTRA_OECMAKE ?= \"\"\n" "\n" "cmake_do_configure() {\n" " cmake -DCMAKE_INSTALL_PREFIX=${prefix} ${EXTRA_OECMAKE} ${S}\n" "}\n" "\n" "cmake_do_compile() {\n" " make ${EXTRA_OEMAKE}\n" "}\n" "\n" "EXPORT_FUNCTIONS do_configure do_compile\n"); ASSERT_TRUE(m.ok); /* GREEN: .bbclass produces a Module node. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* BitBake: `require` in two-file fixture → IMPORTS edge. * RED: BitBake require/include not resolved into IMPORTS edges. */ TEST(probe_bitbake_require_edge) { static const GpfFile files[] = { {"mypackage.inc", "DESCRIPTION = \"Shared package metadata\"\n" "LICENSE = \"MIT\"\n" "SRC_URI = \"https://example.com/pkg-${PV}.tar.gz\"\n"}, {"mypackage_1.0.bb", "require mypackage.inc\n" "\n" "PV = \"1.0\"\n" "\n" "do_compile() {\n" " make\n" "}\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: BitBake `require` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no BitBake require resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 18 — Kconfig (Kconfig) * * Labels histogram: Class:1, Module:1 * Kconfig describes Linux kernel configuration options. The extractor * produces a Module + a Class node (one per Kconfig file, matching histogram). * `source` directive includes other Kconfig files — not resolved as IMPORTS. * ══════════════════════════════════════════════════════════════════ */ /* Kconfig: config entries → Module and Class nodes. */ TEST(probe_kconfig_config_entries) { GpfMetrics m = gpf_metrics("Kconfig", "config MYDRV\n" " tristate \"My Driver\"\n" " depends on PCI\n" " help\n" " Enable this driver for My Device.\n" "\n" "config MYDRV_DEBUG\n" " bool \"Enable debug output\"\n" " depends on MYDRV\n" " default n\n"); ASSERT_TRUE(m.ok); /* GREEN: histogram confirms Class:1,Module:1 for any Kconfig file. */ ASSERT_TRUE(m.modules >= 1); ASSERT_TRUE(m.classes >= 1); PASS(); } /* Kconfig: menu block with multiple entries. */ TEST(probe_kconfig_menu_block) { GpfMetrics m = gpf_metrics("Kconfig", "menu \"Network Support\"\n" "\n" "config NET\n" " bool \"Networking support\"\n" "\n" "config INET\n" " bool \"TCP/IP networking\"\n" " depends on NET\n" "\n" "endmenu\n"); ASSERT_TRUE(m.ok); /* GREEN: Module + Class nodes for the file. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* Kconfig: `source` in two-file fixture → IMPORTS edge. * RED: Kconfig `source` directive not resolved into IMPORTS edges. */ TEST(probe_kconfig_source_edge) { static const GpfFile files[] = { {"drivers/Kconfig", "config DRV_USB\n" " tristate \"USB Driver\"\n" " depends on USB\n"}, {"Kconfig", "mainmenu \"Linux Kernel Configuration\"\n" "\n" "source \"drivers/Kconfig\"\n"} }; GpfMetrics m = gpf_metrics_files(files, 2); ASSERT_TRUE(m.ok); /* RED: Kconfig `source` not resolved into IMPORTS edges. */ ASSERT_TRUE(m.imports >= 1); /* expected RED — no Kconfig source resolver */ PASS(); } /* ══════════════════════════════════════════════════════════════════ * GROUP 19 — Go Mod (go.mod) * * Labels histogram: Module:1 * go.mod is parsed by pass_pkgmap.c which extracts the module path as a * pkgmap entry used for import resolution, not for producing a DEPENDS_ON * edge per `require` directive. A Module node is created per file. * DEPENDS_ON edges are produced for Helm chart dependencies (pass_k8s.c), * not for go.mod `require` blocks in the grammar-only pipeline. * ══════════════════════════════════════════════════════════════════ */ /* Go mod: module directive → Module node. */ TEST(probe_gomod_module_node) { GpfMetrics m = gpf_metrics("go.mod", "module example.com/myapp\n" "\n" "go 1.21\n" "\n" "require (\n" " github.com/gin-gonic/gin v1.9.1\n" " github.com/stretchr/testify v1.8.4\n" ")\n"); ASSERT_TRUE(m.ok); /* GREEN: go.mod produces at least a Module node. */ ASSERT_TRUE(m.total_nodes >= 1); ASSERT_TRUE(m.modules >= 1); PASS(); } /* Go mod: simple module with go directive only. */ TEST(probe_gomod_simple) { GpfMetrics m = gpf_metrics("go.mod", "module example.com/simple\n" "\n" "go 1.21\n"); ASSERT_TRUE(m.ok); /* GREEN: even a minimal go.mod with no requires produces a Module node. */ ASSERT_TRUE(m.modules >= 1); PASS(); } /* Go mod: `require` blocks → DEPENDS_ON edges per dependency. * RED: go.mod `require` directives are not resolved into DEPENDS_ON edges; * pass_pkgmap only uses go.mod for intra-repo module path mapping. */ TEST(probe_gomod_depends_on_edges) { GpfMetrics m = gpf_metrics("go.mod", "module example.com/server\n" "\n" "go 1.21\n" "\n" "require (\n" " github.com/gorilla/mux v1.8.1\n" " github.com/sirupsen/logrus v1.9.3\n" " github.com/prometheus/client_golang v1.17.0\n" ")\n"); ASSERT_TRUE(m.ok); /* RED: go.mod require → DEPENDS_ON not implemented (pkgmap handles path mapping only). */ ASSERT_TRUE(m.depends_on >= 1); /* expected RED — no go.mod DEPENDS_ON producer */ PASS(); } /* Go mod: go.mod used in a multi-file project to enable cross-package IMPORTS. * GREEN: the presence of go.mod enables the pkgmap resolver to link * cross-package imports in Go source files. */ TEST(probe_gomod_enables_go_imports) { static const GpfFile files[] = { {"go.mod", "module example.com/demo\n" "\n" "go 1.21\n"}, {"pkg/calc/calc.go", "package calc\n" "\n" "func Add(a, b int) int { return a + b }\n"}, {"main.go", "package main\n" "\n" "import \"example.com/demo/pkg/calc\"\n" "\n" "func main() { _ = calc.Add(1, 2) }\n"} }; GpfMetrics m = gpf_metrics_files(files, 3); ASSERT_TRUE(m.ok); /* GREEN: go.mod enables the pkgmap resolver; main.go → calc IMPORTS edge. */ ASSERT_TRUE(m.imports >= 1); PASS(); } /* ══════════════════════════════════════════════════════════════════ * SUITE wiring * ══════════════════════════════════════════════════════════════════ */ SUITE(grammar_probe_f) { /* HCL / Terraform */ RUN_TEST(probe_hcl_resource_nodes); RUN_TEST(probe_hcl_class_node); RUN_TEST(probe_hcl_module_block_node); RUN_TEST(probe_hcl_infra_maps_edge); RUN_TEST(probe_hcl_no_spurious_imports); /* Kubernetes manifests */ RUN_TEST(probe_k8s_pod_resource_node); RUN_TEST(probe_k8s_deployment_resource_node); RUN_TEST(probe_k8s_two_manifests); RUN_TEST(probe_k8s_infra_maps_edge); /* Kustomize */ RUN_TEST(probe_kustomize_module_node); RUN_TEST(probe_kustomize_imports_edges); RUN_TEST(probe_kustomize_with_patches); /* Nix */ RUN_TEST(probe_nix_attrset_node); RUN_TEST(probe_nix_let_binding); RUN_TEST(probe_nix_import_edge); /* Nickel */ RUN_TEST(probe_nickel_record_node); RUN_TEST(probe_nickel_let_fn); RUN_TEST(probe_nickel_import_edge); /* Pkl */ RUN_TEST(probe_pkl_properties_node); RUN_TEST(probe_pkl_object_block); RUN_TEST(probe_pkl_import_edge); /* RON */ RUN_TEST(probe_ron_record_node); RUN_TEST(probe_ron_named_struct); RUN_TEST(probe_ron_no_imports); /* Jsonnet */ RUN_TEST(probe_jsonnet_object_node); RUN_TEST(probe_jsonnet_function_in_object); RUN_TEST(probe_jsonnet_import_edge); /* KDL */ RUN_TEST(probe_kdl_node_block); RUN_TEST(probe_kdl_flat_nodes); RUN_TEST(probe_kdl_no_imports); /* Hyprlang */ RUN_TEST(probe_hyprlang_section_node); RUN_TEST(probe_hyprlang_directives); RUN_TEST(probe_hyprlang_source_import_edge); /* DeviceTree */ RUN_TEST(probe_devicetree_root_node); RUN_TEST(probe_devicetree_child_nodes); RUN_TEST(probe_devicetree_include_edge); /* CMake */ RUN_TEST(probe_cmake_function_node); RUN_TEST(probe_cmake_macro_node); RUN_TEST(probe_cmake_include_edge); RUN_TEST(probe_cmake_subdirectory_edge); /* Makefile */ RUN_TEST(probe_makefile_target_nodes); RUN_TEST(probe_makefile_pattern_rule); RUN_TEST(probe_makefile_include_edge); /* Meson */ RUN_TEST(probe_meson_project_node); RUN_TEST(probe_meson_options_node); RUN_TEST(probe_meson_subdir_edge); /* GN */ RUN_TEST(probe_gn_executable_node); RUN_TEST(probe_gn_library_targets); RUN_TEST(probe_gn_import_edge); /* Just */ RUN_TEST(probe_just_recipe_nodes); RUN_TEST(probe_just_recipe_function_nodes); RUN_TEST(probe_just_import_edge); /* BitBake */ RUN_TEST(probe_bitbake_recipe_node); RUN_TEST(probe_bitbake_bbclass_node); RUN_TEST(probe_bitbake_require_edge); /* Kconfig */ RUN_TEST(probe_kconfig_config_entries); RUN_TEST(probe_kconfig_menu_block); RUN_TEST(probe_kconfig_source_edge); /* Go Mod */ RUN_TEST(probe_gomod_module_node); RUN_TEST(probe_gomod_simple); RUN_TEST(probe_gomod_depends_on_edges); RUN_TEST(probe_gomod_enables_go_imports); }