fixture_kit 0.4.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: b9c12d67129c3c71b587913a62cea49c41f55a7ab9c0f4308961906517f7b6f7
4
- data.tar.gz: 92e5dec6ccdb3084d5b699fd1f462fb145667dd46caeb7f65aba8d782244bdbd
3
+ metadata.gz: 9c4966344ca2fed5d11b47404694babb874ee5ac505ed64fda79acab147b4230
4
+ data.tar.gz: da1b34e717a3e6aa1b776b6be68e71c104d28612b879daf1b63c30a0b83244e0
5
5
  SHA512:
6
- metadata.gz: 75484ce7d299f78263f0c268a39eacb4c33896075b90138ed6741e253b95d312144ced9481ae2e7e6328a4b8817bf8e566c7d56df86975e236b2f2a248643376
7
- data.tar.gz: c7644edbec83c88df2d5cc24a206ccabd4d3f1077a199fd2317c81e7e128801a58ed86738b229ed679b6ca3c1b979fe0f2b5adbf2334ed4ab16dbd2fa04b3ccf
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
 
@@ -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,7 +133,7 @@ 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.
121
137
 
122
138
  ### 4. Configure Minitest
123
139
 
@@ -130,7 +146,7 @@ class ActiveSupport::TestCase
130
146
  end
131
147
  ```
132
148
 
133
- When you call `fixture "name"` in a test class, FixtureKit registers that fixture with its runner and mounts it during test setup.
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.
134
150
 
135
151
  ## Configuration
136
152
 
@@ -146,39 +162,63 @@ FixtureKit.configure do |config|
146
162
  # Where cache files are stored (default: tmp/cache/fixture_kit)
147
163
  config.cache_path = Rails.root.join("tmp/cache/fixture_kit").to_s
148
164
 
149
- # Wrapper used to isolate generation work (default: FixtureKit::MinitestIsolator)
150
- # config.isolator = FixtureKit::MinitestIsolator
151
- # 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.
152
172
 
153
173
  # Optional callback, called right before a fixture cache is generated.
154
174
  # Called on first generation and forced regeneration.
155
- # Receives the fixture name as a String.
156
- # config.on_cache = ->(fixture_name) { puts "cached #{fixture_name}" }
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"
178
+ # config.on_cache_save = ->(identifier) { puts "cached #{identifier}" }
179
+
180
+ # Optional callback, called right before a fixture cache is mounted.
181
+ # Receives the same String cache identifier as on_cache_save.
182
+ # config.on_cache_mount = ->(identifier) { puts "mounted #{identifier}" }
157
183
  end
158
184
  ```
159
185
 
160
- Custom isolators should subclass `FixtureKit::Isolator` and implement `#run`.
161
- `#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`
189
+
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`.
162
192
 
163
- By default, FixtureKit uses `FixtureKit::MinitestIsolator`, which runs generation inside an internal `ActiveSupport::TestCase` and removes that harness case from minitest runnables.
193
+ Options passed via `config.adapter(...)` are provided to your adapter initializer as a hash and available as `options`.
164
194
 
165
- 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.
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.
166
198
 
167
199
  ## Lifecycle
168
200
 
169
201
  Fixture generation is managed by `FixtureKit::Runner`.
170
202
 
171
- 1. Calling `fixture "name"` registers the fixture with the runner.
203
+ 1. Calling `fixture "name"` or `fixture do ... end` registers the fixture with the runner.
172
204
  2. Runner `start`:
173
- - clears `cache_path` (unless preserve-cache is enabled),
174
- - generates caches for all already-registered fixtures.
175
- 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.
176
209
  4. At test runtime, `fixture` mounts from cache and returns a `Repository`.
177
210
 
178
211
  When runner start happens:
179
212
 
180
213
  - `fixture_kit/rspec`: in `before(:suite)`.
181
- - `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`.
215
+
216
+ ## Fixture Declaration Rules
217
+
218
+ - Only one `fixture` declaration is allowed per test context.
219
+ - Declaring a fixture twice in the same context raises `FixtureKit::MultipleFixtures`.
220
+ - Child contexts/classes can declare their own fixture and override parent declarations.
221
+ - Providing both a name and a block (or neither) raises `FixtureKit::InvalidFixtureDeclaration`.
182
222
 
183
223
  ### Preserving Cache Locally
184
224
 
@@ -207,9 +247,19 @@ end
207
247
  fixture "teams/sales"
208
248
  ```
