fixture_farm 1.0.0 → 1.1.1

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: 728eb6b44d1deb4df01e4bc03795758f3846e169f6dc07f0f5c7a6a799881976
4
- data.tar.gz: 59fe50410c6fd317537dd653001f8bd8e7b571b2375faa97d95e6dc41e188e76
3
+ metadata.gz: d3a39068e7447367066d1947cf1f14d16366d10e88e91ae922ff3953b7667ff5
4
+ data.tar.gz: 7f28f26923b51368511ed5ef8c55531f2d405ba36d11ea802caedbc554fbcf0a
5
5
  SHA512:
6
- metadata.gz: cd4e4c6609afbe0a09413cf19d71eec01b9366f4e2d793190b8cb9b7aa86eecded615cbeae5daf31036cd3ca6a4bfea42c3bc568236aa9c4c3357b9e81157052
7
- data.tar.gz: 2210cb7393ce70c90d3e0225ba28f9b87ee2f4b6d1d106ce029e352b475b6c1c3eed78c1af4bd15e864330101f0059a06ec2f5c65415faf1228a44e03bcf8693
6
+ metadata.gz: 84a8649ace5e14d8dd09b94cdde3d15f02ebd8bd602f706b48ec48f9bff9ab4ea0170fd26827886ee8b0595909d930ce16e61d21e422df9b9e9a7c545df248b7
7
+ data.tar.gz: d25f84374a79d33ee8ee117656c59e7523d4087dd3dc9065de7a540b882b5cd2fd9897e4aee3aa23af3a8f0eb43ee5168efb2d390dc12ecbde8b1a3b0fd22656
data/README.md CHANGED
@@ -9,6 +9,7 @@ A few things to note:
9
9
  - generated fixture that `belongs_to` a record from an existing fixture, will reference that fixture by name.
10
10
  - models, destroyed during recording, will be removed from fixtures (if they were originally there).
11
11
  - generated `ActiveStorage::Blob` fixtures file names, will be the same as fixture names (so they can be generated multiple times, without generating new file each time).
12
+ - AR models gain `#fixture_name` method
12
13
 
13
14
  ### Limitations
14
15
 
@@ -54,89 +55,127 @@ include FixtureFarm::ActiveJobHook if defined?(FixtureFarm)
54
55
  Then start/stop recording using tasks:
55
56
 
56
57
  ```bash
57
- bundle exec fixture_farm record some_awesome_name_prefix
58
+ bundle exec fixture_farm record
59
+ # OR
60
+ bundle exec fixture_farm record name_prefix
61
+ # OR
62
+ bundle exec fixture_farm record name_prefix:replaces_name
63
+
58
64
  bundle exec fixture_farm status
59
65
  bundle exec fixture_farm stop
60
66
  ```
61
67
 
62
68
  ### Record in tests
63
69
 
64
- To record in tests, wrap some code in `record_new_fixtures` block. For example:
70
+ To record in tests, wrap some code in `record_fixtures` block. For example:
65
71
 
66
72
  ```ruby
67
73
 
68
74
  include FixtureFarm::TestHelper
69
75
 
70
- test 'some stuff does the right thing' do
71
- record_new_fixtures do |recorder|
72
- user = User.create!(name: 'Bob')
73
- post = user.posts.create!(title: 'Stuff')
74
-
75
- recorder.stop!
76
+ test 'parents fixtures have children' do
77
+ offending_records = Parent.where.missing(:children)
76
78
 
77
- assert_difference 'user.published_posts.size' do
78
- post.publish!
79
+ if ENV['GENERATE_FIXTURES']
80
+ record_fixtures do
81
+ offending_records.each do |parent|
82
+ parent.children.create!(name: 'Bob')
83
+ end
79
84
  end
85
+ else
86
+ assert_empty offending_records.map(&:fixture_name),
87
+ "The following parents don't have children:"
80
88
  end
81
89
  end
82
90
  ```
83
91
 
84
- Running this test generates user and post fixtures. Now you can rewrite this test to use them:
92
+ Assuming there was a parent fixture `dave` that didn't have any children, this test will fail. Now, running the same test with `GENERATE_FIXTURES=1` will generate one child fixture named `dave_child_1`. The test is now passing.
85
93
 
86
- ```ruby
87
- test 'some stuff does the right thing' do
88
- user = users('user_1')
94
+ `record_fixtures` accepts optional name prefix, that applies to all new fixture names.
89
95
 
90
- assert_difference 'user.published_posts.size' do
91
- user.posts.first.publish!
92
- end
96
+ #### Fixture Name Replacement
97
+
98
+ `record_fixtures` also supports hash arguments for advanced fixture naming control:
99
+
100
+ ```ruby
101
+ # Replace 'client_1' with 'new_client' in fixture names, or use 'new_client' as prefix if not found
102
+ record_fixtures(new_client: :client_1) do
103
+ User.create!(name: 'Test User', email: 'test@example.com')
93
104
  end
