From 89fa6bf834e63769ff4e95e38da3588fc4453bb8 Mon Sep 17 00:00:00 2001 From: dhruvja Date: Wed, 2 Sep 2026 21:43:38 +0530 Subject: [PATCH] test: add RPS settlement stress workflow --- .github/workflows/diagnose-rps-settlement.yml | 126 ++++++++++++++++++ scripts/test-locally.sh | 50 +++++-- 2 files changed, 165 insertions(+), 11 deletions(-) create mode 100644 .github/workflows/diagnose-rps-settlement.yml diff --git a/.github/workflows/diagnose-rps-settlement.yml b/.github/workflows/diagnose-rps-settlement.yml new file mode 100644 index 0000000..efed3f2 --- /dev/null +++ b/.github/workflows/diagnose-rps-settlement.yml @@ -0,0 +1,126 @@ +name: Diagnose RPS Settlement + +on: + pull_request: + paths: + - .github/workflows/diagnose-rps-settlement.yml + workflow_dispatch: + inputs: + runs_per_stack: + description: RPS runs against each fresh MagicBlock stack + required: true + default: "5" + type: choice + options: + - "3" + - "5" + - "10" + +permissions: + contents: read + +jobs: + stress-rps: + name: stack ${{ matrix.stack }} (${{ inputs.runs_per_stack || '3' }} runs) + runs-on: ubuntu-latest + timeout-minutes: 45 + strategy: + fail-fast: false + matrix: + stack: [1, 2, 3] + + steps: + - uses: actions/checkout@v4 + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: "20.20" + + - name: Enable Corepack + run: | + corepack enable + corepack prepare yarn@1.22.19 --activate + corepack prepare yarn@1.22.22 --activate + + - name: Setup Rust + uses: dtolnay/rust-toolchain@stable + with: + toolchain: "1.89.0" + + - name: Ensure stable Rust for Anchor IDL + run: | + rustup toolchain install stable --profile minimal + rustup default 1.89.0 + + - name: Install native dependencies + run: | + sudo apt-get update + sudo apt-get install -y libudev-dev pkg-config + + - name: Install Solana + run: | + sh -c "$(curl -sSfL https://release.anza.xyz/v3.1.9/install)" + echo "$HOME/.local/share/solana/install/active_release/bin" >> "$GITHUB_PATH" + + - name: Install Anchor + run: cargo install --git https://github.com/solana-foundation/anchor --tag v1.0.2 anchor-cli + + - name: Install MagicBlock Ephemeral Validator + run: npm install -g @magicblock-labs/ephemeral-validator@latest + + - name: Record tool versions + run: | + mkdir -p "rps-diagnostics/stack-${{ matrix.stack }}" + { + node --version + rustc --version + solana --version + anchor --version + npm list -g --depth=0 @magicblock-labs/ephemeral-validator + mb-stack --version || true + ephemeral-validator --version || true + } | tee "rps-diagnostics/stack-${{ matrix.stack }}/versions.log" + + - name: Configure Solana + env: + DEVNET_KEYPAIR_JSON: ${{ secrets.DEVNET_KEYPAIR_JSON }} + run: | + solana config set --url localhost + mkdir -p ~/.config/solana + if [ -n "$DEVNET_KEYPAIR_JSON" ]; then + printf '%s' "$DEVNET_KEYPAIR_JSON" > ~/.config/solana/id.json + chmod 600 ~/.config/solana/id.json + else + solana-keygen new --no-bip39-passphrase --silent --outfile ~/.config/solana/id.json + fi + + - name: Run repeated RPS settlement tests + id: stress + continue-on-error: true + env: + FAIL_FAST: "0" + TEST_REPETITIONS: ${{ inputs.runs_per_stack || '3' }} + TEST_LOG_DIR: ${{ github.workspace }}/rps-diagnostics/stack-${{ matrix.stack }} + run: | + sudo sysctl -w fs.inotify.max_user_instances=1280 + sudo sysctl -w fs.inotify.max_user_watches=655360 + sudo sysctl -w fs.file-max=2097152 + sudo prlimit --pid $$ --nofile=1000000:1000000 + bash scripts/test-example.sh rock-paper-scissor + + - name: Upload diagnostic logs + if: always() + uses: actions/upload-artifact@v4 + with: + name: rps-settlement-stack-${{ matrix.stack }} + if-no-files-found: error + retention-days: 14 + path: | + rps-diagnostics/stack-${{ matrix.stack }} + mb-stack.log + /tmp/build_rock-paper-scissor.log + + - name: Fail when any repetition failed + if: steps.stress.outcome == 'failure' + run: exit 1 diff --git a/scripts/test-locally.sh b/scripts/test-locally.sh index 3ccf8cb..b33ca13 100755 --- a/scripts/test-locally.sh +++ b/scripts/test-locally.sh @@ -31,6 +31,10 @@ cd "$REPO_ROOT" # SKIP_TEE_TESTS=1 skips devnet TEE tests # FAIL_FAST=0 keep running after a test fails (default: stop on the # first failure and exit non-zero — fail fast for CI) +# TEST_REPETITIONS=N run each selected test N times against the same stack +# (default: 1). Useful for reproducing stateful flakes. +# TEST_LOG_DIR=path retain full per-run test logs in this directory +# (default: /tmp). # SETUP_ONLY=1 start the validators/oracles, then keep them running # until a key is pressed (no tests). Useful for poking at # the local cluster by hand. @@ -39,11 +43,19 @@ SKIP_TEE_TESTS="${SKIP_TEE_TESTS:-0}" SKIP_REGULAR_TESTS="${SKIP_REGULAR_TESTS:-0}" SKIP_VRF_TESTS="${SKIP_VRF_TESTS:-0}" FAIL_FAST="${FAIL_FAST:-1}" +TEST_REPETITIONS="${TEST_REPETITIONS:-1}" +TEST_LOG_DIR="${TEST_LOG_DIR:-/tmp}" # EXACT_MATCH=1 turns TEST_FILTER into an exact project-name match instead of the # default substring match. Used by scripts/test-example.sh to select a single # example without over-selecting siblings (e.g. roll-dice vs pinocchio-roll-dice). EXACT_MATCH="${EXACT_MATCH:-0}" +if ! [[ "$TEST_REPETITIONS" =~ ^[1-9][0-9]*$ ]]; then + echo "TEST_REPETITIONS must be a positive integer (got '$TEST_REPETITIONS')." + exit 2 +fi +mkdir -p "$TEST_LOG_DIR" + if [ -n "$TEST_FILTER" ]; then echo "Filter: only running tests matching '$TEST_FILTER'" echo "" @@ -86,13 +98,21 @@ matches_filter() { # just runs `yarn test:local`. run_test() { local test_name=$1 + local repetition=${2:-1} + local result_name="$test_name" + if [ "$TEST_REPETITIONS" -gt 1 ]; then + result_name="$test_name [$repetition/$TEST_REPETITIONS]" + fi local test_dir test_dir="$(project_dir "$test_name")" if [ -z "$test_dir" ]; then echo "Unknown project path for '$test_name'" return 1 fi - local test_log="/tmp/test_${test_name}.log" + local test_log="$TEST_LOG_DIR/test_${test_name}.log" + if [ "$TEST_REPETITIONS" -gt 1 ]; then + test_log="$TEST_LOG_DIR/test_${test_name}_run-${repetition}.log" + fi local test_command="cd \"$test_dir\" && yarn test:local" # TEE examples reach the ER through the QFS, so they read TEE_PROVIDER_ENDPOINT/ @@ -113,7 +133,7 @@ run_test() { echo "" echo "========================================" - echo "Testing: $TEST_COUNT. $test_name" + echo "Testing: $TEST_COUNT. $result_name" echo "========================================" # Program ID is best-effort: scans the project's target/deploy for the first keypair. local program_id="(unknown)" @@ -314,7 +334,7 @@ run_test() { # Classify based on `test_failed` (computed from exit code + log grep above). if [ "$test_failed" = true ]; then - FAILED_TESTS+=("$test_name") + FAILED_TESTS+=("$result_name") # Extract details from the most informative source available. local error_details="" @@ -336,15 +356,15 @@ run_test() { error_details="(exit code $test_exit_code — see $test_log for full output)" fi - FAILED_TESTS_NAMES+=("$test_name") + FAILED_TESTS_NAMES+=("$result_name") FAILED_TESTS_ERRORS+=("$error_details") else - PASSED_TESTS+=("$test_name") + PASSED_TESTS+=("$result_name") fi # Print result echo "" - if [[ " ${FAILED_TESTS[@]} " =~ " ${test_name} " ]]; then + if [ "$test_failed" = true ]; then echo "Result: ✗ FAILED" else echo "Result: ✓ PASSED" @@ -357,16 +377,24 @@ run_test() { # here too so a fail-fast abort still surfaces why we stopped. Exiting triggers # cleanup() (EXIT trap), which stops the validators and propagates code 1. if [ "$test_failed" = true ] && [ "$FAIL_FAST" != "0" ]; then - echo "FAIL_FAST: stopping after first failure ($test_name)." + echo "FAIL_FAST: stopping after first failure ($result_name)." echo " (set FAIL_FAST=0 to run the remaining tests anyway)" echo "" - echo "--- $test_name ---" + echo "--- $result_name ---" echo "$error_details" echo "" exit 1 fi } +run_test_repetitions() { + local test_name=$1 + local repetition + for ((repetition=1; repetition<=TEST_REPETITIONS; repetition++)); do + run_test "$test_name" "$repetition" + done +} + # Cleanup function cleanup() { # Capture the status that triggered this trap *before* running any cleanup @@ -854,7 +882,7 @@ else # Each project's `test:local` runs only the local subset of its tests (skipping # router/TEE/devnet variants). oncurve-delegation is omitted pending an SDK update. for project in "${REGULAR_PROJECTS[@]}"; do - run_test "$project" + run_test_repetitions "$project" done fi @@ -867,7 +895,7 @@ else # VRF integration: roll-dice's delegated test reads VALIDATOR → defaults to the # local-ER validator since EPHEMERAL_PROVIDER_ENDPOINT is localhost. for project in "${VRF_PROJECTS[@]}"; do - run_test "$project" + run_test_repetitions "$project" done fi @@ -880,7 +908,7 @@ else # TEE examples reach the ER through the QFS — run_test exposes TEE_PROVIDER_ENDPOINT/ # TEE_WS_ENDPOINT to these runs (see the TEE_PROJECTS case in run_test). for project in "${TEE_PROJECTS[@]}"; do - run_test "$project" + run_test_repetitions "$project" done fi