Skip to content

Commit dc58acf

Browse files
stefan-burkeclaude
andauthored
Use an in-memory, shared-cache SQLite database for tests (#508)
The test suite ran on per-worker SQLite files. Under mutant's parallel kill-forks that shared one file, the database lock made results non-deterministic (a subject flaked between 5 and 9 surviving mutations across runs). Switch the test environment to `file::memory:?cache=shared`: - cache=shared lets every connection in a process (the test thread and Capybara's Puma server thread) see the same schema and data, so browser (js: true) tests keep working. - Shared cache is per-process, so parallel_test workers and mutant kill-forks are isolated automatically - no file, no lock. TEST_ENV_NUMBER is no longer needed to separate databases. - rails_helper loads db/schema.rb into the empty in-memory database at boot instead of the file-based maintain_test_schema!, and skips the parallel-worker reconnect that would drop the shared cache. - Drop the now-unnecessary db:migrate/parallel:prepare steps from the test and coverage workflows; the schema loads at boot. Verified: the full model/request/service/helper/lib/controller/view/seed suites and the feature suite (including a js browser test) pass, and mutant now returns identical results across repeated parallel runs. Claude-Session: https://claude.ai/code/session_01CLseCgvqE2jdan8K57ENJn Co-authored-by: Claude <noreply@anthropic.com>
1 parent c0cd30e commit dc58acf

5 files changed

Lines changed: 42 additions & 17 deletions

File tree

.github/workflows/coverage.yml

Lines changed: 0 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,8 +23,6 @@ jobs:
2323
RSPEC_JUNIT_FORMATTER: true
2424
RSPEC_JUNIT_OUTPUT: junit.xml
2525
run: |
26-
bundle exec rails db:migrate
27-
bundle exec rails parallel:prepare
2826
bundle exec rake coverage:parallel
2927
3028
- name: Upload coverage to Codecov

.github/workflows/test-ruby.yml

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -28,12 +28,9 @@ jobs:
2828
- name: Setup environment
2929
uses: ./.github/actions/setup-environment
3030

31-
- name: Prepare database
32-
env:
33-
RAILS_ENV: test
34-
run: |
35-
bundle exec rails db:migrate
36-
bundle exec rails parallel:prepare
31+
# No database prep: the test suite uses an in-memory SQLite database and
32+
# loads db/schema.rb into it at boot (see spec/rails_helper.rb), so there
33+
# is nothing to migrate or prepare on disk.
3734

3835
- name: Run tests
3936
env:

CLAUDE.md

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -79,14 +79,17 @@ This is useful for production environments where PDF generation is expensive. Se
7979
- **Run parallel tests with coverage**: `bundle exec rake coverage:parallel`
8080
- Run single test: `bundle exec rspec spec/path/to/file_spec.rb:LINE_NUMBER`
8181
- Run with verbose output: `bundle exec rspec --format documentation`
82-
- Prepare parallel test databases: `bundle exec rails parallel:prepare`
82+
- **Test database is in-memory**: the test env uses shared-cache in-memory
83+
SQLite and loads `db/schema.rb` at boot (see `spec/rails_helper.rb`), so
84+
there is no `parallel:prepare`/`db:migrate` step - each parallel worker and
85+
each mutant kill-fork gets its own isolated in-memory database
8386

8487
## Environment Notes
8588

8689
- **ripgrep (rg) is NOT installed** - use `grep` command instead of `rg` for searching
8790
- **Full test suite is SLOW** - only run `bundle exec rspec` when explicitly requested
8891
- Prefer running individual test files or specific tests during development
89-
- **Database locking**: If tests fail with "database is locked", just inform the user and wait for them to confirm it's unlocked
92+
- **Database locking**: The test database is in-memory (no file lock). If a run still reports "database is locked", inform the user and wait for them to confirm it's unlocked
9093
- **NEVER paste code into Rails console** - it never works. Instead write very specific RSpec tests
9194
- **Active Storage cleanup**: Test suite automatically cleans tmp/storage before and after test runs
9295

config/database.yml

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -23,9 +23,21 @@ development:
2323
# Warning: The database defined as "test" will be erased and
2424
# re-generated from your development database when you run "rake".
2525
# Do not set this db to the same as development or production.
26+
# In-memory, shared-cache SQLite. A URI database with cache=shared lets every
27+
# connection in a process (the test thread and Capybara's Puma server thread)
28+
# see the same schema and data, with no file on disk to lock under mutant's
29+
# parallel kill-forks. Shared cache is per-process, so parallel_test workers are
30+
# isolated automatically - TEST_ENV_NUMBER is no longer needed to separate them.
31+
# WAL/mmap pragmas are dropped because they are meaningless for an in-memory DB.
2632
test:
27-
<<: *default
28-
database: storage/test<%= ENV["TEST_ENV_NUMBER"] %>.sqlite3
33+
adapter: sqlite3
34+
database: "file::memory:?cache=shared"
35+
pool: <%= ENV.fetch("RAILS_MAX_THREADS") { 5 } %>
36+
timeout: 5000
37+
pragmas:
38+
journal_mode: memory
39+
synchronous: "off"
40+
busy_timeout: 5000
2941
# SQLite3 write its data on the local filesystem, as such it requires
3042
# persistent disks. If you are deploying to a managed service, you should
3143
# make sure it provides disk persistence, as many don't.

spec/rails_helper.rb

Lines changed: 20 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -26,10 +26,22 @@
2626

2727
Rails.root.glob("spec/support/**/*.rb").sort_by(&:to_s).each { |f| require f }
2828

29-
begin
30-
ActiveRecord::Migration.maintain_test_schema!
31-
rescue ActiveRecord::PendingMigrationError => e
32-
abort e.to_s.strip
29+
# An in-memory database starts empty on every boot, so load the schema
30+
# straight into the live connection (keeping it in the shared cache) rather
31+
# than the file-based maintain_test_schema! dance.
32+
def in_memory_database?
33+
ActiveRecord::Base.connection_db_config.database.to_s.include?(":memory:")
34+
end
35+
36+
if in_memory_database?
37+
ActiveRecord::Schema.verbose = false
38+
load Rails.root.join("db/schema.rb")
39+
else
40+
begin
41+
ActiveRecord::Migration.maintain_test_schema!
42+
rescue ActiveRecord::PendingMigrationError => e
43+
abort e.to_s.strip
44+
end
3345
end
3446

3547
# Configure ActiveStorage for test environment
@@ -68,7 +80,10 @@
6880

6981
config.before(:suite) do
7082
DatabaseCleaner.clean_with(:truncation)
71-
if ENV["TEST_ENV_NUMBER"]
83+
# Re-establishing the connection for a file-based parallel worker is
84+
# harmless, but for an in-memory DB it would drop the shared cache (and
85+
# the schema loaded above) - each worker already has its own in-memory DB.
86+
if ENV["TEST_ENV_NUMBER"] && !in_memory_database?
7287
ActiveRecord::Base.establish_connection(
7388
ActiveRecord::Base.configurations.configs_for(env_name: Rails.env).first
7489
)

0 commit comments

Comments
 (0)