mutineer-mongoid 2.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
- data/CHANGELOG.md +1194 -0
- data/LICENSE +21 -0
- data/README.md +659 -0
- data/bin/mutineer +6 -0
- data/lib/mutineer/baseline.rb +185 -0
- data/lib/mutineer/changed_lines.rb +136 -0
- data/lib/mutineer/child_stdout.rb +35 -0
- data/lib/mutineer/cli.rb +681 -0
- data/lib/mutineer/config.rb +372 -0
- data/lib/mutineer/coverage_map.rb +1366 -0
- data/lib/mutineer/daemon_backend.rb +351 -0
- data/lib/mutineer/daemon_client.rb +407 -0
- data/lib/mutineer/daemon_server.rb +351 -0
- data/lib/mutineer/external_backend.rb +288 -0
- data/lib/mutineer/file_swap.rb +194 -0
- data/lib/mutineer/isolation.rb +321 -0
- data/lib/mutineer/job_plan.rb +352 -0
- data/lib/mutineer/kill_channel.rb +284 -0
- data/lib/mutineer/kill_matrix.rb +88 -0
- data/lib/mutineer/minitest_integration/kill_recorder.rb +301 -0
- data/lib/mutineer/minitest_integration/outer_reporter.rb +126 -0
- data/lib/mutineer/minitest_integration/stop_at_first_failure.rb +145 -0
- data/lib/mutineer/minitest_integration.rb +137 -0
- data/lib/mutineer/mongoid_failures.rb +82 -0
- data/lib/mutineer/mongoid_worker_db.rb +97 -0
- data/lib/mutineer/mutant_id.rb +126 -0
- data/lib/mutineer/mutation.rb +27 -0
- data/lib/mutineer/mutator_registry.rb +109 -0
- data/lib/mutineer/mutators/arithmetic.rb +35 -0
- data/lib/mutineer/mutators/array_literal.rb +44 -0
- data/lib/mutineer/mutators/base.rb +105 -0
- data/lib/mutineer/mutators/boolean_connector.rb +52 -0
- data/lib/mutineer/mutators/boolean_literal.rb +56 -0
- data/lib/mutineer/mutators/chain_link.rb +132 -0
- data/lib/mutineer/mutators/collection_method.rb +44 -0
- data/lib/mutineer/mutators/comparison.rb +35 -0
- data/lib/mutineer/mutators/condition_forcing.rb +134 -0
- data/lib/mutineer/mutators/condition_negation.rb +49 -0
- data/lib/mutineer/mutators/literal_mutation.rb +51 -0
- data/lib/mutineer/mutators/negation_removal.rb +44 -0
- data/lib/mutineer/mutators/operand_removal.rb +58 -0
- data/lib/mutineer/mutators/operator_assignment.rb +113 -0
- data/lib/mutineer/mutators/range_literal.rb +54 -0
- data/lib/mutineer/mutators/regex_literal.rb +72 -0
- data/lib/mutineer/mutators/return_nil.rb +72 -0
- data/lib/mutineer/mutators/safe_navigation.rb +76 -0
- data/lib/mutineer/mutators/statement_removal.rb +33 -0
- data/lib/mutineer/mutators/string_literal.rb +34 -0
- data/lib/mutineer/orphan_guard.rb +28 -0
- data/lib/mutineer/pairing.rb +298 -0
- data/lib/mutineer/parser.rb +45 -0
- data/lib/mutineer/progress.rb +46 -0
- data/lib/mutineer/project.rb +506 -0
- data/lib/mutineer/project_path.rb +57 -0
- data/lib/mutineer/rails_worker_db.rb +212 -0
- data/lib/mutineer/reporter.rb +926 -0
- data/lib/mutineer/result.rb +240 -0
- data/lib/mutineer/runner.rb +418 -0
- data/lib/mutineer/statement_lines.rb +183 -0
- data/lib/mutineer/subject.rb +38 -0
- data/lib/mutineer/test_runners/minitest.rb +22 -0
- data/lib/mutineer/test_runners/rspec.rb +330 -0
- data/lib/mutineer/test_runners.rb +30 -0
- data/lib/mutineer/version.rb +6 -0
- data/lib/mutineer/worker_pool.rb +147 -0
- data/lib/mutineer.rb +48 -0
- metadata +143 -0
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "digest"
|
|
4
|
+
|
|
5
|
+
module Mutineer
|
|
6
|
+
# Per-worker database isolation for the daemon path.
|
|
7
|
+
#
|
|
8
|
+
# Loaded APP-SIDE by {DaemonServer} (a sibling gem file, pulled in by absolute
|
|
9
|
+
# path so it bypasses the app bundle, the same trick {DaemonClient} uses to run
|
|
10
|
+
# `daemon_server.rb` under a bundle that has no mutineer). It uses the app's
|
|
11
|
+
# OWN already-booted ActiveRecord and NEVER `require "active_record"`: every
|
|
12
|
+
# method that touches AR first confirms {available?}, so the daemon core stays
|
|
13
|
+
# framework-agnostic and the gem keeps its zero-runtime-dependency promise.
|
|
14
|
+
#
|
|
15
|
+
# Isolation model: each parallel worker gets its OWN database so concurrent
|
|
16
|
+
# forks cannot clobber each other's transactional fixtures. {after_fork} runs
|
|
17
|
+
# inside a freshly-forked child and points that child's connection at the
|
|
18
|
+
# worker's database BEFORE any test loads; transactional fixtures then
|
|
19
|
+
# repopulate that isolated database per test. On a slot's first use the worker
|
|
20
|
+
# database starts as a copy of the base test database, so it holds what the
|
|
21
|
+
# booted parent wrote, as the in-process backend sees it.
|
|
22
|
+
#
|
|
23
|
+
# Scope: SQLite adapter only (per-worker file, hermetic). Postgres per-worker
|
|
24
|
+
# DBs (`CREATE DATABASE <db>-<worker>`) are not implemented yet; a non-SQLite
|
|
25
|
+
# config raises a clear NotImplementedError rather than silently mis-routing.
|
|
26
|
+
#
|
|
27
|
+
# Routing failures surface as `error` via {verify_connection!}. Tagging an
|
|
28
|
+
# in-test DB failure as `error` (not `killed`) is only observable under
|
|
29
|
+
# concurrent load and is not yet implemented.
|
|
30
|
+
module RailsWorkerDb
|
|
31
|
+
# True when the app has ActiveRecord loaded. The only condition under which
|
|
32
|
+
# any other method here may touch AR. Never triggers an autoload/require of
|
|
33
|
+
# AR itself.
|
|
34
|
+
#
|
|
35
|
+
# @return [Boolean]
|
|
36
|
+
def self.available?
|
|
37
|
+
defined?(ActiveRecord::Base) ? true : false
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
# Derive a per-worker database path from a base path by inserting `-<worker>`
|
|
41
|
+
# before the extension. Pure string transform (no AR) so it is unit-testable
|
|
42
|
+
# in the zero-dep suite. `storage/test.sqlite3`, worker 1 ->
|
|
43
|
+
# `storage/test-1.sqlite3`.
|
|
44
|
+
#
|
|
45
|
+
# @param database [String] the base database path.
|
|
46
|
+
# @param worker [Integer] the worker slot index (0..N-1).
|
|
47
|
+
# @return [String] the per-worker database path.
|
|
48
|
+
def self.worker_database_path(database, worker)
|
|
49
|
+
ext = File.extname(database)
|
|
50
|
+
"#{database.delete_suffix(ext)}-#{worker}#{ext}"
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
# Build the AR connection config for one worker by copying the app's current
|
|
54
|
+
# (default test) config and swapping in the per-worker database path. SQLite
|
|
55
|
+
# only this pass: a non-SQLite adapter raises so the SQLite-first scope fails
|
|
56
|
+
# loud instead of mis-routing.
|
|
57
|
+
#
|
|
58
|
+
# @param worker [Integer] the worker slot index.
|
|
59
|
+
# @return [Hash] a symbol-keyed AR configuration hash for the worker database.
|
|
60
|
+
# @raise [NotImplementedError] when the app's database is non-SQLite or in-memory.
|
|
61
|
+
def self.worker_db_config(worker)
|
|
62
|
+
hash = ActiveRecord::Base.connection_db_config.configuration_hash
|
|
63
|
+
adapter = hash[:adapter].to_s
|
|
64
|
+
# Config shaping (per_worker_config) is already adapter-general: it derives
|
|
65
|
+
# correct SQLite and Postgres worker-DB names. What is gated is runtime
|
|
66
|
+
# provisioning: SQLite files are created on connect, but Postgres needs an
|
|
67
|
+
# explicit `CREATE DATABASE` per worker. Until that lands, refuse non-SQLite
|
|
68
|
+
# loudly rather than route to a database that does not exist.
|
|
69
|
+
unless adapter.start_with?("sqlite")
|
|
70
|
+
raise NotImplementedError,
|
|
71
|
+
"worker-DB isolation currently provisions SQLite only (got adapter #{adapter.inspect}); " \
|
|
72
|
+
"Postgres per-worker provisioning is not yet supported. Use a SQLite test DB with --daemon, or drop --daemon to run serially."
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
per_worker_config(hash, worker)
|
|
76
|
+
end
|
|
77
|
+
|
|
78
|
+
# Pure config-shaping (no AR): given a connection config hash, return the
|
|
79
|
+
# per-worker variant with its database swapped to the worker's own name.
|
|
80
|
+
# Adapter-general: SQLite (`storage/test.sqlite3` -> `storage/test-<w>.sqlite3`)
|
|
81
|
+
# and Postgres (`myapp_test` -> `myapp_test-<w>`, Rails `parallelize` naming)
|
|
82
|
+
# both fall out of {worker_database_path}. Extracted and unit-tested so the
|
|
83
|
+
# Postgres shape is proven ready without a live database.
|
|
84
|
+
#
|
|
85
|
+
# @param config_hash [Hash] a connection config hash (symbol or string keys).
|
|
86
|
+
# @param worker [Integer] the worker slot index.
|
|
87
|
+
# @return [Hash] the per-worker config (symbol keys), database swapped.
|
|
88
|
+
# @raise [NotImplementedError] for an in-memory or empty database (no per-worker split).
|
|
89
|
+
def self.per_worker_config(config_hash, worker)
|
|
90
|
+
hash = config_hash.transform_keys(&:to_sym)
|
|
91
|
+
database = hash[:database].to_s
|
|
92
|
+
if database.empty? || database == ":memory:"
|
|
93
|
+
raise NotImplementedError,
|
|
94
|
+
"worker-DB isolation needs a file/name-backed database (got #{database.inspect})."
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
hash.merge(database: worker_database_path(database, worker))
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
# Child-side (after fork): route this process's ActiveRecord at the worker's
|
|
101
|
+
# own database and confirm it is reachable, so a routing failure reads as
|
|
102
|
+
# `error` (via the daemon's child rescue) rather than a false verdict.
|
|
103
|
+
#
|
|
104
|
+
# With `seed: true` (the slot's first use) the worker database first becomes
|
|
105
|
+
# a copy of the base test database, schema and rows, so rows the daemon
|
|
106
|
+
# parent wrote while it booted (initializers, `--require` files) are there,
|
|
107
|
+
# as they are for the in-process backend (#222). The schema is then loaded
|
|
108
|
+
# only when the copy differs from `schema.rb` ({schema_current?}: schema
|
|
109
|
+
# version or stored `schema_sha1`, as in a stale or empty base database):
|
|
110
|
+
# `schema.rb` runs with `force: true`, which drops the copied rows of the
|
|
111
|
+
# tables it defines.
|
|
112
|
+
#
|
|
113
|
+
# @param worker [Integer] the worker slot index.
|
|
114
|
+
# @param schema_path [String, nil] absolute path to `db/schema.rb`, or nil to skip.
|
|
115
|
+
# @param seed [Boolean] copy the base database into the worker database first.
|
|
116
|
+
# @return [void]
|
|
117
|
+
def self.after_fork(worker, schema_path = nil, seed: false)
|
|
118
|
+
return unless available?
|
|
119
|
+
|
|
120
|
+
config = worker_db_config(worker)
|
|
121
|
+
base = ActiveRecord::Base.connection.select_value("SELECT file FROM pragma_database_list WHERE name = 'main'") if seed
|
|
122
|
+
ActiveRecord::Base.establish_connection(config)
|
|
123
|
+
seed_from(base) if seed
|
|
124
|
+
load_schema(schema_path) if schema_path && !schema_current?(schema_path)
|
|
125
|
+
verify_connection!
|
|
126
|
+
end
|
|
127
|
+
|
|
128
|
+
# Copy the base test database file into the worker database this fork is
|
|
129
|
+
# now connected to, with the SQLite online backup API: one consistent
|
|
130
|
+
# snapshot of the committed data (WAL included) that replaces whatever the
|
|
131
|
+
# worker file held (an earlier run, or a copy a timeout interrupted). Both
|
|
132
|
+
# ends are files SQLite itself opened, so no path rules are re-derived here.
|
|
133
|
+
#
|
|
134
|
+
# @param base_file [String] the base database file, from `pragma_database_list`.
|
|
135
|
+
# @return [void]
|
|
136
|
+
def self.seed_from(base_file)
|
|
137
|
+
codes = SQLite3::Constants::ErrorCode
|
|
138
|
+
source = SQLite3::Database.new(base_file, readonly: true)
|
|
139
|
+
source.busy_timeout = 5000
|
|
140
|
+
backup = SQLite3::Backup.new(ActiveRecord::Base.connection.raw_connection, "main", source, "main")
|
|
141
|
+
# Another worker's process can hold a lock on the base file for a moment;
|
|
142
|
+
# BUSY/LOCKED steps are retried for up to 5 seconds, then the copy fails
|
|
143
|
+
# with a message that names the database (scored `error`).
|
|
144
|
+
deadline = Process.clock_gettime(Process::CLOCK_MONOTONIC) + 5
|
|
145
|
+
until (status = backup.step(-1)) == codes::DONE
|
|
146
|
+
retry_ok = [codes::BUSY, codes::LOCKED].include?(status) && Process.clock_gettime(Process::CLOCK_MONOTONIC) < deadline
|
|
147
|
+
raise "copying #{base_file} into the worker database failed (SQLite code #{status})" unless retry_ok
|
|
148
|
+
|
|
149
|
+
sleep 0.01
|
|
150
|
+
end
|
|
151
|
+
ensure
|
|
152
|
+
backup&.finish
|
|
153
|
+
source&.close
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
# True when the current database already holds the schema that `schema.rb`
|
|
157
|
+
# declares: its newest `schema_migrations` row matches the declared version,
|
|
158
|
+
# and the `schema_sha1` Rails stores in `ar_internal_metadata` matches the
|
|
159
|
+
# file, which catches an edited schema with the same version. No stored
|
|
160
|
+
# checksum counts as out of date, as in Rails' own `schema_up_to_date?`.
|
|
161
|
+
#
|
|
162
|
+
# @param schema_path [String] absolute path to `db/schema.rb`.
|
|
163
|
+
# @return [Boolean]
|
|
164
|
+
def self.schema_current?(schema_path)
|
|
165
|
+
text = File.read(schema_path)
|
|
166
|
+
version = schema_file_version(text)
|
|
167
|
+
conn = ActiveRecord::Base.connection
|
|
168
|
+
base = ActiveRecord::Base
|
|
169
|
+
migrations = "#{base.table_name_prefix}#{base.schema_migrations_table_name}#{base.table_name_suffix}"
|
|
170
|
+
metadata = "#{base.table_name_prefix}#{base.internal_metadata_table_name}#{base.table_name_suffix}"
|
|
171
|
+
return false unless version && conn.table_exists?(migrations)
|
|
172
|
+
return false unless conn.select_values("SELECT version FROM #{conn.quote_table_name(migrations)}").map(&:to_i).max == version
|
|
173
|
+
return false unless conn.table_exists?(metadata)
|
|
174
|
+
|
|
175
|
+
conn.select_value("SELECT value FROM #{conn.quote_table_name(metadata)} WHERE key = 'schema_sha1'") ==
|
|
176
|
+
Digest::SHA1.hexdigest(text)
|
|
177
|
+
end
|
|
178
|
+
|
|
179
|
+
# The version a `schema.rb` declares in `define(version: ...)`, or nil. Pure
|
|
180
|
+
# string parse (no AR) so it is unit-testable in the zero-dep suite.
|
|
181
|
+
#
|
|
182
|
+
# @param text [String] the `schema.rb` source.
|
|
183
|
+
# @return [Integer, nil]
|
|
184
|
+
def self.schema_file_version(text)
|
|
185
|
+
text[/define\(version:\s*([\d_]+)/, 1]&.delete("_")&.to_i
|
|
186
|
+
end
|
|
187
|
+
|
|
188
|
+
# Load a Rails `schema.rb` into the current connection with output silenced
|
|
189
|
+
# (fork child stdout is already File::NULL; this is belt-and-braces).
|
|
190
|
+
#
|
|
191
|
+
# @param schema_path [String] absolute path to `db/schema.rb`.
|
|
192
|
+
# @return [void]
|
|
193
|
+
def self.load_schema(schema_path)
|
|
194
|
+
ActiveRecord::Migration.verbose = false if defined?(ActiveRecord::Migration)
|
|
195
|
+
original = $stdout
|
|
196
|
+
$stdout = File.open(File::NULL, "w")
|
|
197
|
+
load schema_path
|
|
198
|
+
ensure
|
|
199
|
+
$stdout.close unless $stdout.equal?(original)
|
|
200
|
+
$stdout = original
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
# Force a round-trip to the freshly-routed connection so a broken route fails
|
|
204
|
+
# HERE (→ `error`) instead of later masquerading as a test failure
|
|
205
|
+
# (→ false `killed`).
|
|
206
|
+
#
|
|
207
|
+
# @return [void]
|
|
208
|
+
def self.verify_connection!
|
|
209
|
+
ActiveRecord::Base.connection.execute("SELECT 1")
|
|
210
|
+
end
|
|
211
|
+
end
|
|
212
|
+
end
|