fixture_kit 0.5.0 → 0.6.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: c052323f7423cfeb258b137bc8372833eff2f4c8b826b3fa22b25a171b2b2c0f
4
- data.tar.gz: '0728daee8814033465d2b93a67e15a00eff4abe5543967167077f81e68db7dde'
3
+ metadata.gz: 9c4966344ca2fed5d11b47404694babb874ee5ac505ed64fda79acab147b4230
4
+ data.tar.gz: da1b34e717a3e6aa1b776b6be68e71c104d28612b879daf1b63c30a0b83244e0
5
5
  SHA512:
6
- metadata.gz: 82149fa2c3598b2af00b2cd9b8e3691e8694f18bdc40a0abf3a47540e7c319e0821250bec0e3dce4d8aaf692ea614a17cda09970a476503b0280c7a301b95e2f
7
- data.tar.gz: f4fe1ea329c354aee57ad6f53d22682309b8db9704340f52d18c4b01b42a83a47701d36fe11e739d644e13b72d02171190a530df6847386770a5edc6b786cbf2
6
+ metadata.gz: 8d127ec7c68e6839bda857231d2fe0f70263d208aad9615834cad70e47ea2cbecd3b270dea7f795d02452a72b0f155694b3c6e78eb589349a639e7158bfcf902
7
+ data.tar.gz: 69149bc49f370405cd89043eced96984cc4a3a3efa3e85ce3e45ba15a4859db3805862b14ebfb7171a7b88059d52e1479b201fbb2ee582b379be6c8ef2de9830
data/README.md CHANGED
@@ -10,7 +10,7 @@ Test data setup is slow. Every `Model.create!` or `FactoryBot.create` hits the d
10
10
 
11
11
  FixtureKit caches database records as raw SQL INSERT statements. It executes your fixture definition once, captures the resulting database state, and generates optimized batch INSERT statements. Fixture loads then replay these statements directly: no ORM overhead, no callbacks, just fast SQL.
12
12
 
13
- Combined with RSpec's transactional fixtures, each test runs in a transaction that rolls backso cached data can be reused across tests without cleanup.
13
+ Combined with framework transactions (`use_transactional_fixtures` in RSpec, `use_transactional_tests` in Minitest), each test runs in a transaction that rolls back, so cached data can be reused safely between tests.
14
14
 
15
15
  ## Installation
16
16
 
@@ -162,29 +162,39 @@ FixtureKit.configure do |config|
162
162
  # Where cache files are stored (default: tmp/cache/fixture_kit)
163
163
  config.cache_path = Rails.root.join("tmp/cache/fixture_kit").to_s
164
164
 
165
- # Wrapper used to isolate generation work (default: FixtureKit::MinitestIsolator)
166
- # config.isolator = FixtureKit::MinitestIsolator
167
- # config.isolator = FixtureKit::RSpecIsolator
165
+ # Adapter used to isolate generation work (default: FixtureKit::MinitestAdapter)
166
+ # config.adapter(FixtureKit::MinitestAdapter)
167
+ # config.adapter(FixtureKit::RSpecAdapter)
168
+ # config.adapter(CustomAdapter, option1: "value1")
169
+ #
170
+ # Calling `adapter` with args sets adapter class + options.
171
+ # Calling `adapter` with no args returns the configured adapter class.
168
172
 
169
173
  # Optional callback, called right before a fixture cache is generated.
170
174
  # 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
175
+ # Receives the cache identifier as a String (path-like, without ".json").
176
+ # - named fixtures: "teams/basic"
177
+ # - anonymous fixtures: "_anonymous/foo/with_fixture_kit/hello"
174
178
  # config.on_cache_save = ->(identifier) { puts "cached #{identifier}" }
175
179
 
176
180
  # Optional callback, called right before a fixture cache is mounted.
177
- # Receives the same fixture identifier shape as on_cache_save.
181
+ # Receives the same String cache identifier as on_cache_save.
178
182
  # config.on_cache_mount = ->(identifier) { puts "mounted #{identifier}" }
179
183
  end
