From 680748ff1b0b4fb0ce1a2f70eb842aa8a0655dc6 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:20:40 -0700 Subject: [PATCH 1/2] ci: the way into main installs the packages on all five platforms and runs every verb MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A pull request into main now packs @axiomcode/code-graph once, and on linux-x64, linux-arm64, win32-x64, darwin-arm64 and darwin-x64 installs it with that platform's engine package, with no Soufflé, and for every language indexes a case and runs impact, test-impact, path (SQL and Datalog), path --every (both) and context. The required CI check fails unless every platform passes, so a release is never cut from a main that has not done this. --- .github/scripts/e2e-install.sh | 45 +++++++++++++++------- .github/workflows/ci.yml | 70 +++++++++++++++++++++++++++++++++- 2 files changed, 99 insertions(+), 16 deletions(-) diff --git a/.github/scripts/e2e-install.sh b/.github/scripts/e2e-install.sh index c98b51794..72927f307 100755 --- a/.github/scripts/e2e-install.sh +++ b/.github/scripts/e2e-install.sh @@ -12,6 +12,10 @@ # 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. # +# 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 # unnoticed until someone installs a release. @@ -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" @@ -61,8 +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" +echo "── every query verb on a copy of $(basename "$src") under src/, no souffle" +# under src/, as a repository lays its code out: context names a directory to look in, and a tree with every file +# at its root offers none +mkdir -p "$W/repo"; cp -R "$src" "$W/repo/src" 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" # every program from the engine package: not compiled here (no souffle), not left to the interpreter @@ -79,14 +89,21 @@ pair="$(node -e ' ' "$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" +# verb | the backend (path answers from SQL unless AXIOMCODE_DATALOG=1 runs the shipped programs) | what its answer must say +checks=( + "impact $callee||" + "test-impact $callee||" + "path $caller $callee||reached" + "path $caller $callee|1|reached" + "path $caller $callee --every||simple path" + "path $caller $callee --every|1|simple path" + "context $callee||" +) +for c in "${checks[@]}"; do + IFS='|' read -r verb dl want <<< "$c" + ( cd "$W/repo" && PATH="$SANDBOX_PATH" AXIOMCODE_DATALOG="$dl" "$bin" $verb ) > "$W/q.log" 2>&1; rc=$? + echo " $verb${dl:+ (datalog)}: rc=$rc" + [ "$rc" -eq 0 ] || { sed 's/^/ /' "$W/q.log" | head -12; fail "axiomcode $verb${dl:+ (datalog)} exited $rc"; } + [ -z "$want" ] || grep -q "$want" "$W/q.log" || { sed 's/^/ /' "$W/q.log" | head -12; fail "axiomcode $verb${dl:+ (datalog)} did not say '$want'"; } 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 from the package" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4ea7827eb..23e8010cf 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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-install\.sh$' || 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="" @@ -397,6 +397,70 @@ 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 @@ -404,7 +468,7 @@ jobs: 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 @@ -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" From 838b0fab049a426756ae16b2a3080283ea152125 Mon Sep 17 00:00:00 2001 From: swapnil <78632212+swapnilpaliwal-sd@users.noreply.github.com> Date: Fri, 25 Sep 2026 14:32:53 -0700 Subject: [PATCH 2/2] e2e: every query verb must return the answer a tiny project makes true, in all five languages .github/e2e/ is entry() -> helper() -> leaf() and one test calling entry(). e2e-queries.sh indexes it and requires: impact leaf lists helper as a resolved caller; path entry leaf reaches it through helper, from SQL and from the shipped Datalog programs; --every lists a route through helper; context names leaf; and after a real edit to leaf, changed names it and test-impact selects the test through the graph, not by its name. A non-zero exit or a traceback fails. Removing the call, or the test's call, turns it red. --- .github/e2e/csharp/expect | 1 + .github/e2e/csharp/src/App/Service.cs | 11 ++++ .../csharp/tests/App.Tests/ServiceTests.cs | 10 +++ .github/e2e/java/expect | 1 + .../e2e/java/src/main/java/app/Service.java | 9 +++ .../java/src/test/java/app/ServiceTest.java | 8 +++ .github/e2e/javascript/expect | 1 + .github/e2e/javascript/src/service.js | 5 ++ .github/e2e/javascript/test/service.test.js | 3 + .github/e2e/python/app/__init__.py | 0 .github/e2e/python/app/service.py | 10 +++ .github/e2e/python/expect | 1 + .github/e2e/python/tests/test_service.py | 5 ++ .github/e2e/typescript/expect | 1 + .github/e2e/typescript/src/service.ts | 5 ++ .github/e2e/typescript/test/service.test.ts | 3 + .github/scripts/e2e-install.sh | 43 ++----------- .github/scripts/e2e-queries.sh | 61 +++++++++++++++++++ .github/workflows/ci.yml | 2 +- 19 files changed, 142 insertions(+), 38 deletions(-) create mode 100644 .github/e2e/csharp/expect create mode 100644 .github/e2e/csharp/src/App/Service.cs create mode 100644 .github/e2e/csharp/tests/App.Tests/ServiceTests.cs create mode 100644 .github/e2e/java/expect create mode 100644 .github/e2e/java/src/main/java/app/Service.java create mode 100644 .github/e2e/java/src/test/java/app/ServiceTest.java create mode 100644 .github/e2e/javascript/expect create mode 100644 .github/e2e/javascript/src/service.js create mode 100644 .github/e2e/javascript/test/service.test.js create mode 100644 .github/e2e/python/app/__init__.py create mode 100644 .github/e2e/python/app/service.py create mode 100644 .github/e2e/python/expect create mode 100644 .github/e2e/python/tests/test_service.py create mode 100644 .github/e2e/typescript/expect create mode 100644 .github/e2e/typescript/src/service.ts create mode 100644 .github/e2e/typescript/test/service.test.ts create mode 100755 .github/scripts/e2e-queries.sh diff --git a/.github/e2e/csharp/expect b/.github/e2e/csharp/expect new file mode 100644 index 000000000..88cf48ce5 --- /dev/null +++ b/.github/e2e/csharp/expect @@ -0,0 +1 @@ +ENTRY=Entry HELPER=Helper LEAF=Leaf TEST=ServiceTests LEAF_FILE=src/App/Service.cs diff --git a/.github/e2e/csharp/src/App/Service.cs b/.github/e2e/csharp/src/App/Service.cs new file mode 100644 index 000000000..a52344870 --- /dev/null +++ b/.github/e2e/csharp/src/App/Service.cs @@ -0,0 +1,11 @@ +namespace App +{ + public class Service + { + public int Entry() { return Helper() + 1; } + + int Helper() { return Leaf() * 2; } + + int Leaf() { return 41; } + } +} diff --git a/.github/e2e/csharp/tests/App.Tests/ServiceTests.cs b/.github/e2e/csharp/tests/App.Tests/ServiceTests.cs new file mode 100644 index 000000000..483883fff --- /dev/null +++ b/.github/e2e/csharp/tests/App.Tests/ServiceTests.cs @@ -0,0 +1,10 @@ +using Xunit; + +namespace App.Tests +{ + public class ServiceTests + { + [Fact] + public void EntryWorks() { new App.Service().Entry(); } + } +} diff --git a/.github/e2e/java/expect b/.github/e2e/java/expect new file mode 100644 index 000000000..51fac2eb8 --- /dev/null +++ b/.github/e2e/java/expect @@ -0,0 +1 @@ +ENTRY=entry HELPER=helper LEAF=leaf TEST=ServiceTest LEAF_FILE=src/main/java/app/Service.java diff --git a/.github/e2e/java/src/main/java/app/Service.java b/.github/e2e/java/src/main/java/app/Service.java new file mode 100644 index 000000000..13576f359 --- /dev/null +++ b/.github/e2e/java/src/main/java/app/Service.java @@ -0,0 +1,9 @@ +package app; + +public class Service { + public int entry() { return helper() + 1; } + + int helper() { return leaf() * 2; } + + int leaf() { return 41; } +} diff --git a/.github/e2e/java/src/test/java/app/ServiceTest.java b/.github/e2e/java/src/test/java/app/ServiceTest.java new file mode 100644 index 000000000..039f3a91f --- /dev/null +++ b/.github/e2e/java/src/test/java/app/ServiceTest.java @@ -0,0 +1,8 @@ +package app; + +import org.junit.jupiter.api.Test; + +class ServiceTest { + @Test + void entryWorks() { new Service().entry(); } +} diff --git a/.github/e2e/javascript/expect b/.github/e2e/javascript/expect new file mode 100644 index 000000000..116ddc88d --- /dev/null +++ b/.github/e2e/javascript/expect @@ -0,0 +1 @@ +ENTRY=entry HELPER=helper LEAF=leaf TEST=service.test LEAF_FILE=src/service.js diff --git a/.github/e2e/javascript/src/service.js b/.github/e2e/javascript/src/service.js new file mode 100644 index 000000000..1c4555319 --- /dev/null +++ b/.github/e2e/javascript/src/service.js @@ -0,0 +1,5 @@ +export function leaf() { return 41; } + +export function helper() { return leaf() * 2; } + +export function entry() { return helper() + 1; } diff --git a/.github/e2e/javascript/test/service.test.js b/.github/e2e/javascript/test/service.test.js new file mode 100644 index 000000000..1d7a3ad36 --- /dev/null +++ b/.github/e2e/javascript/test/service.test.js @@ -0,0 +1,3 @@ +import { entry } from '../src/service'; + +test('entry', () => { entry(); }); diff --git a/.github/e2e/python/app/__init__.py b/.github/e2e/python/app/__init__.py new file mode 100644 index 000000000..e69de29bb diff --git a/.github/e2e/python/app/service.py b/.github/e2e/python/app/service.py new file mode 100644 index 000000000..fd2228d9e --- /dev/null +++ b/.github/e2e/python/app/service.py @@ -0,0 +1,10 @@ +def leaf(): + return 41 + + +def helper(): + return leaf() * 2 + + +def entry(): + return helper() + 1 diff --git a/.github/e2e/python/expect b/.github/e2e/python/expect new file mode 100644 index 000000000..0b5c27a40 --- /dev/null +++ b/.github/e2e/python/expect @@ -0,0 +1 @@ +ENTRY=entry HELPER=helper LEAF=leaf TEST=test_service LEAF_FILE=app/service.py diff --git a/.github/e2e/python/tests/test_service.py b/.github/e2e/python/tests/test_service.py new file mode 100644 index 000000000..d62a3216d --- /dev/null +++ b/.github/e2e/python/tests/test_service.py @@ -0,0 +1,5 @@ +from app.service import entry + + +def test_entry(): + assert entry() == 83 diff --git a/.github/e2e/typescript/expect b/.github/e2e/typescript/expect new file mode 100644 index 000000000..ea656b85f --- /dev/null +++ b/.github/e2e/typescript/expect @@ -0,0 +1 @@ +ENTRY=entry HELPER=helper LEAF=leaf TEST=service.test LEAF_FILE=src/service.ts diff --git a/.github/e2e/typescript/src/service.ts b/.github/e2e/typescript/src/service.ts new file mode 100644 index 000000000..c9a8e0192 --- /dev/null +++ b/.github/e2e/typescript/src/service.ts @@ -0,0 +1,5 @@ +export function leaf(): number { return 41; } + +export function helper(): number { return leaf() * 2; } + +export function entry(): number { return helper() + 1; } diff --git a/.github/e2e/typescript/test/service.test.ts b/.github/e2e/typescript/test/service.test.ts new file mode 100644 index 000000000..1d7a3ad36 --- /dev/null +++ b/.github/e2e/typescript/test/service.test.ts @@ -0,0 +1,3 @@ +import { entry } from '../src/service'; + +test('entry', () => { entry(); }); diff --git a/.github/scripts/e2e-install.sh b/.github/scripts/e2e-install.sh index 72927f307..9c732f03e 100755 --- a/.github/scripts/e2e-install.sh +++ b/.github/scripts/e2e-install.sh @@ -9,8 +9,8 @@ # package exactly as publish-npm.yml would, installs both into an empty project, and # runs `axiomcode` on 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/, 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 @@ -69,41 +69,10 @@ edges="$(node -e ' ' "$db" 2>/dev/null)" [ -n "$edges" ] && [ "$edges" -gt 0 ] || fail "the graph has no call edges (${edges:-unreadable})" -echo "── every query verb on a copy of $(basename "$src") under src/, no souffle" -# under src/, as a repository lays its code out: context names a directory to look in, and a tree with every file -# at its root offers none -mkdir -p "$W/repo"; cp -R "$src" "$W/repo/src" -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#* }" -# verb | the backend (path answers from SQL unless AXIOMCODE_DATALOG=1 runs the shipped programs) | what its answer must say -checks=( - "impact $callee||" - "test-impact $callee||" - "path $caller $callee||reached" - "path $caller $callee|1|reached" - "path $caller $callee --every||simple path" - "path $caller $callee --every|1|simple path" - "context $callee||" -) -for c in "${checks[@]}"; do - IFS='|' read -r verb dl want <<< "$c" - ( cd "$W/repo" && PATH="$SANDBOX_PATH" AXIOMCODE_DATALOG="$dl" "$bin" $verb ) > "$W/q.log" 2>&1; rc=$? - echo " $verb${dl:+ (datalog)}: rc=$rc" - [ "$rc" -eq 0 ] || { sed 's/^/ /' "$W/q.log" | head -12; fail "axiomcode $verb${dl:+ (datalog)} exited $rc"; } - [ -z "$want" ] || grep -q "$want" "$W/q.log" || { sed 's/^/ /' "$W/q.log" | head -12; fail "axiomcode $verb${dl:+ (datalog)} did not say '$want'"; } -done -echo "e2e: ok — installed from the tarballs, used the packaged $platform engine, $edges call edges; every query verb answered from the package" +echo "e2e: ok — installed from the tarballs, used the packaged $platform engine, $edges call edges; every query verb answered correctly from the package" diff --git a/.github/scripts/e2e-queries.sh b/.github/scripts/e2e-queries.sh new file mode 100755 index 000000000..14edbb9d9 --- /dev/null +++ b/.github/scripts/e2e-queries.sh @@ -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 +# +# is .github/e2e//: entry() calls helper() calls leaf(), one test +# calls entry(), and `expect` names the four in that language's spelling. The project is +# copied to , 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 }"; 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() { # : 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() { # 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")" diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 23e8010cf..c4eba024a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -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$' -e '^\.github/scripts/e2e-install\.sh$' || 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=""