Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .github/e2e/csharp/expect
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ENTRY=Entry HELPER=Helper LEAF=Leaf TEST=ServiceTests LEAF_FILE=src/App/Service.cs
11 changes: 11 additions & 0 deletions .github/e2e/csharp/src/App/Service.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
namespace App
{
public class Service
{
public int Entry() { return Helper() + 1; }

int Helper() { return Leaf() * 2; }

int Leaf() { return 41; }
}
}
10 changes: 10 additions & 0 deletions .github/e2e/csharp/tests/App.Tests/ServiceTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
using Xunit;

namespace App.Tests
{
public class ServiceTests
{
[Fact]
public void EntryWorks() { new App.Service().Entry(); }
}
}
1 change: 1 addition & 0 deletions .github/e2e/java/expect
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ENTRY=entry HELPER=helper LEAF=leaf TEST=ServiceTest LEAF_FILE=src/main/java/app/Service.java
9 changes: 9 additions & 0 deletions .github/e2e/java/src/main/java/app/Service.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
package app;

public class Service {
public int entry() { return helper() + 1; }

int helper() { return leaf() * 2; }

int leaf() { return 41; }
}
8 changes: 8 additions & 0 deletions .github/e2e/java/src/test/java/app/ServiceTest.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
package app;

import org.junit.jupiter.api.Test;

class ServiceTest {
@Test
void entryWorks() { new Service().entry(); }
}
1 change: 1 addition & 0 deletions .github/e2e/javascript/expect
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ENTRY=entry HELPER=helper LEAF=leaf TEST=service.test LEAF_FILE=src/service.js
5 changes: 5 additions & 0 deletions .github/e2e/javascript/src/service.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function leaf() { return 41; }

export function helper() { return leaf() * 2; }

export function entry() { return helper() + 1; }
3 changes: 3 additions & 0 deletions .github/e2e/javascript/test/service.test.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { entry } from '../src/service';

test('entry', () => { entry(); });
Empty file.
10 changes: 10 additions & 0 deletions .github/e2e/python/app/service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
def leaf():
return 41


def helper():
return leaf() * 2


def entry():
return helper() + 1
1 change: 1 addition & 0 deletions .github/e2e/python/expect
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ENTRY=entry HELPER=helper LEAF=leaf TEST=test_service LEAF_FILE=app/service.py
5 changes: 5 additions & 0 deletions .github/e2e/python/tests/test_service.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
from app.service import entry


def test_entry():
assert entry() == 83
1 change: 1 addition & 0 deletions .github/e2e/typescript/expect
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
ENTRY=entry HELPER=helper LEAF=leaf TEST=service.test LEAF_FILE=src/service.ts
5 changes: 5 additions & 0 deletions .github/e2e/typescript/src/service.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
export function leaf(): number { return 41; }

export function helper(): number { return leaf() * 2; }

export function entry(): number { return helper() + 1; }
3 changes: 3 additions & 0 deletions .github/e2e/typescript/test/service.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
import { entry } from '../src/service';

test('entry', () => { entry(); });
25 changes: 21 additions & 4 deletions .github/scripts/e2e-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,13 @@
# + <lang>/ENGINE_ID). This packs @axiomcode/code-graph and that platform's engine
# package exactly as publish-npm.yml would, installs both into an empty project, and
# runs `axiomcode` on <source-dir> with souffle NOT on PATH. It passes only if the
# installed package found its engine package, used it, and wrote a graph with edges.
# installed package found its engine package, used it, and wrote a graph with edges —
# and then, on .github/e2e/<language>, every query verb returned the answer that tiny
# project makes true (e2e-queries.sh), from the query programs the engine package ships.
#
# CODEGRAPH_TGZ, when set, is an already packed @axiomcode/code-graph to install instead of
# packing this checkout: the tarball is platform-independent, so ci.yml packs it once and every
# platform installs the same file a user would download.
#
# The test suites cannot see any of this: they run from the checkout, where a missing
# `files` entry, a broken bin, or an engine package the driver does not find all go
Expand All @@ -29,8 +35,12 @@ bash "$root/packaging/assemble-engine-package.sh" "$platform" "$version" "$engin
|| fail "the engine package did not assemble"
mkdir -p "$W/tgz"
( cd "$W/engine" && npm pack --silent --pack-destination "$W/tgz" >/dev/null ) || fail "npm pack of the engine package failed"
# --ignore-scripts: the tree is already built; the tarball must carry what the build produced
( cd "$root" && npm pack --silent --ignore-scripts --pack-destination "$W/tgz" >/dev/null ) || fail "npm pack of code-graph failed"
if [ -n "${CODEGRAPH_TGZ:-}" ]; then
cp "$CODEGRAPH_TGZ" "$W/tgz/" || fail "no code-graph tarball at $CODEGRAPH_TGZ"
else
# --ignore-scripts: the tree is already built; the tarball must carry what the build produced
( cd "$root" && npm pack --silent --ignore-scripts --pack-destination "$W/tgz" >/dev/null ) || fail "npm pack of code-graph failed"
fi
ls -1 "$W/tgz" | sed 's/^/ /'

