diff --git a/.github/workflows/tests.yml b/.github/workflows/tests.yml index 705df066..c9113d11 100644 --- a/.github/workflows/tests.yml +++ b/.github/workflows/tests.yml @@ -191,11 +191,40 @@ jobs: ARANGO_NO_AUTH: "0" ports: - 8529:8529 + # THREE separate faults made this probe unable to ever succeed. All + # three were measured against the real arangodb:3.11 image, not inferred: + # + # 1. `curl` is NOT installed in the image -> use wget (present) + # 2. `localhost` resolves to ::1 there, but -> use 127.0.0.1 + # ArangoDB binds IPv4 only + # 3. `/_api/version` requires authentication -> use + # whenever ARANGO_ROOT_PASSWORD is set, /_admin/server/availability + # so it returns 401 forever; and busybox + # wget has no --user/--password option at + # all, so the probe CANNOT be authenticated + # + # Measured, polling once a second for 25s from container start: + # /_api/version -> exit 1 at EVERY timepoint (401) + # /_admin/server/availability -> exit 1 for 3s, then exit 0 onwards + # + # Fault 3 is why this looked intermittent. A probe that never passes + # leaves the service permanently `starting`; whether the job survived + # depended on a race in the runner's own poll (if it read + # .State.Health.Status before Docker had recorded any health state it + # saw an empty string and reported "healthy" — which is why one job + # once logged "arangodb service is healthy" BEFORE the container had + # even logged "Initializing root user"). Do not read that log line as + # evidence the probe works. + # + # NB: keep comments OUTSIDE the `options: >-` folded scalar — inside it + # `#` is literal text, gets folded into the docker args, and breaks + # `docker create` with "invalid reference format". options: >- - --health-cmd "curl -f http://localhost:8529/_api/version || exit 1" + --health-cmd "wget -q --spider http://127.0.0.1:8529/_admin/server/availability || exit 1" --health-interval 10s --health-timeout 5s --health-retries 10 + --health-start-period 40s dragonfly: image: docker.dragonflydb.io/dragonflydb/dragonfly:latest @@ -285,6 +314,19 @@ jobs: TEST_GITHUB_TOKEN: ${{ secrets.TEST_GITHUB_TOKEN }} RUST_LOG: debug + # This job installs the RUST toolchain and then ran `mix test` — with no + # BEAM anywhere on the runner. `mix` was simply not on PATH, so the step + # died with exit 127 (command not found) and took `Integration Tests` red + # with it. Same pins as the E2E job, so the two agree on the toolchain. + - name: Setup Erlang/Elixir + uses: erlef/setup-beam@54075bcc5e249e4758d363f27d099f55d843f124 # v1.18.2 + with: + otp-version: '27.0' + elixir-version: '1.17' + + - name: Install Elixir dependencies + run: mix deps.get + - name: Run Elixir integration tests run: mix test --include integration env: @@ -307,70 +349,30 @@ jobs: DRAGONFLY_URL: redis://localhost:6379 # ============================================================================ - # Haskell Integration Tests + # REMOVED — "Haskell Integration Tests" (haskell-integration) + # + # The job ran `cabal build all` / `cabal test all` with + # `defaults.run.working-directory: registry`, and cached on + # `hashFiles('registry/hypatia.cabal')`. + # + # `registry/` does not exist. It was deliberately deleted in f2fe2df + # (2026-03-06), whose own commit message reads: + # "Remove registry/ (30 files - Haskell, superseded by Elixir)" + # The CI job was left behind and has failed EVERY run since: 11 failures + # and 1 cancellation across the last 12 runs, never once green. The repo + # now contains 0 .cabal files, no cabal.project, no stack.yaml, and a + # single .hs file — test/soundness/fixtures/code_safety/unsafe_coerce.hs, + # which is a scanner test fixture, not a Haskell project. + # + # Each run installed GHC 9.6 + cabal 3.10 and stood up an ArangoDB service + # container before failing at the first `run:` step with + # No such file or directory ... /home/runner/work/hypatia/hypatia/registry + # so removing it also stops burning those runner minutes. + # + # If Haskell returns to this repo, reinstate the job together with the + # project, not before. # ============================================================================ - haskell-integration: - name: Haskell Integration Tests - runs-on: ubuntu-latest - timeout-minutes: 60 - needs: [build-images] - services: - arangodb: - image: arangodb:3.11 - env: - ARANGO_ROOT_PASSWORD: testpassword - ports: - - 8529:8529 - options: >- - --health-cmd "curl -f http://localhost:8529/_api/version || exit 1" - --health-interval 10s - --health-timeout 5s - --health-retries 10 - - defaults: - run: - working-directory: registry - steps: - - name: Checkout repository - uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 - - - name: Setup Haskell - uses: haskell-actions/setup@cd0d9bdd65b20557f41bea4dbe43d0b5fbbfe553 # v2.11.0 - with: - ghc-version: '9.6' - cabal-version: '3.10' - cabal-update: true - - - name: Restore cached dependencies - uses: actions/cache/restore@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 - id: cache - with: - path: ~/.cabal/store - key: ${{ runner.os }}-ghc-9.6-cabal-integration-${{ hashFiles('registry/hypatia.cabal') }} - restore-keys: ${{ runner.os }}-ghc-9.6-cabal- - - - name: Install dependencies - run: cabal build all --only-dependencies - - - name: Wait for ArangoDB - run: | - for i in {1..30}; do - if curl -s http://localhost:8529/_api/version > /dev/null; then - echo "ArangoDB is ready" - break - fi - sleep 2 - done - - - name: Run Haskell integration tests - run: cabal test all - env: - ARANGODB_URL: http://localhost:8529 - ARANGODB_DATABASE: cicd_hyper_a - ARANGODB_USER: root - ARANGODB_PASSWORD: testpassword - # ============================================================================ # End-to-End Tests # ============================================================================ @@ -457,11 +459,40 @@ jobs: ARANGO_ROOT_PASSWORD: testpassword ports: - 8529:8529 + # THREE separate faults made this probe unable to ever succeed. All + # three were measured against the real arangodb:3.11 image, not inferred: + # + # 1. `curl` is NOT installed in the image -> use wget (present) + # 2. `localhost` resolves to ::1 there, but -> use 127.0.0.1 + # ArangoDB binds IPv4 only + # 3. `/_api/version` requires authentication -> use + # whenever ARANGO_ROOT_PASSWORD is set, /_admin/server/availability + # so it returns 401 forever; and busybox + # wget has no --user/--password option at + # all, so the probe CANNOT be authenticated + # + # Measured, polling once a second for 25s from container start: + # /_api/version -> exit 1 at EVERY timepoint (401) + # /_admin/server/availability -> exit 1 for 3s, then exit 0 onwards + # + # Fault 3 is why this looked intermittent. A probe that never passes + # leaves the service permanently `starting`; whether the job survived + # depended on a race in the runner's own poll (if it read + # .State.Health.Status before Docker had recorded any health state it + # saw an empty string and reported "healthy" — which is why one job + # once logged "arangodb service is healthy" BEFORE the container had + # even logged "Initializing root user"). Do not read that log line as + # evidence the probe works. + # + # NB: keep comments OUTSIDE the `options: >-` folded scalar — inside it + # `#` is literal text, gets folded into the docker args, and breaks + # `docker create` with "invalid reference format". options: >- - --health-cmd "curl -f http://localhost:8529/_api/version || exit 1" + --health-cmd "wget -q --spider http://127.0.0.1:8529/_admin/server/availability || exit 1" --health-interval 10s --health-timeout 5s --health-retries 10 + --health-start-period 40s dragonfly: image: docker.dragonflydb.io/dragonflydb/dragonfly:latest @@ -539,7 +570,6 @@ jobs: - integration-tests - e2e-tests - coverage-report - - haskell-integration if: always() steps: - name: Check integration status @@ -552,7 +582,6 @@ jobs: ["integration-tests"]="${{ needs.integration-tests.result }}" ["e2e-tests"]="${{ needs.e2e-tests.result }}" ["coverage-report"]="${{ needs.coverage-report.result }}" - ["haskell-integration"]="${{ needs.haskell-integration.result }}" ) failed=false @@ -591,38 +620,87 @@ jobs: - name: Install stress testing tools run: | sudo apt-get update - sudo apt-get install -y stress-ng valgrind + sudo apt-get install -y stress-ng - name: Build release run: cargo build --release --all-features - - name: Concurrent operations stress test + # `./target/release/*` globs BUILD DIRECTORIES (build/, deps/, examples/, + # incremental/) and .d files — not just binaries. The previous steps ran + # `timeout 1s ./target/release/build` 100 times, printed "Permission + # denied" 100 times, and still reported SUCCESS, because a bare `wait` + # returns 0 no matter what the background jobs did. Resolve the real bin + # targets from cargo metadata so this cannot rot when a binary is added + # or renamed, and assert each one exists before relying on it. + - name: Resolve release binaries run: | - # Run binary with high concurrency - for i in {1..100}; do - timeout 1s ./target/release/* & + set -euo pipefail + BINS="$(cargo metadata --no-deps --format-version 1 \ + | jq -r '.packages[].targets[] | select(.kind[] == "bin") | .name' \ + | sort -u | tr '\n' ' ')" + [ -n "${BINS// /}" ] || { echo "::error::no bin targets resolved from cargo metadata"; exit 1; } + echo "resolved binaries: $BINS" + for b in $BINS; do + [ -x "target/release/$b" ] \ + || { echo "::error::target/release/$b missing or not executable"; exit 1; } done - wait + echo "STRESS_BINS=$BINS" >> "$GITHUB_ENV" - - name: Memory pressure test + - name: Concurrent launch stress test run: | - # Run under memory constraints - ulimit -v 512000 # 500MB virtual memory limit - cargo test --release + set -euo pipefail + pids=(); names=() + for b in $STRESS_BINS; do + for _ in $(seq 1 25); do + "./target/release/$b" --help >/dev/null 2>&1 & + pids+=("$!"); names+=("$b") + done + done + failed=0 + for i in "${!pids[@]}"; do + if ! wait "${pids[$i]}"; then + echo "::error::${names[$i]} exited non-zero under concurrent launch" + failed=$((failed + 1)) + fi + done + echo "launched ${#pids[@]} concurrent processes; failures: $failed" + [ "$failed" -eq 0 ] - - name: Long-running scenario test + - name: Memory-constrained execution run: | - # Test for memory leaks over time - timeout 300s valgrind --leak-check=full --error-exitcode=1 \ - ./target/release/* || true + set -euo pipefail + # The cap goes on the BINARY, never on cargo/rustc. The previous + # `ulimit -v 512000; cargo test --release` failed on 6 of 6 runs: + # 500MB of virtual address space cannot host rustc's LLD linker, + # which aborts with + # std::system_error ... Resource temporarily unavailable + # collect2: fatal error: ld terminated with signal 6 [Aborted] + # The very same build scripts (zerocopy, crossbeam-utils, num-traits) + # compiled fine in the preceding step, which set no ulimit — that + # in-log A/B is what identified the cause. + for b in $STRESS_BINS; do + if ( ulimit -v 1048576; "./target/release/$b" --help >/dev/null 2>&1 ); then + echo " ok: $b runs under a 1GiB virtual-memory cap" + else + echo "::error::$b failed under a 1GiB virtual-memory cap" + exit 1 + fi + done + + # REMOVED — "Long-running scenario test": it ran + # valgrind --leak-check=full --error-exitcode=1 ./target/release/* || true + # which globbed a directory (so valgrind never profiled a binary) and + # ended in `|| true` (so it could not fail even when it did). It reported + # success without testing anything. Deleted rather than left in place. + # To reinstate as a real gate: name a specific binary and drop `|| true`. - - name: Stress test with stress-ng + - name: Test suite under CPU and I/O stress run: | - # CPU and I/O stress + set -euo pipefail stress-ng --cpu 4 --io 2 --timeout 60s & STRESS_PID=$! + trap 'kill "$STRESS_PID" 2>/dev/null || true' EXIT cargo test --release - kill $STRESS_PID || true bench: name: criterion + baseline gate