180
184
  ```
181
185
 
182
- Custom isolators should subclass `FixtureKit::Isolator` and implement `#run`.
183
- `#run` receives the generation block and should execute it in whatever lifecycle you need.
186
+ Custom adapters should subclass `FixtureKit::Adapter` and implement:
187
+ - `#execute`
188
+ - `#identifier_for`
184
189
 
185
- By default, FixtureKit uses `FixtureKit::MinitestIsolator`, which runs generation inside an internal `ActiveSupport::TestCase` and removes that harness case from minitest runnables.
190
+ `#execute` receives the generation block and should run it in whatever lifecycle you need.
191
+ `#identifier_for` receives a non-string fixture identifier (for anonymous fixtures) and must return a normalized String identifier. Cache namespace/prefixing is applied by `FixtureKit::Cache`.
186
192
 
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.
193
+ Options passed via `config.adapter(...)` are provided to your adapter initializer as a hash and available as `options`.
194
+
195
+ By default, FixtureKit uses `FixtureKit::MinitestAdapter`, which runs generation inside an internal `ActiveSupport::TestCase` and removes that harness case from minitest runnables.
196
+
197
+ When using `fixture_kit/rspec`, FixtureKit sets `FixtureKit::RSpecAdapter`. It runs generation inside an internal RSpec example, and uses a null reporter so harness runs do not count toward suite example totals.
188
198
 
189
199
  ## Lifecycle
190
200
 
@@ -192,15 +202,16 @@ Fixture generation is managed by `FixtureKit::Runner`.
192
202
 
193
203
  1. Calling `fixture "name"` or `fixture do ... end` registers the fixture with the runner.
194
204
  2. Runner `start`:
195
- - clears `cache_path` (unless preserve-cache is enabled),
196
- - generates caches for all already-registered fixtures.
197
- 3. If new tests are loaded after start (for example, queue-mode CI runners), newly registered fixtures are cached immediately.
205
+ - clears `cache_path` (unless preserve-cache is enabled).
206
+ 3. Cache generation is framework-driven:
207
+ - RSpec: each declaring example group runs `fixture.cache` in `before(:context)`.
208
+ - Minitest: each declaring test class runs `fixture.cache` in class-level `run_suite` before test methods execute.
198
209
  4. At test runtime, `fixture` mounts from cache and returns a `Repository`.
199
210
 
200
211
  When runner start happens:
201
212
 
202
213
  - `fixture_kit/rspec`: in `before(:suite)`.
203
- - `fixture_kit/minitest`: lazily during test setup for the first test class that declares `fixture`.
214
+ - `fixture_kit/minitest`: in class-level `run_suite` for test classes that declare `fixture`.
204
215
 
205
216
  ## Fixture Declaration Rules
206
217
 
@@ -239,6 +250,7 @@ fixture "teams/sales"
239
250
  ## Anonymous Fixture Cache Paths
240
251
 
241
252
  Anonymous fixture caches are written under the `_anonymous/` directory inside `cache_path`.
253
+ That `_anonymous/...` value is also the cache identifier passed to `on_cache_save` and `on_cache_mount`.
242
254
 
243
255
  - Minitest: class name is underscored into a path.
244
256
  - `MyFeatureTest` -> `_anonymous/my_feature_test.json`
@@ -247,7 +259,7 @@ Anonymous fixture caches are written under the `_anonymous/` directory inside `c
247
259
 
248
260
  ## How It Works
249
261
 
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.
262
+ 1. **Cache generation**: FixtureKit executes your definition block inside the configured adapter, subscribes to `sql.active_record` notifications to track model writes (`Insert`, `Upsert`, `Create`, `Update`, `Delete`, `Destroy`, including bulk variants), queries those model tables, and caches SQL statements for current table contents.
251
263
 
252
264
  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.
253
265
 
@@ -273,7 +285,7 @@ Caches are stored as JSON files in `tmp/cache/fixture_kit/`:
273
285
  }
274
286
  ```
275
287
 