echo "── installing into an empty project"
Expand Down Expand Up @@ -58,4 +68,11 @@ edges="$(node -e '
console.log(db.prepare("SELECT COUNT(*) AS n FROM call_edges").get().n);
' "$db" 2>/dev/null)"
[ -n "$edges" ] && [ "$edges" -gt 0 ] || fail "the graph has no call edges (${edges:-unreadable})"
echo "e2e: ok — installed from the tarballs, used the packaged $platform engine, $edges call edges"

echo "── every query verb on the $lang fixture, answers checked, no souffle"
PATH="$SANDBOX_PATH" bash "$root/.github/scripts/e2e-queries.sh" "$bin" "$root/.github/e2e/$lang" "$W/q" || fail "a query verb answered wrongly (above)"
# every program from the engine package: not compiled here (no souffle), not left to the interpreter
ready="$(grep 'datalog rules ready' "$W/q/.index.log" || true)"
echo "$ready" | grep -Eq 'ready: ([0-9]+)/\1 compiled' && ! echo "$ready" | grep -Eq '=(cached|interpreter)' \
|| fail "the query programs did not all come from the engine package: $ready"
echo "e2e: ok — installed from the tarballs, used the packaged $platform engine, $edges call edges; every query verb answered correctly from the package"
61 changes: 61 additions & 0 deletions .github/scripts/e2e-queries.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,61 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# Every query verb returns the RIGHT answer on a tiny project whose answers are known.
#
# e2e-queries.sh <axiomcode-bin> <fixture-dir> <work-dir>
#
# <fixture-dir> is .github/e2e/<language>/: entry() calls helper() calls leaf(), one test
# calls entry(), and `expect` names the four in that language's spelling. The project is
# copied to <work-dir>, committed, indexed, and asked:
#
# impact LEAF lists HELPER, the method that calls it
# path ENTRY LEAF reached, through HELPER
# path ENTRY LEAF the same from the shipped Datalog programs (AXIOMCODE_DATALOG=1)
# path … --every at least one route listed, both backends
# context LEAF names LEAF
# leaf's 41 becomes 42, then
# changed names LEAF
# test-impact selects TEST
#
# Running is not passing: each answer has to contain what the project makes true, and no
# answer may carry a Python traceback.
# ─────────────────────────────────────────────────────────────────────────────
set -uo pipefail
bin="${1:?usage: e2e-queries.sh <axiomcode-bin> <fixture-dir> <work-dir>}"; fx="${2:?fixture-dir}"; R="${3:?work-dir}"
fail() { echo "::error::e2e queries ($(basename "$fx")): $*"; exit 1; }
# shellcheck disable=SC2046
eval "$(cat "$fx/expect")"
rm -rf "$R"; mkdir -p "$R"; cp -R "$fx"/. "$R"/; rm -f "$R/expect"
git -C "$R" init -q && git -C "$R" add -A && git -C "$R" -c user.email=e2e@axiomcode -c user.name=e2e commit -qm base \
|| fail "could not commit the fixture"

"$bin" index "$R" > "$R/.index.log" 2>&1 || { tail -20 "$R/.index.log"; fail "axiomcode index exited non-zero"; }
grep 'datalog rules ready' "$R/.index.log" | sed 's/^/ /'

run() { # <env> <verb and args…>: the answer lands in $R/.q.log, a non-zero exit or a traceback fails
local env="$1"; shift; LABEL="$* ${env:+($env)}"
( cd "$R" && env $env "$bin" "$@" ) > "$R/.q.log" 2>&1; local rc=$?
if [ "$rc" -ne 0 ] || grep -q 'Traceback (most recent call last)' "$R/.q.log"; then
sed 's/^/ /' "$R/.q.log" | head -25; fail "$LABEL: rc=$rc"
fi
}
must() { # <extended regex> the last answer has to match, and why
grep -Eq -- "$1" "$R/.q.log" || { sed 's/^/ /' "$R/.q.log" | head -25; fail "$LABEL: expected /$1/ ($2)"; }
echo " ok $LABEL — $2"
}
DL="AXIOMCODE_DATALOG=1"
run "" impact "$LEAF"; must "\[resolved\] ([A-Za-z_.]*\.)?$HELPER .*calls it" "$HELPER is a resolved caller"
for e in "" "$DL"; do
run "$e" path "$ENTRY" "$LEAF"; must "reached" "a chain exists"
must "→ .*([A-Za-z_.]*\.)?$HELPER " "the chain goes through $HELPER"
run "$e" path "$ENTRY" "$LEAF" --every; must "[0-9]+ hop\(s\): .*$HELPER.* → .*$LEAF" "a route through $HELPER to $LEAF is listed"
done
run "" context "$LEAF"; must "^ +([A-Za-z_.]*\.)?$LEAF +" "$LEAF is an entry point"

