fixture_farm 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 ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 90fbbd430cc9517a11e666fdb080b4289924871a2b73d23229e6293a16adc677
4
+ data.tar.gz: ff02de63066e6b8b012cb55c6e04785dff300a62b8da6e85542d8d87770462c4
5
+ SHA512:
6
+ metadata.gz: 7eead7fd8cc32ab9b27b1ce2fb825fa84f8ff3ca23073c13d1737e3f4f3c59fee6d6a5c38124d59e7980c1336ac2c3f7a8f93825240dd88f7714b4abb76b2137
7
+ data.tar.gz: 253909f06e93222c1badb80dda9d8b06726080dca76356ddb562ac38df18bdc0dd9873211f79e4a3282967db50971b29ffb9f9e2a187e7eb79d9161791eb3516
data/MIT-LICENSE ADDED
@@ -0,0 +1,20 @@
1
+ Copyright 2021 artemave
2
+
3
+ Permission is hereby granted, free of charge, to any person obtaining
4
+ a copy of this software and associated documentation files (the
5
+ "Software"), to deal in the Software without restriction, including
6
+ without limitation the rights to use, copy, modify, merge, publish,
7
+ distribute, sublicense, and/or sell copies of the Software, and to
8
+ permit persons to whom the Software is furnished to do so, subject to
9
+ the following conditions:
10
+
11
+ The above copyright notice and this permission notice shall be
12
+ included in all copies or substantial portions of the Software.
13
+
14
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
15
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
16
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
17
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
18
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
19
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
20
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,82 @@
1
+ # FixtureFarm
2
+
3
+ This gem lets you do two things:
4
+
5
+ - record fixtures as you browse.
6
+ - record fixtures for a block of code (e.g. setup part of a test).
7
+
8
+ Generated fixture that `belongs_to` a record from an existing fixture, will reference that fixture by name.
9
+
10
+ ## Usage
11
+
12
+ ### Record as you browse
13
+
14
+ To record as you browse in development add this to `ApplicationController`:
15
+
16
+ ```ruby
17
+ include FixtureFarm::ControllerHook if Rails.env.development?
18
+ ```
19
+
20
+ Then start/stop recording using tasks:
21
+
22
+ ```bash
23
+ bundle exec fixture_farm record some_awesome_name_prefix
24
+ bundle exec fixture_farm status
25
+ bundle exec fixture_farm stop
26
+ ```
27
+
28
+ ### Record in tests
29
+
30
+ To record in tests, wrap some code in `record_new_fixtures` block. For example:
31
+
32
+ ```ruby
33
+
34
+ include FixtureFarm::TestHelper
35
+
36
+ test 'some stuff does the right thing' do
37
+ record_new_fixtures('some_stuff') do |stop_recording|
38
+ user = User.create!(name: 'Bob')
39
+ post = user.posts.create!(title: 'Stuff')
40
+
41
+ stop_recording.call
42
+
43
+ assert_difference 'user.published_posts.size' do
44
+ post.publish!
45
+ end
46
+ end
47
+ end
48
+ ```
49
+
50
+ Running this test generates user and post fixtures. Now you can rewrite this test to use them:
51
+
52
+ ```ruby
53
+ test 'some stuff does the right thing' do
54
+ user = users('some_stuff_user_1')
55
+
56
+ assert_difference 'user.published_posts.size' do
57
+ user.posts.first.publish!
58
+ end
59
+ end
60
+ ```
61
+
62
+ ## Installation
63
+ Add this line to your application's Gemfile:
64
+
65
+ ```ruby
66
+ gem 'fixture_farm', group: %i[development test]
67
+ ```
68
+
69
+ And then execute:
70
+
71
+ ```bash
72
+ bundle install
73
+ ```
74
+
75
+ Or install it yourself as:
76
+
77
+ ```bash
78
+ gem install fixture_farm
79
+ ```
80
+
81
+ ## License
82
+ The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
data/Rakefile ADDED
@@ -0,0 +1,13 @@
1
+ require "bundler/setup"
2
+
3
+ require "bundler/gem_tasks"
4
+
5
+ require "rake/testtask"
6
+
7
+ Rake::TestTask.new(:test) do |t|
8
+ t.libs << 'test'
9
+ t.pattern = 'test/**/*_test.rb'
10
+ t.verbose = false
11
+ end
12
+
13
+ task default: :test
data/bin/fixture_farm ADDED
@@ -0,0 +1,7 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ system(
5
+ "rails runner #{File.expand_path('fixture_farm.rb', __dir__)} #{ARGV.join(' ')}",
6
+ exception: true
7
+ )
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FixtureFarm
4
+ module ActiveRecordExtension
5
+ def fixture_name
6
+ require 'active_record/fixtures'
7
+
8
+ return nil unless fixture_file_path
9
+
10
+ fixtures = YAML.load_file(fixture_file_path)
11
+ fixtures.keys.find do |key|
12
+ ActiveRecord::FixtureSet.identify(key) == id
13
+ end
14
+ end
15
+
16
+ def fixture_file_path
17
+ klass = self.class
18
+
19
+ while klass < ActiveRecord::Base
20
+ path = Rails.root.join('test', 'fixtures', "#{klass.to_s.underscore.pluralize}.yml")
21
+ return path if File.exist?(path)
22
+
23
+ klass = klass.superclass
24
+ end
25
+
26
+ nil
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fixture_farm/fixture_recorder"
4
+
5
+ module FixtureFarm
6
+ module ControllerHook
7
+ extend ActiveSupport::Concern
8
+
9
+ included do
10
+ around_action :record_new_fixtures, if: :record_new_fixtures?
11
+ end
12
+
13
+ private
14
+
15
+ def record_new_fixtures(&block)
16
+ fixture_recorder = FixtureRecorder.resume_recording_session
17
+
18
+ fixture_recorder.record_new_fixtures do
19
+ block.call
20
+ end
21
+ ensure
22
+ fixture_recorder.update_recording_session
23
+ end
24
+
25
+ def record_new_fixtures?
26
+ FixtureRecorder.recording_session_in_progress?
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,216 @@
1
+ # frozen_string_literal: true
2
+
3
+ module FixtureFarm
4
+ class FixtureRecorder
5
+ STORE_PATH = Rails.root.join('tmp', 'fixture_farm_store.json')
6
+
7
+ def initialize(fixture_name_prefix, new_models = [])
8
+ @fixture_name_prefix = fixture_name_prefix
9
+ @new_models = new_models
10
+ @initial_now = Time.zone.now
11
+ end
12
+
13
+ def self.resume_recording_session
14
+ start_recording_session! unless recording_session_in_progress?
15
+
16
+ recording_session = JSON.load_file(STORE_PATH)
17
+
18
+ new_models = recording_session['new_models'].map do |(class_name, id)|
19
+ class_name.constantize.find(id)
20
+ end
21
+
22
+ new(recording_session['fixture_name_prefix'], new_models)
23
+ end
24
+
25
+ def self.start_recording_session!(fixture_name_prefix)
26
+ File.write(STORE_PATH, {
27
+ fixture_name_prefix: fixture_name_prefix,
28
+ new_models: []
29
+ }.to_json)
30
+ end
31
+
32
+ def self.stop_recording_session!
33
+ FileUtils.rm_f(STORE_PATH)
34
+ end
35
+
36
+ def self.recording_session_in_progress?
37
+ File.exist?(STORE_PATH)
38
+ end
39
+
40
+ def record_new_fixtures
41
+ stopped = false
42
+
43
+ subscriber = ActiveSupport::Notifications.subscribe 'sql.active_record' do |event|
44
+ payload = event.payload
45
+
46
+ next unless payload[:name] =~ /(\w+) Create/
47
+
48
+ new_fixture_class_name = Regexp.last_match(1)
49
+
50
+ payload[:connection].transaction_manager.current_transaction.records.reject(&:persisted?).reject(&:destroyed?).each do |model_instance|
51
+ next if new_fixture_class_name != model_instance.class.name
52
+
53
+ @new_models << model_instance
54
+ end
55
+ end
56
+
57
+ yield lambda {
58
+ ActiveSupport::Notifications.unsubscribe(subscriber)
59
+ stopped = true
60
+ reload_models
61
+ update_fixture_files(named_new_fixtures)
62
+ }
63
+
64
+ unless stopped
65
+ reload_models
66
+ update_fixture_files(named_new_fixtures)
67
+ end
68
+ ensure
69
+ ActiveSupport::Notifications.unsubscribe(subscriber)
70
+ end
71
+
72
+ def update_recording_session
73
+ return unless FixtureRecorder.recording_session_in_progress?
74
+
75
+ File.write(STORE_PATH, {
76
+ fixture_name_prefix: @fixture_name_prefix,
77
+ new_models: @new_models.map { |model| [model.class.name, model.id] }
78
+ }.to_json)
79
+ end
80
+
81
+ private
82
+
83
+ def reload_models
84
+ @new_models = @new_models.map do |model_instance|
85
+ # reload in case model was updated after initial create
86
+ model_instance.reload
87
+ # Some records are created and then later removed.
88
+ # We don't want to turn those into fixtures
89
+ rescue ActiveRecord::RecordNotFound
90
+ nil
91
+ end.compact
92
+ end
93
+
94
+ def named_new_fixtures
95
+ @new_models.uniq(&:id).each_with_object({}) do |model_instance, named_new_fixtures|
96
+ new_fixture_name = "#{@fixture_name_prefix}_#{model_instance.class.name.underscore.gsub('/', '_')}_1"
97
+
98
+ while named_new_fixtures[new_fixture_name]
99
+ new_fixture_name = new_fixture_name.sub(/_(\d+)$/, "_#{Regexp.last_match(1).to_i + 1}")
100
+ end
101
+
102
+ named_new_fixtures[new_fixture_name] = model_instance
103
+ end
104
+ end
105
+
106
+ def update_fixture_files(named_new_fixtures)
107
+ named_new_fixtures.each do |new_fixture_name, model_instance|
108
+ attributes = model_instance.attributes
109
+
110
+ yaml_attributes = attributes.except('id').compact.map do |k, v|
111
+ belongs_to_association = model_instance.class.reflect_on_all_associations.filter(&:belongs_to?).find do |a|
112
+ a.foreign_key.to_s == k
113
+ end
114
+
115
+ if belongs_to_association
116
+ associated_model_instance = model_instance.public_send(belongs_to_association.name)
117
+
118
+ associated_fixture_name = named_new_fixtures.find do |_, fixture_model|
119
+ fixture_model.id == associated_model_instance.id
120
+ end&.first || associated_model_instance.fixture_name
121
+
122
+ [belongs_to_association.name.to_s, associated_fixture_name]
123
+ else
124
+ [k, serialize_attributes(v)]
125
+ end
126
+ end.to_h
127
+
128
+ fixture_file_path = model_instance.fixture_file_path || Rails.root.join('test', 'fixtures', "#{model_instance.class.table_name}.yml")
129
+
130
+ fixtures = File.exist?(fixture_file_path) ? YAML.load_file(fixture_file_path) : {}
131
+ fixtures[new_fixture_name] = yaml_attributes
132
+
133
+ File.open(fixture_file_path, 'w') do |file|
134
+ yaml = YAML.dump(fixtures).gsub(/\n(?=[^\s])/, "\n\n").delete_prefix("---\n\n")
135
+ file.write(yaml)
136
+ end
137
+ end
138
+ end
139
+
140
+ def serialize_attributes(value)
141
+ case value
142
+ when ActiveSupport::TimeWithZone, Date
143
+ "<%= #{datetime_erb(value)} %>"
144
+ when ActiveSupport::Duration
145
+ value.iso8601
146
+ when BigDecimal
147
+ value.to_f
148
+ else
149
+ value
150
+ end
151
+ end
152
+
153
+ def round_time(value)
154
+ if value.to_datetime.minute == 59
155
+ value += 1.minute
156
+ value = value.beginning_of_hour
157
+ elsif value.to_datetime.minute == 1 || value.to_datetime.minute == 0
158
+ value = value.beginning_of_hour
159
+ end
160
+ value
161
+ end
162
+
163
+ def datetime_erb(value)
164
+ beginning_of_day = value == value.beginning_of_day
165
+
166
+ rounded_initial_now = round_time(@initial_now)
167
+ rounded_now = round_time(Time.zone.now)
168
+
169
+ if value.is_a?(Date)
170
+ rounded_initial_now = rounded_initial_now.to_date
171
+ rounded_now = rounded_now.to_date
172
+ elsif beginning_of_day
173
+ rounded_initial_now = rounded_initial_now.beginning_of_day
174
+ rounded_now = rounded_now.beginning_of_day
175
+ end
176
+
177
+ time_travel_diff = dt_diff(rounded_initial_now, rounded_now)
178
+
179
+ rounded_value = time_travel_diff.inject(value) { |sum, (part, v)| sum + v.public_send(part) }
180
+ rounded_value = round_time(rounded_value) unless value.is_a?(Date)
181
+
182
+ parts = dt_diff(rounded_value, rounded_initial_now)
183
+
184
+ formatted_now = if value.is_a?(Date)
185
+ 'Date.today'
186
+ else
187
+ beginning_of_day ? 'Time.zone.now.beginning_of_day' : 'Time.zone.now'
188
+ end
189
+
190
+ ([formatted_now] + parts.delete_if { |_, v| v.zero? }.map do |(part, v)|
191
+ "#{v.positive? ? '+' : '-'} #{v.abs}.#{part.pluralize(v.abs)}"
192
+ end).join(' ')
193
+ end
194
+
195
+ def dt_diff(left, right)
196
+ units = %w[year month week day]
197
+ units += %w[hour minute] unless left.is_a?(Date)
198
+
199
+ units.each_with_object({ value_rest: left }) do |unit, acc|
200
+ acc[unit] ||= 0
201
+
202
+ if left > right
203
+ while acc[:value_rest] - 1.public_send(unit) >= right
204
+ acc[unit] += 1
205
+ acc[:value_rest] -= 1.public_send(unit)
206
+ end
207
+ else
208
+ while acc[:value_rest] + 1.public_send(unit) <= right
209
+ acc[unit] -= 1
210
+ acc[:value_rest] += 1.public_send(unit)
211
+ end
212
+ end
213
+ end.except(:value_rest)
214
+ end
215
+ end
216
+ end
@@ -0,0 +1,11 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "fixture_farm/fixture_recorder"
4
+
5
+ module FixtureFarm
6
+ module TestHelper
7
+ def record_new_fixtures(fixture_name_prefix, &block)
8
+ FixtureRecorder.new(fixture_name_prefix).record_new_fixtures(&block)
9
+ end
10
+ end
11
+ end
@@ -0,0 +1,3 @@
1
+ module FixtureFarm
2
+ VERSION = '0.1.0'
3
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'fixture_farm/version'
4
+
5
+ module FixtureFarm
6
+ autoload :ActiveRecordExtension, 'fixture_farm/active_record_extension'
7
+ autoload :ControllerHook, 'fixture_farm/controller_hook'
8
+ autoload :TestHelper, 'fixture_farm/test_helper'
9
+ autoload :FixtureRecorder, 'fixture_farm/fixture_recorder'
10
+ end
11
+
12
+ ActiveSupport.on_load(:active_record) do |base|
13
+ base.include FixtureFarm::ActiveRecordExtension
14
+ end
metadata ADDED
@@ -0,0 +1,76 @@
1
+ --- !ruby/object:Gem::Specification
2
+ name: fixture_farm
3
+ version: !ruby/object:Gem::Version
4
+ version: 0.1.0
5
+ platform: ruby
6
+ authors:
7
+ - artemave
8
+ autorequire:
9
+ bindir: bin
10
+ cert_chain: []
11
+ date: 2021-11-26 00:00:00.000000000 Z
12
+ dependencies:
13
+ - !ruby/object:Gem::Dependency
14
+ name: rails
15
+ requirement: !ruby/object:Gem::Requirement
16
+ requirements:
17
+ - - "~>"
18
+ - !ruby/object:Gem::Version
19
+ version: 6.1.4
20
+ - - ">="
21
+ - !ruby/object:Gem::Version
22
+ version: 6.1.4.1
23
+ type: :runtime
24
+ prerelease: false
25
+ version_requirements: !ruby/object:Gem::Requirement
26
+ requirements:
27
+ - - "~>"
28
+ - !ruby/object:Gem::Version
29
+ version: 6.1.4
30
+ - - ">="
31
+ - !ruby/object:Gem::Version
32
+ version: 6.1.4.1
33
+ description:
34
+ email:
35
+ - mr@artem.rocks
36
+ executables:
37
+ - fixture_farm
38
+ extensions: []
39
+ extra_rdoc_files: []
40
+ files:
41
+ - MIT-LICENSE
42
+ - README.md
43
+ - Rakefile
44
+ - bin/fixture_farm
45
+ - lib/fixture_farm.rb
46
+ - lib/fixture_farm/active_record_extension.rb
47
+ - lib/fixture_farm/controller_hook.rb
48
+ - lib/fixture_farm/fixture_recorder.rb
49
+ - lib/fixture_farm/test_helper.rb
50
+ - lib/fixture_farm/version.rb
51
+ homepage: https://github.com/featurist/fixture_farm
52
+ licenses:
53
+ - MIT
54
+ metadata:
55
+ homepage_uri: https://github.com/featurist/fixture_farm
56
+ source_code_uri: https://github.com/featurist/fixture_farm.git
57
+ post_install_message:
58
+ rdoc_options: []
59
+ require_paths:
60
+ - lib
61
+ required_ruby_version: !ruby/object:Gem::Requirement
62
+ requirements:
63
+ - - ">="
64
+ - !ruby/object:Gem::Version
65
+ version: 2.5.0
66
+ required_rubygems_version: !ruby/object:Gem::Requirement
67
+ requirements:
68
+ - - ">="
69
+ - !ruby/object:Gem::Version
70
+ version: '0'
71
+ requirements: []
72
+ rubygems_version: 3.1.6
73
+ signing_key:
74
+ specification_version: 4
75
+ summary: Generate rails fixutures while browsing
76
+ test_files: []