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(); });
46 changes: 16 additions & 30 deletions .github/scripts/e2e-install.sh
Original file line number Diff line number Diff line change
Expand Up @@ -9,8 +9,12 @@
# 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 —
# and then indexed that source and answered `impact` and `path` from the query programs
# the engine package ships (#1330), still with no souffle to compile or interpret them.
# 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 @@ -31,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 @@ -61,32 +69,10 @@ edges="$(node -e '
' "$db" 2>/dev/null)"
[ -n "$edges" ] && [ "$edges" -gt 0 ] || fail "the graph has no call edges (${edges:-unreadable})"

echo "── axiomcode index, impact and path on a copy of $(basename "$src"), no souffle"
cp -R "$src" "$W/repo"
PATH="$SANDBOX_PATH" "$bin" index "$W/repo" > "$W/index.log" 2>&1 || { tail -15 "$W/index.log"; fail "axiomcode index exited non-zero"; }
ready="$(grep 'datalog rules ready' "$W/index.log" || true)"; echo " $ready"
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"
# one resolved call in the graph: its caller and its callee, by simple name
pair="$(node -e '
const { DatabaseSync } = require("node:sqlite");
const db = new DatabaseSync(process.argv[1], { readOnly: true });
const r = db.prepare(`SELECT c.name AS a, m.name AS b FROM call_edges e JOIN methods m ON m.id = e.callee_method_id
JOIN methods c ON c.id = e.caller_id WHERE e.tier = ? AND m.name NOT LIKE ? AND c.name NOT LIKE ? AND c.name <> m.name
ORDER BY c.name, m.name LIMIT 1`).get("known_edge", "%<%", "%<%");
if (r) console.log(r.a + " " + r.b);
' "$W/repo/.axiomcode/out/graph.sqlite" 2>/dev/null)"
[ -n "$pair" ] || fail "the indexed graph has no resolved call to ask about"
caller="${pair% *}"; callee="${pair#* }"
( cd "$W/repo" && PATH="$SANDBOX_PATH" "$bin" impact "$callee" ) > "$W/impact.log" 2>&1; rc=$?
sed 's/^/ /' "$W/impact.log" | head -8
[ "$rc" -eq 0 ] || fail "axiomcode impact $callee exited $rc"
# path answers from SQL by default; AXIOMCODE_DATALOG=1 runs the shipped path programs instead
for every in "" --every; do
( cd "$W/repo" && PATH="$SANDBOX_PATH" AXIOMCODE_DATALOG=1 "$bin" path "$caller" "$callee" $every ) > "$W/path.log" 2>&1; rc=$?
sed 's/^/ /' "$W/path.log" | head -4
[ "$rc" -eq 0 ] || fail "axiomcode path $caller $callee $every exited $rc"
grep -q 'reached' "$W/path.log" || fail "axiomcode path $caller $callee $every found no route over a resolved call"
done
echo "e2e: ok — installed from the tarballs, used the packaged $platform engine, $edges call edges; impact and path answered from the packaged query programs"
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")"
70 changes: 68 additions & 2 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -96,7 +96,7 @@ jobs:
# ENGINES: what the platform build compiles or packages.
engines="$(printf '%s\n' "$files" | grep -E \
-e '\.dl$' -e '^graph/pipeline/' -e '^packaging/' -e 'scripts/dl_program\.py$' -e '^\.github/scripts/query-smoke\.sh$' \
-e '^\.github/workflows/build-engines\.yml$' -e '^package\.json$' || true)"
-e '^\.github/workflows/build-engines\.yml$' -e '^package\.json$' -e '^\.github/scripts/e2e-' -e '^\.github/e2e/' || true)"
[ -n "$code" ] && echo "code=true" >> "$GITHUB_OUTPUT" || echo "code=false" >> "$GITHUB_OUTPUT"
# Only a pull request INTO main builds the platform engines; into dev they wait.
[ "${{ github.base_ref }}" = main ] || engines=""
Expand Down Expand Up @@ -397,14 +397,78 @@ jobs:
with:
fresh: ${{ inputs.fresh == true }}

# THE RELEASE GATE: what a user installs, on every platform, answers every verb. The engines
# job only proves each binary compiles and starts; the suites run from the checkout. Neither
# installs the packages, so a missing `files` entry, an engine package the CLI does not find,
# a query program that needs Soufflé, or a verb that only breaks on Windows reached users.
# Runs wherever the platform engines are built: on the way into main, and in the nightly.
pack:
name: pack @axiomcode/code-graph
needs: [build, changes]
if: needs.changes.outputs.engines == 'true'
runs-on: ubuntu-24.04
timeout-minutes: 15
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- run: npm install --no-audit --no-fund
# --ignore-scripts: `prepare` already built it; the tarball carries what the build produced
- run: mkdir -p tgz && npm pack --ignore-scripts --pack-destination tgz && ls -la tgz
- uses: actions/upload-artifact@v4
with: { name: code-graph-tgz, path: tgz, retention-days: 3, if-no-files-found: error }

e2e:
name: e2e on ${{ matrix.target.platform }}
needs: [changes, engines, pack]
if: needs.changes.outputs.engines == 'true'
strategy:
fail-fast: false
matrix:
target:
- { os: ubuntu-24.04, platform: linux-x64 }
- { os: ubuntu-24.04-arm, platform: linux-arm64 }
- { os: windows-2025, platform: win32-x64 }
- { os: macos-15, platform: darwin-arm64 }
- { os: macos-15-intel, platform: darwin-x64 }
runs-on: ${{ matrix.target.os }}
timeout-minutes: 45
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
with:
node-version: '22'
- uses: actions/setup-python@v5
with:
python-version: '3.12'
- uses: actions/download-artifact@v4
with: { name: 'engines-${{ matrix.target.platform }}', path: artifacts/engines }
- uses: actions/download-artifact@v4
with: { name: code-graph-tgz, path: artifacts/tgz }
- name: installed from the tarballs, no Soufflé, every language, every verb
shell: bash
env:
PLATFORM: ${{ matrix.target.platform }}
run: |
set -euo pipefail
command -v souffle && { echo "::error::this runner has souffle; the gate would not prove anything"; exit 1; }
export CODEGRAPH_TGZ="$(ls "$PWD"/artifacts/tgz/*.tgz)"
for lang in java typescript python javascript csharp; do
case_dir="$(ls -d graph/test/$lang/cases/*/src | head -1)"
echo "::group::$lang ($case_dir)"
bash .github/scripts/e2e-install.sh artifacts/engines "$PLATFORM" "$lang" "$case_dir"
echo "::endgroup::"
done

# A single job the branch ruleset can require. Without it, every new matrix
# entry has to be added to the protection rules by hand, and a matrix job that
# fails to start reports nothing at all — which a ruleset reads as "not
# failing" rather than as "did not run".
ci:
name: CI
runs-on: ubuntu-24.04
needs: [changes, build, hygiene, engine, engines]
needs: [changes, build, hygiene, engine, engines, pack, e2e]
if: always()
steps:
- name: every required job succeeded
Expand All @@ -431,4 +495,6 @@ jobs:
}
check "engine suites" "${{ needs.engine.result }}" "${{ needs.changes.outputs.code }}"
check "platform engines" "${{ needs.engines.result }}" "${{ needs.changes.outputs.engines }}"
check "pack" "${{ needs.pack.result }}" "${{ needs.changes.outputs.engines }}"
check "e2e on every platform" "${{ needs.e2e.result }}" "${{ needs.changes.outputs.engines }}"
echo "all required jobs passed"
Loading