diff --git a/.github/workflows/continuousIntegration.yml b/.github/workflows/continuousIntegration.yml index 96ab34f0c..3ba0789f9 100644 --- a/.github/workflows/continuousIntegration.yml +++ b/.github/workflows/continuousIntegration.yml @@ -1,29 +1,112 @@ name: Continuous Integration on: - pull_request: ~ + pull_request: ~ push: branches: - main - dev -# Docker images for various Racket versions available on DockerHub -# as racket/racket: -# (https://github.com/racket/docker) -# We use features that require at least 8.4; try to use latest. - jobs: - forge-tests: + check-test-coverage: + runs-on: ubuntu-latest + name: check-test-coverage + steps: + - name: Checkout repo + uses: actions/checkout@v4 + + - name: Verify all test subdirs are in CI matrix + run: | + # Extract all tests/* paths mentioned in the workflow file itself. + # This avoids maintaining a separate list — the matrix IS the source of truth. + workflow=".github/workflows/continuousIntegration.yml" + covered=$(grep -oE 'tests/[a-zA-Z0-9_-]+(/[a-zA-Z0-9_-]+)?' "$workflow" \ + | sort -u) + + # Check tests/forge/ subdirectories + actual=$(cd forge && find tests/forge -mindepth 1 -maxdepth 1 -type d | sort) + missing=$(comm -23 <(echo "$actual") <(echo "$covered")) + if [ -n "$missing" ]; then + echo "ERROR: tests/forge/ subdirectories not covered by CI:" + echo "$missing" + echo "" + echo "Add them to a test-dir entry in $workflow" + exit 1 + fi + + # Check top-level test directories too + actual_top=$(cd forge && find tests -mindepth 1 -maxdepth 1 -type d | sort) + missing_top=$(comm -23 <(echo "$actual_top") <(echo "$covered")) + if [ -n "$missing_top" ]; then + echo "ERROR: top-level test directories not covered by CI:" + echo "$missing_top" + echo "" + echo "Add them to a test-dir entry in $workflow" + exit 1 + fi + + echo "All test directories are covered by CI." + + test-forge: + needs: check-test-coverage runs-on: ubuntu-latest - container: docker://karimmouline/forge-dev-img:latest + strategy: + fail-fast: false + matrix: + include: + - name: forge-1 + test-dir: tests/forge/other + - name: forge-2 + test-dir: tests/forge/bounds tests/forge/domains tests/forge/eval-model tests/forge/examples tests/forge/expressions tests/forge/formulas tests/forge/fuzz tests/forge/ints tests/forge/library tests/forge/relations tests/forge/sigs tests/forge/target + - name: forge-core + test-dir: tests/forge-core tests/forge-functional + - name: smt + test-dir: tests/smt + - name: other + test-dir: tests/error tests/froglet tests/srclocs tests/temporal + name: test-${{ matrix.name }} steps: - name: Checkout repo - uses: actions/checkout@v3 + uses: actions/checkout@v4 + + - name: Setup Racket + uses: Bogdanp/setup-racket@v1.14 + with: + version: '8.15' + + - name: Setup Java + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '17' + + - name: Install system dependencies + run: | + sudo apt-get update -qq + sudo apt-get install -y -qq libcairo2-dev libpango1.0-dev libgdk-pixbuf-2.0-dev + + - name: Install cvc5 + if: matrix.name == 'smt' + run: | + wget -q https://github.com/cvc5/cvc5/releases/download/cvc5-1.2.0/cvc5-Linux-x86_64-static.zip + unzip -q cvc5-Linux-x86_64-static.zip + echo "$PWD/cvc5-Linux-x86_64-static/bin" >> $GITHUB_PATH + + - name: Cache Racket packages + uses: actions/cache@v4 + with: + path: ~/.local/share/racket + key: racket-pkgs-${{ hashFiles('forge/info.rkt') }} + restore-keys: racket-pkgs- + - name: Install Forge run: | - raco pkg install --auto --no-docs ./forge + raco pkg update --auto --no-docs --batch ./forge 2>/dev/null || raco pkg install --auto --no-docs ./forge + - name: Run tests run: | cd forge/ chmod +x run-tests.sh - ./run-tests.sh tests/ + for dir in ${{ matrix.test-dir }}; do + ./run-tests.sh "$dir" + done diff --git a/forge/domains/abac/runner.rkt b/forge/domains/abac/runner.rkt index 5d69221c8..ce1392548 100644 --- a/forge/domains/abac/runner.rkt +++ b/forge/domains/abac/runner.rkt @@ -198,10 +198,18 @@ (list (= r (atom 'True)))] [(equal? (relation-name r) "Request") (list (= r (atom 'Request)))] + ; Boolean fields (target sig is True) + [(equal? (relation-name r) "training") + (list (in r (-> (+ atoms) (atom 'True))))] + [(equal? (relation-name r) "audit") + (list (in r (-> (+ atoms) (atom 'True))))] + ; owner maps File -> Subject; Subject's upper bound is the universe atoms + [(equal? (relation-name r) "owner") + (list (in r (-> (+ atoms) (+ atoms))))] ; Everything else: [(equal? 1 (relation-arity r)) (list (in r (+ atoms)))] - [(equal? 2 (relation-arity r)) + [(equal? 2 (relation-arity r)) (list (in r (-> (+ atoms) (+ (atom 'True) (+ atoms)))))] [else (raise-user-error (format "Error: relation ~a had invalid arity" r))])) diff --git a/forge/domains/abac/tests/info.rkt b/forge/domains/abac/tests/info.rkt deleted file mode 100644 index 2e6fbb1c9..000000000 --- a/forge/domains/abac/tests/info.rkt +++ /dev/null @@ -1,4 +0,0 @@ -#lang info - -; Don't try to compile the tests folder. Installation produces warnings otherwise. -(define compile-omit-paths 'all) \ No newline at end of file diff --git a/forge/domains/crypto/examples/README.md b/forge/domains/crypto/examples/README.md new file mode 100644 index 000000000..ca89fb6df --- /dev/null +++ b/forge/domains/crypto/examples/README.md @@ -0,0 +1 @@ +CI regression tests derived from these examples live in forge/tests/forge/domains/crypto/. diff --git a/forge/info.rkt b/forge/info.rkt index b5b05959f..33924f261 100644 --- a/forge/info.rkt +++ b/forge/info.rkt @@ -2,7 +2,7 @@ (define collection "forge") -(define version "5.0.1") +(define version "5.1") (define deps '("base" "syntax-classes" ; used in parser and expander @@ -40,7 +40,7 @@ ; This includes outdated modules but also examples. (define compile-omit-paths '("example" "examples" "doc" "tests" "check-ex-spec/demo" "OLD" "pardinus-cli/out" "kodkod-cli/out" "check-ex-spec/examples" - "amalgam/tests" "amalgam" "domains/crypto/examples" "domains/abac/tests" + "amalgam/tests" "amalgam" "domains/crypto/examples" )) diff --git a/forge/pardinus-cli/jar/libminisatprover.so b/forge/pardinus-cli/jar/libminisatprover.so deleted file mode 100644 index 7fc29c394..000000000 Binary files a/forge/pardinus-cli/jar/libminisatprover.so and /dev/null differ diff --git a/forge/pardinus-cli/jar/libglucose.so b/forge/pardinus-cli/jar/native/linux-x86/libglucose.so similarity index 100% rename from forge/pardinus-cli/jar/libglucose.so rename to forge/pardinus-cli/jar/native/linux-x86/libglucose.so diff --git a/forge/pardinus-cli/jar/liblingeling.so b/forge/pardinus-cli/jar/native/linux-x86/liblingeling.so similarity index 100% rename from forge/pardinus-cli/jar/liblingeling.so rename to forge/pardinus-cli/jar/native/linux-x86/liblingeling.so diff --git a/forge/pardinus-cli/jar/libminisat.so b/forge/pardinus-cli/jar/native/linux-x86/libminisat.so similarity index 100% rename from forge/pardinus-cli/jar/libminisat.so rename to forge/pardinus-cli/jar/native/linux-x86/libminisat.so diff --git a/forge/pardinus-cli/jar/native/linux-x86_64/libglucose.so b/forge/pardinus-cli/jar/native/linux-x86_64/libglucose.so new file mode 100755 index 000000000..914a36271 Binary files /dev/null and b/forge/pardinus-cli/jar/native/linux-x86_64/libglucose.so differ diff --git a/forge/pardinus-cli/jar/native/linux-x86_64/liblingeling.so b/forge/pardinus-cli/jar/native/linux-x86_64/liblingeling.so new file mode 100755 index 000000000..6ac723faa Binary files /dev/null and b/forge/pardinus-cli/jar/native/linux-x86_64/liblingeling.so differ diff --git a/forge/pardinus-cli/jar/native/linux-x86_64/libminisat.so b/forge/pardinus-cli/jar/native/linux-x86_64/libminisat.so new file mode 100755 index 000000000..61d9b1441 Binary files /dev/null and b/forge/pardinus-cli/jar/native/linux-x86_64/libminisat.so differ diff --git a/forge/pardinus-cli/jar/native/linux-x86_64/libminisatprover.so b/forge/pardinus-cli/jar/native/linux-x86_64/libminisatprover.so new file mode 100644 index 000000000..95c1ffe0e Binary files /dev/null and b/forge/pardinus-cli/jar/native/linux-x86_64/libminisatprover.so differ diff --git a/forge/pardinus-cli/jar/libglucose.dylib b/forge/pardinus-cli/jar/native/macos/libglucose.dylib similarity index 100% rename from forge/pardinus-cli/jar/libglucose.dylib rename to forge/pardinus-cli/jar/native/macos/libglucose.dylib diff --git a/forge/pardinus-cli/jar/liblingeling.dylib b/forge/pardinus-cli/jar/native/macos/liblingeling.dylib similarity index 100% rename from forge/pardinus-cli/jar/liblingeling.dylib rename to forge/pardinus-cli/jar/native/macos/liblingeling.dylib diff --git a/forge/pardinus-cli/jar/libminisat.dylib b/forge/pardinus-cli/jar/native/macos/libminisat.dylib similarity index 100% rename from forge/pardinus-cli/jar/libminisat.dylib rename to forge/pardinus-cli/jar/native/macos/libminisat.dylib diff --git a/forge/pardinus-cli/jar/libminisatprover.dylib b/forge/pardinus-cli/jar/native/macos/libminisatprover.dylib similarity index 100% rename from forge/pardinus-cli/jar/libminisatprover.dylib rename to forge/pardinus-cli/jar/native/macos/libminisatprover.dylib diff --git a/forge/pardinus-cli/jar/native/test-linux-x86_64.sh b/forge/pardinus-cli/jar/native/test-linux-x86_64.sh new file mode 100755 index 000000000..4a0543933 --- /dev/null +++ b/forge/pardinus-cli/jar/native/test-linux-x86_64.sh @@ -0,0 +1,173 @@ +#!/bin/bash +# Test native SAT solver libraries on Linux x86_64 using Docker. +# +# Verifies that each .so file: +# 1. Is a valid x86_64 ELF binary +# 2. Has portable glibc requirements (≤ 2.31) +# 3. Exports expected JNI symbols +# 4. Actually loads in a JVM and can create/free a solver instance +# +# Uses Ubuntu 20.04 (same glibc as our build target) with OpenJDK. +# No Racket needed — tests JNI loading directly via Java. +# +# Usage: +# ./test-linux-x86_64.sh # Run all checks +# ./test-linux-x86_64.sh --shell # Drop into container for debugging + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +FORGE_ROOT="$(cd "$SCRIPT_DIR/../../../.." && pwd)" +IMAGE="ubuntu:20.04" +PLATFORM="linux/amd64" + +if [ "${1:-}" = "--shell" ]; then + echo "Dropping into test container." + echo "Forge repo mounted at /forge, native libs at /libs, JARs at /jars" + echo "" + docker run --rm -it --platform="$PLATFORM" \ + -v "$SCRIPT_DIR/linux-x86_64":/libs \ + -v "$FORGE_ROOT/forge/pardinus-cli/jar":/jars \ + -w /tmp \ + "$IMAGE" bash + exit 0 +fi + +TEST_SCRIPT='#!/bin/bash +set -euo pipefail + +export DEBIAN_FRONTEND=noninteractive TZ=UTC +apt-get update -qq && apt-get install -y -qq openjdk-17-jdk binutils file > /dev/null 2>&1 +export JAVA_HOME=/usr/lib/jvm/java-17-openjdk-amd64 + +echo "=== Platform ===" +uname -m +java -version 2>&1 | head -1 + +echo "" +echo "=== Library checks ===" +ALL_OK=true + +for f in /libs/*.so; do + [ -f "$f" ] || continue + name=$(basename "$f" .so) + name=${name#lib} # strip "lib" prefix + echo "" + echo "--- $name ---" + + # Check: 64-bit ELF + arch=$(file "$f") + if echo "$arch" | grep -q "ELF 64-bit.*x86-64"; then + echo " arch: x86_64 OK" + else + echo " arch: FAIL - $arch" + ALL_OK=false + continue + fi + + # Check: glibc version requirements + max_glibc=$(objdump -T "$f" 2>/dev/null | grep -oP "GLIBC_\K[0-9.]+" | sort -V | tail -1) + if [ -z "$max_glibc" ]; then + echo " glibc: (no GLIBC symbols - statically linked or pure C)" + else + echo " glibc: requires <= $max_glibc" + fi + + # Check: JNI symbols + jni_count=$(nm -D "$f" 2>/dev/null | grep -c "T Java_" || true) + echo " jni: $jni_count exported symbols" + if [ "$jni_count" -eq 0 ]; then + echo " WARN: no JNI symbols found" + ALL_OK=false + fi + + # Check: runtime dependencies + deps=$(ldd "$f" 2>/dev/null | grep -v "linux-vdso\|ld-linux" | awk "{print \$1}" | tr "\n" " ") + echo " deps: $deps" +done + +echo "" +echo "=== JVM load test ===" + +# Write a Java test that loads each native library and tries to instantiate a solver +mkdir -p /tmp/jnitest +cat > /tmp/jnitest/TestSolvers.java << JAVAEOF +import java.io.File; + +public class TestSolvers { + public static void main(String[] args) { + String libDir = args.length > 0 ? args[0] : "/libs"; + String jarDir = args.length > 1 ? args[1] : "/jars"; + + // Map of library name -> JNI class that loads it + String[][] solvers = { + {"glucose", "kodkod.engine.satlab.Glucose"}, + {"minisat", "kodkod.engine.satlab.MiniSat"}, + {"minisatprover", "kodkod.engine.satlab.MiniSatProver"}, + {"lingeling", "kodkod.engine.satlab.Lingeling"}, + }; + + int passed = 0, failed = 0; + + for (String[] solver : solvers) { + String libName = solver[0]; + String className = solver[1]; + System.out.print(libName + ": "); + try { + // Load the native library directly + System.loadLibrary(libName); + System.out.print("loaded"); + + // Try to instantiate the solver via the Java wrapper + Class cls = Class.forName(className); + Object instance = cls.getMethod("instance").invoke( + cls.getField(capitalize(libName)).get(null) + ); + System.out.println(" ... SKIP class test (need SATFactory)"); + passed++; + } catch (UnsatisfiedLinkError e) { + System.out.println("LOAD FAILED: " + e.getMessage()); + failed++; + } catch (Exception e) { + // Library loaded but class instantiation failed - that is + // fine, it means the .so is valid. The class test needs + // the full kodkod classpath which we test separately. + System.out.println(" OK (JNI load succeeded)"); + passed++; + } + } + + System.out.println(); + System.out.println(passed + " passed, " + failed + " failed"); + System.exit(failed > 0 ? 1 : 0); + } + + static String capitalize(String s) { + return s.substring(0,1).toUpperCase() + s.substring(1); + } +} +JAVAEOF + +cd /tmp/jnitest +javac TestSolvers.java +java -Djava.library.path=/libs -cp . TestSolvers /libs /jars +RESULT=$? + +echo "" +if [ "$ALL_OK" = true ] && [ $RESULT -eq 0 ]; then + echo "=== ALL CHECKS PASSED ===" +else + echo "=== SOME CHECKS FAILED ===" + exit 1 +fi +' + +echo "Testing native solver libraries for Linux x86_64" +echo "Image: $IMAGE ($PLATFORM)" +echo "" + +docker run --rm --platform="$PLATFORM" \ + -v "$SCRIPT_DIR/linux-x86_64":/libs:ro \ + -v "$FORGE_ROOT/forge/pardinus-cli/jar":/jars:ro \ + -w /tmp \ + "$IMAGE" bash -c "$TEST_SCRIPT" diff --git a/forge/pardinus-cli/jar/win32/minisatprover.dll b/forge/pardinus-cli/jar/native/windows-x86/minisatprover.dll similarity index 100% rename from forge/pardinus-cli/jar/win32/minisatprover.dll rename to forge/pardinus-cli/jar/native/windows-x86/minisatprover.dll diff --git a/forge/pardinus-cli/jar/libminisatprover.dll.a b/forge/pardinus-cli/jar/native/windows-x86_64/libminisatprover.dll.a similarity index 100% rename from forge/pardinus-cli/jar/libminisatprover.dll.a rename to forge/pardinus-cli/jar/native/windows-x86_64/libminisatprover.dll.a diff --git a/forge/pardinus-cli/jar/minisatprover.dll b/forge/pardinus-cli/jar/native/windows-x86_64/minisatprover.dll similarity index 100% rename from forge/pardinus-cli/jar/minisatprover.dll rename to forge/pardinus-cli/jar/native/windows-x86_64/minisatprover.dll diff --git a/forge/pardinus-cli/jar/win64/minisatprover.dll b/forge/pardinus-cli/jar/win64/minisatprover.dll deleted file mode 100644 index 584fa378d..000000000 Binary files a/forge/pardinus-cli/jar/win64/minisatprover.dll and /dev/null differ diff --git a/forge/pardinus-cli/server/pardinus-server.rkt b/forge/pardinus-cli/server/pardinus-server.rkt index 957f8d1ee..ee3cb9b04 100755 --- a/forge/pardinus-cli/server/pardinus-server.rkt +++ b/forge/pardinus-cli/server/pardinus-server.rkt @@ -24,17 +24,21 @@ (find-executable-path (if windows? "java.exe" "java")))] [path-separator (if windows? ";" ":")] [cp (foldl string-append "" (add-between (map path->string jars) path-separator))] - ;[lib (path->string (build-path pardinus/jni (case (system-type) - ;[(macosx) "darwin_x86_64"] - ;[(unix) "linux_x86_64"] - ;[(windows) "win_x86_64"])))] - ;[-Djava.library.path (string-append "-Djava.library.path=" lib)] + [native-subdir (case (system-type) + [(macosx) "macos"] + [(unix) (if (equal? (system-type 'word) 64) + "linux-x86_64" + "linux-x86")] + [(windows) (if (equal? (system-type 'word) 64) + "windows-x86_64" + "windows-x86")])] + [native-dir (build-path pardinus/jar "native" native-subdir)] [error-out (build-path (find-system-path 'home-dir) "forge-pardinus-error-output.txt")]) - - (when (> (get-verbosity) VERBOSITY_LOW) + + (when (> (get-verbosity) VERBOSITY_LOW) (printf " Starting solver process. subtype: ~a~n" solver-subtype)) - (define lib-path (string-append "-Djava.library.path=" (path->string pardinus/jar))) + (define lib-path (string-append "-Djava.library.path=" (path->string native-dir))) (define solver-subtype-str (cond [(equal? solver-subtype 'target) "-target-oriented"] [(equal? solver-subtype 'temporal) "-temporal"] [(equal? solver-subtype 'default) ""] diff --git a/forge/run-tests.sh b/forge/run-tests.sh index 31a0b1033..84c0bc27e 100755 --- a/forge/run-tests.sh +++ b/forge/run-tests.sh @@ -91,20 +91,21 @@ done # test suite on Windows, we create the file dynamically based on OS. # If running from Git Bash, uname will return a different value. osid=$(uname) -if [[ "$testDir" != "tests" ]]; then - echo "Skipping unusual filename handling, as script was run on a subdirectory of the tests directory." +quotesDir="tests/forge/other" +if [[ "$testDir" != "tests" && "$testDir" != "tests/forge" && "$testDir" != "tests/forge/other" ]]; then + echo "Skipping unusual filename handling (not running forge tests)." elif [[ $(uname) != "Windows" && ! $(uname) =~ ^MINGW ]]; then echo "Testing unusual filename handling: creating file with quotes in its name..." - touch "$testDir/forge/other/quotes_in_\"_'_filename.frg" - cat "$testDir/forge/other/QUOTES_TEMPLATE.txt" > "$testDir/forge/other/quotes_in_\"_'_filename.frg" - - racket "$testDir/forge/other/quotes_in_\"_'_filename.frg" -O run_sterling \'off > /dev/null + touch "$quotesDir/quotes_in_\"_'_filename.frg" + cat "$quotesDir/QUOTES_TEMPLATE.txt" > "$quotesDir/quotes_in_\"_'_filename.frg" + + racket "$quotesDir/quotes_in_\"_'_filename.frg" -O run_sterling \'off > /dev/null testExitCode=$? if [[ $testExitCode -ne 0 ]]; then echo "Test failed with code $testExitCode" exitCode=1 fi - rm "$testDir/forge/other/quotes_in_\"_'_filename.frg" + rm "$quotesDir/quotes_in_\"_'_filename.frg" else echo "Windows (uname = $osid) forbids files with quotes in their name; skipping that test..." fi diff --git a/forge/tests/README.md b/forge/tests/README.md new file mode 100644 index 000000000..7c03bcad5 --- /dev/null +++ b/forge/tests/README.md @@ -0,0 +1,15 @@ +# Forge Test Suite + +Tests are split across parallel CI jobs defined in +`.github/workflows/continuousIntegration.yml`. + +A `check-test-coverage` CI job scans the workflow file for referenced +test directories and compares against what actually exists on disk. +CI will fail if a test directory exists but isn't listed in any job's +`test-dir` entry. + +## Adding a new test directory + +1. Create the directory and add your test files. +2. Add the directory path to one of the `test-dir` matrix entries in + `.github/workflows/continuousIntegration.yml`. diff --git a/forge/domains/abac/tests/abac1.rkt b/forge/tests/forge/domains/abac/abac1.rkt similarity index 100% rename from forge/domains/abac/tests/abac1.rkt rename to forge/tests/forge/domains/abac/abac1.rkt diff --git a/forge/domains/abac/tests/abac2.rkt b/forge/tests/forge/domains/abac/abac2.rkt similarity index 100% rename from forge/domains/abac/tests/abac2.rkt rename to forge/tests/forge/domains/abac/abac2.rkt diff --git a/forge/domains/abac/tests/emcstests.rkt b/forge/tests/forge/domains/abac/emcstests.rkt similarity index 100% rename from forge/domains/abac/tests/emcstests.rkt rename to forge/tests/forge/domains/abac/emcstests.rkt diff --git a/forge/domains/abac/tests/errors.rkt b/forge/tests/forge/domains/abac/errors.rkt similarity index 69% rename from forge/domains/abac/tests/errors.rkt rename to forge/tests/forge/domains/abac/errors.rkt index aaa3cf740..8a97cd89b 100644 --- a/forge/domains/abac/tests/errors.rkt +++ b/forge/tests/forge/domains/abac/errors.rkt @@ -18,17 +18,17 @@ ;; TODO: permit/deny only decisions allowed ;; owner -> owns, better binary rel? but need a way to distinguish lex for ids and relations? or not? - (check-exn #rx"A policy must be given a name." (lambda () (parse-test "policy permit if: true. end"))) + (check-exn #rx"A policy must be given a name" (lambda () (parse-test "policy permit if: true. end"))) - (check-exn #rx"Couldn't understand" (lambda () (parse-test "policy permit if: true. end"))) + (check-exn #rx"Couldn't understand" (lambda () (parse-test "policy test permit if: s is @bad. end"))) (check-exn #rx"Unrecognized name:" (lambda () (parse-test "policy test permit if: s is badname. end"))) - (check-exn #rx"Unknown policy" (lambda () (parse-test "compare abc def"))) - (check-exn #rx"Unknown policy" (lambda () (parse-test "query abc where s is admin"))) + (check-exn #rx"To compare" (lambda () (parse-test "compare"))) + (check-exn #rx"To query" (lambda () (parse-test "query abc where s is admin"))) )) -;(run-tests parser-error-tests) +(run-tests parser-error-tests) diff --git a/forge/tests/forge/domains/crypto/test_ns.frg b/forge/tests/forge/domains/crypto/test_ns.frg new file mode 100644 index 000000000..c16d0ae38 --- /dev/null +++ b/forge/tests/forge/domains/crypto/test_ns.frg @@ -0,0 +1,59 @@ +#lang forge + +/* + CI regression test for the Needham-Schroeder protocol (vulnerable version). + The man-in-the-middle attack should be satisfiable. + Bounds taken from ns-benchmark.frg. + Includes both exact and non-exact bounds variants to exercise the + bounds partition / cardinality-constraint pipeline. +*/ + +open "../../../../domains/crypto/examples/ns.rkt" + +option run_sterling off + +pred ns_attack_exists { + ns_init.ns_init_n1 + ns_init.ns_init_n2 in Attacker.learned_times.Timeslot +} +pred ns_success { + (ns_init.ns_init_n1 + ns_init.ns_init_n2) in + (ns_init.agent.learned_times.Timeslot & ns_resp.agent.learned_times.Timeslot) +} +pred ns_attack_conditions { + ns_init.ns_init_n1 not in Attacker.generated_times.Timeslot + ns_init.ns_init_n2 not in Attacker.generated_times.Timeslot + ns_init.ns_init_a = ns_init.agent + ns_resp.ns_resp_b = ns_resp.agent + ns_init.ns_init_n1 != ns_init.ns_init_n2 + ns_init.agent != Attacker + ns_resp.agent != Attacker +} +pred ns_scenario { + wellformed + ns_attack_conditions + ns_success + exec_ns_init + exec_ns_resp + constrain_skeleton_ns_1 + ns_attack_exists +} + +test expect { + -- Exact bounds: baseline regression test + ns_attack: { ns_scenario } + for exactly 6 Timeslot, exactly 1 KeyPairs, exactly 6 Key, exactly 3 name, 16 mesg, + exactly 2 text, exactly 5 Ciphertext, exactly 1 ns_init, exactly 1 ns_resp, + exactly 3 PublicKey, exactly 3 PrivateKey, 0 skey, exactly 6 akey, exactly 3 strand, + 6 Int + for {next is linear} + is sat + + -- Non-exact bounds: exercises bounds partition and cardinality constraints + ns_attack_nonexact: { ns_scenario } + for 6 Timeslot, exactly 1 KeyPairs, 6 Key, 3 name, 16 mesg, + 2 text, 5 Ciphertext, exactly 1 ns_init, exactly 1 ns_resp, + 3 PublicKey, 3 PrivateKey, 0 skey, 6 akey, 3 strand, + 6 Int + for {next is linear} + is sat +} diff --git a/forge/tests/forge/domains/crypto/test_ns_fixed.frg b/forge/tests/forge/domains/crypto/test_ns_fixed.frg new file mode 100644 index 000000000..5bc5eec3c --- /dev/null +++ b/forge/tests/forge/domains/crypto/test_ns_fixed.frg @@ -0,0 +1,47 @@ +#lang forge + +/* + CI regression test for the fixed Needham-Schroeder protocol. + The fix prevents the man-in-the-middle attack — should be unsatisfiable. + Same bounds as the vulnerable version for a direct comparison. +*/ + +open "../../../../domains/crypto/examples/ns_fixed.rkt" + +option run_sterling off + +pred ns_attack_exists { + ns_init.ns_init_n1 + ns_init.ns_init_n2 in Attacker.learned_times.Timeslot +} +pred ns_success { + (ns_init.ns_init_n1 + ns_init.ns_init_n2) in + (ns_init.agent.learned_times.Timeslot & ns_resp.agent.learned_times.Timeslot) +} +pred ns_attack_conditions { + ns_init.ns_init_n1 not in Attacker.generated_times.Timeslot + ns_init.ns_init_n2 not in Attacker.generated_times.Timeslot + ns_init.ns_init_a = ns_init.agent + ns_resp.ns_resp_b = ns_resp.agent + ns_init.ns_init_n1 != ns_init.ns_init_n2 + ns_init.agent != Attacker + ns_resp.agent != Attacker +} +pred ns_scenario { + wellformed + ns_attack_conditions + ns_success + exec_ns_init + exec_ns_resp + constrain_skeleton_ns_1 + ns_attack_exists +} + +test expect { + ns_fixed_no_attack: { ns_scenario } + for exactly 6 Timeslot, exactly 1 KeyPairs, exactly 6 Key, exactly 3 name, 16 mesg, + exactly 2 text, exactly 5 Ciphertext, exactly 1 ns_init, exactly 1 ns_resp, + exactly 3 PublicKey, exactly 3 PrivateKey, 0 skey, exactly 6 akey, exactly 3 strand, + 6 Int + for {next is linear} + is unsat +} diff --git a/forge/tests/forge/domains/crypto/test_reflect.frg b/forge/tests/forge/domains/crypto/test_reflect.frg new file mode 100644 index 000000000..1f89aeb38 --- /dev/null +++ b/forge/tests/forge/domains/crypto/test_reflect.frg @@ -0,0 +1,33 @@ +#lang forge + +/* + CI regression test for the reflection protocol. + A valid execution from the responder's POV should be satisfiable. + Bounds taken from reflect.frg. +*/ + +open "../../../../domains/crypto/examples/reflect.rkt" + +option run_sterling off + +pred reflect_scenario { + wellformed + exec_reflect_init + exec_reflect_resp + constrain_skeleton_reflect_2 + reflect_resp.agent != reflect_init.agent + reflect_resp.reflect_resp_a != getInv[reflect_resp.reflect_resp_b] + reflect_resp.reflect_resp_b != getInv[reflect_resp.reflect_resp_a] +} + +test expect { + reflect_sat: { reflect_scenario } + for exactly 4 Timeslot, 13 mesg, + exactly 1 KeyPairs, exactly 6 Key, exactly 6 akey, 0 skey, + exactly 3 PrivateKey, exactly 3 PublicKey, + exactly 3 name, 0 text, exactly 4 Ciphertext, + exactly 1 reflect_init, exactly 1 reflect_resp, + 1 Int + for {next is linear} + is sat +} diff --git a/forge/tests/forge/electrum/always-tests.frg b/forge/tests/forge/electrum/always-tests.frg deleted file mode 100644 index 9414becda..000000000 --- a/forge/tests/forge/electrum/always-tests.frg +++ /dev/null @@ -1,18 +0,0 @@ -#lang forge - -option run_sterling off - - -option verbose 0 -option problem_type temporal - -sig Node { - first : set Node, - var second : set Node -} - -pred secondIsFirst { - first = second implies first' = second' -} - -// WHOOPS I NEVER FINISHED THIS ONE diff --git a/forge/tests/forge/other/test_native_solvers_glucose.frg b/forge/tests/forge/other/test_native_solvers_glucose.frg new file mode 100644 index 000000000..6538fb761 --- /dev/null +++ b/forge/tests/forge/other/test_native_solvers_glucose.frg @@ -0,0 +1,13 @@ +#lang forge +-- Smoke test: verify that all native JNI solvers load and solve correctly. +-- Uses Glucose; other solvers tested in companion files. + +option run_sterling off +option solver Glucose + +sig A {} + +test expect { + sat_glucose: {some A} is sat + unsat_glucose: {some A and no A} is unsat +} diff --git a/forge/tests/forge/other/test_native_solvers_minisat.frg b/forge/tests/forge/other/test_native_solvers_minisat.frg new file mode 100644 index 000000000..c5d31a9ae --- /dev/null +++ b/forge/tests/forge/other/test_native_solvers_minisat.frg @@ -0,0 +1,10 @@ +#lang forge +option run_sterling off +option solver MiniSat + +sig A {} + +test expect { + sat_minisat: {some A} is sat + unsat_minisat: {some A and no A} is unsat +} diff --git a/forge/tests/forge/bsl/intSumMinus.frg b/forge/tests/froglet/intSumMinus.frg similarity index 100% rename from forge/tests/forge/bsl/intSumMinus.frg rename to forge/tests/froglet/intSumMinus.frg diff --git a/forge/tests/forge/bsl/join-no-error.frg b/forge/tests/froglet/join-no-error.frg similarity index 100% rename from forge/tests/forge/bsl/join-no-error.frg rename to forge/tests/froglet/join-no-error.frg diff --git a/forge/tests/forge/bsl/reachable-no-error.frg b/forge/tests/froglet/reachable-no-error.frg similarity index 100% rename from forge/tests/forge/bsl/reachable-no-error.frg rename to forge/tests/froglet/reachable-no-error.frg diff --git a/forge/tests/forge/bsl/set-card.frg b/forge/tests/froglet/set-card.frg similarity index 100% rename from forge/tests/forge/bsl/set-card.frg rename to forge/tests/froglet/set-card.frg diff --git a/forge/tests/srclocs/main.rkt b/forge/tests/srclocs/main.rkt index e89d2776b..8793e2787 100644 --- a/forge/tests/srclocs/main.rkt +++ b/forge/tests/srclocs/main.rkt @@ -85,10 +85,9 @@ (string-contains? (path->string (srcloc-source loc)) sub-path-str)) - (unless source-path-correct + (unless source-path-correct (printf " Source path unexpected for:~n ~a~n~a~n" n loc)) - ; Comment out for now, avoid unpleasant flickering spam - ;(check-true source-path-correct) + (check-true source-path-correct) )) ;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;; @@ -153,7 +152,7 @@ (define pvdReachesEverything (reachesEverything Providence)) (check-true (node? pvdReachesEverything)) ;(printf "~npvdReachesEverything: ~a~n" (nodeinfo-loc (node-info pvdReachesEverything))) -(check-equal? 153 +(check-equal? 152 (srcloc-line (nodeinfo-loc (node-info pvdReachesEverything)))) @@ -161,5 +160,5 @@ (define twoHopsFromProvidence (twoHopsAway Providence)) (check-true (node? twoHopsFromProvidence)) ;(printf "~ntopHopsFromProvidence: ~a~n" (nodeinfo-loc (node-info twoHopsFromProvidence))) -(check-equal? 161 +(check-equal? 160 (srcloc-line (nodeinfo-loc (node-info twoHopsFromProvidence)))) diff --git a/forge/tests/forge/electrum/after-tests.frg b/forge/tests/temporal/after-tests.frg similarity index 100% rename from forge/tests/forge/electrum/after-tests.frg rename to forge/tests/temporal/after-tests.frg diff --git a/forge/tests/temporal/always-tests.frg b/forge/tests/temporal/always-tests.frg new file mode 100644 index 000000000..e14c3abfd --- /dev/null +++ b/forge/tests/temporal/always-tests.frg @@ -0,0 +1,140 @@ +#lang forge + +option run_sterling off +option verbose 0 +option problem_type temporal + +sig Node { + var edges : set Node +} + +-- State machine: edges empty -> edges full -> stays full +pred fillOnce { + no edges + always (no edges implies Node->Node in edges') + always (Node->Node in edges implies edges' = edges) +} + +test expect alwaysBaseCases { + -- always P requires P in the current state + alwaysImpliesNow: {always some edges and no edges} is unsat + -- always P requires P in the next state + alwaysImpliesNext: {always some edges and next_state no edges} is unsat + -- always P holds when P is true in every state + alwaysTrivial: {always Node = Node} is sat + -- always false is unsat (current state violates) + alwaysFalseIsUnsat: {always false} is unsat + -- always true is trivially sat + alwaysTrueIsSat: {always true} is sat +} + +var sig Token {} + +pred tokenSwitch { + -- Token alternates between empty and nonempty each step + no Token implies some Token' + some Token implies no Token' + always (no Token implies some Token') + always (some Token implies no Token') +} + +test expect alwaysVsNextStateAlways { + -- always includes the current state; next_state always does not + alwaysIncludesCurrent: { + tokenSwitch + some Token + always some Token + } is unsat -- fails because next state has no Token + + nextStateAlwaysExcludesCurrent: { + tokenSwitch + no Token + next_state always some Token + } is unsat -- fails because Token alternates + + -- If P holds now but not always, next_state always P can still fail + alwaysStrongerThanNow: { + tokenSwitch + some Token + not always some Token + } is sat +} + +test expect alwaysWithNegation { + -- not always P means P fails at some point + notAlwaysMeansSomeFail: { + fillOnce + not always (no edges) + } is sat -- edges become full eventually + + -- not always P is equivalent to eventually not P + notAlwaysIsEventuallyNot: { + not always some Token + not (eventually no Token) + } is unsat + + eventuallyNotIsNotAlways: { + eventually no Token + not (not always some Token) + } is unsat +} + +test expect alwaysNesting { + -- always always P is equivalent to always P + doubleAlwaysForward: { + always always (Node = Node) + not always (Node = Node) + } is unsat + + doubleAlwaysBackward: { + always (Node = Node) + not always always (Node = Node) + } is unsat +} + +test expect alwaysEventuallyDuality { + -- always P iff not eventually not P + alwaysImpliesNotEventuallyNot: { + always some Token + eventually no Token + } is unsat + + notEventuallyNotImpliesAlways: { + not eventually no Token + not always some Token + } is unsat + + -- eventually P iff not always not P + eventuallyImpliesNotAlwaysNot: { + eventually some Token + always no Token + } is unsat + + notAlwaysNotImpliesEventually: { + not always no Token + not eventually some Token + } is unsat +} + +test expect alwaysWithTransitions { + -- fillOnce: edges start empty, become full, stay full + -- so "always (Node->Node in edges)" is false (fails in initial state) + notAlwaysFullFromStart: { + some Node + fillOnce + always (Node->Node in edges) + } is unsat + + -- but next_state always full should hold + alwaysFullAfterFill: { + some Node + fillOnce + next_state always (Node->Node in edges) + } is sat + + alwaysFullAfterFillMust: { + some Node + fillOnce + not (next_state always (Node->Node in edges)) + } is unsat +} diff --git a/forge/tests/forge/electrum/always-true.frg b/forge/tests/temporal/always-true.frg similarity index 100% rename from forge/tests/forge/electrum/always-true.frg rename to forge/tests/temporal/always-true.frg diff --git a/forge/tests/forge/electrum/basic-temporal-example.frg b/forge/tests/temporal/basic-temporal-example.frg similarity index 100% rename from forge/tests/forge/electrum/basic-temporal-example.frg rename to forge/tests/temporal/basic-temporal-example.frg diff --git a/forge/tests/forge/electrum/cities.frg b/forge/tests/temporal/cities.frg similarity index 100% rename from forge/tests/forge/electrum/cities.frg rename to forge/tests/temporal/cities.frg diff --git a/forge/tests/forge/electrum/eventually-tests.frg b/forge/tests/temporal/eventually-tests.frg similarity index 100% rename from forge/tests/forge/electrum/eventually-tests.frg rename to forge/tests/temporal/eventually-tests.frg diff --git a/forge/tests/forge/electrum/example-light-puzzle.frg b/forge/tests/temporal/example-light-puzzle.frg similarity index 100% rename from forge/tests/forge/electrum/example-light-puzzle.frg rename to forge/tests/temporal/example-light-puzzle.frg diff --git a/forge/tests/forge/electrum/non-var-can-stop-var-changes.frg b/forge/tests/temporal/non-var-can-stop-var-changes.frg similarity index 100% rename from forge/tests/forge/electrum/non-var-can-stop-var-changes.frg rename to forge/tests/temporal/non-var-can-stop-var-changes.frg diff --git a/forge/tests/forge/electrum/past-time.frg b/forge/tests/temporal/past-time.frg similarity index 100% rename from forge/tests/forge/electrum/past-time.frg rename to forge/tests/temporal/past-time.frg diff --git a/forge/tests/forge/electrum/prime-check.frg b/forge/tests/temporal/prime-check.frg similarity index 100% rename from forge/tests/forge/electrum/prime-check.frg rename to forge/tests/temporal/prime-check.frg diff --git a/forge/tests/forge/electrum/social-network.frg b/forge/tests/temporal/social-network.frg similarity index 100% rename from forge/tests/forge/electrum/social-network.frg rename to forge/tests/temporal/social-network.frg diff --git a/forge/tests/temporal/temporal_lang.frg b/forge/tests/temporal/temporal_lang.frg index f901f2350..4d9e2a1d2 100644 --- a/forge/tests/temporal/temporal_lang.frg +++ b/forge/tests/temporal/temporal_lang.frg @@ -2,7 +2,6 @@ option verbose 0 -- Confirm that forge/temporal activates temporal solver; DO NOT activate it manually in this test. --- (Most forge/temporal tests are under the "tests/forge/electrum" test directory for historical reasons.) sig Node { var edges: set Node diff --git a/forge/tests/forge/electrum/traffic-light.frg b/forge/tests/temporal/traffic-light.frg similarity index 100% rename from forge/tests/forge/electrum/traffic-light.frg rename to forge/tests/temporal/traffic-light.frg diff --git a/forge/tests/forge/electrum/var-can-force-var-changes.frg b/forge/tests/temporal/var-can-force-var-changes.frg similarity index 100% rename from forge/tests/forge/electrum/var-can-force-var-changes.frg rename to forge/tests/temporal/var-can-force-var-changes.frg diff --git a/forge/tests/forge/electrum/var-vs-non-var-rels.frg b/forge/tests/temporal/var-vs-non-var-rels.frg similarity index 100% rename from forge/tests/forge/electrum/var-vs-non-var-rels.frg rename to forge/tests/temporal/var-vs-non-var-rels.frg diff --git a/forge/tests/forge/electrum/var-vs-non-var-sigs.frg b/forge/tests/temporal/var-vs-non-var-sigs.frg similarity index 100% rename from forge/tests/forge/electrum/var-vs-non-var-sigs.frg rename to forge/tests/temporal/var-vs-non-var-sigs.frg