209
249
 
250
+ ## Anonymous Fixture Cache Paths
251
+
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`.
254
+
255
+ - Minitest: class name is underscored into a path.
256
+ - `MyFeatureTest` -> `_anonymous/my_feature_test.json`
257
+ - RSpec: class name is underscored after removing `RSpec::ExampleGroups::`.
258
+ - `RSpec::ExampleGroups::Foo::WithFixtureKit::Hello` -> `_anonymous/foo/with_fixture_kit/hello.json`
259
+
210
260
  ## How It Works
211
261
 
212
- 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 INSERT 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.
213
263
 
214
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.
215
265
 
@@ -224,8 +274,8 @@ Caches are stored as JSON files in `tmp/cache/fixture_kit/`:
224
274
  ```json
225
275
  {
226
276
  "records": {
227
- "User": "INSERT OR IGNORE INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com'), (2, 'Bob', 'bob@example.com')",
228
- "Project": "INSERT OR IGNORE INTO projects (id, name, user_id) VALUES (1, 'Website', 1)"
277
+ "User": "INSERT INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com'), (2, 'Bob', 'bob@example.com')",
278
+ "Project": "INSERT INTO projects (id, name, user_id) VALUES (1, 'Website', 1)"
229
279
  },
230
280
  "exposed": {
231
281
  "alice": { "model": "User", "id": 1 },
@@ -235,7 +285,7 @@ Caches are stored as JSON files in `tmp/cache/fixture_kit/`:
235
285
  }
236
286
  ```
237
287
 
238
- - **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.
239
289
  - **exposed**: Maps fixture accessor names to their model class and ID for querying after cache replay.
240
290
 
241
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
@@ -7,17 +7,29 @@ require "active_support/inflector"
7
7
 
8
8
  module FixtureKit
9
9
  class Cache
10
+ ANONYMOUS_DIRECTORY = "_anonymous"
11
+
10
12
  include ConfigurationHelper
11
13
 
12
14
  attr_reader :fixture
13
15
 
14
- def initialize(fixture, definition)
16
+ def initialize(fixture)
15
17
  @fixture = fixture
16
- @definition = definition
17
18
  end
18
19
 
19
20
  def path
20
- File.join(configuration.cache_path, "#{fixture.name}.json")
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
+ File.join(ANONYMOUS_DIRECTORY, FixtureKit.runner.adapter.identifier_for(raw_identifier))
31
+ end
32
+ end
21
33
  end
22
34
 
23
35
  def exists?
@@ -26,17 +38,15 @@ module FixtureKit
26
38
 
27
39
  def load
28
40
  unless exists?
29
- raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{fixture.name}'"
41
+ raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{fixture.identifier}'"
30
42
  end
31
43
 
32
44
  @data ||= JSON.parse(File.read(path))
33
- @data.fetch("records").each do |model_name, sql|
34
- model = ActiveSupport::Inflector.constantize(model_name)
35
- connection = model.connection
45
+ statements_by_connection(@data.fetch("records")).each do |connection, statements|
36
46
  connection.disable_referential_integrity do
37
47
  # execute_batch is private in current supported Rails versions.
38
48
  # This should be revisited when Rails 8.2 makes it public.
39
- connection.__send__(:execute_batch, [build_delete_sql(model), sql].compact, "FixtureKit Load")
49
+ connection.__send__(:execute_batch, statements, "FixtureKit Load")
40
50
  end
41
51
  end
42
52
 
@@ -44,14 +54,14 @@ module FixtureKit
44
54
  end
45
55
 
46
56
  def save
47
- FixtureKit.runner.isolator.run do
57
+ FixtureKit.runner.adapter.execute do
48
58
  models = SqlSubscriber.capture do
49
- @definition.evaluate
59
+ fixture.definition.evaluate
50
60
  end
51
61
 
52
62
  @data = {
53
63
  "records" => generate_statements(models),
54
- "exposed" => build_exposed_mapping(@definition.exposed)
64
+ "exposed" => build_exposed_mapping(fixture.definition.exposed)
55
65
  }
56
66
  end
57
67
 
@@ -77,7 +87,7 @@ module FixtureKit
77
87
  model.find(id)
78
88
  rescue ActiveRecord::RecordNotFound
79
89
  raise FixtureKit::ExposedRecordNotFound,
80
- "Could not find #{model_name} with id=#{id} for exposed record '#{exposed_name}' in fixture '#{@fixture.name}'"
90
+ "Could not find #{model_name} with id=#{id} for exposed record '#{exposed_name}' in fixture '#{@fixture.identifier}'"
81
91
  end
82
92
 
83
93
  def generate_statements(models)
@@ -120,5 +130,15 @@ module FixtureKit
120
130
  hash[name] = was_array ? records : records.first
121
131
  end
122
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
123
143
  end
124
144
  end
@@ -4,14 +4,17 @@ module FixtureKit
4
4
  class Configuration
5
5
  attr_writer :fixture_path
6
6
  attr_writer :cache_path
7
- attr_accessor :isolator
8
- attr_accessor :on_cache
7
+ attr_accessor :on_cache_save
8
+ attr_accessor :on_cache_mount
9
+ attr_reader :adapter_options
9
10
 
10
11
  def initialize
11
12
  @fixture_path = "fixture_kit"
12
13
  @cache_path = nil
13
- @isolator = FixtureKit::MinitestIsolator
14
- @on_cache = nil
14
+ @adapter_class = FixtureKit::MinitestAdapter
15
+ @adapter_options = {}
16
+ @on_cache_save = nil
17
+ @on_cache_mount = nil
15
18
  end
16
19
 
17
20
  def fixture_path
@@ -22,6 +25,13 @@ module FixtureKit
22
25
  @cache_path ||= detect_cache_path
23
26
  end
24
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
+
25
35
  private
26
36
 
27
37
  def detect_cache_path
@@ -2,10 +2,11 @@
2
2
 
3
3
  module FixtureKit
4
4
  class Definition
5
- attr_reader :exposed
5
+ attr_reader :exposed, :source_location
6
6
 
7
7
  def initialize(&definition)
8
8
  @definition = definition
9
+ @source_location = definition.source_location
9
10
  @exposed = {}
10
11
  end
11
12
 
@@ -4,37 +4,28 @@ module FixtureKit
4
4
  class Fixture
5
5
  include ConfigurationHelper
6
6
 
7
- attr_reader :name, :path
7
+ attr_reader :identifier, :definition
8
8
 
9
- def initialize(name, path)
10
- @name = name
11
- @path = path
12
- @cache = Cache.new(self, definition)
9
+ def initialize(identifier, definition)
10
+ @identifier = identifier
11
+ @definition = definition
12
+ @cache = Cache.new(self)
13
13
  end
14
14
 
15
15
  def cache(force: false)
16
16
  return if @cache.exists? && !force
17
17
 
18
- configuration.on_cache&.call(name)
18
+ configuration.on_cache_save&.call(@cache.identifier)
19
19
  @cache.save
20
20
  end
21
21
 
22
22
  def mount
23
23
  unless @cache.exists?
24
- raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{name}'"
24
+ raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{identifier}'"
25
25
  end
26
26
 
27
+ configuration.on_cache_mount&.call(@cache.identifier)
27
28
  @cache.load
28
29
  end
29
-
30
- private
31
-
32
- def definition
33
- @definition ||= begin
34
- definition = eval(File.read(@path), TOPLEVEL_BINDING.dup, @path)
35
- raise FixtureKit::FixtureDefinitionNotFound, "Could not find fixture definition at '#{@path}'" unless definition.is_a?(Definition)
36
- definition
37
- end
38
- end
39
30
  end
40
31
  end
@@ -8,8 +8,19 @@ module FixtureKit
8
8
  DECLARATION_CLASS_ATTRIBUTE = :fixture_kit_declaration
9
9
 
10
10
  module ClassMethods
11
- def fixture(name)
12
- self.fixture_kit_declaration = FixtureKit.runner.register(name)
11
+ def fixture(name = nil, &definition_block)
12
+ self.fixture_kit_declaration = FixtureKit.runner.register(self, name, &definition_block)
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
13
24
  end
14
25
  end
15
26
 
@@ -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
@@ -1,35 +1,56 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- require "pathname"
4
-
5
3
  module FixtureKit
6
4
  class Registry
7
5
  include ConfigurationHelper
8
6
 
9
7
  def initialize
10
- @registry = {}
8
+ @declarations = {}
9
+ @fixtures = {}
11
10
  end
12
11
 
13
- def add(name)
14
- return @registry[name] if @registry.key?(name)
15
-
16
- file_path = fixture_file_path(name)
17
- unless File.file?(file_path)
18
- raise FixtureKit::FixtureDefinitionNotFound,
19
- "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"
20
15
  end
21
16
 
22
- @registry[name] = Fixture.new(name, file_path)
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
23
26
  end
24
27
 
25
28
  def fixtures
26
- @registry.values
29
+ @fixtures.values
27
30
  end
28
31
 
29
32
  private
30
33
 
31
- def fixture_file_path(name)
32
- File.expand_path(File.join(configuration.fixture_path, "#{name}.rb"))
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}'"
33
54
  end
34
55
  end
35
56
  end
@@ -27,8 +27,13 @@ module FixtureKit
27
27
  # end
28
28
  # end
29
29
  # end
30
- def fixture(name)
31
- metadata[DECLARATION_METADATA_KEY] = ::RSpec.configuration.fixture_kit.register(name)
30
+ def fixture(name = nil, &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
- def register(name)
19
- registry.add(name).tap do |fixture|
20
- fixture.cache if started?
21
- end
18
+ def register(scope, name = nil, &definition_block)
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?
@@ -46,5 +43,16 @@ module FixtureKit
46
43
  def preserve_cache?
47
44
  ENV[PRESERVE_CACHE_ENV_KEY].to_s.match?(/\A(1|true|yes)\z/i)
48
45
  end
46
+
47
+ def normalize_registration(name, definition_block)
48
+ if name && definition_block
49
+ raise FixtureKit::InvalidFixtureDeclaration, "cannot provide both fixture name and definition block"
50
+ end
51
+
52
+ name_or_block = name || definition_block
53
+ return name_or_block if name_or_block
54
+
55
+ raise FixtureKit::InvalidFixtureDeclaration, "must provide fixture name or definition block"
56
+ end
49
57
  end
50
58
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FixtureKit
4
- VERSION = "0.4.0"
4
+ VERSION = "0.6.0"
5
5
  end
data/lib/fixture_kit.rb CHANGED
@@ -3,6 +3,8 @@
3
3
  module FixtureKit
4
4
  class Error < StandardError; end
5
5
  class DuplicateNameError < Error; end
6
+ class InvalidFixtureDeclaration < Error; end
7
+ class MultipleFixtures < Error; end
6
8
  class CacheMissingError < Error; end
7
9
  class FixtureDefinitionNotFound < Error; end
8
10
  class ExposedRecordNotFound < Error; end
@@ -19,9 +21,9 @@ module FixtureKit
19
21
  autoload :SqlSubscriber, File.expand_path("fixture_kit/sql_subscriber", __dir__)
20
22
  autoload :Cache, File.expand_path("fixture_kit/cache", __dir__)
21
23
  autoload :Runner, File.expand_path("fixture_kit/runner", __dir__)
22
- autoload :Isolator, File.expand_path("fixture_kit/isolator", __dir__)
23
- autoload :MinitestIsolator, File.expand_path("fixture_kit/isolators/minitest_isolator", __dir__)
24
- 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__)
25
27
 
26
28
  extend Singleton
27
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.4.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