fixture_kit 0.3.0 → 0.5.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 +4 -4
- data/README.md +80 -22
- data/lib/fixture_kit/cache.rb +35 -30
- data/lib/fixture_kit/configuration.rb +7 -19
- data/lib/fixture_kit/configuration_helper.rb +11 -0
- data/lib/fixture_kit/definition.rb +2 -1
- data/lib/fixture_kit/fixture.rb +11 -19
- data/lib/fixture_kit/isolators/minitest_isolator.rb +32 -0
- data/lib/fixture_kit/isolators/rspec_isolator.rb +33 -0
- data/lib/fixture_kit/minitest.rb +43 -0
- data/lib/fixture_kit/registry.rb +38 -16
- data/lib/fixture_kit/rspec.rb +6 -7
- data/lib/fixture_kit/runner.rb +19 -3
- data/lib/fixture_kit/singleton.rb +0 -4
- data/lib/fixture_kit/sql_subscriber.rb +4 -7
- data/lib/fixture_kit/version.rb +1 -1
- data/lib/fixture_kit.rb +16 -14
- metadata +6 -5
- data/lib/fixture_kit/rspec/isolator.rb +0 -45
- data/lib/fixture_kit/test_case/isolator.rb +0 -37
- data/lib/fixture_kit/test_case.rb +0 -7
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: c052323f7423cfeb258b137bc8372833eff2f4c8b826b3fa22b25a171b2b2c0f
|
|
4
|
+
data.tar.gz: '0728daee8814033465d2b93a67e15a00eff4abe5543967167077f81e68db7dde'
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 82149fa2c3598b2af00b2cd9b8e3691e8694f18bdc40a0abf3a47540e7c319e0821250bec0e3dce4d8aaf692ea614a17cda09970a476503b0280c7a301b95e2f
|
|
7
|
+
data.tar.gz: f4fe1ea329c354aee57ad6f53d22682309b8db9704340f52d18c4b01b42a83a47701d36fe11e739d644e13b72d02171190a530df6847386770a5edc6b786cbf2
|
data/README.md
CHANGED
|
@@ -106,6 +106,22 @@ end
|
|
|
106
106
|
|
|
107
107
|
`fixture` returns a `Repository` and exposes records as methods (for example, `fixture.owner`).
|
|
108
108
|
|
|
109
|
+
You can also define fixtures anonymously inline:
|
|
110
|
+
|
|
111
|
+
```ruby
|
|
112
|
+
RSpec.describe Book do
|
|
113
|
+
fixture do
|
|
114
|
+
owner = User.create!(name: "Alice", email: "alice@example.com")
|
|
115
|
+
featured = Book.create!(title: "Dune", owner: owner)
|
|
116
|
+
expose(owner: owner, featured: featured)
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
it "uses inline fixture data" do
|
|
120
|
+
expect(fixture.featured.owner).to eq(fixture.owner)
|
|
121
|
+
end
|
|
122
|
+
end
|
|
123
|
+
```
|
|
124
|
+
|
|
109
125
|
### 3. Configure RSpec
|
|
110
126
|
|
|
111
127
|
```ruby
|
|
@@ -117,52 +133,85 @@ RSpec.configure do |config|
|
|
|
117
133
|
end
|
|
118
134
|
```
|
|
119
135
|
|
|
120
|
-
When you call `fixture "name"` in an example group, FixtureKit registers that fixture with its runner.
|
|
136
|
+
When you call `fixture "name"` or `fixture do ... end` in an example group, FixtureKit registers that fixture with its runner.
|
|
137
|
+
|
|
138
|
+
### 4. Configure Minitest
|
|
139
|
+
|
|
140
|
+
```ruby
|
|
141
|
+
# test/test_helper.rb
|
|
142
|
+
require "fixture_kit/minitest"
|
|
143
|
+
|
|
144
|
+
class ActiveSupport::TestCase
|
|
145
|
+
self.use_transactional_tests = true
|
|
146
|
+
end
|
|
147
|
+
```
|
|
148
|
+
|
|
149
|
+
When you call `fixture "name"` or `fixture do ... end` in a test class, FixtureKit registers that fixture with its runner and mounts it during test setup.
|
|
121
150
|
|
|
122
151
|
## Configuration
|
|
123
152
|
|
|
124
153
|
```ruby
|
|
125
154
|
# spec/support/fixture_kit.rb
|
|
126
155
|
FixtureKit.configure do |config|
|
|
127
|
-
# Where fixture definitions live (default:
|
|
156
|
+
# Where fixture definitions live (default: fixture_kit).
|
|
157
|
+
# Framework entrypoints set a framework-specific default:
|
|
158
|
+
# - fixture_kit/rspec -> spec/fixture_kit
|
|
159
|
+
# - fixture_kit/minitest -> test/fixture_kit
|
|
128
160
|
config.fixture_path = Rails.root.join("spec/fixture_kit").to_s
|
|
129
161
|
|
|
130
162
|
# Where cache files are stored (default: tmp/cache/fixture_kit)
|
|
131
163
|
config.cache_path = Rails.root.join("tmp/cache/fixture_kit").to_s
|
|
132
164
|
|
|
133
|
-
# Wrapper used to isolate generation work (default: FixtureKit::
|
|
134
|
-
# config.isolator = FixtureKit::
|
|
135
|
-
# config.isolator = FixtureKit::
|
|
165
|
+
# Wrapper used to isolate generation work (default: FixtureKit::MinitestIsolator)
|
|
166
|
+
# config.isolator = FixtureKit::MinitestIsolator
|
|
167
|
+
# config.isolator = FixtureKit::RSpecIsolator
|
|
136
168
|
|
|
137
|
-
# Optional callback, called
|
|
138
|
-
#
|
|
139
|
-
#
|
|
169
|
+
# Optional callback, called right before a fixture cache is generated.
|
|
170
|
+
# Called on first generation and forced regeneration.
|
|
171
|
+
# Receives the fixture identifier:
|
|
172
|
+
# - named fixtures: String (e.g. "teams/basic")
|
|
173
|
+
# - anonymous fixtures: scope class
|
|
174
|
+
# config.on_cache_save = ->(identifier) { puts "cached #{identifier}" }
|
|
175
|
+
|
|
176
|
+
# Optional callback, called right before a fixture cache is mounted.
|
|
177
|
+
# Receives the same fixture identifier shape as on_cache_save.
|
|
178
|
+
# config.on_cache_mount = ->(identifier) { puts "mounted #{identifier}" }
|
|
140
179
|
end
|
|
141
180
|
```
|
|
142
181
|
|
|
143
182
|
Custom isolators should subclass `FixtureKit::Isolator` and implement `#run`.
|
|
144
183
|
`#run` receives the generation block and should execute it in whatever lifecycle you need.
|
|
145
184
|
|
|
146
|
-
By default, FixtureKit uses `FixtureKit::
|
|
185
|
+
By default, FixtureKit uses `FixtureKit::MinitestIsolator`, which runs generation inside an internal `ActiveSupport::TestCase` and removes that harness case from minitest runnables.
|
|
147
186
|
|
|
148
|
-
When using `fixture_kit/rspec`, FixtureKit sets `FixtureKit::
|
|
187
|
+
When using `fixture_kit/rspec`, FixtureKit sets `FixtureKit::RSpecIsolator`. It runs generation inside an internal RSpec example, and uses a null reporter so harness runs do not count toward suite example totals.
|
|
149
188
|
|
|
150
189
|
## Lifecycle
|
|
151
190
|
|
|
152
191
|
Fixture generation is managed by `FixtureKit::Runner`.
|
|
153
192
|
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
1. `fixture "name"` registers the fixture with the runner during spec file load.
|
|
157
|
-
2. In `before(:suite)`, runner `start`:
|
|
193
|
+
1. Calling `fixture "name"` or `fixture do ... end` registers the fixture with the runner.
|
|
194
|
+
2. Runner `start`:
|
|
158
195
|
- clears `cache_path` (unless preserve-cache is enabled),
|
|
159
196
|
- generates caches for all already-registered fixtures.
|
|
160
|
-
3. If new
|
|
161
|
-
4. At
|
|
197
|
+
3. If new tests are loaded after start (for example, queue-mode CI runners), newly registered fixtures are cached immediately.
|
|
198
|
+
4. At test runtime, `fixture` mounts from cache and returns a `Repository`.
|
|
199
|
+
|
|
200
|
+
When runner start happens:
|
|
201
|
+
|
|
202
|
+
- `fixture_kit/rspec`: in `before(:suite)`.
|
|
203
|
+
- `fixture_kit/minitest`: lazily during test setup for the first test class that declares `fixture`.
|
|
204
|
+
|
|
205
|
+
## Fixture Declaration Rules
|
|
206
|
+
|
|
207
|
+
- Only one `fixture` declaration is allowed per test context.
|
|
208
|
+
- Declaring a fixture twice in the same context raises `FixtureKit::MultipleFixtures`.
|
|
209
|
+
- Child contexts/classes can declare their own fixture and override parent declarations.
|
|
210
|
+
- Providing both a name and a block (or neither) raises `FixtureKit::InvalidFixtureDeclaration`.
|
|
162
211
|
|
|
163
212
|
### Preserving Cache Locally
|
|
164
213
|
|
|
165
|
-
If you want to skip cache clearing
|
|
214
|
+
If you want to skip cache clearing when the runner starts (e.g., to reuse caches across test runs during local development), set the `FIXTURE_KIT_PRESERVE_CACHE` environment variable:
|
|
166
215
|
|
|
167
216
|
```bash
|
|
168
217
|
FIXTURE_KIT_PRESERVE_CACHE=1 bundle exec rspec
|
|
@@ -187,15 +236,24 @@ end
|
|
|
187
236
|
fixture "teams/sales"
|
|
188
237
|
```
|
|
189
238
|
|
|
239
|
+
## Anonymous Fixture Cache Paths
|
|
240
|
+
|
|
241
|
+
Anonymous fixture caches are written under the `_anonymous/` directory inside `cache_path`.
|
|
242
|
+
|
|
243
|
+
- Minitest: class name is underscored into a path.
|
|
244
|
+
- `MyFeatureTest` -> `_anonymous/my_feature_test.json`
|
|
245
|
+
- RSpec: class name is underscored after removing `RSpec::ExampleGroups::`.
|
|
246
|
+
- `RSpec::ExampleGroups::Foo::WithFixtureKit::Hello` -> `_anonymous/foo/with_fixture_kit/hello.json`
|
|
247
|
+
|
|
190
248
|
## How It Works
|
|
191
249
|
|
|
192
|
-
1. **Cache generation**: FixtureKit executes your definition block inside the configured isolator, subscribes to `sql.active_record` notifications to track
|
|
250
|
+
1. **Cache generation**: FixtureKit executes your definition block inside the configured isolator, subscribes to `sql.active_record` notifications to track created/updated/deleted models, queries those model tables, and caches SQL statements for current table contents.
|
|
193
251
|
|
|
194
|
-
2. **Mounting**: FixtureKit loads the cached JSON file and executes the raw SQL INSERT statements directly. No ORM instantiation, no callbacks.
|
|
252
|
+
2. **Mounting**: FixtureKit loads the cached JSON file, clears each tracked table, and executes the raw SQL INSERT statements directly. No ORM instantiation, no callbacks.
|
|
195
253
|
|
|
196
254
|
3. **Repository build**: FixtureKit resolves exposed records by model + id and returns a `Repository` for method-based access.
|
|
197
255
|
|
|
198
|
-
4. **Transaction isolation**:
|
|
256
|
+
4. **Transaction isolation**: Use framework transactions (`use_transactional_fixtures` in RSpec, `use_transactional_tests` in Minitest) so test writes roll back and cached data can be reused safely between tests.
|
|
199
257
|
|
|
200
258
|
### Cache Format
|
|
201
259
|
|
|
@@ -204,8 +262,8 @@ Caches are stored as JSON files in `tmp/cache/fixture_kit/`:
|
|
|
204
262
|
```json
|
|
205
263
|
{
|
|
206
264
|
"records": {
|
|
207
|
-
"User": "INSERT
|
|
208
|
-
"Project": "INSERT
|
|
265
|
+
"User": "INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com'), (2, 'Bob', 'bob@example.com')",
|
|
266
|
+
"Project": "INSERT INTO projects (id, name, user_id) VALUES (1, 'Website', 1)"
|
|
209
267
|
},
|
|
210
268
|
"exposed": {
|
|
211
269
|
"alice": { "model": "User", "id": 1 },
|
data/lib/fixture_kit/cache.rb
CHANGED
|
@@ -7,15 +7,30 @@ require "active_support/inflector"
|
|
|
7
7
|
|
|
8
8
|
module FixtureKit
|
|
9
9
|
class Cache
|
|
10
|
+
ANONYMOUS_DIRECTORY = "_anonymous"
|
|
11
|
+
|
|
12
|
+
include ConfigurationHelper
|
|
13
|
+
|
|
10
14
|
attr_reader :fixture
|
|
11
15
|
|
|
12
|
-
def initialize(fixture
|
|
16
|
+
def initialize(fixture)
|
|
13
17
|
@fixture = fixture
|
|
14
|
-
@definition = definition
|
|
15
18
|
end
|
|
16
19
|
|
|
17
20
|
def path
|
|
18
|
-
File.join(
|
|
21
|
+
File.join(configuration.cache_path, "#{identifier}.json")
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def identifier
|
|
25
|
+
@identifier ||= begin
|
|
26
|
+
raw_identifier = fixture.identifier
|
|
27
|
+
if raw_identifier.is_a?(String)
|
|
28
|
+
raw_identifier
|
|
29
|
+
else
|
|
30
|
+
normalized_scope = raw_identifier.to_s.sub(/\ARSpec::ExampleGroups::/, "")
|
|
31
|
+
File.join(ANONYMOUS_DIRECTORY, ActiveSupport::Inflector.underscore(normalized_scope))
|
|
32
|
+
end
|
|
33
|
+
end
|
|
19
34
|
end
|
|
20
35
|
|
|
21
36
|
def exists?
|
|
@@ -24,27 +39,32 @@ module FixtureKit
|
|
|
24
39
|
|
|
25
40
|
def load
|
|
26
41
|
unless exists?
|
|
27
|
-
raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{fixture.
|
|
42
|
+
raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{fixture.identifier}'"
|
|
28
43
|
end
|
|
29
44
|
|
|
30
45
|
@data ||= JSON.parse(File.read(path))
|
|
31
46
|
@data.fetch("records").each do |model_name, sql|
|
|
32
47
|
model = ActiveSupport::Inflector.constantize(model_name)
|
|
33
|
-
model.connection
|
|
48
|
+
connection = model.connection
|
|
49
|
+
connection.disable_referential_integrity do
|
|
50
|
+
# execute_batch is private in current supported Rails versions.
|
|
51
|
+
# This should be revisited when Rails 8.2 makes it public.
|
|
52
|
+
connection.__send__(:execute_batch, [build_delete_sql(model), sql].compact, "FixtureKit Load")
|
|
53
|
+
end
|
|
34
54
|
end
|
|
35
55
|
|
|
36
56
|
build_repository(@data.fetch("exposed"))
|
|
37
57
|
end
|
|
38
58
|
|
|
39
59
|
def save
|
|
40
|
-
FixtureKit.
|
|
60
|
+
FixtureKit.runner.isolator.run do
|
|
41
61
|
models = SqlSubscriber.capture do
|
|
42
|
-
|
|
62
|
+
fixture.definition.evaluate
|
|
43
63
|
end
|
|
44
64
|
|
|
45
65
|
@data = {
|
|
46
66
|
"records" => generate_statements(models),
|
|
47
|
-
"exposed" => build_exposed_mapping(
|
|
67
|
+
"exposed" => build_exposed_mapping(fixture.definition.exposed)
|
|
48
68
|
}
|
|
49
69
|
end
|
|
50
70
|
|
|
@@ -70,7 +90,7 @@ module FixtureKit
|
|
|
70
90
|
model.find(id)
|
|
71
91
|
rescue ActiveRecord::RecordNotFound
|
|
72
92
|
raise FixtureKit::ExposedRecordNotFound,
|
|
73
|
-
"Could not find #{model_name} with id=#{id} for exposed record '#{exposed_name}' in fixture '#{@fixture.
|
|
93
|
+
"Could not find #{model_name} with id=#{id} for exposed record '#{exposed_name}' in fixture '#{@fixture.identifier}'"
|
|
74
94
|
end
|
|
75
95
|
|
|
76
96
|
def generate_statements(models)
|
|
@@ -88,37 +108,22 @@ module FixtureKit
|
|
|
88
108
|
rows << "(#{row_values.join(", ")})"
|
|
89
109
|
end
|
|
90
110
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
sql = build_insert_sql(model.table_name, columns, rows, model.connection)
|
|
111
|
+
sql = rows.empty? ? nil : build_insert_sql(model.table_name, columns, rows, model.connection)
|
|
94
112
|
statements_by_model[model.name] = sql
|
|
95
113
|
end
|
|
96
114
|
|
|
97
115
|
statements_by_model
|
|
98
116
|
end
|
|
99
117
|
|
|
118
|
+
def build_delete_sql(model)
|
|
119
|
+
"DELETE FROM #{model.quoted_table_name}"
|
|
120
|
+
end
|
|
121
|
+
|
|
100
122
|
def build_insert_sql(table_name, columns, rows, connection)
|
|
101
123
|
quoted_table = connection.quote_table_name(table_name)
|
|
102
124
|
quoted_columns = columns.map { |c| connection.quote_column_name(c) }
|
|
103
125
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
add_conflict_handling(sql, connection)
|
|
107
|
-
end
|
|
108
|
-
|
|
109
|
-
def add_conflict_handling(sql, connection)
|
|
110
|
-
adapter_name = connection.adapter_name.downcase
|
|
111
|
-
|
|
112
|
-
case adapter_name
|
|
113
|
-
when /sqlite/
|
|
114
|
-
sql.sub(/\AINSERT INTO/i, "INSERT OR IGNORE INTO")
|
|
115
|
-
when /postgresql/, /postgis/
|
|
116
|
-
"#{sql} ON CONFLICT DO NOTHING"
|
|
117
|
-
when /mysql/, /trilogy/
|
|
118
|
-
sql.sub(/\AINSERT INTO/i, "INSERT IGNORE INTO")
|
|
119
|
-
else
|
|
120
|
-
sql
|
|
121
|
-
end
|
|
126
|
+
"INSERT INTO #{quoted_table} (#{quoted_columns.join(", ")}) VALUES #{rows.join(", ")}"
|
|
122
127
|
end
|
|
123
128
|
|
|
124
129
|
def build_exposed_mapping(exposed)
|
|
@@ -5,17 +5,19 @@ module FixtureKit
|
|
|
5
5
|
attr_writer :fixture_path
|
|
6
6
|
attr_writer :cache_path
|
|
7
7
|
attr_accessor :isolator
|
|
8
|
-
attr_accessor :
|
|
8
|
+
attr_accessor :on_cache_save
|
|
9
|
+
attr_accessor :on_cache_mount
|
|
9
10
|
|
|
10
11
|
def initialize
|
|
11
|
-
@fixture_path =
|
|
12
|
+
@fixture_path = "fixture_kit"
|
|
12
13
|
@cache_path = nil
|
|
13
|
-
@isolator = FixtureKit::
|
|
14
|
-
@
|
|
14
|
+
@isolator = FixtureKit::MinitestIsolator
|
|
15
|
+
@on_cache_save = nil
|
|
16
|
+
@on_cache_mount = nil
|
|
15
17
|
end
|
|
16
18
|
|
|
17
19
|
def fixture_path
|
|
18
|
-
@fixture_path
|
|
20
|
+
@fixture_path
|
|
19
21
|
end
|
|
20
22
|
|
|
21
23
|
def cache_path
|
|
@@ -24,20 +26,6 @@ module FixtureKit
|
|
|
24
26
|
|
|
25
27
|
private
|
|
26
28
|
|
|
27
|
-
def detect_fixture_path
|
|
28
|
-
if defined?(RSpec)
|
|
29
|
-
"spec/fixture_kit"
|
|
30
|
-
elsif defined?(Minitest)
|
|
31
|
-
"test/fixture_kit"
|
|
32
|
-
elsif Dir.exist?("spec")
|
|
33
|
-
"spec/fixture_kit"
|
|
34
|
-
elsif Dir.exist?("test")
|
|
35
|
-
"test/fixture_kit"
|
|
36
|
-
else
|
|
37
|
-
"spec/fixture_kit"
|
|
38
|
-
end
|
|
39
|
-
end
|
|
40
|
-
|
|
41
29
|
def detect_cache_path
|
|
42
30
|
"tmp/cache/fixture_kit"
|
|
43
31
|
end
|
data/lib/fixture_kit/fixture.rb
CHANGED
|
@@ -2,38 +2,30 @@
|
|
|
2
2
|
|
|
3
3
|
module FixtureKit
|
|
4
4
|
class Fixture
|
|
5
|
-
|
|
5
|
+
include ConfigurationHelper
|
|
6
6
|
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
@
|
|
7
|
+
attr_reader :identifier, :definition
|
|
8
|
+
|
|
9
|
+
def initialize(identifier, definition)
|
|
10
|
+
@identifier = identifier
|
|
11
|
+
@definition = definition
|
|
12
|
+
@cache = Cache.new(self)
|
|
11
13
|
end
|
|
12
14
|
|
|
13
15
|
def cache(force: false)
|
|
14
|
-
|
|
15
|
-
return if already_cached && !force
|
|
16
|
+
return if @cache.exists? && !force
|
|
16
17
|
|
|
18
|
+
configuration.on_cache_save&.call(@cache.identifier)
|
|
17
19
|
@cache.save
|
|
18
|
-
FixtureKit.configuration.on_cache&.call(name) unless already_cached
|
|
19
20
|
end
|
|
20
21
|
|
|
21
22
|
def mount
|
|
22
23
|
unless @cache.exists?
|
|
23
|
-
raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{
|
|
24
|
+
raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{identifier}'"
|
|
24
25
|
end
|
|
25
26
|
|
|
27
|
+
configuration.on_cache_mount&.call(@cache.identifier)
|
|
26
28
|
@cache.load
|
|
27
29
|
end
|
|
28
|
-
|
|
29
|
-
private
|
|
30
|
-
|
|
31
|
-
def definition
|
|
32
|
-
@definition ||= begin
|
|
33
|
-
definition = eval(File.read(@path), TOPLEVEL_BINDING.dup, @path)
|
|
34
|
-
raise FixtureKit::FixtureDefinitionNotFound, "Could not find fixture definition at '#{@path}'" unless definition.is_a?(Definition)
|
|
35
|
-
definition
|
|
36
|
-
end
|
|
37
|
-
end
|
|
38
30
|
end
|
|
39
31
|
end
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/test_case"
|
|
4
|
+
require "active_record/fixtures"
|
|
5
|
+
|
|
6
|
+
module FixtureKit
|
|
7
|
+
class MinitestIsolator < FixtureKit::Isolator
|
|
8
|
+
TEST_NAME = "fixture kit cache pregeneration"
|
|
9
|
+
|
|
10
|
+
def run(&block)
|
|
11
|
+
test_class = build_test_class
|
|
12
|
+
test_method = test_class.test(TEST_NAME) do
|
|
13
|
+
block.call
|
|
14
|
+
pass
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
result = test_class.new(test_method).run
|
|
18
|
+
return if result.passed?
|
|
19
|
+
|
|
20
|
+
raise result.failures.first.error
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def build_test_class
|
|
26
|
+
Class.new(ActiveSupport::TestCase) do
|
|
27
|
+
::Minitest::Runnable.runnables.delete(self)
|
|
28
|
+
include(::ActiveRecord::TestFixtures)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
class RSpecIsolator < FixtureKit::Isolator
|
|
5
|
+
def run(&block)
|
|
6
|
+
previous_example = ::RSpec.current_example
|
|
7
|
+
previous_scope = ::RSpec.current_scope
|
|
8
|
+
example_group = build_example_group
|
|
9
|
+
example = example_group.example { block.call }
|
|
10
|
+
instance = example_group.new
|
|
11
|
+
succeeded =
|
|
12
|
+
begin
|
|
13
|
+
example.run(instance, ::RSpec::Core::NullReporter)
|
|
14
|
+
ensure
|
|
15
|
+
::RSpec.current_example = previous_example
|
|
16
|
+
::RSpec.current_scope = previous_scope
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
raise example.exception unless succeeded
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
private
|
|
23
|
+
|
|
24
|
+
def build_example_group
|
|
25
|
+
::RSpec::Core::ExampleGroup.subclass(
|
|
26
|
+
::RSpec::Core::ExampleGroup,
|
|
27
|
+
"FixtureKit",
|
|
28
|
+
[],
|
|
29
|
+
[]
|
|
30
|
+
)
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fixture_kit"
|
|
4
|
+
require "active_support/lazy_load_hooks"
|
|
5
|
+
|
|
6
|
+
module FixtureKit
|
|
7
|
+
module Minitest
|
|
8
|
+
DECLARATION_CLASS_ATTRIBUTE = :fixture_kit_declaration
|
|
9
|
+
|
|
10
|
+
module ClassMethods
|
|
11
|
+
def fixture(name = nil, &definition_block)
|
|
12
|
+
self.fixture_kit_declaration = FixtureKit.runner.register(self, name, &definition_block)
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
module InstanceMethods
|
|
17
|
+
def fixture
|
|
18
|
+
@_fixture_kit_repository || raise("No fixture declared for this test class. Use `fixture \"name\"` in your test class.")
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.configure!(test_case)
|
|
23
|
+
FixtureKit.runner.configuration.fixture_path = "test/fixture_kit"
|
|
24
|
+
FixtureKit.runner.configuration.isolator = FixtureKit::MinitestIsolator
|
|
25
|
+
|
|
26
|
+
test_case.class_attribute DECLARATION_CLASS_ATTRIBUTE, instance_accessor: false
|
|
27
|
+
test_case.extend ClassMethods
|
|
28
|
+
test_case.include InstanceMethods
|
|
29
|
+
|
|
30
|
+
test_case.setup do
|
|
31
|
+
declaration = self.class.fixture_kit_declaration
|
|
32
|
+
next unless declaration
|
|
33
|
+
|
|
34
|
+
FixtureKit.runner.start unless FixtureKit.runner.started?
|
|
35
|
+
@_fixture_kit_repository = declaration.mount
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
ActiveSupport.on_load(:active_support_test_case) do
|
|
42
|
+
FixtureKit::Minitest.configure!(self)
|
|
43
|
+
end
|
data/lib/fixture_kit/registry.rb
CHANGED
|
@@ -1,34 +1,56 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "pathname"
|
|
4
|
-
|
|
5
3
|
module FixtureKit
|
|
6
4
|
class Registry
|
|
7
|
-
|
|
8
|
-
@configuration = configuration
|
|
9
|
-
@registry = {}
|
|
10
|
-
end
|
|
5
|
+
include ConfigurationHelper
|
|
11
6
|
|
|
12
|
-
def
|
|
13
|
-
|
|
7
|
+
def initialize
|
|
8
|
+
@declarations = {}
|
|
9
|
+
@fixtures = {}
|
|
10
|
+
end
|
|
14
11
|
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
raise FixtureKit::
|
|
18
|
-
"Could not find fixture definition file for '#{name}' at '#{file_path}'"
|
|
12
|
+
def add(scope, name_or_block)
|
|
13
|
+
if @declarations.key?(scope)
|
|
14
|
+
raise FixtureKit::MultipleFixtures, "cannot load multiple fixtures in the same context"
|
|
19
15
|
end
|
|
20
16
|
|
|
21
|
-
@
|
|
17
|
+
@declarations[scope] =
|
|
18
|
+
case name_or_block
|
|
19
|
+
when String
|
|
20
|
+
fetch_named_fixture(name_or_block)
|
|
21
|
+
when Proc
|
|
22
|
+
fetch_anonymous_fixture(scope, name_or_block)
|
|
23
|
+
else
|
|
24
|
+
raise FixtureKit::InvalidFixtureDeclaration, "unsupported fixture declaration type: #{name_or_block.class}"
|
|
25
|
+
end
|
|
22
26
|
end
|
|
23
27
|
|
|
24
28
|
def fixtures
|
|
25
|
-
@
|
|
29
|
+
@fixtures.values
|
|
26
30
|
end
|
|
27
31
|
|
|
28
32
|
private
|
|
29
33
|
|
|
30
|
-
def
|
|
31
|
-
|
|
34
|
+
def fetch_named_fixture(name)
|
|
35
|
+
@fixtures[name] ||= Fixture.new(name, load_named_definition(name))
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def fetch_anonymous_fixture(scope, definition)
|
|
39
|
+
@fixtures[scope] ||= Fixture.new(scope, Definition.new(&definition))
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def load_named_definition(name)
|
|
43
|
+
file_path = File.expand_path(File.join(configuration.fixture_path, "#{name}.rb"))
|
|
44
|
+
|
|
45
|
+
unless File.file?(file_path)
|
|
46
|
+
raise FixtureKit::FixtureDefinitionNotFound,
|
|
47
|
+
"cannot find fixture definition file for '#{name}' at '#{file_path}'"
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
definition = eval(File.read(file_path), TOPLEVEL_BINDING.dup, file_path)
|
|
51
|
+
return definition if definition.is_a?(Definition)
|
|
52
|
+
|
|
53
|
+
raise FixtureKit::FixtureDefinitionNotFound, "cannot find fixture definition at '#{file_path}'"
|
|
32
54
|
end
|
|
33
55
|
end
|
|
34
56
|
end
|
data/lib/fixture_kit/rspec.rb
CHANGED
|
@@ -4,8 +4,6 @@ require "fixture_kit"
|
|
|
4
4
|
|
|
5
5
|
module FixtureKit
|
|
6
6
|
module RSpec
|
|
7
|
-
autoload :Isolator, File.expand_path("rspec/isolator", __dir__)
|
|
8
|
-
|
|
9
7
|
DECLARATION_METADATA_KEY = :fixture_kit_declaration
|
|
10
8
|
|
|
11
9
|
# Class methods (extended via config.extend)
|
|
@@ -29,8 +27,8 @@ module FixtureKit
|
|
|
29
27
|
# end
|
|
30
28
|
# end
|
|
31
29
|
# end
|
|
32
|
-
def fixture(name)
|
|
33
|
-
metadata[DECLARATION_METADATA_KEY] = ::RSpec.configuration.fixture_kit.register(name)
|
|
30
|
+
def fixture(name = nil, &definition_block)
|
|
31
|
+
metadata[DECLARATION_METADATA_KEY] = ::RSpec.configuration.fixture_kit.register(self, name, &definition_block)
|
|
34
32
|
end
|
|
35
33
|
end
|
|
36
34
|
|
|
@@ -39,13 +37,14 @@ module FixtureKit
|
|
|
39
37
|
# Returns the Repository for the current example's fixture.
|
|
40
38
|
# Access exposed records as methods: fixture.alice, fixture.posts
|
|
41
39
|
def fixture
|
|
42
|
-
@
|
|
40
|
+
@_fixture_kit_repository || raise("No fixture declared for this example group. Use `fixture \"name\"` in your describe/context block.")
|
|
43
41
|
end
|
|
44
42
|
end
|
|
45
43
|
|
|
46
44
|
def self.configure!(config)
|
|
45
|
+
FixtureKit.runner.configuration.fixture_path = "spec/fixture_kit"
|
|
47
46
|
config.add_setting(:fixture_kit, default: FixtureKit.runner)
|
|
48
|
-
FixtureKit.configuration.isolator =
|
|
47
|
+
FixtureKit.runner.configuration.isolator = FixtureKit::RSpecIsolator
|
|
49
48
|
|
|
50
49
|
config.extend ClassMethods
|
|
51
50
|
config.include InstanceMethods, DECLARATION_METADATA_KEY
|
|
@@ -53,7 +52,7 @@ module FixtureKit
|
|
|
53
52
|
# Load declared fixtures at the beginning of each example.
|
|
54
53
|
# Runs inside transactional fixtures and before user-defined before hooks.
|
|
55
54
|
config.prepend_before(:example, DECLARATION_METADATA_KEY) do |example|
|
|
56
|
-
@
|
|
55
|
+
@_fixture_kit_repository = example.metadata[DECLARATION_METADATA_KEY].mount
|
|
57
56
|
end
|
|
58
57
|
|
|
59
58
|
config.append_before(:suite) do
|
data/lib/fixture_kit/runner.rb
CHANGED
|
@@ -10,12 +10,13 @@ module FixtureKit
|
|
|
10
10
|
|
|
11
11
|
def initialize
|
|
12
12
|
@configuration = Configuration.new
|
|
13
|
-
@registry = Registry.new
|
|
13
|
+
@registry = Registry.new
|
|
14
|
+
@isolator = nil
|
|
14
15
|
@started = false
|
|
15
16
|
end
|
|
16
17
|
|
|
17
|
-
def register(name)
|
|
18
|
-
registry.add(name).tap do |fixture|
|
|
18
|
+
def register(scope, name = nil, &definition_block)
|
|
19
|
+
registry.add(scope, normalize_registration(name, definition_block)).tap do |fixture|
|
|
19
20
|
fixture.cache if started?
|
|
20
21
|
end
|
|
21
22
|
end
|
|
@@ -28,6 +29,10 @@ module FixtureKit
|
|
|
28
29
|
registry.fixtures.each(&:cache)
|
|
29
30
|
end
|
|
30
31
|
|
|
32
|
+
def isolator
|
|
33
|
+
@isolator ||= configuration.isolator.new
|
|
34
|
+
end
|
|
35
|
+
|
|
31
36
|
def started?
|
|
32
37
|
@started
|
|
33
38
|
end
|
|
@@ -41,5 +46,16 @@ module FixtureKit
|
|
|
41
46
|
def preserve_cache?
|
|
42
47
|
ENV[PRESERVE_CACHE_ENV_KEY].to_s.match?(/\A(1|true|yes)\z/i)
|
|
43
48
|
end
|
|
49
|
+
|
|
50
|
+
def normalize_registration(name, definition_block)
|
|
51
|
+
if name && definition_block
|
|
52
|
+
raise FixtureKit::InvalidFixtureDeclaration, "cannot provide both fixture name and definition block"
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
name_or_block = name || definition_block
|
|
56
|
+
return name_or_block if name_or_block
|
|
57
|
+
|
|
58
|
+
raise FixtureKit::InvalidFixtureDeclaration, "must provide fixture name or definition block"
|
|
59
|
+
end
|
|
44
60
|
end
|
|
45
61
|
end
|
|
@@ -6,18 +6,15 @@ require "active_support/inflector"
|
|
|
6
6
|
module FixtureKit
|
|
7
7
|
class SqlSubscriber
|
|
8
8
|
EVENT = "sql.active_record"
|
|
9
|
+
NAME_PATTERN = /\A(?<model_name>.+?) (?:(?:Bulk )?(?:Insert|Upsert)|Create|Destroy|(?:Update|Delete)(?: All)?)\z/
|
|
9
10
|
|
|
10
11
|
def self.capture(&block)
|
|
11
12
|
models = Set.new
|
|
12
13
|
subscriber = lambda do |_event_name, _start, _finish, _id, payload|
|
|
13
|
-
|
|
14
|
-
|
|
14
|
+
name = payload[:name].to_s
|
|
15
|
+
model_name = name[NAME_PATTERN, :model_name]
|
|
16
|
+
next unless model_name
|
|
15
17
|
|
|
16
|
-
# payload[:name] is like "User Create" - extract model name
|
|
17
|
-
name = payload[:name]
|
|
18
|
-
next unless name&.end_with?(" Create")
|
|
19
|
-
|
|
20
|
-
model_name = name.sub(/ Create\z/, "")
|
|
21
18
|
models.add(ActiveSupport::Inflector.constantize(model_name))
|
|
22
19
|
end
|
|
23
20
|
|
data/lib/fixture_kit/version.rb
CHANGED
data/lib/fixture_kit.rb
CHANGED
|
@@ -2,26 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
module FixtureKit
|
|
4
4
|
class Error < StandardError; end
|
|
5
|
-
class DuplicateFixtureError < Error; end
|
|
6
5
|
class DuplicateNameError < Error; end
|
|
6
|
+
class InvalidFixtureDeclaration < Error; end
|
|
7
|
+
class MultipleFixtures < Error; end
|
|
7
8
|
class CacheMissingError < Error; end
|
|
8
|
-
class PregenerationError < Error; end
|
|
9
9
|
class FixtureDefinitionNotFound < Error; end
|
|
10
10
|
class ExposedRecordNotFound < Error; end
|
|
11
11
|
class RunnerAlreadyStartedError < Error; end
|
|
12
12
|
|
|
13
|
-
autoload :VERSION,
|
|
14
|
-
autoload :Configuration,
|
|
15
|
-
autoload :
|
|
16
|
-
autoload :
|
|
17
|
-
autoload :
|
|
18
|
-
autoload :
|
|
19
|
-
autoload :
|
|
20
|
-
autoload :
|
|
21
|
-
autoload :
|
|
22
|
-
autoload :
|
|
23
|
-
autoload :
|
|
24
|
-
autoload :
|
|
13
|
+
autoload :VERSION, File.expand_path("fixture_kit/version", __dir__)
|
|
14
|
+
autoload :Configuration, File.expand_path("fixture_kit/configuration", __dir__)
|
|
15
|
+
autoload :ConfigurationHelper, File.expand_path("fixture_kit/configuration_helper", __dir__)
|
|
16
|
+
autoload :Singleton, File.expand_path("fixture_kit/singleton", __dir__)
|
|
17
|
+
autoload :Fixture, File.expand_path("fixture_kit/fixture", __dir__)
|
|
18
|
+
autoload :Definition, File.expand_path("fixture_kit/definition", __dir__)
|
|
19
|
+
autoload :Registry, File.expand_path("fixture_kit/registry", __dir__)
|
|
20
|
+
autoload :Repository, File.expand_path("fixture_kit/repository", __dir__)
|
|
21
|
+
autoload :SqlSubscriber, File.expand_path("fixture_kit/sql_subscriber", __dir__)
|
|
22
|
+
autoload :Cache, File.expand_path("fixture_kit/cache", __dir__)
|
|
23
|
+
autoload :Runner, File.expand_path("fixture_kit/runner", __dir__)
|
|
24
|
+
autoload :Isolator, File.expand_path("fixture_kit/isolator", __dir__)
|
|
25
|
+
autoload :MinitestIsolator, File.expand_path("fixture_kit/isolators/minitest_isolator", __dir__)
|
|
26
|
+
autoload :RSpecIsolator, File.expand_path("fixture_kit/isolators/rspec_isolator", __dir__)
|
|
25
27
|
|
|
26
28
|
extend Singleton
|
|
27
29
|
end
|
metadata
CHANGED
|
@@ -1,14 +1,14 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: fixture_kit
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.
|
|
4
|
+
version: 0.5.0
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Ngan Pham
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2026-02-
|
|
11
|
+
date: 2026-02-23 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -136,18 +136,19 @@ files:
|
|
|
136
136
|
- lib/fixture_kit.rb
|
|
137
137
|
- lib/fixture_kit/cache.rb
|
|
138
138
|
- lib/fixture_kit/configuration.rb
|
|
139
|
+
- lib/fixture_kit/configuration_helper.rb
|
|
139
140
|
- lib/fixture_kit/definition.rb
|
|
140
141
|
- lib/fixture_kit/fixture.rb
|
|
141
142
|
- lib/fixture_kit/isolator.rb
|
|
143
|
+
- lib/fixture_kit/isolators/minitest_isolator.rb
|
|
144
|
+
- lib/fixture_kit/isolators/rspec_isolator.rb
|
|
145
|
+
- lib/fixture_kit/minitest.rb
|
|
142
146
|
- lib/fixture_kit/registry.rb
|
|
143
147
|
- lib/fixture_kit/repository.rb
|
|
144
148
|
- lib/fixture_kit/rspec.rb
|
|
145
|
-
- lib/fixture_kit/rspec/isolator.rb
|
|
146
149
|
- lib/fixture_kit/runner.rb
|
|
147
150
|
- lib/fixture_kit/singleton.rb
|
|
148
151
|
- lib/fixture_kit/sql_subscriber.rb
|
|
149
|
-
- lib/fixture_kit/test_case.rb
|
|
150
|
-
- lib/fixture_kit/test_case/isolator.rb
|
|
151
152
|
- lib/fixture_kit/version.rb
|
|
152
153
|
homepage: https://github.com/Gusto/fixture_kit
|
|
153
154
|
licenses:
|
|
@@ -1,45 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
module FixtureKit
|
|
4
|
-
module RSpec
|
|
5
|
-
class Isolator < FixtureKit::Isolator
|
|
6
|
-
def run(&block)
|
|
7
|
-
previous_example = ::RSpec.current_example
|
|
8
|
-
previous_scope = ::RSpec.current_scope
|
|
9
|
-
example_group = build_example_group
|
|
10
|
-
example = build_example(example_group, &block)
|
|
11
|
-
instance = example_group.new(example.inspect_output)
|
|
12
|
-
succeeded =
|
|
13
|
-
begin
|
|
14
|
-
example.run(instance, ::RSpec::Core::NullReporter)
|
|
15
|
-
ensure
|
|
16
|
-
example_group.remove_example(example)
|
|
17
|
-
::RSpec.current_example = previous_example
|
|
18
|
-
::RSpec.current_scope = previous_scope
|
|
19
|
-
end
|
|
20
|
-
|
|
21
|
-
unless succeeded
|
|
22
|
-
raise example.exception if example.exception
|
|
23
|
-
raise FixtureKit::PregenerationError, "FixtureKit pregeneration failed"
|
|
24
|
-
end
|
|
25
|
-
end
|
|
26
|
-
|
|
27
|
-
private
|
|
28
|
-
|
|
29
|
-
def build_example(example_group, &block)
|
|
30
|
-
example_group.example(
|
|
31
|
-
"FixtureKit cache pregeneration"
|
|
32
|
-
) { block.call }
|
|
33
|
-
end
|
|
34
|
-
|
|
35
|
-
def build_example_group
|
|
36
|
-
::RSpec::Core::ExampleGroup.subclass(
|
|
37
|
-
::RSpec::Core::ExampleGroup,
|
|
38
|
-
"FixtureKit::RSpec::Isolator",
|
|
39
|
-
[],
|
|
40
|
-
[]
|
|
41
|
-
)
|
|
42
|
-
end
|
|
43
|
-
end
|
|
44
|
-
end
|
|
45
|
-
end
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "active_support/test_case"
|
|
4
|
-
require "active_record/fixtures"
|
|
5
|
-
|
|
6
|
-
module FixtureKit
|
|
7
|
-
module TestCase
|
|
8
|
-
class Isolator < FixtureKit::Isolator
|
|
9
|
-
TEST_METHOD_NAME = "test_fixture_kit_cache_pregeneration"
|
|
10
|
-
|
|
11
|
-
def run(&block)
|
|
12
|
-
result = build_test_class(&block).run
|
|
13
|
-
return if result.passed?
|
|
14
|
-
|
|
15
|
-
failure = result.failures.first
|
|
16
|
-
raise failure.error if failure.respond_to?(:error)
|
|
17
|
-
raise failure if failure
|
|
18
|
-
|
|
19
|
-
raise FixtureKit::PregenerationError, "FixtureKit pregeneration failed"
|
|
20
|
-
end
|
|
21
|
-
|
|
22
|
-
private
|
|
23
|
-
|
|
24
|
-
def build_test_class(&block)
|
|
25
|
-
Class.new(ActiveSupport::TestCase) do
|
|
26
|
-
::Minitest::Runnable.runnables.delete(self)
|
|
27
|
-
include(::ActiveRecord::TestFixtures)
|
|
28
|
-
|
|
29
|
-
define_method(TEST_METHOD_NAME) do
|
|
30
|
-
block.call
|
|
31
|
-
pass
|
|
32
|
-
end
|
|
33
|
-
end.new(TEST_METHOD_NAME)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|