94
105
  ```
95
106
 
96
- `record_new_fixtures` accepts optional name prefix, that applies to all new fixture names.
107
+ This works in two ways:
108
+ - **Replacement**: If a generated fixture name contains `client_1`, it gets replaced with `new_client`
109
+ - **Prefixing**: If a generated fixture name doesn't contain `client_1`, it gets prefixed with `new_client_`
110
+
111
+ For example:
112
+ - A user fixture that would be named `client_1_user_1` becomes `new_client_user_1` (replacement)
113
+ - A user fixture that would be named `user_1` becomes `new_client_user_1` (prefixing)
97
114
 
98
- A more robust approach is to have dedicated fixture tests that normally fail, but can be optionally run in "record mode" (think VCR).
115
+ ### Automatic fixture naming
99
116
 
100
- For example, let's say we have `Author` model that `has_many :posts` and we require authors to have at least one post. Here's the test to enforce `authors` fixtures to comply with this rule:
117
+ Generated fixture names are based on the first `belongs_to` association of the model. E.g., if a new post fixtures belongs_to to a user fixture `bob`, the name is going to be `bob_post_1`.
101
118
 
102
- ```ruby
103
- test 'authors fixtures must have at least one post' do
104
- offending_records = Author.where.missing(:posts)
119
+ It's possible to lower the priority of given parent assiciations when it comes to naming, so that certain names are only picked when there are no other suitable parent associations. This is useful, for example, to exclude `acts_as_tenant` association:
105
120
 
106
- assert_empty offending_records
107
- end
121
+ ```ruby
122
+ FixtureFarm.low_priority_parent_model_for_naming = -> { _1.is_a?(TenantModel) }
108
123
  ```
109
124
 
110
- Let's say this test is currently failing.
125
+ ### Attachment fixtures
111
126
 