276
- - **records**: Maps model names to their INSERT statements. Using model names (not table names) allows FixtureKit to use the correct database connection for multi-database setups.
288
+ - **records**: Maps model names to their INSERT statements (or `null` when a tracked table is empty). Using model names (not table names) allows FixtureKit to use the correct database connection for multi-database setups.
277
289
  - **exposed**: Maps fixture accessor names to their model class and ID for querying after cache replay.
278
290
 
279
291
  ## Cache Management
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FixtureKit
4
+ class Adapter
5
+ attr_reader :options
6
+
7
+ def initialize(options = {})
8
+ @options = options
9
+ end
10
+
11
+ def execute
12
+ raise NotImplementedError, "#{self.class} must implement #execute"
13
+ end
14
+
15
+ def identifier_for(_identifier)
16
+ raise NotImplementedError, "#{self.class} must implement #identifier_for"
17
+ end
18
+ end
19
+ end
@@ -2,12 +2,13 @@
2
2
 
3
3
  require "active_support/test_case"
4
4
  require "active_record/fixtures"
5
+ require "active_support/inflector"
5
6
 
6
7
  module FixtureKit
7
- class MinitestIsolator < FixtureKit::Isolator
8
+ class MinitestAdapter < FixtureKit::Adapter
8
9
  TEST_NAME = "fixture kit cache pregeneration"
9
10
 
10
- def run(&block)
11
+ def execute(&block)
11
12
  test_class = build_test_class
12
13
  test_method = test_class.test(TEST_NAME) do
13
14
  block.call
@@ -20,6 +21,10 @@ module FixtureKit
20
21
  raise result.failures.first.error
21
22
  end
22
23
 
24
+ def identifier_for(identifier)
25
+ ActiveSupport::Inflector.underscore(identifier.to_s)
26
+ end
27
+
23
28
  private
24
29
 
25
30
  def build_test_class
@@ -1,8 +1,10 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/inflector"
4
+
3
5
  module FixtureKit
4
- class RSpecIsolator < FixtureKit::Isolator
5
- def run(&block)
6
+ class RSpecAdapter < FixtureKit::Adapter
7
+ def execute(&block)
6
8
  previous_example = ::RSpec.current_example
7
9
  previous_scope = ::RSpec.current_scope
8
10
  example_group = build_example_group
@@ -19,6 +21,11 @@ module FixtureKit
19
21
  raise example.exception unless succeeded
20
22
  end
21
23
 
24
+ def identifier_for(identifier)
25
+ normalized_scope = identifier.to_s.sub(/\ARSpec::ExampleGroups::/, "")
26
+ ActiveSupport::Inflector.underscore(normalized_scope)
27
+ end
28
+
22
29
  private
23
30
 
24
31
  def build_example_group
@@ -27,8 +27,7 @@ module FixtureKit
27
27
  if raw_identifier.is_a?(String)
28
28
  raw_identifier
29
29
  else
30
- normalized_scope = raw_identifier.to_s.sub(/\ARSpec::ExampleGroups::/, "")
31
- File.join(ANONYMOUS_DIRECTORY, ActiveSupport::Inflector.underscore(normalized_scope))
30
+ File.join(ANONYMOUS_DIRECTORY, FixtureKit.runner.adapter.identifier_for(raw_identifier))
32
31
  end
33
32
  end
34
33
  end
@@ -43,13 +42,11 @@ module FixtureKit
43
42
  end
44
43
 
45
44
  @data ||= JSON.parse(File.read(path))
46
- @data.fetch("records").each do |model_name, sql|
47
- model = ActiveSupport::Inflector.constantize(model_name)
48
- connection = model.connection
45
+ statements_by_connection(@data.fetch("records")).each do |connection, statements|
49
46
  connection.disable_referential_integrity do
50
47
  # execute_batch is private in current supported Rails versions.
51
48
  # This should be revisited when Rails 8.2 makes it public.
52
- connection.__send__(:execute_batch, [build_delete_sql(model), sql].compact, "FixtureKit Load")
49
+ connection.__send__(:execute_batch, statements, "FixtureKit Load")
53
50
  end
54
51
  end
55
52
 
@@ -57,7 +54,7 @@ module FixtureKit
57
54
  end
58
55
 
59
56
  def save