# a real edit to leaf's body: what changed, and which tests have to run for it
sed 's/41/42/' "$R/$LEAF_FILE" > "$R/.edit" && mv "$R/.edit" "$R/$LEAF_FILE"
git -C "$R" diff --quiet && fail "the edit to $LEAF_FILE changed nothing"
run "" changed; must "([A-Za-z_.]*\.)?$LEAF\b" "$LEAF is reported changed"
run "" test-impact; must "^tests to run: [1-9]" "a test reaches the change through the graph"
must "^ +\S*$TEST\S* +\(" "$TEST is the test selected"
echo "e2e queries: every verb answered correctly for $(basename "$fx")"
60 changes: 60 additions & 0 deletions .github/scripts/query-smoke.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
#!/usr/bin/env bash
# ─────────────────────────────────────────────────────────────────────────────
# The query programs answer the same compiled as interpreted, on every platform.
#
# query-smoke.sh expect <gen-queries-dir> (generate job, with Soufflé)
# query-smoke.sh check <gen-queries-dir> <bin-dir> (each platform, no Soufflé)
#
# `expect` writes a small call graph as facts (fixture/) and what the Soufflé
# interpreter derives from it for every <name>.dl (expected/<name>/). `check` runs
# each platform's axiomcode-query-<name> on the same facts and compares, relation
# by relation. Starting on empty inputs would pass a binary that answers nothing;
# this fails one whose answers differ, and fails if the fixture reaches nothing.
# ─────────────────────────────────────────────────────────────────────────────
set -euo pipefail
mode="${1:?usage: query-smoke.sh expect|check <gen-queries-dir> [bin-dir]}"; G="${2:?gen-queries-dir}"
EXE=""; case "$(uname -s)" in MINGW*|MSYS*|CYGWIN*) EXE=".exe";; esac
inputs() { sed -n 's/.*\.input \([A-Za-z0-9_]*\).*/\1/p' "$1"; }
norm() { for f in "$1"/*.csv; do [ -e "$f" ] || continue; printf '== %s\n' "$(basename "$f")"; tr -d '\r' < "$f" | LC_ALL=C sort; done; }

case "$mode" in
expect)
F="$G/fixture"; rm -rf "$F"; mkdir -p "$F"
for dl in "$G"/*.dl; do inputs "$dl" | while read -r r; do : > "$F/$r.facts"; done; done
# t -> a -> b -> c, and x -> c by name only; one path query from t to c
printf 'm:t\tm:a\tcall\nm:a\tm:b\tcall\nm:b\tm:c\tcall\n' > "$F/edge.facts"
printf 't\tm:t\na\tm:a\nb\tm:b\nc\tm:c\nx\tm:x\n' > "$F/named.facts"
printf 'm:x\tc\n' > "$F/byname.facts"
printf 'q\tm:t\n' > "$F/src.facts"
printf 'q\tm:c\n' > "$F/dst.facts"
# impact: what changing c reaches, through the same chain's resolved call sites
printf 'q\tmethod\tm:c\tc\n' > "$F/target.facts"
printf 'm:t\tm:a\tclient\tT.java\t3\nm:a\tm:b\tclient\tA.java\t4\nm:b\tm:c\tclient\tB.java\t5\n' > "$F/calls.facts"
printf 'm:a\tmethod\nm:b\tmethod\nm:c\tmethod\nm:t\tmethod\n' > "$F/kind.facts"
for dl in "$G"/*.dl; do
q="$(basename "$dl" .dl)"; E="$G/expected/$q"; rm -rf "$E"; mkdir -p "$E"
souffle -F "$F" -D "$E" "$dl"
n="$(cat "$E"/*.csv 2>/dev/null | wc -l | tr -d ' ')"
echo "query $q: interpreter derived $n rows"
done
for q in impact path; do
[ "$(cat "$G"/expected/$q/*.csv | wc -l)" -gt 0 ] || { echo "::error::the fixture reaches nothing in $q.dl"; exit 1; }
done
;;
check)
B="${3:?bin-dir}"; W="$(mktemp -d)"; trap 'rm -rf "$W"' EXIT; bad=0
for dl in "$G"/*.dl; do
q="$(basename "$dl" .dl)"; bin="$B/axiomcode-query-$q$EXE"
[ -f "$bin" ] || { echo "::error::no binary for $q"; bad=1; continue; }
chmod +x "$bin" 2>/dev/null || true
mkdir -p "$W/$q"; "$bin" -F "$G/fixture" -D "$W/$q"
if diff <(norm "$G/expected/$q") <(norm "$W/$q") > "$W/$q.diff"; then
echo "query $q: ok ($(norm "$W/$q" | grep -vc '^==') rows, same as the interpreter)"
else
echo "::error::query $q: the compiled program answers differently"; head -40 "$W/$q.diff"; bad=1
fi
done
exit "$bad"
;;
*) echo "unknown mode $mode" >&2; exit 2;;
esac
8 changes: 4 additions & 4 deletions .github/scripts/version.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@
//
// The release version is package.json's `version`. It is repeated in the engine
// pins (optionalDependencies — each engine package is published under the same
// version as this one), the vendored parser, and every agent plugin manifest a
// marketplace reads. A release where they disagree ships a plugin that reports a
// version nobody can install, or an install that pins engines that were never
// version as this one) and every agent plugin manifest a marketplace reads. The
// vendored parser keeps its own version: it moves only when the parser changes.
// A release where they disagree ships a plugin that reports a version nobody can
// install, or an install that pins engines that were never
// published.
//
// node .github/scripts/version.mjs check every manifest agrees
Expand All @@ -24,7 +25,6 @@ const root = join(dirname(fileURLToPath(import.meta.url)), '..', '..');
// reading the file, so a fifth platform is covered without editing this list.
const MANIFESTS = [
'package.json',
'parser/package.json',
'gemini-extension.json',
'plugins/axiomcode/.claude-plugin/plugin.json',
'plugins/axiomcode/.codex-plugin/plugin.json',
Expand Down
48 changes: 46 additions & 2 deletions .github/workflows/build-engines.yml
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,12 @@
# one self-contained executable per language per platform, linked against nothing but the
# C++ runtime. Same flags as the local compile (no OpenMP/zlib/sqlite), portable targets.
#
# Artifacts: engines-<platform>/ holding <lang>/axiomcode-engine-<lang>[.exe] + <lang>/ENGINE_ID
# Artifacts: engines-<platform>/ holding <lang>/axiomcode-engine-<lang>[.exe] + <lang>/ENGINE_ID,
# and queries/axiomcode-query-<name>[.exe] + queries/<name>.id for every query program the CLI
# runs (plugins/axiomcode/skills/axiomcode/scripts/dl/<name>.dl: impact, path, path-opt,
# path-every). Without those, `axiomcode impact` needs Soufflé and a C++ compiler on the user's
# machine (#1330). A query binary's id is dl_program.py's own key, so the CLI finds it by the
# same hash it would cache a local compile under.
# Platforms are named the npm way (process.platform-process.arch): darwin-arm64, darwin-x64,
# linux-x64, linux-arm64, win32-x64, each on a standard GitHub-hosted runner (free for a
# public repository; macOS builds on macos-15 for Apple Silicon and macos-15-intel for Intel).
Expand Down Expand Up @@ -71,9 +76,21 @@ jobs:
souffle -I graph -g "gen/$lang.cpp" "gen/$lang.dl" 2> "gen/$lang.gen.log" || { cat "gen/$lang.gen.log"; exit 1; }
awk '/No rules\/facts defined/{skip=2;next} skip>0{skip--;next} {print}' "gen/$lang.gen.log"
done
# the query programs: self-contained (no #include), keyed by dl_program.py itself
mkdir -p gen/queries
for dl in plugins/axiomcode/skills/axiomcode/scripts/dl/*.dl; do
q="$(basename "$dl" .dl)"
python3 plugins/axiomcode/skills/axiomcode/scripts/dl_program.py --print-id "$dl" | tr -d '\n' > "gen/queries/$q.id"
echo "query $q: $(cat "gen/queries/$q.id")"
cp "$dl" "gen/queries/$q.dl"
souffle -g "gen/queries/$q.cpp" "$dl" 2> "gen/queries/$q.gen.log" || { cat "gen/queries/$q.gen.log"; exit 1; }
done
# the fixture and what the interpreter derives from it; each platform's binaries must match
bash .github/scripts/query-smoke.sh expect gen/queries
cp .github/scripts/query-smoke.sh gen/queries/smoke.sh
cp -r /usr/include/souffle gen/souffle
# one key for the whole set; a partial match restores the previous set
echo "key=$(cat gen/*.id | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
echo "key=$(cat gen/*.id gen/queries/*.id | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT"
# The compile flags live in THIS file and ENGINE_ID does not cover them, so its
# hash prefixes the key AND the restore prefix: a flag change restores nothing.
# Computed here because the build jobs never check the repository out.
Expand Down Expand Up @@ -112,6 +129,13 @@ jobs:
c++ -std=c++17 -O3 -w -static-libstdc++ -static-libgcc -I gen "gen/$lang.cpp" -o "engines/$lang/axiomcode-engine-$lang"
cp "gen/$lang.id" "engines/$lang/ENGINE_ID"
done
mkdir -p engines/queries
for cpp in gen/queries/*.cpp; do
q="$(basename "$cpp" .cpp)"
if cmp -s "gen/queries/$q.id" "engines/queries/$q.id"; then echo "query $q: cached, rules unchanged"; continue; fi
c++ -std=c++17 -O3 -w -static-libstdc++ -static-libgcc -I gen "$cpp" -o "engines/queries/axiomcode-query-$q"
cp "gen/queries/$q.id" "engines/queries/$q.id"
done
ls -la engines/*; ldd engines/java/axiomcode-engine-java || true
- uses: ilammy/msvc-dev-cmd@v1
if: startsWith(matrix.target.platform, 'win32')
Expand All @@ -131,6 +155,17 @@ jobs:
echo %%L: cached, rules unchanged
)
)
if not exist engines\queries mkdir engines\queries
for %%Q in (gen\queries\*.cpp) do (
fc /b gen\queries\%%~nQ.id engines\queries\%%~nQ.id >nul 2>&1
if errorlevel 1 (
cl /nologo /std:c++17 /O2 /EHsc /bigobj /w /permissive- /Zc:__cplusplus /D_CRT_SECURE_NO_WARNINGS /DNOMINMAX /DUSE_CUSTOM_GETOPTLONG /I gen %%Q /Fe:engines\queries\axiomcode-query-%%~nQ.exe
if errorlevel 1 exit /b 1
copy /y gen\queries\%%~nQ.id engines\queries\%%~nQ.id
) else (
echo query %%~nQ: cached, rules unchanged
)
)
dir /s engines
- name: Smoke — every binary starts on empty inputs
shell: bash
Expand All @@ -143,6 +178,7 @@ jobs:
"./$bin" -F "facts-$lang" -D "out-$lang"
echo "$lang: ok ($(ls out-$lang | wc -l) relations written)"
done
bash gen/queries/smoke.sh check gen/queries engines/queries
- name: save the engines for the next run
if: ${{ !inputs.fresh && steps.restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
Expand Down Expand Up @@ -187,6 +223,13 @@ jobs:
c++ -std=c++17 -O3 -w -arch "$ARCH" -mmacosx-version-min=12.0 -I gen "gen/$lang.cpp" -o "engines/$lang/axiomcode-engine-$lang"
cp "gen/$lang.id" "engines/$lang/ENGINE_ID"
done
mkdir -p engines/queries
for cpp in gen/queries/*.cpp; do
q="$(basename "$cpp" .cpp)"
if cmp -s "gen/queries/$q.id" "engines/queries/$q.id"; then echo "query $q: cached, rules unchanged"; continue; fi
c++ -std=c++17 -O3 -w -arch "$ARCH" -mmacosx-version-min=12.0 -I gen "$cpp" -o "engines/queries/axiomcode-query-$q"
cp "gen/queries/$q.id" "engines/queries/$q.id"
done
file engines/java/axiomcode-engine-java
otool -L engines/java/axiomcode-engine-java
- name: Smoke — every binary starts on empty inputs
Expand All @@ -197,6 +240,7 @@ jobs:
sed -n 's/^\.input \([A-Za-z0-9_]*\)(.*/\1/p' "gen/$lang.dl" | while read -r r; do : > "facts-$lang/$r.facts"; done
"./engines/$lang/axiomcode-engine-$lang" -F "facts-$lang" -D "out-$lang"
done
bash gen/queries/smoke.sh check gen/queries engines/queries
- name: save the engines for the next run
if: ${{ !inputs.fresh && steps.restore.outputs.cache-hit != 'true' }}
uses: actions/cache/save@v4
Expand Down
Loading
Loading