112
- Now let's add the option to automatically record missing fixtures:
127
+ Rather than [manually crafting attachment fixtures](https://guides.rubyonrails.org/v8.0/active_storage_overview.html#adding-attachments-to-fixtures), we can get the gem to do the work. Not only is this less boring, but it's also going to generate variant fixtures.
113
128
 
114
- ```ruby
115
- test 'authors fixtures must have at least one post' do
116
- offending_records = Author.where.missing(:posts)
129
+ If we then check the generated blob files into git (along with the fixture files themselves), no attachment processing will be happening in tests or after `rails db:fixtures:load`.
117
130
 
118
- if ENV['RECORD_FIXTURES']
119
- record_new_fixtures do
120
- offending_records.each do |author|
121
- author.posts.create!(text: 'some text')
122
- end
123
- end
124
- end
131
+ We'll need a special storage service for the fixture blobs we want to keep versioned. For example:
125
132
 
126
- assert_empty offending_records
127
- end
133
+ ```yml
134
+ # config/storage.yml
135
+ test_fixtures:
136
+ service: Disk
137
+ root: <%= Rails.root.join("test/fixtures/files/active_storage_blobs") %>
128
138
  ```
129
139
 
130
- Running this test with `RECORD_FIXTURES=1` will generate missing fixture entries in `test/fixtures/posts.yml`. Now re-run the test again and it's passing.
140
+ Now a test like the one below is either going to fail if some product fixtures have no attachments, or, if run with `GENERATE_FIXTURES=1`, is going to generate those attachment fixtures, their variant fixtures if needed, along with all the blob files tucked away in a separate (from regular throw away storage) folder that can be checked in:
131
141
 
132
- ### Automatic fixture naming
142
+ ```ruby
143
+ if ENV["GENERATE_FIXTURES"]
144
+ setup do
145
+ @original_queue_adapter = Rails.configuration.active_job.queue_adapter
146
+ # This is so that variants get generated and blobs analyzed
147
+ Rails.configuration.active_job.queue_adapter = :inline
148
+
149
+ @original_storage_service = ActiveStorage::Blob.service
150
+ ActiveStorage::Blob.service = ActiveStorage::Blob.services.fetch(:test_fixtures)
151
+ end
133
152
 
134
- Generated fixture names are based on the first `belongs_to` association of the model. E.g., if a new post fixtures belongs_to to a user fixture `bob`, the name is going to be `bob_post_1`.
153
+ teardown do
154
+ Rails.configuration.active_job.queue_adapter = @original_queue_adapter
155
+ ActiveStorage::Blob.service = @original_storage_service
156
+ end
157
+ end
135
158
 
136
- It's possible to lower the priority of given parent assiciations when it comes to naming, so that certain names are only picked when there are no other suitable parent associations. This is useful, for example, to exclude `acts_as_tenant` association:
159
+ test "product fixtures have images" do
160
+ offending_records = Product.where.missing(:images_attachments)
137
161
 
138
- ```ruby
139
- FixtureFarm.low_priority_parent_model_for_naming = -> { _1.is_a?(TenantModel) }
162
+ if ENV["GENERATE_FIXTURES"]
163
+ record_fixtures do |recorder|
164
+ ActiveStorage::Attachment.where(record_type: 'Product').destroy_all
165
+
166
+ Product.find_each do |product|
167
+ product.images.attach(
168
+ io: File.open(file_fixture("products/#{product.fixture_name}.jpg")),
169
+ filename: "#{product.fixture_name}.jpg",
170
+ content_type: "image/jpeg"
171
+ )
172
+ end
173
+ end
174
+ else
175
+ assert_empty offending_records.map(&:fixture_name),
176
+ "Expected the following product fixtures to have images:"
177
+ end
178
+ end
140
179
  ```
141
180
 
142
181
  ## License
data/bin/fixture_farm.rb CHANGED
@@ -4,14 +4,24 @@
4
4
  require_relative '../lib/fixture_farm/fixture_recorder'
5
5
 
6
6
  def usage
7
- puts 'Usage: bundle exec fixture_farm <record|status|stop> [fixture_name_prefix]'
7
+ puts 'Usage: bundle exec fixture_farm <record|status|stop> [name_prefix|name_prefix:replaces_name]'
8
8
  exit 1
9
9
  end
10
10
 
11
11
  case ARGV[0]
12
12
  when 'record'
13
- FixtureFarm::FixtureRecorder.start_recording_session!(ARGV[1])
14
- puts "Recording fixtures#{" with prefix #{ARGV[1]}" unless ARGV[1].nil?}"
13
+ prefix_arg = ARGV[1]
14
+
15
+ # Parse hash syntax like "new_user:user_1" into {new_user: :user_1}
16
+ if prefix_arg&.include?(':')
17
+ parts = prefix_arg.split(':', 2)
18
+ parsed_prefix = { parts[0].to_sym => parts[1].to_sym }
19
+ else
20
+ parsed_prefix = prefix_arg
21
+ end
22
+
23
+ FixtureFarm::FixtureRecorder.start_recording_session!(parsed_prefix)
24
+ puts "Recording fixtures#{" with prefix #{prefix_arg}" unless prefix_arg.nil?}"
15
25
  when 'status'
16
26
  if FixtureFarm::FixtureRecorder.recording_session_in_progress?
17
27
  puts 'Recording is on'
@@ -8,7 +8,7 @@ module FixtureFarm
8
8
  include Hook
9
9
 
10
10
  included do
11
- around_perform :record_new_fixtures, if: :record_new_fixtures?
11
+ around_perform :record_fixtures, if: :record_fixtures?
12
12
  end
13
13
  end
14
14
  end
@@ -8,7 +8,7 @@ module FixtureFarm
8
8
  include Hook
9
9
 
10
10
  included do
11
- around_action :record_new_fixtures, if: :record_new_fixtures?
11
+ around_action :record_fixtures, if: :record_fixtures?
12
12
  end
13
13
  end
14
14
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FixtureFarm
4
-
5
4
  mattr_accessor :low_priority_parent_model_for_naming
6
5
 
7
6
  class FixtureRecorder
@@ -16,7 +15,13 @@ module FixtureFarm
16
15
  end
17
16
 
18
17
  def initialize(fixture_name_prefix, new_models = [])
19
- @fixture_name_prefix = fixture_name_prefix
18
+ if fixture_name_prefix.is_a?(Hash)
19
+ @fixture_name_replacements = fixture_name_prefix
20
+ @fixture_name_prefix = nil
21
+ else
22
+ @fixture_name_prefix = fixture_name_prefix
23
+ @fixture_name_replacements = {}
24
+ end
20
25
  @new_models = new_models
21
26
  @deleted_models = {}
22
27
  @initial_now = Time.zone.now
@@ -72,7 +77,7 @@ module FixtureFarm
72
77
  recording_session['error']
73
78
  end
74
79
 
75
- def record_new_fixtures
80
+ def record_fixtures
76
81
  @stopped = false
77
82
 
78
83
  @subscriber = ActiveSupport::Notifications.subscribe 'sql.active_record' do |event|
@@ -95,9 +100,11 @@ module FixtureFarm
95
100
  end
96
101
  end
97
102
 
98
- yield self
103
+ result = yield self
99
104
 
100
105
  stop! unless @stopped
106
+
107
+ result
101
108
  ensure
102
109
  ActiveSupport::Notifications.unsubscribe(@subscriber)
103
110
  end
@@ -144,8 +151,10 @@ module FixtureFarm
144
151
 
145
152
  blob.update!(key: new_key)
146
153
 
147
- from_path = Rails.root.join('storage', old_key[0..1], old_key[2..3], old_key)
148
- to_dir = Rails.root.join('storage', new_key[0..1], new_key[2..3])
154
+ blobs_root_path = Pathname.new(ActiveStorage::Blob.service.root)
155
+
156
+ from_path = blobs_root_path.join(old_key[0..1], old_key[2..3], old_key)
157
+ to_dir = blobs_root_path.join(new_key[0..1], new_key[2..3])
149
158
  to_path = to_dir.join(new_key)
150
159
 
151
160
  `mkdir -p #{to_dir}`
@@ -366,12 +375,25 @@ module FixtureFarm
366
375
  fixture_name(model_instance) || begin
367
376
  existing_fixtures = existing_fixtures_for_model(model_instance)
368
377
 
369
- new_fixture_name = [
378
+ base_name = [
370
379
  first_belongs_to_fixture_name(model_instance).presence || @fixture_name_prefix,
371
380
  "#{model_instance.class.name.underscore.split('/').last}_1"
372
381
  ].select(&:present?).join('_')
373
382
 
374
- while @named_new_fixtures[new_fixture_name] || existing_fixtures[new_fixture_name] && !@deleted_models[new_fixture_name]
383
+ @fixture_name_replacements.each do |new_name, old_name|
384
+ # Only apply replacement if the base_name doesn't already start with new_name
385
+ # This prevents double-application of replacements
386
+ next if base_name.start_with?("#{new_name}_")
387
+
388
+ original_name = base_name
389
+ base_name = base_name.gsub(/\b#{Regexp.escape(old_name.to_s)}\b/, new_name.to_s)
390
+
391
+ # If no replacement occurred, use new_name as prefix
392
+ base_name = "#{new_name}_#{base_name}" if base_name == original_name
393
+ end
394
+
395
+ new_fixture_name = base_name
396
+ while @named_new_fixtures[new_fixture_name] || (existing_fixtures[new_fixture_name] && !@deleted_models[new_fixture_name])
375
397
  new_fixture_name = new_fixture_name.sub(/_(\d+)$/, "_#{Regexp.last_match(1).to_i + 1}")
376
398
  end
377
399
 
@@ -4,12 +4,12 @@ require 'fixture_farm/fixture_recorder'
4
4
 
5
5
  module FixtureFarm
6
6
  module Hook
7
- def record_new_fixtures(&block)
7
+ def record_fixtures(&block)
8
8
  fixture_recorder = FixtureRecorder.resume_recording_session
9
9
  return unless fixture_recorder # Bail if session was stopped due to error
10
10
 
11
11
  begin
12
- fixture_recorder.record_new_fixtures { block.call }
12
+ fixture_recorder.record_fixtures { block.call }
13
13
  ensure
14
14
  fixture_recorder.update_recording_session
15
15
  end
@@ -17,7 +17,7 @@ module FixtureFarm
17
17
 
18
18
  private
19
19
 
20
- def record_new_fixtures?
20
+ def record_fixtures?
21
21
  FixtureRecorder.recording_session_in_progress?
22
22
  end
23
23
  end
@@ -4,8 +4,8 @@ require 'fixture_farm/fixture_recorder'
4
4
 
5
5
  module FixtureFarm
6
6
  module TestHelper
7
- def record_new_fixtures(fixture_name_prefix = nil, &block)
8
- FixtureRecorder.new(fixture_name_prefix).record_new_fixtures(&block)
7
+ def record_fixtures(fixture_name_prefix = nil, &block)
8
+ FixtureRecorder.new(fixture_name_prefix).record_fixtures(&block)
9
9
  end
10
10
  end
11
11
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module FixtureFarm
4
- VERSION = '1.0.0'
4
+ VERSION = '1.1.1'
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: fixture_farm
3
3
  version: !ruby/object:Gem::Version
4
- version: 1.0.0
4
+ version: 1.1.1
5
5
  platform: ruby
6
6
  authors:
7
7
  - artemave
8
8
  autorequire:
9
9
  bindir: bin
10
10
  cert_chain: []
11
- date: 2025-07-28 00:00:00.000000000 Z
11
+ date: 2025-08-10 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: rails