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 +7 -0
- data/MIT-LICENSE +20 -0
- data/README.md +82 -0
- data/Rakefile +13 -0
- data/bin/fixture_farm +7 -0
- data/lib/fixture_farm/active_record_extension.rb +29 -0
- data/lib/fixture_farm/controller_hook.rb +29 -0
- data/lib/fixture_farm/fixture_recorder.rb +216 -0
- data/lib/fixture_farm/test_helper.rb +11 -0
- data/lib/fixture_farm/version.rb +3 -0
- data/lib/fixture_farm.rb +14 -0
- metadata +76 -0
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
data/bin/fixture_farm
ADDED
@@ -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
|
data/lib/fixture_farm.rb
ADDED
@@ -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: []
|