From 44f88e2b25c1f26c1ad7a94b8e6932693dfc03fc Mon Sep 17 00:00:00 2001 From: maebeale Date: Sat, 6 Jun 2026 14:23:26 -0400 Subject: [PATCH] Parallelize the test suite in CI with parallel_tests The CI build-and-test job ran the full RSpec suite in a single process, making it the wall-clock bottleneck (~8 min). GitHub's ubuntu-latest runners have 4 vCPUs that sat mostly idle. Split the suite across 4 parallel_tests processes, each with its own database, to use them. - Add parallel_tests; append TEST_ENV_NUMBER to the test DB name so each process gets an isolated database. - CI now runs `parallel:create parallel:load_schema` then `parallel_rspec`. - Each process writes its own JSON results file (a single --out would clobber). - Move SimpleCov behind a COVERAGE flag set only by the coverage-badge workflow on main. It no longer runs on PRs, removing branch-coverage overhead and avoiding cross-process result-merge complexity under parallel. Co-Authored-By: Claude Opus 4.8 --- .github/workflows/ci.yml | 13 +++++++++---- .github/workflows/sanity-check-main.yml | 2 +- AGENTS.md | 13 +++++++++++-- Gemfile | 1 + Gemfile.lock | 4 ++++ config/database.yml | 4 +++- spec/spec_helper.rb | 14 +++++++++++++- 7 files changed, 42 insertions(+), 9 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index e57a1eeb1..bbbb9600d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -29,6 +29,11 @@ jobs: build-and-test: runs-on: ubuntu-latest + # GitHub-hosted ubuntu-latest runners have 4 vCPUs; split the suite across + # 4 parallel_tests processes (each with its own database) to cut wall time. + env: + PARALLEL_TEST_PROCESSORS: 4 + services: mysql: image: mysql:8.0 @@ -54,9 +59,9 @@ jobs: node-version: 22 - run: npm ci - - name: Setup database + - name: Setup databases run: | - bundle exec rake db:create db:schema:load + bundle exec rake parallel:create parallel:load_schema env: RAILS_ENV: test # AWS credentials @@ -71,7 +76,7 @@ jobs: SMTP_PASSWORD: ${{ secrets.SMTP_PASSWORD }} - name: Run tests and publish results - run: bundle exec rspec --format progress --format json --out tmp/rspec_results.json + run: bundle exec parallel_rspec spec/ env: RAILS_ENV: test # AWS credentials @@ -91,4 +96,4 @@ jobs: if: always() with: name: rspec-results - path: tmp/rspec_results.json \ No newline at end of file + path: tmp/rspec_results/ \ No newline at end of file diff --git a/.github/workflows/sanity-check-main.yml b/.github/workflows/sanity-check-main.yml index 441223eea..f9aa8a928 100644 --- a/.github/workflows/sanity-check-main.yml +++ b/.github/workflows/sanity-check-main.yml @@ -53,7 +53,7 @@ jobs: - name: Run RSpec (SimpleCov JSON + summary via at_exit) env: RAILS_ENV: test - CI: "true" + COVERAGE: "true" run: bundle exec rspec - name: Generate coverage badge diff --git a/AGENTS.md b/AGENTS.md index 2d8088b91..1d72aece9 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -326,7 +326,16 @@ Custom colors defined in `app/frontend/stylesheets/application.tailwind.css`: ### Configuration - **rails_helper.rb**: Loads RSpec Rails, FactoryBot, Shoulda Matchers, ActionPolicy, Devise test helpers, ActiveStorage validation matchers. Transactional fixtures enabled. ActiveJob uses `:test` adapter. -- **spec_helper.rb**: SimpleCov with branch coverage (minimum 20%), random test ordering, profile of 10 slowest examples. +- **spec_helper.rb**: Random test ordering, profile of 10 slowest examples. SimpleCov (branch coverage, minimum 20%) runs only when `COVERAGE=true` (set by the coverage-badge workflow on `main`), not on every run. Under `CI=true`, each parallel process writes its own JSON results file to `tmp/rspec_results/` keyed on `TEST_ENV_NUMBER`. + +### Running in parallel + +The suite runs across multiple processes via [`parallel_tests`](https://github.com/grosser/parallel_tests), each with its own database (`awbw_test`, `awbw_test2`, … — the `TEST_ENV_NUMBER` suffix is appended in `config/database.yml`): + +``` +bundle exec rake parallel:create parallel:load_schema # one-time per schema change +bundle exec parallel_rspec spec/ # run the whole suite in parallel +``` ### Support Files @@ -359,7 +368,7 @@ bundle exec bundle-audit check --update ### ci.yml 1. **scan_ruby**: Brakeman security analysis + bundler-audit -2. **build-and-test**: MySQL 8.0 service, Ruby + Node 22 setup, `npm ci`, schema load, `bundle exec rspec` +2. **build-and-test**: MySQL 8.0 service, Ruby + Node 22 setup, `npm ci`, `parallel:create parallel:load_schema`, then `parallel_rspec spec/` across 4 processes (`PARALLEL_TEST_PROCESSORS=4`) ### rubocop.yml diff --git a/Gemfile b/Gemfile index 265cf2ac3..73474e43e 100644 --- a/Gemfile +++ b/Gemfile @@ -92,6 +92,7 @@ group :development, :test do gem "pry-coolline" gem "pry-rails" gem "rspec-rails" + gem "parallel_tests" gem "simplecov", require: false gem "simplecov_json_formatter", require: false gem "selenium-webdriver" diff --git a/Gemfile.lock b/Gemfile.lock index 16d0e7d71..854d9f6bf 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -517,6 +517,8 @@ GEM orm_adapter (0.5.0) ostruct (0.6.3) parallel (1.27.0) + parallel_tests (5.7.0) + parallel parser (3.3.10.1) ast (~> 2.4.1) racc @@ -803,6 +805,7 @@ DEPENDENCIES opentelemetry-instrumentation-all opentelemetry-sdk ostruct + parallel_tests pay (~> 11.4) positioning (~> 0.4.7) premailer-rails @@ -1019,6 +1022,7 @@ CHECKSUMS orm_adapter (0.5.0) sha256=aa5d0be5d540cbb46d3a93e88061f4ece6a25f6e97d6a47122beb84fe595e9b9 ostruct (0.6.3) sha256=95a2ed4a4bd1d190784e666b47b2d3f078e4a9efda2fccf18f84ddc6538ed912 parallel (1.27.0) sha256=4ac151e1806b755fb4e2dc2332cbf0e54f2e24ba821ff2d3dcf86bf6dc4ae130 + parallel_tests (5.7.0) sha256=3f1762c46ca2c223b8af8ef877217f9d76974e191bfa934f2580b58bcf1d005c parser (3.3.10.1) sha256=06f6a725d2cd91e5e7f2b7c32ba143631e1f7c8ae2fb918fc4cebec187e6a688 pay (11.4.3) sha256=f3d50b1a900ab0a7bfe9ba9fda631b2f5faf0c95236dd3d2afa9effc9bfb15e8 polyglot (0.3.5) sha256=59d66ef5e3c166431c39cb8b7c1d02af419051352f27912f6a43981b3def16af diff --git a/config/database.yml b/config/database.yml index 1552268a4..98d40e896 100644 --- a/config/database.yml +++ b/config/database.yml @@ -41,7 +41,9 @@ development: # Do not set this db to the same as development or production. test: <<: *base - database: awbw_test<%= "_#{workspace_port}" if workspace_port %> + # TEST_ENV_NUMBER is set by parallel_tests to give each parallel process its + # own database (blank for the first process, "2", "3", ... for the rest). + database: awbw_test<%= "_#{workspace_port}" if workspace_port %><%= ENV['TEST_ENV_NUMBER'] %> staging: primary: &primary_staging diff --git a/spec/spec_helper.rb b/spec/spec_helper.rb index bcc1ac94d..71972a453 100644 --- a/spec/spec_helper.rb +++ b/spec/spec_helper.rb @@ -18,7 +18,7 @@ # See https://rubydoc.info/gems/rspec-core/RSpec/Core/Configuration require "active_storage_validations/matchers" -if ENV["CI"] +if ENV["COVERAGE"] require "simplecov" require "simplecov_json_formatter" SimpleCov.formatter = @@ -42,6 +42,18 @@ end end +# Under parallel_tests in CI, a single shared --out file would be clobbered by +# each process. Give every process its own results file, keyed on the +# TEST_ENV_NUMBER that parallel_tests assigns (blank for the first process). +if ENV["CI"] + require "fileutils" + FileUtils.mkdir_p("tmp/rspec_results") + RSpec.configure do |config| + config.add_formatter "progress" + config.add_formatter "json", "tmp/rspec_results/results#{ENV['TEST_ENV_NUMBER']}.json" + end +end + RSpec.configure do |config| # rspec-expectations config goes here. You can use an alternate # assertion/expectation library such as wrong or the stdlib/minitest