60
- FixtureKit.runner.isolator.run do
57
+ FixtureKit.runner.adapter.execute do
61
58
  models = SqlSubscriber.capture do
62
59
  fixture.definition.evaluate
63
60
  end
@@ -133,5 +130,15 @@ module FixtureKit
133
130
  hash[name] = was_array ? records : records.first
134
131
  end
135
132
  end
133
+
134
+ def statements_by_connection(records)
135
+ records.each_with_object({}) do |(model_name, sql), grouped|
136
+ model = ActiveSupport::Inflector.constantize(model_name)
137
+ connection = model.connection
138
+ grouped[connection] ||= []
139
+ grouped[connection] << build_delete_sql(model)
140
+ grouped[connection] << sql if sql
141
+ end
142
+ end
136
143
  end
137
144
  end
@@ -4,14 +4,15 @@ module FixtureKit
4
4
  class Configuration
5
5
  attr_writer :fixture_path
6
6
  attr_writer :cache_path
7
- attr_accessor :isolator
8
7
  attr_accessor :on_cache_save
9
8
  attr_accessor :on_cache_mount
9
+ attr_reader :adapter_options
10
10
 
11
11
  def initialize
12
12
  @fixture_path = "fixture_kit"
13
13
  @cache_path = nil
14
- @isolator = FixtureKit::MinitestIsolator
14
+ @adapter_class = FixtureKit::MinitestAdapter
15
+ @adapter_options = {}
15
16
  @on_cache_save = nil
16
17
  @on_cache_mount = nil
17
18
  end
@@ -24,6 +25,13 @@ module FixtureKit
24
25
  @cache_path ||= detect_cache_path
25
26
  end
26
27
 
28
+ def adapter(adapter_class = nil, **options)
29
+ return @adapter_class if adapter_class.nil? && options.empty?
30
+
31
+ @adapter_class = adapter_class
32
+ @adapter_options = options
33
+ end
34
+
27
35
  private
28
36
 
29
37
  def detect_cache_path
@@ -11,6 +11,17 @@ module FixtureKit
11
11
  def fixture(name = nil, &definition_block)
12
12
  self.fixture_kit_declaration = FixtureKit.runner.register(self, name, &definition_block)
13
13
  end
14
+
15
+ def run_suite(reporter, options = {})
16
+ declaration = fixture_kit_declaration
17
+ if declaration && !filter_runnable_methods(options).empty?
18
+ runner = FixtureKit.runner
19
+ runner.start unless runner.started?
20
+ declaration.cache
21
+ end
22
+
23
+ super
24
+ end
14
25
  end
15
26
 
16
27
  module InstanceMethods
@@ -21,7 +32,7 @@ module FixtureKit
21
32
 
22
33
  def self.configure!(test_case)
23
34
  FixtureKit.runner.configuration.fixture_path = "test/fixture_kit"
24
- FixtureKit.runner.configuration.isolator = FixtureKit::MinitestIsolator
35
+ FixtureKit.runner.configuration.adapter(FixtureKit::MinitestAdapter)
25
36
 
26
37
  test_case.class_attribute DECLARATION_CLASS_ATTRIBUTE, instance_accessor: false
27
38
  test_case.extend ClassMethods
@@ -31,7 +42,6 @@ module FixtureKit
31
42
  declaration = self.class.fixture_kit_declaration
32
43
  next unless declaration
33
44
 
34
- FixtureKit.runner.start unless FixtureKit.runner.started?
35
45
  @_fixture_kit_repository = declaration.mount
36
46
  end
37
47
  end
@@ -28,7 +28,12 @@ module FixtureKit
28
28
  # end
29
29
  # end
30
30
  def fixture(name = nil, &definition_block)
31
- metadata[DECLARATION_METADATA_KEY] = ::RSpec.configuration.fixture_kit.register(self, name, &definition_block)
31
+ declaration = ::RSpec.configuration.fixture_kit.register(self, name, &definition_block)
32
+ metadata[DECLARATION_METADATA_KEY] = declaration
33
+
34
+ prepend_before(:context) do
35
+ self.class.metadata[DECLARATION_METADATA_KEY].cache
36
+ end
32
37
  end
