fixture_kit 0.2.0 → 0.3.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 +31 -31
- data/lib/fixture_kit/cache.rb +42 -89
- data/lib/fixture_kit/configuration.rb +4 -4
- data/lib/fixture_kit/{definition_context.rb → definition.rb} +8 -9
- data/lib/fixture_kit/fixture.rb +28 -7
- data/lib/fixture_kit/{generator.rb → isolator.rb} +2 -2
- data/lib/fixture_kit/registry.rb +21 -45
- data/lib/fixture_kit/rspec/{generator.rb → isolator.rb} +2 -2
- data/lib/fixture_kit/rspec.rb +9 -30
- data/lib/fixture_kit/runner.rb +24 -72
- data/lib/fixture_kit/singleton.rb +8 -17
- data/lib/fixture_kit/sql_subscriber.rb +29 -0
- data/lib/fixture_kit/test_case/{generator.rb → isolator.rb} +1 -1
- data/lib/fixture_kit/test_case.rb +1 -1
- data/lib/fixture_kit/version.rb +1 -1
- data/lib/fixture_kit.rb +4 -3
- metadata +7 -8
- data/lib/fixture_kit/rspec/declaration.rb +0 -17
- data/lib/fixture_kit/sql_capture.rb +0 -37
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 23151d16a20be7d13a1c4a15ce4dca10a2c742ba68a1d43af339bdad65490679
|
|
4
|
+
data.tar.gz: 1c9da63eae90bbe6843d204d138fafbf3b933f0ea4fd9f42c4108331509f472b
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 3b75d5d28b153bb082f8097309c39c6d350633faac76e1b30e5ea7413cf3bccfa9b93a3941f6eff607306f43cd53a7bc5420fd1d331fc99c66ede324f82e3902
|
|
7
|
+
data.tar.gz: 92d48a55f2a92f4f1673a890e9bba8ce6695204b7a020e3240a9db21217747c3d0f7446297706cc634a15ea3246e29d0b0adcad870152cd449056c67b75cae45
|
data/README.md
CHANGED
|
@@ -8,7 +8,7 @@ Test data setup is slow. Every `Model.create!` or `FactoryBot.create` hits the d
|
|
|
8
8
|
|
|
9
9
|
## The Solution
|
|
10
10
|
|
|
11
|
-
FixtureKit caches database records as raw SQL INSERT statements.
|
|
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
13
|
Combined with RSpec's transactional fixtures, each test runs in a transaction that rolls back—so cached data can be reused across tests without cleanup.
|
|
14
14
|
|
|
@@ -26,7 +26,7 @@ end
|
|
|
26
26
|
|
|
27
27
|
### 1. Define a Fixture
|
|
28
28
|
|
|
29
|
-
Create fixture files in `spec/fixture_kit
|
|
29
|
+
Create fixture files in `spec/fixture_kit/` (or `test/fixture_kit/` for test-unit/minitest-style setups). Use whatever method you prefer to create records.
|
|
30
30
|
|
|
31
31
|
**Using ActiveRecord directly:**
|
|
32
32
|
|
|
@@ -117,6 +117,8 @@ RSpec.configure do |config|
|
|
|
117
117
|
end
|
|
118
118
|
```
|
|
119
119
|
|
|
120
|
+
When you call `fixture "name"` in an example group, FixtureKit registers that fixture with its runner.
|
|
121
|
+
|
|
120
122
|
## Configuration
|
|
121
123
|
|
|
122
124
|
```ruby
|
|
@@ -128,27 +130,35 @@ FixtureKit.configure do |config|
|
|
|
128
130
|
# Where cache files are stored (default: tmp/cache/fixture_kit)
|
|
129
131
|
config.cache_path = Rails.root.join("tmp/cache/fixture_kit").to_s
|
|
130
132
|
|
|
131
|
-
#
|
|
132
|
-
config.
|
|
133
|
+
# Wrapper used to isolate generation work (default: FixtureKit::TestCase::Isolator)
|
|
134
|
+
# config.isolator = FixtureKit::TestCase::Isolator
|
|
135
|
+
# config.isolator = FixtureKit::RSpec::Isolator
|
|
133
136
|
|
|
134
|
-
# Optional
|
|
135
|
-
#
|
|
136
|
-
# config.
|
|
137
|
+
# Optional callback, called once when a fixture cache is first generated.
|
|
138
|
+
# Receives the fixture name as a String.
|
|
139
|
+
# config.on_cache = ->(fixture_name) { puts "cached #{fixture_name}" }
|
|
137
140
|
end
|
|
138
141
|
```
|
|
139
142
|
|
|
140
|
-
Custom
|
|
141
|
-
`#run` receives the
|
|
143
|
+
Custom isolators should subclass `FixtureKit::Isolator` and implement `#run`.
|
|
144
|
+
`#run` receives the generation block and should execute it in whatever lifecycle you need.
|
|
145
|
+
|
|
146
|
+
By default, FixtureKit uses `FixtureKit::TestCase::Isolator`, which runs generation inside an internal `ActiveSupport::TestCase` and removes that harness case from minitest runnables.
|
|
142
147
|
|
|
143
|
-
|
|
148
|
+
When using `fixture_kit/rspec`, FixtureKit sets `FixtureKit::RSpec::Isolator`. It runs generation inside an internal RSpec example, and uses a null reporter so harness runs do not count toward suite example totals.
|
|
144
149
|
|
|
145
|
-
|
|
150
|
+
## Lifecycle
|
|
146
151
|
|
|
147
|
-
|
|
152
|
+
Fixture generation is managed by `FixtureKit::Runner`.
|
|
148
153
|
|
|
149
|
-
|
|
154
|
+
With `fixture_kit/rspec`:
|
|
150
155
|
|
|
151
|
-
|
|
156
|
+
1. `fixture "name"` registers the fixture with the runner during spec file load.
|
|
157
|
+
2. In `before(:suite)`, runner `start`:
|
|
158
|
+
- clears `cache_path` (unless preserve-cache is enabled),
|
|
159
|
+
- generates caches for all already-registered fixtures.
|
|
160
|
+
3. If new spec files are loaded later (for example, queue-mode CI runners), newly registered fixtures are generated immediately because the runner has already started.
|
|
161
|
+
4. At example runtime, fixture mounting loads from cache.
|
|
152
162
|
|
|
153
163
|
### Preserving Cache Locally
|
|
154
164
|
|
|
@@ -158,19 +168,9 @@ If you want to skip cache clearing at suite start (e.g., to reuse caches across
|
|
|
158
168
|
FIXTURE_KIT_PRESERVE_CACHE=1 bundle exec rspec
|
|
159
169
|
```
|
|
160
170
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
### CI Setup
|
|
164
|
-
|
|
165
|
-
For CI, set `autogenerate` to `false`. FixtureKit will automatically generate any missing caches at suite start:
|
|
171
|
+
Truthy values are case-insensitive: `1`, `true`, `yes`.
|
|
166
172
|
|
|
167
|
-
|
|
168
|
-
FixtureKit.configure do |config|
|
|
169
|
-
config.autogenerate = !ENV["CI"]
|
|
170
|
-
end
|
|
171
|
-
```
|
|
172
|
-
|
|
173
|
-
This means CI "just works" - no need to pre-generate caches or commit them to the repository. The first test run will generate all caches, and subsequent runs (if caches are preserved between builds) will reuse them.
|
|
173
|
+
This is useful when you're iterating on tests and your fixture definitions haven't changed.
|
|
174
174
|
|
|
175
175
|
## Nested Fixtures
|
|
176
176
|
|
|
@@ -189,11 +189,11 @@ fixture "teams/sales"
|
|
|
189
189
|
|
|
190
190
|
## How It Works
|
|
191
191
|
|
|
192
|
-
1. **
|
|
192
|
+
1. **Cache generation**: FixtureKit executes your definition block inside the configured isolator, subscribes to `sql.active_record` notifications to track inserted models, queries those model tables, and generates batch INSERT statements with conflict handling (`INSERT OR IGNORE` for SQLite, `ON CONFLICT DO NOTHING` for PostgreSQL, `INSERT IGNORE` for MySQL).
|
|
193
193
|
|
|
194
|
-
2. **
|
|
194
|
+
2. **Mounting**: FixtureKit loads the cached JSON file and executes the raw SQL INSERT statements directly. No ORM instantiation, no callbacks.
|
|
195
195
|
|
|
196
|
-
3. **
|
|
196
|
+
3. **Repository build**: FixtureKit resolves exposed records by model + id and returns a `Repository` for method-based access.
|
|
197
197
|
|
|
198
198
|
4. **Transaction isolation**: RSpec's `use_transactional_fixtures` wraps each test in a transaction that rolls back, so data doesn't persist between tests.
|
|
199
199
|
|
|
@@ -216,7 +216,7 @@ Caches are stored as JSON files in `tmp/cache/fixture_kit/`:
|
|
|
216
216
|
```
|
|
217
217
|
|
|
218
218
|
- **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.
|
|
219
|
-
- **exposed**: Maps fixture accessor names to their model class and ID for querying after cache replay
|
|
219
|
+
- **exposed**: Maps fixture accessor names to their model class and ID for querying after cache replay.
|
|
220
220
|
|
|
221
221
|
## Cache Management
|
|
222
222
|
|
|
@@ -225,7 +225,7 @@ Delete the cache directory to force regeneration:
|
|
|
225
225
|
rm -rf tmp/cache/fixture_kit
|
|
226
226
|
```
|
|
227
227
|
|
|
228
|
-
Caches are
|
|
228
|
+
Caches are cleared at runner start unless `FIXTURE_KIT_PRESERVE_CACHE` is truthy.
|
|
229
229
|
|
|
230
230
|
## Multi-Database Support
|
|
231
231
|
|
data/lib/fixture_kit/cache.rb
CHANGED
|
@@ -7,109 +7,54 @@ require "active_support/inflector"
|
|
|
7
7
|
|
|
8
8
|
module FixtureKit
|
|
9
9
|
class Cache
|
|
10
|
-
|
|
11
|
-
@memory_cache = {}
|
|
10
|
+
attr_reader :fixture
|
|
12
11
|
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
def clear_memory_cache(fixture_name = nil)
|
|
17
|
-
if fixture_name
|
|
18
|
-
@memory_cache.delete(fixture_name)
|
|
19
|
-
else
|
|
20
|
-
@memory_cache.clear
|
|
21
|
-
end
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Clear fixture cache (both memory and disk)
|
|
25
|
-
def clear(fixture_name = nil)
|
|
26
|
-
clear_memory_cache(fixture_name)
|
|
27
|
-
|
|
28
|
-
cache_path = FixtureKit.configuration.cache_path
|
|
29
|
-
if fixture_name
|
|
30
|
-
cache_file = File.join(cache_path, "#{fixture_name}.json")
|
|
31
|
-
FileUtils.rm_f(cache_file)
|
|
32
|
-
else
|
|
33
|
-
FileUtils.rm_rf(cache_path)
|
|
34
|
-
end
|
|
35
|
-
end
|
|
36
|
-
|
|
37
|
-
# Generate caches for all fixtures.
|
|
38
|
-
# Each fixture is generated in a transaction that rolls back, so no data persists.
|
|
39
|
-
def generate_all
|
|
40
|
-
Registry.load_definitions
|
|
41
|
-
Registry.fixtures.each { |fixture| generate(fixture.name) }
|
|
42
|
-
end
|
|
43
|
-
|
|
44
|
-
def generate(fixture_name)
|
|
45
|
-
clear(fixture_name)
|
|
46
|
-
|
|
47
|
-
FixtureKit.configuration.generator.run do
|
|
48
|
-
Runner.run(fixture_name, force: true)
|
|
49
|
-
end
|
|
50
|
-
end
|
|
12
|
+
def initialize(fixture, definition)
|
|
13
|
+
@fixture = fixture
|
|
14
|
+
@definition = definition
|
|
51
15
|
end
|
|
52
16
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
def initialize(fixture_name)
|
|
56
|
-
@fixture_name = fixture_name
|
|
57
|
-
@records = {}
|
|
58
|
-
@exposed = {}
|
|
59
|
-
end
|
|
60
|
-
|
|
61
|
-
def cache_file_path
|
|
62
|
-
cache_path = FixtureKit.configuration.cache_path
|
|
63
|
-
File.join(cache_path, "#{@fixture_name}.json")
|
|
17
|
+
def path
|
|
18
|
+
File.join(FixtureKit.configuration.cache_path, "#{fixture.name}.json")
|
|
64
19
|
end
|
|
65
20
|
|
|
66
21
|
def exists?
|
|
67
|
-
|
|
68
|
-
self.class.memory_cache.key?(@fixture_name) || File.exist?(cache_file_path)
|
|
22
|
+
@data || File.exist?(path)
|
|
69
23
|
end
|
|
70
24
|
|
|
71
25
|
def load
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
data = self.class.memory_cache[@fixture_name]
|
|
75
|
-
@records = data.fetch("records")
|
|
76
|
-
@exposed = data.fetch("exposed")
|
|
77
|
-
return true
|
|
26
|
+
unless exists?
|
|
27
|
+
raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{fixture.name}'"
|
|
78
28
|
end
|
|
79
29
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
@exposed = data.fetch("exposed")
|
|
86
|
-
|
|
87
|
-
# Store in memory for subsequent loads
|
|
88
|
-
self.class.memory_cache[@fixture_name] = data
|
|
30
|
+
@data ||= JSON.parse(File.read(path))
|
|
31
|
+
@data.fetch("records").each do |model_name, sql|
|
|
32
|
+
model = ActiveSupport::Inflector.constantize(model_name)
|
|
33
|
+
model.connection.execute(sql)
|
|
34
|
+
end
|
|
89
35
|
|
|
90
|
-
|
|
36
|
+
build_repository(@data.fetch("exposed"))
|
|
91
37
|
end
|
|
92
38
|
|
|
93
|
-
def save
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
data = {
|
|
100
|
-
"records" => @records,
|
|
101
|
-
"exposed" => @exposed
|
|
102
|
-
}
|
|
39
|
+
def save
|
|
40
|
+
FixtureKit.configuration.isolator.run do
|
|
41
|
+
models = SqlSubscriber.capture do
|
|
42
|
+
@definition.evaluate
|
|
43
|
+
end
|
|
103
44
|
|
|
104
|
-
|
|
105
|
-
|
|
45
|
+
@data = {
|
|
46
|
+
"records" => generate_statements(models),
|
|
47
|
+
"exposed" => build_exposed_mapping(@definition.exposed)
|
|
48
|
+
}
|
|
49
|
+
end
|
|
106
50
|
|
|
107
|
-
|
|
51
|
+
FileUtils.mkdir_p(File.dirname(path))
|
|
52
|
+
File.write(path, JSON.pretty_generate(@data))
|
|
108
53
|
end
|
|
109
54
|
|
|
110
55
|
# Query exposed records from the database and return a Repository.
|
|
111
|
-
def build_repository
|
|
112
|
-
exposed_records =
|
|
56
|
+
def build_repository(exposed)
|
|
57
|
+
exposed_records = exposed.each_with_object({}) do |(name, value), hash|
|
|
113
58
|
was_array = value.is_a?(Array)
|
|
114
59
|
records = Array.wrap(value).map { |record_info| find_exposed_record(record_info.fetch("model"), record_info.fetch("id"), name) }
|
|
115
60
|
hash[name.to_sym] = was_array ? records : records.first
|
|
@@ -125,27 +70,27 @@ module FixtureKit
|
|
|
125
70
|
model.find(id)
|
|
126
71
|
rescue ActiveRecord::RecordNotFound
|
|
127
72
|
raise FixtureKit::ExposedRecordNotFound,
|
|
128
|
-
"Could not find #{model_name} with id=#{id} for exposed record '#{exposed_name}' in fixture '#{@
|
|
73
|
+
"Could not find #{model_name} with id=#{id} for exposed record '#{exposed_name}' in fixture '#{@fixture.name}'"
|
|
129
74
|
end
|
|
130
75
|
|
|
131
|
-
def generate_statements(
|
|
76
|
+
def generate_statements(models)
|
|
132
77
|
statements_by_model = {}
|
|
133
78
|
|
|
134
|
-
|
|
79
|
+
models.each do |model|
|
|
135
80
|
columns = model.column_names
|
|
136
81
|
|
|
137
82
|
rows = []
|
|
138
83
|
model.order(:id).find_each do |record|
|
|
139
84
|
row_values = columns.map do |col|
|
|
140
85
|
value = record.read_attribute_before_type_cast(col)
|
|
141
|
-
connection.quote(value)
|
|
86
|
+
model.connection.quote(value)
|
|
142
87
|
end
|
|
143
88
|
rows << "(#{row_values.join(", ")})"
|
|
144
89
|
end
|
|
145
90
|
|
|
146
91
|
next if rows.empty?
|
|
147
92
|
|
|
148
|
-
sql = build_insert_sql(model.table_name, columns, rows, connection)
|
|
93
|
+
sql = build_insert_sql(model.table_name, columns, rows, model.connection)
|
|
149
94
|
statements_by_model[model.name] = sql
|
|
150
95
|
end
|
|
151
96
|
|
|
@@ -175,5 +120,13 @@ module FixtureKit
|
|
|
175
120
|
sql
|
|
176
121
|
end
|
|
177
122
|
end
|
|
123
|
+
|
|
124
|
+
def build_exposed_mapping(exposed)
|
|
125
|
+
exposed.each_with_object({}) do |(name, value), hash|
|
|
126
|
+
was_array = value.is_a?(Array)
|
|
127
|
+
records = Array.wrap(value).map { |record| { "model" => record.class.name, "id" => record.id } }
|
|
128
|
+
hash[name] = was_array ? records : records.first
|
|
129
|
+
end
|
|
130
|
+
end
|
|
178
131
|
end
|
|
179
132
|
end
|
|
@@ -4,14 +4,14 @@ module FixtureKit
|
|
|
4
4
|
class Configuration
|
|
5
5
|
attr_writer :fixture_path
|
|
6
6
|
attr_writer :cache_path
|
|
7
|
-
attr_accessor :
|
|
8
|
-
attr_accessor :
|
|
7
|
+
attr_accessor :isolator
|
|
8
|
+
attr_accessor :on_cache
|
|
9
9
|
|
|
10
10
|
def initialize
|
|
11
11
|
@fixture_path = nil
|
|
12
12
|
@cache_path = nil
|
|
13
|
-
@
|
|
14
|
-
@
|
|
13
|
+
@isolator = FixtureKit::TestCase::Isolator
|
|
14
|
+
@on_cache = nil
|
|
15
15
|
end
|
|
16
16
|
|
|
17
17
|
def fixture_path
|
|
@@ -1,23 +1,22 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
3
|
module FixtureKit
|
|
4
|
-
class
|
|
4
|
+
class Definition
|
|
5
5
|
attr_reader :exposed
|
|
6
6
|
|
|
7
|
-
def initialize
|
|
7
|
+
def initialize(&definition)
|
|
8
|
+
@definition = definition
|
|
8
9
|
@exposed = {}
|
|
9
10
|
end
|
|
10
11
|
|
|
12
|
+
def evaluate
|
|
13
|
+
instance_eval(&@definition)
|
|
14
|
+
end
|
|
15
|
+
|
|
11
16
|
def expose(**records)
|
|
12
17
|
records.each do |name, record|
|
|
13
|
-
name = name.to_sym
|
|
14
|
-
|
|
15
18
|
if @exposed.key?(name)
|
|
16
|
-
raise FixtureKit::DuplicateNameError,
|
|
17
|
-
Duplicate expose name :#{name}
|
|
18
|
-
|
|
19
|
-
A record with this name has already been exposed in this fixture.
|
|
20
|
-
ERROR
|
|
19
|
+
raise FixtureKit::DuplicateNameError, "Name #{name} already exposed"
|
|
21
20
|
end
|
|
22
21
|
|
|
23
22
|
@exposed[name] = record
|
data/lib/fixture_kit/fixture.rb
CHANGED
|
@@ -2,17 +2,38 @@
|
|
|
2
2
|
|
|
3
3
|
module FixtureKit
|
|
4
4
|
class Fixture
|
|
5
|
-
attr_reader :name, :
|
|
5
|
+
attr_reader :name, :path
|
|
6
6
|
|
|
7
|
-
def initialize(name,
|
|
7
|
+
def initialize(name, path)
|
|
8
8
|
@name = name
|
|
9
|
-
@
|
|
9
|
+
@path = path
|
|
10
|
+
@cache = Cache.new(self, definition)
|
|
10
11
|
end
|
|
11
12
|
|
|
12
|
-
def
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
def cache(force: false)
|
|
14
|
+
already_cached = @cache.exists?
|
|
15
|
+
return if already_cached && !force
|
|
16
|
+
|
|
17
|
+
@cache.save
|
|
18
|
+
FixtureKit.configuration.on_cache&.call(name) unless already_cached
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def mount
|
|
22
|
+
unless @cache.exists?
|
|
23
|
+
raise FixtureKit::CacheMissingError, "Cache does not exist for fixture '#{name}'"
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
@cache.load
|
|
27
|
+
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
|
|
16
37
|
end
|
|
17
38
|
end
|
|
18
39
|
end
|
data/lib/fixture_kit/registry.rb
CHANGED
|
@@ -1,58 +1,34 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
|
|
4
|
-
module Registry
|
|
5
|
-
class << self
|
|
6
|
-
def fetch(name)
|
|
7
|
-
fixture = find(name)
|
|
8
|
-
return fixture if fixture
|
|
9
|
-
|
|
10
|
-
file_path = fixture_file_path(name)
|
|
11
|
-
unless File.file?(file_path)
|
|
12
|
-
raise FixtureKit::FixtureDefinitionNotFound,
|
|
13
|
-
"Could not find fixture definition file for '#{name}' at '#{file_path}'"
|
|
14
|
-
end
|
|
15
|
-
|
|
16
|
-
load file_path
|
|
17
|
-
find(name)
|
|
18
|
-
end
|
|
3
|
+
require "pathname"
|
|
19
4
|
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
5
|
+
module FixtureKit
|
|
6
|
+
class Registry
|
|
7
|
+
def initialize(configuration)
|
|
8
|
+
@configuration = configuration
|
|
9
|
+
@registry = {}
|
|
10
|
+
end
|
|
23
11
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
end
|
|
12
|
+
def add(name)
|
|
13
|
+
return @registry[name] if @registry.key?(name)
|
|
27
14
|
|
|
28
|
-
|
|
29
|
-
|
|
15
|
+
file_path = fixture_file_path(name)
|
|
16
|
+
unless File.file?(file_path)
|
|
17
|
+
raise FixtureKit::FixtureDefinitionNotFound,
|
|
18
|
+
"Could not find fixture definition file for '#{name}' at '#{file_path}'"
|
|
30
19
|
end
|
|
31
20
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
# even if the files were previously required (e.g., after a reset).
|
|
35
|
-
def load_definitions
|
|
36
|
-
fixture_path = FixtureKit.configuration.fixture_path
|
|
37
|
-
Dir.glob(File.join(fixture_path, "**/*.rb")).each do |file|
|
|
38
|
-
load file
|
|
39
|
-
end
|
|
40
|
-
end
|
|
41
|
-
|
|
42
|
-
def reset
|
|
43
|
-
@registry = nil
|
|
44
|
-
end
|
|
21
|
+
@registry[name] = Fixture.new(name, file_path)
|
|
22
|
+
end
|
|
45
23
|
|
|
46
|
-
|
|
24
|
+
def fixtures
|
|
25
|
+
@registry.values
|
|
26
|
+
end
|
|
47
27
|
|
|
48
|
-
|
|
49
|
-
fixture_path = FixtureKit.configuration.fixture_path
|
|
50
|
-
File.expand_path(File.join(fixture_path, "#{name}.rb"))
|
|
51
|
-
end
|
|
28
|
+
private
|
|
52
29
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
end
|
|
30
|
+
def fixture_file_path(name)
|
|
31
|
+
File.expand_path(File.join(@configuration.fixture_path, "#{name}.rb"))
|
|
56
32
|
end
|
|
57
33
|
end
|
|
58
34
|
end
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module FixtureKit
|
|
4
4
|
module RSpec
|
|
5
|
-
class
|
|
5
|
+
class Isolator < FixtureKit::Isolator
|
|
6
6
|
def run(&block)
|
|
7
7
|
previous_example = ::RSpec.current_example
|
|
8
8
|
previous_scope = ::RSpec.current_scope
|
|
@@ -35,7 +35,7 @@ module FixtureKit
|
|
|
35
35
|
def build_example_group
|
|
36
36
|
::RSpec::Core::ExampleGroup.subclass(
|
|
37
37
|
::RSpec::Core::ExampleGroup,
|
|
38
|
-
"FixtureKit::RSpec::
|
|
38
|
+
"FixtureKit::RSpec::Isolator",
|
|
39
39
|
[],
|
|
40
40
|
[]
|
|
41
41
|
)
|
data/lib/fixture_kit/rspec.rb
CHANGED
|
@@ -4,11 +4,9 @@ require "fixture_kit"
|
|
|
4
4
|
|
|
5
5
|
module FixtureKit
|
|
6
6
|
module RSpec
|
|
7
|
-
autoload :
|
|
8
|
-
autoload :Generator, File.expand_path("rspec/generator", __dir__)
|
|
7
|
+
autoload :Isolator, File.expand_path("rspec/isolator", __dir__)
|
|
9
8
|
|
|
10
9
|
DECLARATION_METADATA_KEY = :fixture_kit_declaration
|
|
11
|
-
PRESERVE_CACHE_ENV_KEY = "FIXTURE_KIT_PRESERVE_CACHE"
|
|
12
10
|
|
|
13
11
|
# Class methods (extended via config.extend)
|
|
14
12
|
module ClassMethods
|
|
@@ -32,7 +30,7 @@ module FixtureKit
|
|
|
32
30
|
# end
|
|
33
31
|
# end
|
|
34
32
|
def fixture(name)
|
|
35
|
-
metadata[DECLARATION_METADATA_KEY] =
|
|
33
|
+
metadata[DECLARATION_METADATA_KEY] = ::RSpec.configuration.fixture_kit.register(name)
|
|
36
34
|
end
|
|
37
35
|
end
|
|
38
36
|
|
|
@@ -46,41 +44,22 @@ module FixtureKit
|
|
|
46
44
|
end
|
|
47
45
|
|
|
48
46
|
def self.configure!(config)
|
|
49
|
-
|
|
47
|
+
config.add_setting(:fixture_kit, default: FixtureKit.runner)
|
|
48
|
+
FixtureKit.configuration.isolator = Isolator
|
|
49
|
+
|
|
50
50
|
config.extend ClassMethods
|
|
51
|
-
config.include InstanceMethods
|
|
51
|
+
config.include InstanceMethods, DECLARATION_METADATA_KEY
|
|
52
52
|
|
|
53
53
|
# Load declared fixtures at the beginning of each example.
|
|
54
54
|
# Runs inside transactional fixtures and before user-defined before hooks.
|
|
55
55
|
config.prepend_before(:example, DECLARATION_METADATA_KEY) do |example|
|
|
56
|
-
|
|
57
|
-
@_fixture_kit_fixture_set = declaration.fixture_set
|
|
56
|
+
@_fixture_kit_fixture_set = example.metadata[DECLARATION_METADATA_KEY].mount
|
|
58
57
|
end
|
|
59
58
|
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
config.when_first_matching_example_defined(DECLARATION_METADATA_KEY) do
|
|
63
|
-
config.before(:suite) do
|
|
64
|
-
if FixtureKit.configuration.autogenerate
|
|
65
|
-
preserve_cache = ENV[PRESERVE_CACHE_ENV_KEY].to_s.match?(/\A(1|true|yes)\z/i)
|
|
66
|
-
Cache.clear unless preserve_cache
|
|
67
|
-
else
|
|
68
|
-
fixture_names_for_loaded_examples.each do |fixture_name|
|
|
69
|
-
Cache.generate(fixture_name)
|
|
70
|
-
end
|
|
71
|
-
end
|
|
72
|
-
end
|
|
59
|
+
config.append_before(:suite) do
|
|
60
|
+
config.fixture_kit.start
|
|
73
61
|
end
|
|
74
62
|
end
|
|
75
|
-
|
|
76
|
-
def self.fixture_names_for_loaded_examples
|
|
77
|
-
::RSpec.world.filtered_examples.each_value.with_object(Set.new) do |examples, names|
|
|
78
|
-
examples.each do |example|
|
|
79
|
-
declaration = example.metadata[DECLARATION_METADATA_KEY]
|
|
80
|
-
names << declaration.name if declaration
|
|
81
|
-
end
|
|
82
|
-
end.to_a
|
|
83
|
-
end
|
|
84
63
|
end
|
|
85
64
|
end
|
|
86
65
|
|
data/lib/fixture_kit/runner.rb
CHANGED
|
@@ -1,93 +1,45 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "
|
|
3
|
+
require "fileutils"
|
|
4
4
|
|
|
5
5
|
module FixtureKit
|
|
6
6
|
class Runner
|
|
7
|
-
|
|
8
|
-
new(fixture_name).run(force: force)
|
|
9
|
-
end
|
|
10
|
-
|
|
11
|
-
def initialize(fixture_name)
|
|
12
|
-
@fixture_name = fixture_name
|
|
13
|
-
@cache = Cache.new(@fixture_name)
|
|
14
|
-
end
|
|
7
|
+
PRESERVE_CACHE_ENV_KEY = "FIXTURE_KIT_PRESERVE_CACHE"
|
|
15
8
|
|
|
16
|
-
|
|
17
|
-
if force
|
|
18
|
-
execute_and_cache
|
|
19
|
-
elsif @cache.exists?
|
|
20
|
-
execute_from_cache
|
|
21
|
-
elsif FixtureKit.configuration.autogenerate
|
|
22
|
-
execute_and_cache
|
|
23
|
-
else
|
|
24
|
-
raise FixtureKit::CacheMissingError, <<~ERROR
|
|
25
|
-
Cache not found for fixture '#{@fixture_name}'.
|
|
9
|
+
attr_reader :configuration, :registry
|
|
26
10
|
|
|
27
|
-
|
|
28
|
-
|
|
11
|
+
def initialize
|
|
12
|
+
@configuration = Configuration.new
|
|
13
|
+
@registry = Registry.new(configuration)
|
|
14
|
+
@started = false
|
|
15
|
+
end
|
|
29
16
|
|
|
30
|
-
|
|
31
|
-
|
|
17
|
+
def register(name)
|
|
18
|
+
registry.add(name).tap do |fixture|
|
|
19
|
+
fixture.cache if started?
|
|
32
20
|
end
|
|
33
21
|
end
|
|
34
22
|
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
fixture = Registry.fetch(@fixture_name)
|
|
39
|
-
|
|
40
|
-
# Start capturing SQL
|
|
41
|
-
capture = SqlCapture.new
|
|
42
|
-
capture.start
|
|
43
|
-
|
|
44
|
-
# Execute fixture definition - returns exposed records hash
|
|
45
|
-
exposed = fixture.execute
|
|
46
|
-
|
|
47
|
-
# Stop capturing and get affected models with their connections
|
|
48
|
-
models_with_connections = capture.stop
|
|
23
|
+
def start
|
|
24
|
+
raise RunnerAlreadyStartedError, "FixtureKit::Runner has already been started" if started?
|
|
25
|
+
@started = true
|
|
49
26
|
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
models_with_connections: models_with_connections,
|
|
53
|
-
exposed_mapping: build_exposed_mapping(exposed)
|
|
54
|
-
)
|
|
55
|
-
|
|
56
|
-
# Return Repository from the exposed records
|
|
57
|
-
Repository.new(exposed)
|
|
27
|
+
clear_cache unless preserve_cache?
|
|
28
|
+
registry.fixtures.each(&:cache)
|
|
58
29
|
end
|
|
59
30
|
|
|
60
|
-
def
|
|
61
|
-
@
|
|
62
|
-
|
|
63
|
-
# Execute cached SQL statements by model
|
|
64
|
-
@cache.records.each do |model_name, sql|
|
|
65
|
-
next if sql.nil? || sql.empty?
|
|
66
|
-
|
|
67
|
-
model = ActiveSupport::Inflector.constantize(model_name)
|
|
68
|
-
connection = model.connection
|
|
69
|
-
connection.execute(sql)
|
|
70
|
-
end
|
|
71
|
-
|
|
72
|
-
# Query exposed records and build Repository.
|
|
73
|
-
@cache.build_repository
|
|
31
|
+
def started?
|
|
32
|
+
@started
|
|
74
33
|
end
|
|
75
34
|
|
|
76
|
-
|
|
77
|
-
mapping = {}
|
|
35
|
+
private
|
|
78
36
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
{ "model" => record.class.name, "id" => record.id }
|
|
83
|
-
end
|
|
84
|
-
else
|
|
85
|
-
record = record_or_records
|
|
86
|
-
mapping[name.to_s] = { "model" => record.class.name, "id" => record.id }
|
|
87
|
-
end
|
|
88
|
-
end
|
|
37
|
+
def clear_cache
|
|
38
|
+
FileUtils.rm_rf(configuration.cache_path)
|
|
39
|
+
end
|
|
89
40
|
|
|
90
|
-
|
|
41
|
+
def preserve_cache?
|
|
42
|
+
ENV[PRESERVE_CACHE_ENV_KEY].to_s.match?(/\A(1|true|yes)\z/i)
|
|
91
43
|
end
|
|
92
44
|
end
|
|
93
45
|
end
|
|
@@ -1,35 +1,26 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
-
require "pathname"
|
|
4
|
-
|
|
5
3
|
module FixtureKit
|
|
6
4
|
module Singleton
|
|
7
5
|
def configure
|
|
8
|
-
yield(configuration) if block_given?
|
|
6
|
+
yield(runner.configuration) if block_given?
|
|
9
7
|
self
|
|
10
8
|
end
|
|
11
9
|
|
|
12
10
|
def configuration
|
|
13
|
-
|
|
11
|
+
runner.configuration
|
|
14
12
|
end
|
|
15
13
|
|
|
16
|
-
def
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
# "/abs/path/spec/fixtures/teams/basic.rb" -> "teams/basic"
|
|
21
|
-
relative_path = Pathname.new(caller_file).relative_path_from(Pathname.new(fixture_path))
|
|
22
|
-
name = relative_path.to_s.sub(/\.rb$/, "")
|
|
14
|
+
def runner
|
|
15
|
+
@runner ||= Runner.new
|
|
16
|
+
end
|
|
23
17
|
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
fixture
|
|
18
|
+
def define(&block)
|
|
19
|
+
Definition.new(&block)
|
|
27
20
|
end
|
|
28
21
|
|
|
29
22
|
def reset
|
|
30
|
-
@
|
|
31
|
-
Registry.reset
|
|
32
|
-
Cache.clear_memory_cache
|
|
23
|
+
@runner = nil
|
|
33
24
|
end
|
|
34
25
|
end
|
|
35
26
|
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/notifications"
|
|
4
|
+
require "active_support/inflector"
|
|
5
|
+
|
|
6
|
+
module FixtureKit
|
|
7
|
+
class SqlSubscriber
|
|
8
|
+
EVENT = "sql.active_record"
|
|
9
|
+
|
|
10
|
+
def self.capture(&block)
|
|
11
|
+
models = Set.new
|
|
12
|
+
subscriber = lambda do |_event_name, _start, _finish, _id, payload|
|
|
13
|
+
sql = payload[:sql]
|
|
14
|
+
next unless sql =~ /\AINSERT INTO/i
|
|
15
|
+
|
|
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
|
+
models.add(ActiveSupport::Inflector.constantize(model_name))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
ActiveSupport::Notifications.subscribed(subscriber, EVENT, monotonic: true, &block)
|
|
25
|
+
|
|
26
|
+
models.to_a
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
data/lib/fixture_kit/version.rb
CHANGED
data/lib/fixture_kit.rb
CHANGED
|
@@ -8,18 +8,19 @@ module FixtureKit
|
|
|
8
8
|
class PregenerationError < Error; end
|
|
9
9
|
class FixtureDefinitionNotFound < Error; end
|
|
10
10
|
class ExposedRecordNotFound < Error; end
|
|
11
|
+
class RunnerAlreadyStartedError < Error; end
|
|
11
12
|
|
|
12
13
|
autoload :VERSION, File.expand_path("fixture_kit/version", __dir__)
|
|
13
14
|
autoload :Configuration, File.expand_path("fixture_kit/configuration", __dir__)
|
|
14
15
|
autoload :Singleton, File.expand_path("fixture_kit/singleton", __dir__)
|
|
15
16
|
autoload :Fixture, File.expand_path("fixture_kit/fixture", __dir__)
|
|
16
|
-
autoload :
|
|
17
|
+
autoload :Definition, File.expand_path("fixture_kit/definition", __dir__)
|
|
17
18
|
autoload :Registry, File.expand_path("fixture_kit/registry", __dir__)
|
|
18
19
|
autoload :Repository, File.expand_path("fixture_kit/repository", __dir__)
|
|
19
|
-
autoload :
|
|
20
|
+
autoload :SqlSubscriber, File.expand_path("fixture_kit/sql_subscriber", __dir__)
|
|
20
21
|
autoload :Cache, File.expand_path("fixture_kit/cache", __dir__)
|
|
21
22
|
autoload :Runner, File.expand_path("fixture_kit/runner", __dir__)
|
|
22
|
-
autoload :
|
|
23
|
+
autoload :Isolator, File.expand_path("fixture_kit/isolator", __dir__)
|
|
23
24
|
autoload :TestCase, File.expand_path("fixture_kit/test_case", __dir__)
|
|
24
25
|
|
|
25
26
|
extend Singleton
|
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.3.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-22 00:00:00.000000000 Z
|
|
12
12
|
dependencies:
|
|
13
13
|
- !ruby/object:Gem::Dependency
|
|
14
14
|
name: activesupport
|
|
@@ -136,19 +136,18 @@ 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/
|
|
139
|
+
- lib/fixture_kit/definition.rb
|
|
140
140
|
- lib/fixture_kit/fixture.rb
|
|
141
|
-
- lib/fixture_kit/
|
|
141
|
+
- lib/fixture_kit/isolator.rb
|
|
142
142
|
- lib/fixture_kit/registry.rb
|
|
143
143
|
- lib/fixture_kit/repository.rb
|
|
144
144
|
- lib/fixture_kit/rspec.rb
|
|
145
|
-
- lib/fixture_kit/rspec/
|
|
146
|
-
- lib/fixture_kit/rspec/generator.rb
|
|
145
|
+
- lib/fixture_kit/rspec/isolator.rb
|
|
147
146
|
- lib/fixture_kit/runner.rb
|
|
148
147
|
- lib/fixture_kit/singleton.rb
|
|
149
|
-
- lib/fixture_kit/
|
|
148
|
+
- lib/fixture_kit/sql_subscriber.rb
|
|
150
149
|
- lib/fixture_kit/test_case.rb
|
|
151
|
-
- lib/fixture_kit/test_case/
|
|
150
|
+
- lib/fixture_kit/test_case/isolator.rb
|
|
152
151
|
- lib/fixture_kit/version.rb
|
|
153
152
|
homepage: https://github.com/Gusto/fixture_kit
|
|
154
153
|
licenses:
|
|
@@ -1,37 +0,0 @@
|
|
|
1
|
-
# frozen_string_literal: true
|
|
2
|
-
|
|
3
|
-
require "active_support/notifications"
|
|
4
|
-
require "active_support/inflector"
|
|
5
|
-
|
|
6
|
-
module FixtureKit
|
|
7
|
-
class SqlCapture
|
|
8
|
-
SQL_EVENT = "sql.active_record"
|
|
9
|
-
|
|
10
|
-
def initialize
|
|
11
|
-
@models = {} # { Model => connection }
|
|
12
|
-
@subscription = nil
|
|
13
|
-
end
|
|
14
|
-
|
|
15
|
-
def start
|
|
16
|
-
@subscription = ActiveSupport::Notifications.subscribe(SQL_EVENT) do |*, payload|
|
|
17
|
-
next unless payload[:sql] =~ /\AINSERT INTO/i
|
|
18
|
-
|
|
19
|
-
# payload[:name] is like "User Create" - extract model name
|
|
20
|
-
name = payload[:name]
|
|
21
|
-
next unless name&.end_with?(" Create")
|
|
22
|
-
|
|
23
|
-
model_name = name.sub(/ Create\z/, "")
|
|
24
|
-
model = ActiveSupport::Inflector.constantize(model_name)
|
|
25
|
-
@models[model] ||= payload[:connection]
|
|
26
|
-
rescue NameError
|
|
27
|
-
# Skip if model can't be found
|
|
28
|
-
end
|
|
29
|
-
end
|
|
30
|
-
|
|
31
|
-
def stop
|
|
32
|
-
ActiveSupport::Notifications.unsubscribe(@subscription) if @subscription
|
|
33
|
-
@subscription = nil
|
|
34
|
-
@models
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
end
|