diff --git a/.github/scripts/test_shard.sh b/.github/scripts/test_shard.sh new file mode 100755 index 000000000..c059988a1 --- /dev/null +++ b/.github/scripts/test_shard.sh @@ -0,0 +1,39 @@ +#!/usr/bin/env bash +# Run one slice of the test suite: test_shard.sh . +# +# Every *_test.dart under test/ belongs to exactly one slice (its position in +# the sorted list, modulo ), so the slices together are the whole suite. +# Inside a slice the files still run one at a time (--concurrency=1): the suite +# uses sqflite_common_ffi against real database files and parallel workers on +# ONE machine race on them. Separate CI jobs are separate machines. +# +# ponytail: slices are balanced by file COUNT, not by duration. If one slice +# becomes the slow one, split by recorded timings instead. +set -euo pipefail + +index="$1" +total="$2" + +# Listed into a variable first, so a failed `find` or `sort` fails the slice +# (set -e, pipefail). Feeding the loop straight from the pipeline would let a +# half-read test tree pass with some of its files never run. +all="$(find test -name '*_test.dart' | LC_ALL=C sort)" + +files=() +i=0 +while IFS= read -r f; do + if [ $((i % total)) -eq "$index" ]; then files+=("$f"); fi + i=$((i + 1)) +done <<< "$all" + +if [ "${#files[@]}" -eq 0 ]; then + echo "slice $index of $total holds no test files" >&2 + exit 1 +fi + +echo "slice $index of $total: ${#files[@]} of $i test files" +if [ "${TEST_SHARD_LIST_ONLY:-}" = 1 ]; then + printf '%s\n' "${files[@]}" + exit 0 +fi +flutter test --concurrency=1 --reporter=expanded "${files[@]}" diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml index 64385d7c9..e48ae940e 100644 --- a/.github/workflows/test.yml +++ b/.github/workflows/test.yml @@ -16,7 +16,9 @@ permissions: contents: read jobs: - test: + # Analyze, the lock guard and the zone-dependent tests: everything that is + # not the main suite. + checks: runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 @@ -68,17 +70,8 @@ jobs: - name: Analyze run: flutter analyze - # --concurrency=1: the suite uses sqflite_common_ffi against real database - # files, and parallel workers race on them. - # - # The golden capture (whoop_hist.jsonl) is a real band recording kept - # beside the repo rather than committed to it, so the two derivation-replay - # tests SKIP here and run locally. - - name: Test - run: flutter test --concurrency=1 --reporter=expanded - # The WHOOP freeze goldens are per timezone (day labels are local), and - # the run above is UTC, so it only enforces the *_utc files and the other + # the main suite runs in UTC, so it only enforces the *_utc files and the other # two skip. Re-run that one file under each committed offset. The Mi # scale clock test reads the process zone and is a no-op under UTC, so it # rides along. @@ -86,3 +79,49 @@ jobs: run: | TZ=Asia/Kolkata flutter test --concurrency=1 test/whoop_freeze_golden_test.dart test/adapters/miscale_test.dart TZ=America/New_York flutter test --concurrency=1 test/whoop_freeze_golden_test.dart test/adapters/miscale_test.dart + + # The main suite, split across machines. It ran as ONE job for about 75 + # minutes, and a newer push to main cancels the run in progress + # (`cancel-in-progress` above), so runs on main were cancelled about as often + # as they finished. Each slice still runs its files one at a time; see + # .github/scripts/test_shard.sh. + # + # The golden capture (whoop_hist.jsonl) is a real band recording kept beside + # the repo rather than committed to it, so the two derivation-replay tests + # SKIP here and run locally. + suite: + runs-on: ubuntu-latest + strategy: + # One failing slice must not hide failures in the others. + fail-fast: false + matrix: + shard: [0, 1, 2, 3, 4, 5] + steps: + - uses: actions/checkout@v4 + with: + persist-credentials: false + + # Same pin as `checks`; see the note there. + - uses: subosito/flutter-action@v2 + with: + flutter-version: 3.41.6 + channel: stable + cache: true + + - name: Install dependencies + run: flutter pub get + + - name: Test (slice ${{ matrix.shard }} of 6) + run: bash .github/scripts/test_shard.sh ${{ matrix.shard }} 6 + + # One check with the old job's name, so anything that waits for `test` + # (branch protection, a badge) keeps working. Fails unless every job above + # passed. + test: + if: always() + needs: [checks, suite] + runs-on: ubuntu-latest + steps: + - name: All checks and slices passed + run: | + [ "${{ needs.checks.result }}" = success ] && [ "${{ needs.suite.result }}" = success ]