fixture_kit 0.1.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 +7 -0
- data/LICENSE +21 -0
- data/README.md +229 -0
- data/lib/fixture_kit/configuration.rb +43 -0
- data/lib/fixture_kit/fixture.rb +18 -0
- data/lib/fixture_kit/fixture_cache.rb +179 -0
- data/lib/fixture_kit/fixture_context.rb +27 -0
- data/lib/fixture_kit/fixture_registry.rb +54 -0
- data/lib/fixture_kit/fixture_runner.rb +103 -0
- data/lib/fixture_kit/fixture_set.rb +26 -0
- data/lib/fixture_kit/rspec.rb +65 -0
- data/lib/fixture_kit/singleton.rb +36 -0
- data/lib/fixture_kit/sql_capture.rb +37 -0
- data/lib/fixture_kit/version.rb +5 -0
- data/lib/fixture_kit.rb +22 -0
- metadata +160 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: 88808419ae66dfd1982be86a60e46c0b866e6e509ef50469e912a9906f99892f
|
|
4
|
+
data.tar.gz: 6a4f8cf65a36c0aff62abc8cf51dbcb58709a083cf30a5699ad1d21533968c44
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: facc136d48b32856bb74f7bae845f0c9cadbf4ef46797ff0a64884b042c9b20383dfb3cfcfdf17f46462b6b46fc1104bcd2e7f111dfd11b1371c055654b32da7
|
|
7
|
+
data.tar.gz: 72da841bb3e72a48f074c3f03468781e0039af6f417776d354c5249cd33591a232caf09eb408c3810e7a419b8485c6985141e277d7e3128555a185a881453ea1
|
data/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 Gusto
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,229 @@
|
|
|
1
|
+
# FixtureKit
|
|
2
|
+
|
|
3
|
+
Fast test fixtures with SQL caching.
|
|
4
|
+
|
|
5
|
+
## The Problem
|
|
6
|
+
|
|
7
|
+
Test data setup is slow. Every `Model.create!` or `FactoryBot.create` hits the database, and complex test scenarios can require dozens of inserts per test.
|
|
8
|
+
|
|
9
|
+
## The Solution
|
|
10
|
+
|
|
11
|
+
FixtureKit caches database records as raw SQL INSERT statements. On first use, it executes your fixture definition, captures the resulting database state, and generates optimized batch INSERT statements. Subsequent loads replay these statements directly—no ORM overhead, no callbacks, just fast SQL.
|
|
12
|
+
|
|
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
|
+
|
|
15
|
+
## Installation
|
|
16
|
+
|
|
17
|
+
Add to your Gemfile:
|
|
18
|
+
|
|
19
|
+
```ruby
|
|
20
|
+
group :test do
|
|
21
|
+
gem "fixture_kit"
|
|
22
|
+
end
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Quick Start
|
|
26
|
+
|
|
27
|
+
### 1. Define a Fixture
|
|
28
|
+
|
|
29
|
+
Create fixture files in `spec/fixture_kit/`. Use whatever method you prefer to create records—FixtureKit doesn't care.
|
|
30
|
+
|
|
31
|
+
**Using ActiveRecord directly:**
|
|
32
|
+
|
|
33
|
+
```ruby
|
|
34
|
+
# spec/fixture_kit/bookstore.rb
|
|
35
|
+
FixtureKit.define do
|
|
36
|
+
store = Store.create!(name: "Powell's Books")
|
|
37
|
+
owner = User.create!(name: "Alice", email: "alice@example.com", store: store)
|
|
38
|
+
|
|
39
|
+
books = 3.times.map do |i|
|
|
40
|
+
Book.create!(title: "Book #{i + 1}", store: store)
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
featured = Book.create!(title: "Dune", store: store, featured: true)
|
|
44
|
+
|
|
45
|
+
expose(store: store, owner: owner, books: books, featured: featured)
|
|
46
|
+
end
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
**Using FactoryBot:**
|
|
50
|
+
|
|
51
|
+
```ruby
|
|
52
|
+
# spec/fixture_kit/bookstore.rb
|
|
53
|
+
FixtureKit.define do
|
|
54
|
+
store = FactoryBot.create(:store, name: "Powell's Books")
|
|
55
|
+
owner = FactoryBot.create(:user, :admin, store: store)
|
|
56
|
+
books = FactoryBot.create_list(:book, 3, store: store)
|
|
57
|
+
featured = FactoryBot.create(:book, :bestseller, store: store, title: "Dune")
|
|
58
|
+
|
|
59
|
+
expose(store: store, owner: owner, books: books, featured: featured)
|
|
60
|
+
end
|
|
61
|
+
```
|
|
62
|
+
|
|
63
|
+
The filename determines the fixture name—no need to pass a name to `define`.
|
|
64
|
+
|
|
65
|
+
You can call `expose` multiple times to organize your setup code:
|
|
66
|
+
|
|
67
|
+
```ruby
|
|
68
|
+
FixtureKit.define do
|
|
69
|
+
# Set up users
|
|
70
|
+
admin = User.create!(name: "Admin", role: "admin")
|
|
71
|
+
member = User.create!(name: "Member", role: "member")
|
|
72
|
+
expose(admin: admin, member: member)
|
|
73
|
+
|
|
74
|
+
# Set up projects
|
|
75
|
+
project = Project.create!(name: "Website", owner: admin)
|
|
76
|
+
expose(project: project)
|
|
77
|
+
|
|
78
|
+
# Set up tasks
|
|
79
|
+
tasks = 3.times.map { |i| Task.create!(title: "Task #{i + 1}", project: project) }
|
|
80
|
+
expose(tasks: tasks)
|
|
81
|
+
end
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
Exposing the same name twice raises `FixtureKit::DuplicateNameError`.
|
|
85
|
+
|
|
86
|
+
### 2. Use in Tests
|
|
87
|
+
|
|
88
|
+
```ruby
|
|
89
|
+
# spec/models/book_spec.rb
|
|
90
|
+
RSpec.describe Book do
|
|
91
|
+
fixture "bookstore"
|
|
92
|
+
|
|
93
|
+
it "belongs to a store" do
|
|
94
|
+
expect(fixture.featured.store).to eq(fixture.store)
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
it "has multiple books" do
|
|
98
|
+
expect(fixture.books.size).to eq(3)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
it "supports hash-style access" do
|
|
102
|
+
expect(fixture[:owner]).to eq(fixture.owner)
|
|
103
|
+
end
|
|
104
|
+
end
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
### 3. Configure RSpec
|
|
108
|
+
|
|
109
|
+
```ruby
|
|
110
|
+
# spec/rails_helper.rb
|
|
111
|
+
require "fixture_kit/rspec"
|
|
112
|
+
|
|
113
|
+
RSpec.configure do |config|
|
|
114
|
+
config.use_transactional_fixtures = true
|
|
115
|
+
end
|
|
116
|
+
```
|
|
117
|
+
|
|
118
|
+
## Configuration
|
|
119
|
+
|
|
120
|
+
```ruby
|
|
121
|
+
# spec/support/fixture_kit.rb
|
|
122
|
+
FixtureKit.configure do |config|
|
|
123
|
+
# Where fixture definitions live (default: spec/fixture_kit)
|
|
124
|
+
config.fixture_path = Rails.root.join("spec/fixture_kit").to_s
|
|
125
|
+
|
|
126
|
+
# Where cache files are stored (default: tmp/cache/fixture_kit)
|
|
127
|
+
config.cache_path = Rails.root.join("tmp/cache/fixture_kit").to_s
|
|
128
|
+
|
|
129
|
+
# Whether to regenerate caches on every run (default: true)
|
|
130
|
+
config.autogenerate = true
|
|
131
|
+
end
|
|
132
|
+
```
|
|
133
|
+
|
|
134
|
+
### Autogenerate
|
|
135
|
+
|
|
136
|
+
When `autogenerate` is `true` (the default), FixtureKit clears all caches at the start of each test run, then regenerates them on first use. Subsequent tests that use the same fixture reuse the cache from earlier in the run. This ensures your test data always matches your fixture definitions.
|
|
137
|
+
|
|
138
|
+
When `autogenerate` is `false`, FixtureKit pre-generates all fixture caches at suite start. This happens in rolled-back transactions so no data persists to the database. Any fixtures that already have caches are skipped. This mode is useful for CI where you want consistent, predictable cache generation.
|
|
139
|
+
|
|
140
|
+
### Preserving Cache Locally
|
|
141
|
+
|
|
142
|
+
If you want to skip cache clearing at suite start (e.g., to reuse caches across test runs during local development), set the `FIXTURE_KIT_PRESERVE_CACHE` environment variable:
|
|
143
|
+
|
|
144
|
+
```bash
|
|
145
|
+
FIXTURE_KIT_PRESERVE_CACHE=1 bundle exec rspec
|
|
146
|
+
```
|
|
147
|
+
|
|
148
|
+
This is useful when you're iterating on tests and your fixture definitions haven't changed.
|
|
149
|
+
|
|
150
|
+
### CI Setup
|
|
151
|
+
|
|
152
|
+
For CI, set `autogenerate` to `false`. FixtureKit will automatically generate any missing caches at suite start:
|
|
153
|
+
|
|
154
|
+
```ruby
|
|
155
|
+
FixtureKit.configure do |config|
|
|
156
|
+
config.autogenerate = !ENV["CI"]
|
|
157
|
+
end
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
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.
|
|
161
|
+
|
|
162
|
+
## Nested Fixtures
|
|
163
|
+
|
|
164
|
+
Organize fixtures in subdirectories:
|
|
165
|
+
|
|
166
|
+
```ruby
|
|
167
|
+
# spec/fixture_kit/teams/sales.rb
|
|
168
|
+
FixtureKit.define do
|
|
169
|
+
# ...
|
|
170
|
+
end
|
|
171
|
+
```
|
|
172
|
+
|
|
173
|
+
```ruby
|
|
174
|
+
fixture "teams/sales"
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
## How It Works
|
|
178
|
+
|
|
179
|
+
1. **First load (cache miss)**: FixtureKit executes your definition block, subscribes to `sql.active_record` notifications to track which tables received INSERTs, queries all records from those tables, and generates batch INSERT statements with conflict handling (`INSERT OR IGNORE` for SQLite, `ON CONFLICT DO NOTHING` for PostgreSQL, `INSERT IGNORE` for MySQL).
|
|
180
|
+
|
|
181
|
+
2. **Subsequent loads (cache hit)**: FixtureKit loads the cached JSON file and executes the raw SQL INSERT statements directly. No ORM instantiation, no callbacks—just fast SQL execution.
|
|
182
|
+
|
|
183
|
+
3. **In-memory caching**: Once a cache file is parsed, the data is stored in memory. Multiple tests using the same fixture within a single test run don't re-read or re-parse the JSON file.
|
|
184
|
+
|
|
185
|
+
4. **Transaction isolation**: RSpec's `use_transactional_fixtures` wraps each test in a transaction that rolls back, so data doesn't persist between tests.
|
|
186
|
+
|
|
187
|
+
### Cache Format
|
|
188
|
+
|
|
189
|
+
Caches are stored as JSON files in `tmp/cache/fixture_kit/`:
|
|
190
|
+
|
|
191
|
+
```json
|
|
192
|
+
{
|
|
193
|
+
"records": {
|
|
194
|
+
"User": "INSERT OR IGNORE INTO users (id, name, email) VALUES (1, 'Alice', 'alice@example.com'), (2, 'Bob', 'bob@example.com')",
|
|
195
|
+
"Project": "INSERT OR IGNORE INTO projects (id, name, user_id) VALUES (1, 'Website', 1)"
|
|
196
|
+
},
|
|
197
|
+
"exposed": {
|
|
198
|
+
"alice": { "model": "User", "id": 1 },
|
|
199
|
+
"bob": { "model": "User", "id": 2 },
|
|
200
|
+
"project": { "model": "Project", "id": 1 }
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
```
|
|
204
|
+
|
|
205
|
+
- **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.
|
|
206
|
+
- **exposed**: Maps fixture accessor names to their model class and ID for querying after cache replay
|
|
207
|
+
|
|
208
|
+
## Cache Management
|
|
209
|
+
|
|
210
|
+
Delete the cache directory to force regeneration:
|
|
211
|
+
```bash
|
|
212
|
+
rm -rf tmp/cache/fixture_kit
|
|
213
|
+
```
|
|
214
|
+
|
|
215
|
+
Caches are automatically cleared at suite start when `autogenerate` is enabled, so manual clearing is rarely needed.
|
|
216
|
+
|
|
217
|
+
## Multi-Database Support
|
|
218
|
+
|
|
219
|
+
FixtureKit automatically handles multiple databases. Records are stored by model name in the cache, and when replaying, FixtureKit uses each model's database connection to execute the INSERT statements. This means records are automatically inserted into the correct database without any additional configuration.
|
|
220
|
+
|
|
221
|
+
## Requirements
|
|
222
|
+
|
|
223
|
+
- Ruby >= 3.3
|
|
224
|
+
- ActiveRecord >= 8.0
|
|
225
|
+
- ActiveSupport >= 8.0
|
|
226
|
+
|
|
227
|
+
## License
|
|
228
|
+
|
|
229
|
+
MIT License. See [LICENSE](LICENSE) for details.
|
|
@@ -0,0 +1,43 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
class Configuration
|
|
5
|
+
attr_writer :fixture_path
|
|
6
|
+
attr_writer :cache_path
|
|
7
|
+
attr_accessor :autogenerate
|
|
8
|
+
|
|
9
|
+
def initialize
|
|
10
|
+
@fixture_path = nil
|
|
11
|
+
@cache_path = nil
|
|
12
|
+
@autogenerate = true
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def fixture_path
|
|
16
|
+
@fixture_path ||= detect_fixture_path
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def cache_path
|
|
20
|
+
@cache_path ||= detect_cache_path
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private
|
|
24
|
+
|
|
25
|
+
def detect_fixture_path
|
|
26
|
+
if defined?(RSpec)
|
|
27
|
+
"spec/fixture_kit"
|
|
28
|
+
elsif defined?(Minitest)
|
|
29
|
+
"test/fixture_kit"
|
|
30
|
+
elsif Dir.exist?("spec")
|
|
31
|
+
"spec/fixture_kit"
|
|
32
|
+
elsif Dir.exist?("test")
|
|
33
|
+
"test/fixture_kit"
|
|
34
|
+
else
|
|
35
|
+
"spec/fixture_kit"
|
|
36
|
+
end
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def detect_cache_path
|
|
40
|
+
"tmp/cache/fixture_kit"
|
|
41
|
+
end
|
|
42
|
+
end
|
|
43
|
+
end
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
class Fixture
|
|
5
|
+
attr_reader :name, :block
|
|
6
|
+
|
|
7
|
+
def initialize(name, &block)
|
|
8
|
+
@name = name.to_sym
|
|
9
|
+
@block = block
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def execute
|
|
13
|
+
context = FixtureContext.new
|
|
14
|
+
context.instance_eval(&block) if block
|
|
15
|
+
context.exposed
|
|
16
|
+
end
|
|
17
|
+
end
|
|
18
|
+
end
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "json"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "active_support/core_ext/array/wrap"
|
|
6
|
+
require "active_support/inflector"
|
|
7
|
+
|
|
8
|
+
module FixtureKit
|
|
9
|
+
class FixtureCache
|
|
10
|
+
# In-memory cache to avoid re-reading/parsing JSON for every test
|
|
11
|
+
@memory_cache = {}
|
|
12
|
+
|
|
13
|
+
class << self
|
|
14
|
+
attr_accessor :memory_cache
|
|
15
|
+
|
|
16
|
+
def clear_memory_cache(fixture_name = nil)
|
|
17
|
+
if fixture_name
|
|
18
|
+
@memory_cache.delete(fixture_name.to_s)
|
|
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
|
+
# Pre-generate caches for all fixtures.
|
|
38
|
+
# Each fixture is generated in a transaction that rolls back, so no data persists.
|
|
39
|
+
def pregenerate_all
|
|
40
|
+
fixture_path = FixtureKit.configuration.fixture_path
|
|
41
|
+
|
|
42
|
+
# First, load all fixture files to register them
|
|
43
|
+
FixtureRegistry.load_definitions(fixture_path)
|
|
44
|
+
|
|
45
|
+
# Then iterate over the registry and generate caches
|
|
46
|
+
FixtureRegistry.all_names.each do |name|
|
|
47
|
+
runner = FixtureRunner.new(name)
|
|
48
|
+
runner.generate_cache_only
|
|
49
|
+
end
|
|
50
|
+
end
|
|
51
|
+
end
|
|
52
|
+
|
|
53
|
+
attr_reader :records, :exposed
|
|
54
|
+
|
|
55
|
+
def initialize(fixture_name)
|
|
56
|
+
@fixture_name = fixture_name.to_s
|
|
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")
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
def exists?
|
|
67
|
+
# Check in-memory cache first, then disk
|
|
68
|
+
self.class.memory_cache.key?(@fixture_name) || File.exist?(cache_file_path)
|
|
69
|
+
end
|
|
70
|
+
|
|
71
|
+
def load
|
|
72
|
+
# Check in-memory cache first
|
|
73
|
+
if self.class.memory_cache.key?(@fixture_name)
|
|
74
|
+
data = self.class.memory_cache[@fixture_name]
|
|
75
|
+
@records = data.fetch("records")
|
|
76
|
+
@exposed = data.fetch("exposed")
|
|
77
|
+
return true
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
# Fall back to disk
|
|
81
|
+
return false unless File.exist?(cache_file_path)
|
|
82
|
+
|
|
83
|
+
data = JSON.parse(File.read(cache_file_path))
|
|
84
|
+
@records = data.fetch("records")
|
|
85
|
+
@exposed = data.fetch("exposed")
|
|
86
|
+
|
|
87
|
+
# Store in memory for subsequent loads
|
|
88
|
+
self.class.memory_cache[@fixture_name] = data
|
|
89
|
+
|
|
90
|
+
true
|
|
91
|
+
end
|
|
92
|
+
|
|
93
|
+
def save(models_with_connections:, exposed_mapping:)
|
|
94
|
+
@records = generate_statements(models_with_connections)
|
|
95
|
+
@exposed = exposed_mapping
|
|
96
|
+
|
|
97
|
+
FileUtils.mkdir_p(File.dirname(cache_file_path))
|
|
98
|
+
|
|
99
|
+
data = {
|
|
100
|
+
"records" => @records,
|
|
101
|
+
"exposed" => @exposed
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
# Store in memory cache
|
|
105
|
+
self.class.memory_cache[@fixture_name] = data
|
|
106
|
+
|
|
107
|
+
File.write(cache_file_path, JSON.pretty_generate(data))
|
|
108
|
+
end
|
|
109
|
+
|
|
110
|
+
# Query exposed records from the database and return a FixtureSet
|
|
111
|
+
def build_fixture_set
|
|
112
|
+
exposed_records = @exposed.each_with_object({}) do |(name, value), hash|
|
|
113
|
+
was_array = value.is_a?(Array)
|
|
114
|
+
records = Array.wrap(value).map { |record_info| find_exposed_record(record_info.fetch("model"), record_info.fetch("id"), name) }
|
|
115
|
+
hash[name.to_sym] = was_array ? records : records.first
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
FixtureSet.new(exposed_records)
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
private
|
|
122
|
+
|
|
123
|
+
def find_exposed_record(model_name, id, exposed_name)
|
|
124
|
+
model = ActiveSupport::Inflector.constantize(model_name)
|
|
125
|
+
model.find(id)
|
|
126
|
+
rescue ActiveRecord::RecordNotFound
|
|
127
|
+
raise FixtureKit::ExposedRecordNotFound,
|
|
128
|
+
"Could not find #{model_name} with id=#{id} for exposed record '#{exposed_name}' in fixture '#{@fixture_name}'"
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
def generate_statements(models_with_connections)
|
|
132
|
+
statements_by_model = {}
|
|
133
|
+
|
|
134
|
+
models_with_connections.each do |model, connection|
|
|
135
|
+
columns = model.column_names
|
|
136
|
+
|
|
137
|
+
rows = []
|
|
138
|
+
model.order(:id).find_each do |record|
|
|
139
|
+
row_values = columns.map do |col|
|
|
140
|
+
value = record.read_attribute_before_type_cast(col)
|
|
141
|
+
connection.quote(value)
|
|
142
|
+
end
|
|
143
|
+
rows << "(#{row_values.join(", ")})"
|
|
144
|
+
end
|
|
145
|
+
|
|
146
|
+
next if rows.empty?
|
|
147
|
+
|
|
148
|
+
sql = build_insert_sql(model.table_name, columns, rows, connection)
|
|
149
|
+
statements_by_model[model.name] = sql
|
|
150
|
+
end
|
|
151
|
+
|
|
152
|
+
statements_by_model
|
|
153
|
+
end
|
|
154
|
+
|
|
155
|
+
def build_insert_sql(table_name, columns, rows, connection)
|
|
156
|
+
quoted_table = connection.quote_table_name(table_name)
|
|
157
|
+
quoted_columns = columns.map { |c| connection.quote_column_name(c) }
|
|
158
|
+
|
|
159
|
+
sql = "INSERT INTO #{quoted_table} (#{quoted_columns.join(", ")}) VALUES #{rows.join(", ")}"
|
|
160
|
+
|
|
161
|
+
add_conflict_handling(sql, connection)
|
|
162
|
+
end
|
|
163
|
+
|
|
164
|
+
def add_conflict_handling(sql, connection)
|
|
165
|
+
adapter_name = connection.adapter_name.downcase
|
|
166
|
+
|
|
167
|
+
case adapter_name
|
|
168
|
+
when /sqlite/
|
|
169
|
+
sql.sub(/\AINSERT INTO/i, "INSERT OR IGNORE INTO")
|
|
170
|
+
when /postgresql/, /postgis/
|
|
171
|
+
"#{sql} ON CONFLICT DO NOTHING"
|
|
172
|
+
when /mysql/, /trilogy/
|
|
173
|
+
sql.sub(/\AINSERT INTO/i, "INSERT IGNORE INTO")
|
|
174
|
+
else
|
|
175
|
+
sql
|
|
176
|
+
end
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
end
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
class FixtureContext
|
|
5
|
+
attr_reader :exposed
|
|
6
|
+
|
|
7
|
+
def initialize
|
|
8
|
+
@exposed = {}
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def expose(**records)
|
|
12
|
+
records.each do |name, record|
|
|
13
|
+
name = name.to_sym
|
|
14
|
+
|
|
15
|
+
if @exposed.key?(name)
|
|
16
|
+
raise FixtureKit::DuplicateNameError, <<~ERROR
|
|
17
|
+
Duplicate expose name :#{name}
|
|
18
|
+
|
|
19
|
+
A record with this name has already been exposed in this fixture.
|
|
20
|
+
ERROR
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
@exposed[name] = record
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
module FixtureRegistry
|
|
5
|
+
class << self
|
|
6
|
+
def register(fixture)
|
|
7
|
+
fixtures[fixture.name.to_s] = fixture
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def find(name)
|
|
11
|
+
fixtures[name.to_s]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def all_names
|
|
15
|
+
fixtures.keys
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
# Load all fixture definition files from the given path.
|
|
19
|
+
# Uses `load` instead of `require` to ensure fixtures are registered
|
|
20
|
+
# even if the files were previously required (e.g., after a reset).
|
|
21
|
+
def load_definitions(fixture_path)
|
|
22
|
+
Dir.glob(File.join(fixture_path, "**/*.rb")).each do |file|
|
|
23
|
+
load file
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def reset
|
|
28
|
+
@fixtures = nil
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Load a fixture's records into the database and return a FixtureSet.
|
|
32
|
+
# Uses cached INSERT statements if available, otherwise executes fixture and caches.
|
|
33
|
+
def load_fixture(name)
|
|
34
|
+
name = name.to_s
|
|
35
|
+
|
|
36
|
+
# Load the file on-demand if fixture not yet registered
|
|
37
|
+
unless find(name)
|
|
38
|
+
fixture_path = FixtureKit.configuration.fixture_path
|
|
39
|
+
file_path = File.expand_path(File.join(fixture_path, "#{name}.rb"))
|
|
40
|
+
load file_path
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
runner = FixtureRunner.new(name)
|
|
44
|
+
runner.run
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
private
|
|
48
|
+
|
|
49
|
+
def fixtures
|
|
50
|
+
@fixtures ||= {}
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "active_support/inflector"
|
|
4
|
+
|
|
5
|
+
module FixtureKit
|
|
6
|
+
class FixtureRunner
|
|
7
|
+
def initialize(fixture_name)
|
|
8
|
+
@fixture_name = fixture_name.to_sym
|
|
9
|
+
@cache = FixtureCache.new(@fixture_name)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def run
|
|
13
|
+
if @cache.exists?
|
|
14
|
+
execute_from_cache
|
|
15
|
+
elsif FixtureKit.configuration.autogenerate
|
|
16
|
+
execute_and_cache
|
|
17
|
+
else
|
|
18
|
+
raise FixtureKit::CacheMissingError, <<~ERROR
|
|
19
|
+
Cache not found for fixture '#{@fixture_name}'.
|
|
20
|
+
|
|
21
|
+
Run your tests with autogenerate enabled to generate the cache:
|
|
22
|
+
FixtureKit.configuration.autogenerate = true
|
|
23
|
+
|
|
24
|
+
Or generate caches by running your test suite once with autogenerate enabled.
|
|
25
|
+
ERROR
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
# Generate cache only (used for pregeneration in before(:suite))
|
|
30
|
+
# Wraps execution in a transaction that rolls back, so no data persists
|
|
31
|
+
# Always regenerates the cache, even if one exists
|
|
32
|
+
def generate_cache_only
|
|
33
|
+
# Clear any existing cache for this fixture
|
|
34
|
+
FixtureCache.clear(@fixture_name.to_s)
|
|
35
|
+
|
|
36
|
+
ActiveRecord::Base.transaction do
|
|
37
|
+
execute_and_cache
|
|
38
|
+
raise ActiveRecord::Rollback
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
true
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
private
|
|
45
|
+
|
|
46
|
+
def execute_and_cache
|
|
47
|
+
fixture = FixtureRegistry.find(@fixture_name)
|
|
48
|
+
raise ArgumentError, "Fixture '#{@fixture_name}' not found" unless fixture
|
|
49
|
+
|
|
50
|
+
# Start capturing SQL
|
|
51
|
+
capture = SqlCapture.new
|
|
52
|
+
capture.start
|
|
53
|
+
|
|
54
|
+
# Execute fixture definition - returns exposed records hash
|
|
55
|
+
exposed = fixture.execute
|
|
56
|
+
|
|
57
|
+
# Stop capturing and get affected models with their connections
|
|
58
|
+
models_with_connections = capture.stop
|
|
59
|
+
|
|
60
|
+
# Save cache
|
|
61
|
+
@cache.save(
|
|
62
|
+
models_with_connections: models_with_connections,
|
|
63
|
+
exposed_mapping: build_exposed_mapping(exposed)
|
|
64
|
+
)
|
|
65
|
+
|
|
66
|
+
# Return FixtureSet from the exposed records
|
|
67
|
+
FixtureSet.new(exposed)
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
def execute_from_cache
|
|
71
|
+
@cache.load
|
|
72
|
+
|
|
73
|
+
# Execute cached SQL statements by model
|
|
74
|
+
@cache.records.each do |model_name, sql|
|
|
75
|
+
next if sql.nil? || sql.empty?
|
|
76
|
+
|
|
77
|
+
model = ActiveSupport::Inflector.constantize(model_name)
|
|
78
|
+
connection = model.connection
|
|
79
|
+
connection.execute(sql)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# Query exposed records and build FixtureSet
|
|
83
|
+
@cache.build_fixture_set
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
def build_exposed_mapping(exposed)
|
|
87
|
+
mapping = {}
|
|
88
|
+
|
|
89
|
+
exposed.each do |name, record_or_records|
|
|
90
|
+
if record_or_records.is_a?(Array)
|
|
91
|
+
mapping[name.to_s] = record_or_records.map do |record|
|
|
92
|
+
{ "model" => record.class.name, "id" => record.id }
|
|
93
|
+
end
|
|
94
|
+
else
|
|
95
|
+
record = record_or_records
|
|
96
|
+
mapping[name.to_s] = { "model" => record.class.name, "id" => record.id }
|
|
97
|
+
end
|
|
98
|
+
end
|
|
99
|
+
|
|
100
|
+
mapping
|
|
101
|
+
end
|
|
102
|
+
end
|
|
103
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
class FixtureSet
|
|
5
|
+
def initialize(exposed_records)
|
|
6
|
+
@records = exposed_records
|
|
7
|
+
define_accessors
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def [](name)
|
|
11
|
+
@records[name.to_sym]
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
def to_h
|
|
15
|
+
@records.dup
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
private
|
|
19
|
+
|
|
20
|
+
def define_accessors
|
|
21
|
+
@records.each do |name, value|
|
|
22
|
+
define_singleton_method(name) { value }
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "fixture_kit"
|
|
4
|
+
|
|
5
|
+
module FixtureKit
|
|
6
|
+
module RSpec
|
|
7
|
+
# Class methods (extended via config.extend)
|
|
8
|
+
module ClassMethods
|
|
9
|
+
# Declare which fixture to use for this example group.
|
|
10
|
+
# Can be overridden in nested groups (like `let`).
|
|
11
|
+
#
|
|
12
|
+
# Example:
|
|
13
|
+
# RSpec.describe User do
|
|
14
|
+
# fixture "basic_users"
|
|
15
|
+
#
|
|
16
|
+
# it "has users" do
|
|
17
|
+
# expect(fixture.alice).to be_present
|
|
18
|
+
# end
|
|
19
|
+
#
|
|
20
|
+
# context "with admins" do
|
|
21
|
+
# fixture "admin_users" # Override
|
|
22
|
+
#
|
|
23
|
+
# it "has admin" do
|
|
24
|
+
# expect(fixture.admin).to be_present
|
|
25
|
+
# end
|
|
26
|
+
# end
|
|
27
|
+
# end
|
|
28
|
+
def fixture(name)
|
|
29
|
+
metadata[:fixture_name] = name.to_s
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Instance methods (included via config.include)
|
|
34
|
+
module InstanceMethods
|
|
35
|
+
# Returns the FixtureSet for the current example's fixture.
|
|
36
|
+
# Access exposed records as methods: fixture.alice, fixture.posts
|
|
37
|
+
def fixture
|
|
38
|
+
@_fixture_loaded ||= begin
|
|
39
|
+
fixture_name = self.class.metadata[:fixture_name]
|
|
40
|
+
raise "No fixture declared for this example group. Use `fixture \"name\"` in your describe/context block." unless fixture_name
|
|
41
|
+
|
|
42
|
+
FixtureKit::FixtureRegistry.load_fixture(fixture_name)
|
|
43
|
+
end
|
|
44
|
+
end
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
# Configure RSpec integration
|
|
50
|
+
RSpec.configure do |config|
|
|
51
|
+
config.extend FixtureKit::RSpec::ClassMethods
|
|
52
|
+
config.include FixtureKit::RSpec::InstanceMethods
|
|
53
|
+
|
|
54
|
+
# Setup caches at suite start based on autogenerate setting
|
|
55
|
+
# - autogenerate=true: Clear all caches (unless FIXTURE_KIT_PRESERVE_CACHE is set)
|
|
56
|
+
# - autogenerate=false: Pre-generate all caches so tests don't fail
|
|
57
|
+
config.before(:suite) do
|
|
58
|
+
if FixtureKit.configuration.autogenerate
|
|
59
|
+
preserve_cache = ENV["FIXTURE_KIT_PRESERVE_CACHE"].to_s.match?(/\A(1|true|yes)\z/i)
|
|
60
|
+
FixtureKit::FixtureCache.clear unless preserve_cache
|
|
61
|
+
else
|
|
62
|
+
FixtureKit::FixtureCache.pregenerate_all
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require "pathname"
|
|
4
|
+
|
|
5
|
+
module FixtureKit
|
|
6
|
+
module Singleton
|
|
7
|
+
def configure
|
|
8
|
+
@configuration = Configuration.new
|
|
9
|
+
yield(@configuration) if block_given?
|
|
10
|
+
self
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def configuration
|
|
14
|
+
@configuration ||= Configuration.new
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def define(&block)
|
|
18
|
+
caller_file = File.expand_path(caller_locations(1, 1).first.path)
|
|
19
|
+
fixture_path = File.expand_path(configuration.fixture_path)
|
|
20
|
+
|
|
21
|
+
# "/abs/path/spec/fixtures/teams/basic.rb" -> "teams/basic"
|
|
22
|
+
relative_path = Pathname.new(caller_file).relative_path_from(Pathname.new(fixture_path))
|
|
23
|
+
name = relative_path.to_s.sub(/\.rb$/, "")
|
|
24
|
+
|
|
25
|
+
fixture = Fixture.new(name, &block)
|
|
26
|
+
FixtureRegistry.register(fixture)
|
|
27
|
+
fixture
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def reset
|
|
31
|
+
@configuration = nil
|
|
32
|
+
FixtureRegistry.reset
|
|
33
|
+
FixtureCache.clear_memory_cache
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
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
|
data/lib/fixture_kit.rb
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module FixtureKit
|
|
4
|
+
class Error < StandardError; end
|
|
5
|
+
class DuplicateFixtureError < Error; end
|
|
6
|
+
class DuplicateNameError < Error; end
|
|
7
|
+
class CacheMissingError < Error; end
|
|
8
|
+
class ExposedRecordNotFound < Error; end
|
|
9
|
+
|
|
10
|
+
autoload :VERSION, File.expand_path("fixture_kit/version", __dir__)
|
|
11
|
+
autoload :Configuration, File.expand_path("fixture_kit/configuration", __dir__)
|
|
12
|
+
autoload :Singleton, File.expand_path("fixture_kit/singleton", __dir__)
|
|
13
|
+
autoload :Fixture, File.expand_path("fixture_kit/fixture", __dir__)
|
|
14
|
+
autoload :FixtureContext, File.expand_path("fixture_kit/fixture_context", __dir__)
|
|
15
|
+
autoload :FixtureRegistry, File.expand_path("fixture_kit/fixture_registry", __dir__)
|
|
16
|
+
autoload :FixtureSet, File.expand_path("fixture_kit/fixture_set", __dir__)
|
|
17
|
+
autoload :SqlCapture, File.expand_path("fixture_kit/sql_capture", __dir__)
|
|
18
|
+
autoload :FixtureCache, File.expand_path("fixture_kit/fixture_cache", __dir__)
|
|
19
|
+
autoload :FixtureRunner, File.expand_path("fixture_kit/fixture_runner", __dir__)
|
|
20
|
+
|
|
21
|
+
extend Singleton
|
|
22
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: fixture_kit
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Ngan Pham
|
|
8
|
+
autorequire:
|
|
9
|
+
bindir: bin
|
|
10
|
+
cert_chain: []
|
|
11
|
+
date: 2026-02-19 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: activesupport
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '8.0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '8.0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: activerecord
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '8.0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '8.0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: irb
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :development
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: rake
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :development
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: rspec-rails
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :development
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
83
|
+
- !ruby/object:Gem::Dependency
|
|
84
|
+
name: sqlite3
|
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
|
86
|
+
requirements:
|
|
87
|
+
- - ">="
|
|
88
|
+
- !ruby/object:Gem::Version
|
|
89
|
+
version: '0'
|
|
90
|
+
type: :development
|
|
91
|
+
prerelease: false
|
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
93
|
+
requirements:
|
|
94
|
+
- - ">="
|
|
95
|
+
- !ruby/object:Gem::Version
|
|
96
|
+
version: '0'
|
|
97
|
+
- !ruby/object:Gem::Dependency
|
|
98
|
+
name: railties
|
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
|
100
|
+
requirements:
|
|
101
|
+
- - ">="
|
|
102
|
+
- !ruby/object:Gem::Version
|
|
103
|
+
version: '0'
|
|
104
|
+
type: :development
|
|
105
|
+
prerelease: false
|
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
107
|
+
requirements:
|
|
108
|
+
- - ">="
|
|
109
|
+
- !ruby/object:Gem::Version
|
|
110
|
+
version: '0'
|
|
111
|
+
description: FixtureKit provides lightning-fast test setup by caching database records.
|
|
112
|
+
Define fixtures using any tool (FactoryBot, raw ActiveRecord, etc.), and FixtureKit
|
|
113
|
+
caches the SQL to replay in subsequent test runs.
|
|
114
|
+
email:
|
|
115
|
+
- ngan.pham@gusto.com
|
|
116
|
+
executables: []
|
|
117
|
+
extensions: []
|
|
118
|
+
extra_rdoc_files: []
|
|
119
|
+
files:
|
|
120
|
+
- LICENSE
|
|
121
|
+
- README.md
|
|
122
|
+
- lib/fixture_kit.rb
|
|
123
|
+
- lib/fixture_kit/configuration.rb
|
|
124
|
+
- lib/fixture_kit/fixture.rb
|
|
125
|
+
- lib/fixture_kit/fixture_cache.rb
|
|
126
|
+
- lib/fixture_kit/fixture_context.rb
|
|
127
|
+
- lib/fixture_kit/fixture_registry.rb
|
|
128
|
+
- lib/fixture_kit/fixture_runner.rb
|
|
129
|
+
- lib/fixture_kit/fixture_set.rb
|
|
130
|
+
- lib/fixture_kit/rspec.rb
|
|
131
|
+
- lib/fixture_kit/singleton.rb
|
|
132
|
+
- lib/fixture_kit/sql_capture.rb
|
|
133
|
+
- lib/fixture_kit/version.rb
|
|
134
|
+
homepage: https://github.com/Gusto/fixture_kit
|
|
135
|
+
licenses:
|
|
136
|
+
- MIT
|
|
137
|
+
metadata:
|
|
138
|
+
homepage_uri: https://github.com/Gusto/fixture_kit
|
|
139
|
+
source_code_uri: https://github.com/Gusto/fixture_kit
|
|
140
|
+
changelog_uri: https://github.com/Gusto/fixture_kit/releases
|
|
141
|
+
post_install_message:
|
|
142
|
+
rdoc_options: []
|
|
143
|
+
require_paths:
|
|
144
|
+
- lib
|
|
145
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
146
|
+
requirements:
|
|
147
|
+
- - ">="
|
|
148
|
+
- !ruby/object:Gem::Version
|
|
149
|
+
version: 3.3.0
|
|
150
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
151
|
+
requirements:
|
|
152
|
+
- - ">="
|
|
153
|
+
- !ruby/object:Gem::Version
|
|
154
|
+
version: '0'
|
|
155
|
+
requirements: []
|
|
156
|
+
rubygems_version: 3.5.22
|
|
157
|
+
signing_key:
|
|
158
|
+
specification_version: 4
|
|
159
|
+
summary: Fast test fixtures with SQL caching
|
|
160
|
+
test_files: []
|