fixture_kit 0.4.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: b9c12d67129c3c71b587913a62cea49c41f55a7ab9c0f4308961906517f7b6f7
4
- data.tar.gz: 92e5dec6ccdb3084d5b699fd1f462fb145667dd46caeb7f65aba8d782244bdbd
3
+ metadata.gz: c052323f7423cfeb258b137bc8372833eff2f4c8b826b3fa22b25a171b2b2c0f
4
+ data.tar.gz: '0728daee8814033465d2b93a67e15a00eff4abe5543967167077f81e68db7dde'
5
5
  SHA512:
6
- metadata.gz: 75484ce7d299f78263f0c268a39eacb4c33896075b90138ed6741e253b95d312144ced9481ae2e7e6328a4b8817bf8e566c7d56df86975e236b2f2a248643376
7
- data.tar.gz: c7644edbec83c88df2d5cc24a206ccabd4d3f1077a199fd2317c81e7e128801a58ed86738b229ed679b6ca3c1b979fe0f2b5adbf2334ed4ab16dbd2fa04b3ccf
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,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
 
@@ -152,8 +168,14 @@ FixtureKit.configure do |config|
152
168
 
153
169
  # Optional callback, called right before a fixture cache is generated.
154
170
  # 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}" }
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}" }
157
179
  end
158
180
  ```
159
181
 
@@ -168,7 +190,7 @@ When using `fixture_kit/rspec`, FixtureKit sets `FixtureKit::RSpecIsolator`. It
168
190
 
169
191
  Fixture generation is managed by `FixtureKit::Runner`.
170
192
 
171
- 1. Calling `fixture "name"` registers the fixture with the runner.
193
+ 1. Calling `fixture "name"` or `fixture do ... end` registers the fixture with the runner.
172
194
  2. Runner `start`:
173
195
  - clears `cache_path` (unless preserve-cache is enabled),
174
196
  - generates caches for all already-registered fixtures.
@@ -180,6 +202,13 @@ When runner start happens:
180
202
  - `fixture_kit/rspec`: in `before(:suite)`.
181
203
  - `fixture_kit/minitest`: lazily during test setup for the first test class that declares `fixture`.
182
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`.
211
+
183
212
  ### Preserving Cache Locally
184
213
 
185
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:
@@ -207,9 +236,18 @@ end
207
236
  fixture "teams/sales"
208
237
  ```
209
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
+
210
248
  ## How It Works
211
249
 
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.
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.
213
251
 
214
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.
215
253
 
@@ -224,8 +262,8 @@ Caches are stored as JSON files in `tmp/cache/fixture_kit/`:
224
262
  ```json
225
263
  {
226
264
  "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)"
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)"
229
267
  },
230
268
  "exposed": {
231
269
  "alice": { "model": "User", "id": 1 },
@@ -7,17 +7,30 @@ 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
+ normalized_scope = raw_identifier.to_s.sub(/\ARSpec::ExampleGroups::/, "")
31
+ File.join(ANONYMOUS_DIRECTORY, ActiveSupport::Inflector.underscore(normalized_scope))
32
+ end
33
+ end
21
34
  end
22
35
 
23
36
  def exists?
@@ -26,7 +39,7 @@ module FixtureKit
26
39
 
27
40
  def load
28
41
  unless exists?
29
- raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{fixture.name}'"
42
+ raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{fixture.identifier}'"
30
43
  end
31
44
 
32
45
  @data ||= JSON.parse(File.read(path))
@@ -46,12 +59,12 @@ module FixtureKit
46
59
  def save
47
60
  FixtureKit.runner.isolator.run do
48
61
  models = SqlSubscriber.capture do
49
- @definition.evaluate
62
+ fixture.definition.evaluate
50
63
  end
51
64
 
52
65
  @data = {
53
66
  "records" => generate_statements(models),
54
- "exposed" => build_exposed_mapping(@definition.exposed)
67
+ "exposed" => build_exposed_mapping(fixture.definition.exposed)
55
68
  }
56
69
  end
57
70
 
@@ -77,7 +90,7 @@ module FixtureKit
77
90
  model.find(id)
78
91
  rescue ActiveRecord::RecordNotFound
79
92
  raise FixtureKit::ExposedRecordNotFound,
80
- "Could not find #{model_name} with id=#{id} for exposed record '#{exposed_name}' in fixture '#{@fixture.name}'"
93
+ "Could not find #{model_name} with id=#{id} for exposed record '#{exposed_name}' in fixture '#{@fixture.identifier}'"
81
94
  end
82
95
 
83
96
  def generate_statements(models)
@@ -5,13 +5,15 @@ module FixtureKit
5
5
  attr_writer :fixture_path
6
6
  attr_writer :cache_path
7
7
  attr_accessor :isolator
8
- attr_accessor :on_cache
8
+ attr_accessor :on_cache_save
9
+ attr_accessor :on_cache_mount
9
10
 
10
11
  def initialize
11
12
  @fixture_path = "fixture_kit"
12
13
  @cache_path = nil
13
14
  @isolator = FixtureKit::MinitestIsolator
14
- @on_cache = nil
15
+ @on_cache_save = nil
16
+ @on_cache_mount = nil
15
17
  end
16
18
 
17
19
  def fixture_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,8 @@ 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
13
  end
14
14
  end
15
15
 
@@ -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,8 @@ 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
+ metadata[DECLARATION_METADATA_KEY] = ::RSpec.configuration.fixture_kit.register(self, name, &definition_block)
32
32
  end
33
33
  end
34
34
 
@@ -15,8 +15,8 @@ module FixtureKit
15
15
  @started = false
16
16
  end
17
17
 
18
- def register(name)
19
- 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|
20
20
  fixture.cache if started?
21
21
  end
22
22
  end
@@ -46,5 +46,16 @@ module FixtureKit
46
46
  def preserve_cache?
47
47
  ENV[PRESERVE_CACHE_ENV_KEY].to_s.match?(/\A(1|true|yes)\z/i)
48
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
49
60
  end
50
61
  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.5.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
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.5.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Ngan Pham