From 872d8937292f64462161b8d5f734b919c2648070 Mon Sep 17 00:00:00 2001 From: Alexander Kiselev Date: Fri, 6 Feb 2026 16:36:16 -0800 Subject: [PATCH] ci: add ghidra-setup job, unify project names for cache sharing All test files now use "ci-test" as the project name so they share a single import+analyze cycle. A dedicated ghidra-setup job runs first to install Ghidra and create the test project, seeding caches for the test jobs that follow via `needs:`. This solves the chicken-and-egg problem where Windows cold runs timeout before caches can be saved. Co-Authored-By: Claude Opus 4.6 --- .github/workflows/test.yml | 115 ++++++++++++++++++++++++++++++------- tests/comment_tests.rs | 2 +- tests/daemon_tests.rs | 2 +- tests/patch_tests.rs | 2 +- tests/readonly_tests.rs | 2 +- tests/reliability_tests.rs | 2 +- tests/script_tests.rs | 2 +- tests/symbol_tests.rs | 2 +- tests/type_tests.rs | 2 +- 9 files changed, 102 insertions(+), 29 deletions(-) diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 5af2289..7ba5666 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -44,11 +44,11 @@ jobs: - name: Run CLI tests run: cargo test --test e2e --test command_tests --test output_format_integration --verbose - # Job 2: Read-only integration tests (one bridge) - # First Windows run is slow (~60min) due to Ghidra headless JVM; cached runs ~15-20min - readonly-integration: + # Job 2: Ghidra setup - installs Ghidra and creates test projects to seed caches. + # This runs once per OS, then test jobs use the cached artifacts. + ghidra-setup: runs-on: ${{ matrix.os }} - timeout-minutes: 90 + timeout-minutes: 45 strategy: fail-fast: false matrix: @@ -70,6 +70,7 @@ jobs: uses: Swatinem/rust-cache@v2 - name: Cache Ghidra installation and config + id: ghidra-cache uses: actions/cache@v4 with: path: | @@ -82,13 +83,14 @@ jobs: key: ghidra-${{ matrix.os }}-v4 - name: Cache Ghidra projects + id: project-cache uses: actions/cache@v4 with: path: | ~/.cache/ghidra-cli/projects ~/Library/Caches/ghidra-cli/projects ~/AppData/Local/ghidra-cli/cache/projects - key: ghidra-projects-${{ github.job }}-${{ matrix.os }}-${{ hashFiles('tests/fixtures/sample_binary.rs') }} + key: ghidra-projects-${{ matrix.os }}-${{ hashFiles('tests/fixtures/sample_binary.rs') }} - name: Build run: cargo build --verbose @@ -106,15 +108,85 @@ jobs: fi shell: bash + - name: Create test project (skip if cached) + run: | + cargo run -- import tests/fixtures/sample_binary --project ci-test --program sample_binary || true + cargo run -- analyze --project ci-test --program sample_binary || true + shell: bash + timeout-minutes: 20 + + # Job 3: Read-only integration tests (one bridge) + readonly-integration: + needs: ghidra-setup + runs-on: ${{ matrix.os }} + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + os: [ubuntu-latest, macos-latest, windows-latest] + + steps: + - uses: actions/checkout@v4 + + - name: Install Java 21 + uses: actions/setup-java@v4 + with: + distribution: 'temurin' + java-version: '21' + + - name: Install Rust toolchain + uses: dtolnay/rust-toolchain@stable + + - name: Cache Rust + uses: Swatinem/rust-cache@v2 + + - name: Restore Ghidra installation and config + uses: actions/cache@v4 + with: + path: | + ~/.local/share/ghidra-cli + ~/.config/ghidra-cli + ~/Library/Application Support/ghidra-cli + ~/Library/Preferences/ghidra-cli + ~/AppData/Local/ghidra-cli + ~/AppData/Roaming/ghidra-cli + key: ghidra-${{ matrix.os }}-v4 + + - name: Restore Ghidra projects + uses: actions/cache@v4 + with: + path: | + ~/.cache/ghidra-cli/projects + ~/Library/Caches/ghidra-cli/projects + ~/AppData/Local/ghidra-cli/cache/projects + key: ghidra-projects-${{ matrix.os }}-${{ hashFiles('tests/fixtures/sample_binary.rs') }} + + - name: Build + run: cargo build --verbose + + - name: Build test fixture + run: rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs + + - name: Setup Ghidra (fallback if cache missed) + run: | + if cargo run -- doctor 2>&1 | grep -q "analyzeHeadless: OK"; then + echo "Ghidra already installed (cache hit)" + else + echo "Cache miss, running setup..." + cargo run -- setup + fi + shell: bash + - name: Run read-only integration tests run: cargo test --test readonly_tests --verbose env: RUST_LOG: info - # Job 3: Mutation tests (parallel with job 2) - ~10-15 min + # Job 4: Mutation tests (parallel with job 3) mutation-integration: + needs: ghidra-setup runs-on: ${{ matrix.os }} - timeout-minutes: 90 + timeout-minutes: 45 strategy: fail-fast: false matrix: @@ -135,7 +207,7 @@ jobs: - name: Cache Rust uses: Swatinem/rust-cache@v2 - - name: Cache Ghidra installation and config + - name: Restore Ghidra installation and config uses: actions/cache@v4 with: path: | @@ -147,14 +219,14 @@ jobs: ~/AppData/Roaming/ghidra-cli key: ghidra-${{ matrix.os }}-v4 - - name: Cache Ghidra projects + - name: Restore Ghidra projects uses: actions/cache@v4 with: path: | ~/.cache/ghidra-cli/projects ~/Library/Caches/ghidra-cli/projects ~/AppData/Local/ghidra-cli/cache/projects - key: ghidra-projects-${{ github.job }}-${{ matrix.os }}-${{ hashFiles('tests/fixtures/sample_binary.rs') }} + key: ghidra-projects-${{ matrix.os }}-${{ hashFiles('tests/fixtures/sample_binary.rs') }} - name: Build run: cargo build --verbose @@ -162,12 +234,12 @@ jobs: - name: Build test fixture run: rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs - - name: Setup Ghidra (skip if cached) + - name: Setup Ghidra (fallback if cache missed) run: | if cargo run -- doctor 2>&1 | grep -q "analyzeHeadless: OK"; then - echo "Ghidra already installed (cache hit), skipping setup" + echo "Ghidra already installed (cache hit)" else - echo "Ghidra not found, running setup..." + echo "Cache miss, running setup..." cargo run -- setup fi shell: bash @@ -177,10 +249,11 @@ jobs: env: RUST_LOG: info - # Job 4: Infrastructure tests (parallel with jobs 2+3) - ~8-10 min + # Job 5: Infrastructure tests (parallel with jobs 3+4) infrastructure: + needs: ghidra-setup runs-on: ${{ matrix.os }} - timeout-minutes: 90 + timeout-minutes: 45 strategy: fail-fast: false matrix: @@ -201,7 +274,7 @@ jobs: - name: Cache Rust uses: Swatinem/rust-cache@v2 - - name: Cache Ghidra installation and config + - name: Restore Ghidra installation and config uses: actions/cache@v4 with: path: | @@ -213,14 +286,14 @@ jobs: ~/AppData/Roaming/ghidra-cli key: ghidra-${{ matrix.os }}-v4 - - name: Cache Ghidra projects + - name: Restore Ghidra projects uses: actions/cache@v4 with: path: | ~/.cache/ghidra-cli/projects ~/Library/Caches/ghidra-cli/projects ~/AppData/Local/ghidra-cli/cache/projects - key: ghidra-projects-${{ github.job }}-${{ matrix.os }}-${{ hashFiles('tests/fixtures/sample_binary.rs') }} + key: ghidra-projects-${{ matrix.os }}-${{ hashFiles('tests/fixtures/sample_binary.rs') }} - name: Build run: cargo build --verbose @@ -228,12 +301,12 @@ jobs: - name: Build test fixture run: rustc --edition 2021 -o tests/fixtures/sample_binary tests/fixtures/sample_binary.rs - - name: Setup Ghidra (skip if cached) + - name: Setup Ghidra (fallback if cache missed) run: | if cargo run -- doctor 2>&1 | grep -q "analyzeHeadless: OK"; then - echo "Ghidra already installed (cache hit), skipping setup" + echo "Ghidra already installed (cache hit)" else - echo "Ghidra not found, running setup..." + echo "Cache miss, running setup..." cargo run -- setup fi shell: bash diff --git a/tests/comment_tests.rs b/tests/comment_tests.rs index fff76ec..e28f8d5 100644 --- a/tests/comment_tests.rs +++ b/tests/comment_tests.rs @@ -10,7 +10,7 @@ use common::{ ensure_test_project, get_function_address, get_function_addresses, DaemonTestHarness, }; -const TEST_PROJECT: &str = "comment-test"; +const TEST_PROJECT: &str = "ci-test"; const TEST_PROGRAM: &str = "sample_binary"; #[test] diff --git a/tests/daemon_tests.rs b/tests/daemon_tests.rs index 36d67ac..02027c7 100644 --- a/tests/daemon_tests.rs +++ b/tests/daemon_tests.rs @@ -8,7 +8,7 @@ use serial_test::serial; mod common; use common::{ensure_test_project, DaemonTestHarness}; -const TEST_PROJECT: &str = "daemon-test"; +const TEST_PROJECT: &str = "ci-test"; const TEST_PROGRAM: &str = "sample_binary"; /// Try to create a DaemonTestHarness. Returns None (and skips the test) if diff --git a/tests/patch_tests.rs b/tests/patch_tests.rs index 0ff5017..8880b84 100644 --- a/tests/patch_tests.rs +++ b/tests/patch_tests.rs @@ -12,7 +12,7 @@ use serial_test::serial; mod common; use common::{ensure_test_project, get_function_address, ghidra, DaemonTestHarness}; -const TEST_PROJECT: &str = "patch-test"; +const TEST_PROJECT: &str = "ci-test"; const TEST_PROGRAM: &str = "sample_binary"; /// Test patching bytes at a dynamically resolved address. diff --git a/tests/readonly_tests.rs b/tests/readonly_tests.rs index 8f6c843..5e0839c 100644 --- a/tests/readonly_tests.rs +++ b/tests/readonly_tests.rs @@ -17,7 +17,7 @@ use common::{ DaemonTestHarness, GhidraCommand, }; -const TEST_PROJECT: &str = "readonly-test"; +const TEST_PROJECT: &str = "ci-test"; const TEST_PROGRAM: &str = "sample_binary"; /// Known exported function names from sample_binary diff --git a/tests/reliability_tests.rs b/tests/reliability_tests.rs index 025d58b..7a451b8 100644 --- a/tests/reliability_tests.rs +++ b/tests/reliability_tests.rs @@ -7,7 +7,7 @@ use std::time::Duration; mod common; use common::{ensure_test_project, DaemonTestHarness}; -const TEST_PROJECT: &str = "reliability-test"; +const TEST_PROJECT: &str = "ci-test"; const TEST_PROGRAM: &str = "sample_binary"; /// Try to create a DaemonTestHarness. Returns None (and prints skip message) if diff --git a/tests/script_tests.rs b/tests/script_tests.rs index bdbb830..a42997a 100644 --- a/tests/script_tests.rs +++ b/tests/script_tests.rs @@ -9,7 +9,7 @@ use std::path::PathBuf; mod common; use common::{ensure_test_project, DaemonTestHarness}; -const TEST_PROJECT: &str = "script-test"; +const TEST_PROJECT: &str = "ci-test"; const TEST_PROGRAM: &str = "sample_binary"; fn get_test_script_path() -> PathBuf { diff --git a/tests/symbol_tests.rs b/tests/symbol_tests.rs index db39a47..dde95e4 100644 --- a/tests/symbol_tests.rs +++ b/tests/symbol_tests.rs @@ -10,7 +10,7 @@ use common::{ ensure_test_project, get_function_address, get_function_addresses, DaemonTestHarness, }; -const TEST_PROJECT: &str = "symbol-test"; +const TEST_PROJECT: &str = "ci-test"; const TEST_PROGRAM: &str = "sample_binary"; #[test] diff --git a/tests/type_tests.rs b/tests/type_tests.rs index ad550d8..30b746d 100644 --- a/tests/type_tests.rs +++ b/tests/type_tests.rs @@ -8,7 +8,7 @@ use serial_test::serial; mod common; use common::{ensure_test_project, get_function_address, DaemonTestHarness}; -const TEST_PROJECT: &str = "type-test"; +const TEST_PROJECT: &str = "ci-test"; const TEST_PROGRAM: &str = "sample_binary"; #[test]