diff --git a/justfile b/justfile index ecd141f..ab14801 100644 --- a/justfile +++ b/justfile @@ -1,29 +1,87 @@ set shell := ["bash", "-c"] +# How many jobs the parallel gate runs at once. Lower this when another +# worktree stack is active, or use 1 for a serial debugging run. +jobs := env('JUST_JOBS', '4') + default: @just --list -# Full completion gate. Start the worktree stack before running it because -# Cypress exercises the frontend and its backend wiring. +# Full completion gate. Every check runs in one bounded pool, with likely +# long Cypress groups scheduled first and shorter jobs filling free lanes. +# Start the worktree stack before running it because Cypress needs Vite. test-all: - @echo "==> frontend format + lint checks" - just frontend-format-check - just frontend-lint-check - @echo "==> frontend type check" - just frontend-type-check - @echo "==> backend static analysis" - just backend-types-check - @echo "==> frontend production build" - just frontend-build - @echo "==> backend tests" - just backend-test - @echo "==> frontend Cypress tests" - just frontend-cypress-run + just par "gate" {{ jobs }} \ + frontend-cypress-account frontend-cypress-session \ + frontend-cypress-sets backend-types-check \ + frontend-cypress-scheduling frontend-build \ + backend-test-feature frontend-cypress-today \ + frontend-lint-check frontend-type-check backend-test-unit \ + frontend-format-check cypress-spec-coverage + +# Run recipes concurrently, buffering each one's output to its own log. +# Report jobs as they finish, then print every failed job's complete log. +[private] +par label max +targets: + #!/usr/bin/env bash + set -uo pipefail + + targets=({{ targets }}) + logs=$(mktemp -d) + declare -A target_of started_at + failed=() + next=0 + running=0 + + echo "==> {{ label }} (${#targets[@]} jobs, up to {{ max }} at a time)" + + while (( next < ${#targets[@]} || running > 0 )); do + while (( next < ${#targets[@]} && running < {{ max }} )); do + target="${targets[next]}" + just "$target" > "$logs/$target.log" 2>&1 & + target_of[$!]="$target" + started_at[$!]=$SECONDS + next=$(( next + 1 )) + running=$(( running + 1 )) + done + + wait -n -p finished + status=$? + running=$(( running - 1 )) + target="${target_of[$finished]}" + elapsed=$(( SECONDS - started_at[$finished] )) + + if (( status == 0 )); then + printf ' ok %-28s %4ds\n' "$target" "$elapsed" + else + printf ' FAIL %-28s %4ds\n' "$target" "$elapsed" + failed+=("$target") + fi + done + + if (( ${#failed[@]} == 0 )); then + rm -rf "$logs" + exit 0 + fi + + for target in "${failed[@]}"; do + printf '\n--- %s ---\n' "$target" + cat "$logs/$target.log" + done + printf '\n{{ label }} failed: %s\n' "${failed[*]}" + printf 'logs kept in %s\n' "$logs" + exit 1 # Backend backend-test *args: - cd backend && php artisan test {{args}} + cd backend && php artisan test {{ args }} + +backend-test-unit *args: + cd backend && php artisan test --testsuite=Unit {{ args }} + +backend-test-feature *args: + cd backend && php artisan test --testsuite=Feature {{ args }} backend-types-check: cd backend && composer types:check @@ -53,3 +111,74 @@ frontend-build: frontend-cypress-run: cd frontend/website && npm run test:e2e + +# Each Cypress spec must belong to exactly one feature group. The complete +# gate runs these groups concurrently; the whole-suite recipe above remains +# available for focused iteration. +frontend_specs_account := "confirm-email login signup" +frontend_specs_session := "guest-auth session-auth" +frontend_specs_sets := "set-layout sets-dashboard" +frontend_specs_scheduling := "set-scheduling" +frontend_specs_today := "today-assignments" + +frontend-cypress-account: + just _cypress {{ frontend_specs_account }} + +frontend-cypress-session: + just _cypress {{ frontend_specs_session }} + +frontend-cypress-sets: + just _cypress {{ frontend_specs_sets }} + +frontend-cypress-scheduling: + just _cypress {{ frontend_specs_scheduling }} + +frontend-cypress-today: + just _cypress {{ frontend_specs_today }} + +[private] +_cypress +names: + cd frontend/website && specs=$(for name in {{ names }}; do \ + printf 'cypress/e2e/%s.cy.ts,' "$name"; done) && \ + npm run test:e2e -- --spec "${specs%,}" + +cypress-spec-coverage: + just _spec-coverage {{ frontend_specs_account }} \ + {{ frontend_specs_session }} {{ frontend_specs_sets }} \ + {{ frontend_specs_scheduling }} {{ frontend_specs_today }} + +[private] +_spec-coverage +names: + #!/usr/bin/env bash + set -uo pipefail + + cd frontend/website + listed=$(printf '%s\n' {{ names }} | sort) + on_disk=$(ls cypress/e2e/*.cy.ts | xargs -n1 basename \ + | sed 's/\.cy\.ts$//' | sort) + + duplicated=$(echo "$listed" | uniq -d) + unique=$(echo "$listed" | uniq) + ungrouped=$(comm -13 <(echo "$unique") <(echo "$on_disk")) + missing=$(comm -23 <(echo "$unique") <(echo "$on_disk")) + status=0 + + if [ -n "$ungrouped" ]; then + echo "in no group, so never runs in the gate:" + printf ' %s\n' $ungrouped + status=1 + fi + + if [ -n "$missing" ]; then + echo "listed in a group but not on disk:" + printf ' %s\n' $missing + status=1 + fi + + if [ -n "$duplicated" ]; then + echo "in more than one group, so runs twice:" + printf ' %s\n' $duplicated + status=1 + fi + + exit $status