Files
wehub-resource-sync a06f331eb8
CI / benchmark (push) Has been skipped
install-script / posix-syntax (push) Successful in 6m1s
CI / build-onnx (push) Failing after 6m43s
init-smoke / dry-run (push) Failing after 15m57s
security / govulncheck (push) Has been cancelled
security / trivy-fs (push) Has been cancelled
CI / test (1.26, ubuntu-latest) (push) Has been cancelled
Scorecard supply-chain security / Scorecard analysis (push) Has been cancelled
CI / test (1.26, macos-latest) (push) Has been cancelled
CI / build-windows (push) Has been cancelled
CI / lint (push) Has been cancelled
install-script / powershell-syntax (push) Has been cancelled
install-script / install (macos-14) (push) Has been cancelled
install-script / install (ubuntu-latest) (push) Has been cancelled
chore: import upstream snapshot with attribution
2026-07-13 12:33:42 +08:00

377 lines
10 KiB
Go

package languages
import (
"strings"
"testing"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
"github.com/zzet/gortex/internal/graph"
"github.com/zzet/gortex/internal/parser"
)
func TestCExtractor_Function(t *testing.T) {
src := []byte(`#include <stdio.h>
void greet(const char* name) {
printf("Hello %s\n", name);
}
int add(int a, int b) {
return a + b;
}
`)
e := NewCExtractor()
result, err := e.Extract("main.c", src)
require.NoError(t, err)
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
assert.Len(t, funcs, 2)
}
func TestCExtractor_Struct(t *testing.T) {
src := []byte(`struct Point {
int x;
int y;
};
`)
e := NewCExtractor()
result, err := e.Extract("point.c", src)
require.NoError(t, err)
types := nodesOfKind(result.Nodes, graph.KindType)
assert.GreaterOrEqual(t, len(types), 1)
}
func TestCExtractor_Include(t *testing.T) {
src := []byte(`#include <stdio.h>
#include "mylib.h"
`)
e := NewCExtractor()
result, err := e.Extract("main.c", src)
require.NoError(t, err)
imports := edgesOfKind(result.Edges, graph.EdgeImports)
assert.Len(t, imports, 2)
}
func TestCExtractor_Enum(t *testing.T) {
src := []byte(`enum Color {
RED,
GREEN,
BLUE
};
`)
e := NewCExtractor()
result, err := e.Extract("color.c", src)
require.NoError(t, err)
types := nodesOfKind(result.Nodes, graph.KindType)
require.GreaterOrEqual(t, len(types), 1)
var found bool
for _, n := range types {
if n.Name == "Color" {
found = true
break
}
}
assert.True(t, found, "expected to find enum Color as a type")
}
func TestCExtractor_Typedef(t *testing.T) {
src := []byte(`typedef int MyInt;
typedef struct {
int x;
int y;
} Point;
`)
e := NewCExtractor()
result, err := e.Extract("types.c", src)
require.NoError(t, err)
types := nodesOfKind(result.Nodes, graph.KindType)
require.GreaterOrEqual(t, len(types), 2)
names := make([]string, len(types))
for i, n := range types {
names[i] = n.Name
}
assert.Contains(t, names, "MyInt")
assert.Contains(t, names, "Point")
}
func TestCExtractor_CallSites(t *testing.T) {
src := []byte(`#include <stdio.h>
void helper(void) {}
void greet(const char* name) {
printf("Hello %s\n", name);
helper();
}
`)
e := NewCExtractor()
result, err := e.Extract("main.c", src)
require.NoError(t, err)
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
require.GreaterOrEqual(t, len(calls), 2)
var targets []string
for _, c := range calls {
targets = append(targets, c.To)
}
assert.Contains(t, targets, "unresolved::printf")
assert.Contains(t, targets, "unresolved::helper")
}
func TestCExtractor_Macros(t *testing.T) {
src := []byte(`#define PI 3.14159
#define SQUARE(x) ((x) * (x))
#define LOG(msg) write_log(stderr, msg)
int area(int r) {
return SQUARE(r) * PI;
}
`)
e := NewCExtractor()
result, err := e.Extract("calc.c", src)
require.NoError(t, err)
macros := nodesOfKind(result.Nodes, graph.KindMacro)
byName := map[string]*graph.Node{}
for _, m := range macros {
byName[m.Name] = m
}
require.Contains(t, byName, "PI")
assert.Equal(t, "object", byName["PI"].Meta["macro_kind"])
require.Contains(t, byName, "SQUARE")
assert.Equal(t, "function", byName["SQUARE"].Meta["macro_kind"])
require.Contains(t, byName, "LOG")
// The function-like macro LOG hides a call to write_log; that edge is
// recovered from the macro's replacement list. SQUARE's body has no
// real call (x is a parameter), so it emits none.
var logCalls, squareCalls []string
for _, ed := range result.Edges {
if ed.Kind != graph.EdgeCalls {
continue
}
switch ed.From {
case "calc.c::LOG":
logCalls = append(logCalls, ed.To)
case "calc.c::SQUARE":
squareCalls = append(squareCalls, ed.To)
}
}
assert.Contains(t, logCalls, "unresolved::write_log")
assert.Empty(t, squareCalls, "SQUARE body has no hidden call (x is a param)")
}
func TestCExtractor_GlobalVariable(t *testing.T) {
src := []byte(`int max_retries = 3;
const char* app_name = "test";
void foo(void) {
int local = 42;
}
`)
e := NewCExtractor()
result, err := e.Extract("globals.c", src)
require.NoError(t, err)
vars := nodesOfKind(result.Nodes, graph.KindVariable)
names := make([]string, len(vars))
for i, v := range vars {
names[i] = v.Name
}
assert.Contains(t, names, "max_retries")
// local should NOT be extracted
assert.NotContains(t, names, "local")
}
func TestCExtractor_FunctionPrototype(t *testing.T) {
src := []byte(`int add(int a, int b);
void greet(const char* name);
`)
e := NewCExtractor()
result, err := e.Extract("header.h", src)
require.NoError(t, err)
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
require.Len(t, funcs, 2)
names := []string{funcs[0].Name, funcs[1].Name}
assert.Contains(t, names, "add")
assert.Contains(t, names, "greet")
}
func TestCExtractor_FullFile(t *testing.T) {
src := []byte(`#include <stdio.h>
#include <stdlib.h>
typedef unsigned int uint;
struct Config {
int port;
const char* host;
};
enum LogLevel {
DEBUG,
INFO,
ERROR
};
int global_count = 0;
void process(struct Config* cfg) {
printf("port: %d\n", cfg->port);
global_count++;
}
int main(int argc, char* argv[]) {
struct Config cfg;
cfg.port = 8080;
cfg.host = "localhost";
process(&cfg);
return 0;
}
`)
e := NewCExtractor()
result, err := e.Extract("main.c", src)
require.NoError(t, err)
// File node.
files := nodesOfKind(result.Nodes, graph.KindFile)
require.Len(t, files, 1)
// Functions.
funcs := nodesOfKind(result.Nodes, graph.KindFunction)
require.Len(t, funcs, 2)
// Types (struct Config, enum LogLevel, typedef uint).
types := nodesOfKind(result.Nodes, graph.KindType)
require.GreaterOrEqual(t, len(types), 3)
// Imports.
imports := edgesOfKind(result.Edges, graph.EdgeImports)
assert.Len(t, imports, 2)
// Calls from process -> printf.
calls := edgesOfKind(result.Edges, graph.EdgeCalls)
assert.GreaterOrEqual(t, len(calls), 1)
}
// TestIncludeKindClassification is part of the C11 set: a quoted #include is a
// local include (resolvable), an angle #include is a system header — each is
// tagged on the import edge so the resolver can bind locals and leave system
// headers external.
func TestIncludeKindClassification(t *testing.T) {
src := []byte("#include \"local.h\"\n#include <stdio.h>\nint main(){return 0;}\n")
res, err := NewCExtractor().Extract("src/main.c", src)
require.NoError(t, err)
kinds := map[string]string{}
for _, e := range res.Edges {
if e.Kind == graph.EdgeImports {
k, _ := e.Meta["include_kind"].(string)
kinds[e.To] = k
}
}
assert.Equal(t, "quoted", kinds["unresolved::import::local.h"])
assert.Equal(t, "system", kinds["unresolved::import::stdio.h"])
}
// TestMisparseSkipButVisit is part of the C11 set: a construct the grammar
// cannot parse (here an unknown macro-shaped declaration) does not stop
// extraction — the valid functions around it are still recovered.
func TestMisparseSkipButVisit(t *testing.T) {
src := []byte("int before(){return 1;}\n" +
"WEIRD_MACRO_DECL(@@@ not valid c @@@);\n" +
"int after(){return 2;}\n")
res, err := NewCExtractor().Extract("m.c", src)
require.NoError(t, err)
names := map[string]bool{}
for _, n := range res.Nodes {
if n.Kind == graph.KindFunction {
names[n.Name] = true
}
}
assert.True(t, names["before"], "function before the misparse survives")
assert.True(t, names["after"], "function after the misparse survives")
}
// macroCallTargets collects the EdgeCalls targets a macro hides, keyed by
// the macro's node id (`<file>::<NAME>`).
func macroCallTargets(result *parser.ExtractionResult, macroID string) []string {
var out []string
for _, ed := range result.Edges {
if ed.Kind == graph.EdgeCalls && ed.From == macroID {
out = append(out, ed.To)
}
}
return out
}
// A member call hidden in a macro body — `(o)->run()`, `o->run()`,
// `o.run()` — is recovered by sub-parsing the replacement list. The regex
// scan could never see `run`; the field/method name is the new recovery.
func TestCExtractor_MacroMemberCall(t *testing.T) {
src := []byte(`#define CALL_M(o) (o)->run()
#define ARROW(o) o->step()
#define DOT(o) o.tick()
`)
result, err := NewCExtractor().Extract("m.c", src)
require.NoError(t, err)
assert.Contains(t, macroCallTargets(result, "m.c::CALL_M"), "unresolved::run")
assert.Contains(t, macroCallTargets(result, "m.c::ARROW"), "unresolved::step")
assert.Contains(t, macroCallTargets(result, "m.c::DOT"), "unresolved::tick")
}
// A plain `name(...)` call the regex already handled must still be
// recovered via the sub-parse (no regression).
func TestCExtractor_MacroPlainCallRegression(t *testing.T) {
src := []byte("#define LOG(x) logger_write(x)\n")
result, err := NewCExtractor().Extract("m.c", src)
require.NoError(t, err)
assert.Contains(t, macroCallTargets(result, "m.c::LOG"),
"unresolved::logger_write")
}
// A malformed replacement list must not crash and must degrade
// gracefully to the regex fallback (which here recovers nothing — the
// only `name(` token is the macro's own parameter).
func TestCExtractor_MacroMalformedGraceful(t *testing.T) {
src := []byte("#define BAD(x) )(*&^ x (\n")
require.NotPanics(t, func() {
result, err := NewCExtractor().Extract("m.c", src)
require.NoError(t, err)
macros := nodesOfKind(result.Nodes, graph.KindMacro)
var found bool
for _, m := range macros {
if m.Name == "BAD" {
found = true
}
}
assert.True(t, found, "the malformed macro node is still emitted")
assert.Empty(t, macroCallTargets(result, "m.c::BAD"),
"a malformed body recovers no spurious calls")
})
}
// A long replacement list (>200 chars, past the old truncation cap) with
// a call near the end is fully scanned now that the cap is lifted.
func TestCExtractor_MacroLongBodyCallRecovered(t *testing.T) {
pad := strings.Repeat("(a) + (b) + ", 40) // ~480 chars, no calls
src := []byte("#define BIG(a,b) " + pad + "tail_call(a, b)\n")
require.Greater(t, len(pad), 200, "padding exceeds the old 200-char cap")
result, err := NewCExtractor().Extract("m.c", src)
require.NoError(t, err)
assert.Contains(t, macroCallTargets(result, "m.c::BIG"),
"unresolved::tail_call",
"a call past the old 200-char cap is recovered")
}