33
38
  end
34
39
 
@@ -42,9 +47,9 @@ module FixtureKit
42
47
  end
43
48
 
44
49
  def self.configure!(config)
45
- FixtureKit.runner.configuration.fixture_path = "spec/fixture_kit"
46
50
  config.add_setting(:fixture_kit, default: FixtureKit.runner)
47
- FixtureKit.runner.configuration.isolator = FixtureKit::RSpecIsolator
51
+ FixtureKit.runner.configuration.fixture_path = "spec/fixture_kit"
52
+ FixtureKit.runner.configuration.adapter(FixtureKit::RSpecAdapter)
48
53
 
49
54
  config.extend ClassMethods
50
55
  config.include InstanceMethods, DECLARATION_METADATA_KEY
@@ -11,14 +11,12 @@ module FixtureKit
11
11
  def initialize
12
12
  @configuration = Configuration.new
13
13
  @registry = Registry.new
14
- @isolator = nil
14
+ @adapter = nil
15
15
  @started = false
16
16
  end
17
17
 
18
18
  def register(scope, name = nil, &definition_block)
19
- registry.add(scope, normalize_registration(name, definition_block)).tap do |fixture|
20
- fixture.cache if started?
21
- end
19
+ registry.add(scope, normalize_registration(name, definition_block))
22
20
  end
23
21
 
24
22
  def start
@@ -26,11 +24,10 @@ module FixtureKit
26
24
  @started = true
27
25
 
28
26
  clear_cache unless preserve_cache?
29
- registry.fixtures.each(&:cache)
30
27
  end
31
28
 
32
- def isolator
33
- @isolator ||= configuration.isolator.new
29
+ def adapter
30
+ @adapter ||= configuration.adapter.new(configuration.adapter_options)
34
31
  end
35
32
 
36
33
  def started?
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FixtureKit
4
- VERSION = "0.5.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/fixture_kit.rb CHANGED
@@ -21,9 +21,9 @@ module FixtureKit
21
21
  autoload :SqlSubscriber, File.expand_path("fixture_kit/sql_subscriber", __dir__)
22
22
  autoload :Cache, File.expand_path("fixture_kit/cache", __dir__)
23
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__)
24
+ autoload :Adapter, File.expand_path("fixture_kit/adapter", __dir__)
25
+ autoload :MinitestAdapter, File.expand_path("fixture_kit/adapters/minitest_adapter", __dir__)
26
+ autoload :RSpecAdapter, File.expand_path("fixture_kit/adapters/rspec_adapter", __dir__)
27
27
 
28
28
  extend Singleton
29
29
  end
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fixture_kit
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.5.0
4
+ version: 0.6.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ngan Pham
@@ -134,14 +134,14 @@ files:
134
134
  - LICENSE
135
135
  - README.md
136
136
  - lib/fixture_kit.rb
137
+ - lib/fixture_kit/adapter.rb
138
+ - lib/fixture_kit/adapters/minitest_adapter.rb
139
+ - lib/fixture_kit/adapters/rspec_adapter.rb
137
140
  - lib/fixture_kit/cache.rb
138
141
  - lib/fixture_kit/configuration.rb
139
142
  - lib/fixture_kit/configuration_helper.rb
140
143
  - lib/fixture_kit/definition.rb
141
144
  - lib/fixture_kit/fixture.rb
142
- - lib/fixture_kit/isolator.rb
143
- - lib/fixture_kit/isolators/minitest_isolator.rb
144
- - lib/fixture_kit/isolators/rspec_isolator.rb
145
145
  - lib/fixture_kit/minitest.rb
146
146
  - lib/fixture_kit/registry.rb
147
147
  - lib/fixture_kit/repository.rb
@@ -1,14 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- module FixtureKit
4
- # Base class for fixture cache isolators.
5
- class Isolator
6
- def self.run(&block)
7
- new.run(&block)
8
- end
9
-
10
- def run
11
- raise NotImplementedError, "#{self.class} must implement #run"
12
- end
13
- end
14
- end