test_data 0.0.1 → 0.2.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +41 -0
- data/.standard.yml +2 -0
- data/CHANGELOG.md +43 -0
- data/Gemfile.lock +17 -15
- data/LICENSE.txt +1 -6
- data/README.md +1232 -17
- data/example/.gitignore +1 -4
- data/example/Gemfile +3 -0
- data/example/Gemfile.lock +100 -71
- data/example/README.md +2 -22
- data/example/config/application.rb +3 -0
- data/example/config/credentials.yml.enc +1 -1
- data/example/config/database.yml +2 -0
- data/example/spec/rails_helper.rb +64 -0
- data/example/spec/requests/boops_spec.rb +17 -0
- data/example/spec/requests/rails_fixtures_override_spec.rb +106 -0
- data/example/spec/spec_helper.rb +94 -0
- data/example/test/factories.rb +4 -0
- data/example/test/integration/better_mode_switching_demo_test.rb +41 -0
- data/example/test/integration/boops_that_boop_boops_test.rb +17 -0
- data/example/test/integration/dont_dump_tables_test.rb +7 -0
- data/example/test/integration/load_rollback_truncate_test.rb +190 -0
- data/example/test/integration/mode_switching_demo_test.rb +38 -0
- data/example/test/integration/parallel_boops_with_fixtures_test.rb +10 -0
- data/example/test/integration/parallel_boops_without_fixtures_test.rb +9 -0
- data/example/test/integration/rails_fixtures_double_load_test.rb +10 -0
- data/example/test/integration/rails_fixtures_override_test.rb +110 -0
- data/example/test/integration/test_data_hooks_test.rb +89 -0
- data/example/test/integration/transaction_committing_boops_test.rb +27 -0
- data/example/test/test_helper.rb +4 -31
- data/lib/generators/test_data/cable_yaml_generator.rb +18 -0
- data/lib/generators/test_data/database_yaml_generator.rb +3 -4
- data/lib/generators/test_data/environment_file_generator.rb +7 -14
- data/lib/generators/test_data/initializer_generator.rb +51 -0
- data/lib/generators/test_data/secrets_yaml_generator.rb +19 -0
- data/lib/generators/test_data/webpacker_yaml_generator.rb +4 -3
- data/lib/test_data.rb +42 -1
- data/lib/test_data/active_record_ext.rb +11 -0
- data/lib/test_data/config.rb +57 -4
- data/lib/test_data/configurators.rb +3 -0
- data/lib/test_data/configurators/cable_yaml.rb +25 -0
- data/lib/test_data/configurators/environment_file.rb +3 -2
- data/lib/test_data/configurators/initializer.rb +26 -0
- data/lib/test_data/configurators/secrets_yaml.rb +25 -0
- data/lib/test_data/configurators/webpacker_yaml.rb +4 -3
- data/lib/test_data/custom_loaders/abstract_base.rb +25 -0
- data/lib/test_data/custom_loaders/rails_fixtures.rb +42 -0
- data/lib/test_data/dumps_database.rb +55 -5
- data/lib/test_data/generator_support.rb +3 -0
- data/lib/test_data/inserts_test_data.rb +25 -0
- data/lib/test_data/loads_database_dumps.rb +8 -8
- data/lib/test_data/log.rb +76 -0
- data/lib/test_data/manager.rb +187 -0
- data/lib/test_data/rake.rb +20 -9
- data/lib/test_data/save_point.rb +34 -0
- data/lib/test_data/statistics.rb +31 -0
- data/lib/test_data/truncates_test_data.rb +31 -0
- data/lib/test_data/verifies_dumps_are_loadable.rb +4 -4
- data/lib/test_data/version.rb +1 -1
- data/script/reset_example_app +18 -0
- data/script/test +78 -13
- data/test_data.gemspec +1 -1
- metadata +36 -4
- data/lib/test_data/transactional_data_loader.rb +0 -77
@@ -0,0 +1,25 @@
|
|
1
|
+
module TestData
|
2
|
+
class InsertsTestData
|
3
|
+
def initialize
|
4
|
+
@config = TestData.config
|
5
|
+
@statistics = TestData.statistics
|
6
|
+
end
|
7
|
+
|
8
|
+
def call
|
9
|
+
search_path = connection.execute("show search_path").first["search_path"]
|
10
|
+
connection.disable_referential_integrity do
|
11
|
+
connection.execute(File.read(@config.data_dump_full_path))
|
12
|
+
end
|
13
|
+
connection.execute <<~SQL
|
14
|
+
select pg_catalog.set_config('search_path', '#{search_path}', false)
|
15
|
+
SQL
|
16
|
+
@statistics.count_load!
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def connection
|
22
|
+
ActiveRecord::Base.connection
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -16,17 +16,17 @@ module TestData
|
|
16
16
|
)
|
17
17
|
|
18
18
|
load_dump(
|
19
|
-
name: "test data",
|
19
|
+
name: "non-test data",
|
20
20
|
database_name: @config.database_name,
|
21
|
-
relative_path: @config.
|
22
|
-
full_path: @config.
|
21
|
+
relative_path: @config.non_test_data_dump_path,
|
22
|
+
full_path: @config.non_test_data_dump_full_path
|
23
23
|
)
|
24
24
|
|
25
25
|
load_dump(
|
26
|
-
name: "
|
26
|
+
name: "test data",
|
27
27
|
database_name: @config.database_name,
|
28
|
-
relative_path: @config.
|
29
|
-
full_path: @config.
|
28
|
+
relative_path: @config.data_dump_path,
|
29
|
+
full_path: @config.data_dump_full_path
|
30
30
|
)
|
31
31
|
end
|
32
32
|
|
@@ -36,9 +36,9 @@ module TestData
|
|
36
36
|
dump_pathname = Pathname.new(full_path)
|
37
37
|
FileUtils.mkdir_p(File.dirname(dump_pathname))
|
38
38
|
if system "psql -q -d #{database_name} < #{dump_pathname}"
|
39
|
-
|
39
|
+
TestData.log.info "Loaded #{name} from '#{relative_path}' into database '#{database_name}' "
|
40
40
|
else
|
41
|
-
raise "Failed while attempting to load #{name} from '#{relative_path}' into database '#{database_name}'"
|
41
|
+
raise Error.new("Failed while attempting to load #{name} from '#{relative_path}' into database '#{database_name}'")
|
42
42
|
end
|
43
43
|
end
|
44
44
|
end
|
@@ -0,0 +1,76 @@
|
|
1
|
+
module TestData
|
2
|
+
def self.log
|
3
|
+
@log ||= Log.new
|
4
|
+
end
|
5
|
+
|
6
|
+
class Log
|
7
|
+
LEVELS = [:debug, :info, :warn, :error, :quiet]
|
8
|
+
DEFAULT_WRITER = ->(message, level) do
|
9
|
+
output = "[test_data:#{level}] #{message}"
|
10
|
+
if [:warn, :error].include?(level)
|
11
|
+
warn output
|
12
|
+
else
|
13
|
+
puts output
|
14
|
+
end
|
15
|
+
end
|
16
|
+
PLAIN_WRITER = ->(message, level) do
|
17
|
+
if [:warn, :error].include?(level)
|
18
|
+
warn message
|
19
|
+
else
|
20
|
+
puts message
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :level, :writer
|
25
|
+
|
26
|
+
def initialize
|
27
|
+
reset
|
28
|
+
end
|
29
|
+
|
30
|
+
LEVELS[0...4].each do |level|
|
31
|
+
define_method level.to_s do |message|
|
32
|
+
next unless message.strip.present?
|
33
|
+
|
34
|
+
@writer.call(message, level) if enabled?(level)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def reset
|
39
|
+
self.level = ENV["TEST_DATA_LOG_LEVEL"]&.to_sym || :info
|
40
|
+
@writer = DEFAULT_WRITER
|
41
|
+
end
|
42
|
+
|
43
|
+
def level=(level)
|
44
|
+
if LEVELS.include?(level)
|
45
|
+
@level = level
|
46
|
+
else
|
47
|
+
raise Error.new("Not a valid level")
|
48
|
+
end
|
49
|
+
end
|
50
|
+
|
51
|
+
def writer=(writer)
|
52
|
+
if writer.respond_to?(:call)
|
53
|
+
@writer = writer
|
54
|
+
else
|
55
|
+
raise Error.new("Log writer must be callable")
|
56
|
+
end
|
57
|
+
end
|
58
|
+
|
59
|
+
def with_writer(writer, &blk)
|
60
|
+
og_writer = self.writer
|
61
|
+
self.writer = writer
|
62
|
+
blk.call
|
63
|
+
self.writer = og_writer
|
64
|
+
end
|
65
|
+
|
66
|
+
def with_plain_writer(&blk)
|
67
|
+
with_writer(PLAIN_WRITER, &blk)
|
68
|
+
end
|
69
|
+
|
70
|
+
private
|
71
|
+
|
72
|
+
def enabled?(level)
|
73
|
+
LEVELS.index(level) >= LEVELS.index(@level)
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,187 @@
|
|
1
|
+
module TestData
|
2
|
+
class Manager
|
3
|
+
def initialize
|
4
|
+
@inserts_test_data = InsertsTestData.new
|
5
|
+
@truncates_test_data = TruncatesTestData.new
|
6
|
+
@config = TestData.config
|
7
|
+
@statistics = TestData.statistics
|
8
|
+
@save_points = []
|
9
|
+
end
|
10
|
+
|
11
|
+
def load
|
12
|
+
ensure_after_load_save_point_is_active_if_data_is_loaded!
|
13
|
+
return rollback_to_after_data_load if save_point_active?(:after_data_load)
|
14
|
+
|
15
|
+
create_save_point(:before_data_load)
|
16
|
+
@inserts_test_data.call
|
17
|
+
@config.after_test_data_load_hook.call
|
18
|
+
record_ar_internal_metadata_that_test_data_is_loaded
|
19
|
+
create_save_point(:after_data_load)
|
20
|
+
end
|
21
|
+
|
22
|
+
def truncate
|
23
|
+
ensure_after_load_save_point_is_active_if_data_is_loaded!
|
24
|
+
ensure_after_truncate_save_point_is_active_if_data_is_truncated!
|
25
|
+
return rollback_to_after_data_truncate if save_point_active?(:after_data_truncate)
|
26
|
+
|
27
|
+
if save_point_active?(:after_data_load)
|
28
|
+
# If a test that uses the test data runs before a test that starts by
|
29
|
+
# calling truncate, tables in the database that would NOT be truncated
|
30
|
+
# may have been changed. To avoid this category of test pollution, start
|
31
|
+
# the truncation by rolling back to the known clean point
|
32
|
+
rollback_to_after_data_load
|
33
|
+
else
|
34
|
+
# Seems silly loading data when the user asked us to truncate, but
|
35
|
+
# it's important that the state of the transaction stack matches the
|
36
|
+
# mental model we advertise, because any _other_ test in their suite
|
37
|
+
# should expect that the existence of :after_data_truncate save point
|
38
|
+
# implies that it's safe to rollback to the :after_data_load save
|
39
|
+
# point; since tests run in random order, it's likely to happen
|
40
|
+
TestData.log.debug("TestData.uses_clean_slate was called, but data was not loaded. Loading data before truncate to preserve the transaction save point ordering")
|
41
|
+
load
|
42
|
+
end
|
43
|
+
|
44
|
+
@truncates_test_data.call
|
45
|
+
@config.after_test_data_truncate_hook.call
|
46
|
+
record_ar_internal_metadata_that_test_data_is_truncated
|
47
|
+
create_save_point(:after_data_truncate)
|
48
|
+
end
|
49
|
+
|
50
|
+
def load_custom_data(loader, **options)
|
51
|
+
loader.validate!(**options)
|
52
|
+
snapshot_name = "user_#{loader.name}".to_sym
|
53
|
+
|
54
|
+
ensure_after_load_save_point_is_active_if_data_is_loaded!
|
55
|
+
ensure_after_truncate_save_point_is_active_if_data_is_truncated!
|
56
|
+
ensure_custom_save_point_is_active_if_memo_exists!(snapshot_name)
|
57
|
+
|
58
|
+
loader.load_requested(**options)
|
59
|
+
if save_point_active?(snapshot_name) && loader.loaded?(**options)
|
60
|
+
return rollback_to_custom_savepoint(snapshot_name)
|
61
|
+
end
|
62
|
+
|
63
|
+
if save_point_active?(:after_data_truncate)
|
64
|
+
rollback_to_after_data_truncate
|
65
|
+
else
|
66
|
+
truncate
|
67
|
+
end
|
68
|
+
|
69
|
+
loader.load(**options)
|
70
|
+
record_ar_internal_metadata_of_custom_save_point(snapshot_name)
|
71
|
+
create_save_point(snapshot_name)
|
72
|
+
end
|
73
|
+
|
74
|
+
def rollback_to_before_data_load
|
75
|
+
if save_point_active?(:before_data_load)
|
76
|
+
rollback_save_point(:before_data_load)
|
77
|
+
# No need to recreate the save point
|
78
|
+
# (TestData.uses_test_data will if called)
|
79
|
+
end
|
80
|
+
end
|
81
|
+
|
82
|
+
private
|
83
|
+
|
84
|
+
def rollback_to_after_data_load
|
85
|
+
if save_point_active?(:after_data_load)
|
86
|
+
rollback_save_point(:after_data_load)
|
87
|
+
create_save_point(:after_data_load)
|
88
|
+
end
|
89
|
+
end
|
90
|
+
|
91
|
+
def rollback_to_after_data_truncate
|
92
|
+
if save_point_active?(:after_data_truncate)
|
93
|
+
rollback_save_point(:after_data_truncate)
|
94
|
+
create_save_point(:after_data_truncate)
|
95
|
+
end
|
96
|
+
end
|
97
|
+
|
98
|
+
def rollback_to_custom_savepoint(name)
|
99
|
+
if save_point_active?(name)
|
100
|
+
rollback_save_point(name)
|
101
|
+
create_save_point(name)
|
102
|
+
end
|
103
|
+
end
|
104
|
+
|
105
|
+
def connection
|
106
|
+
ActiveRecord::Base.connection
|
107
|
+
end
|
108
|
+
|
109
|
+
def ensure_after_load_save_point_is_active_if_data_is_loaded!
|
110
|
+
if !save_point_active?(:after_data_load) && ar_internal_metadata_shows_test_data_is_loaded?
|
111
|
+
TestData.log.debug "Test data appears to be loaded, but the :after_data_load save point was rolled back (and not by this gem). Recreating the :after_data_load save point"
|
112
|
+
create_save_point(:after_data_load)
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
def record_ar_internal_metadata_that_test_data_is_loaded
|
117
|
+
if ar_internal_metadata_shows_test_data_is_loaded?
|
118
|
+
TestData.log.warn "Attempted to record that test data is loaded in ar_internal_metadata, but record already existed. Perhaps a previous test run committed your test data?"
|
119
|
+
else
|
120
|
+
ActiveRecord::InternalMetadata.create!(key: "test_data:loaded", value: "true")
|
121
|
+
end
|
122
|
+
end
|
123
|
+
|
124
|
+
def ar_internal_metadata_shows_test_data_is_loaded?
|
125
|
+
ActiveRecord::InternalMetadata.find_by(key: "test_data:loaded")&.value == "true"
|
126
|
+
end
|
127
|
+
|
128
|
+
def ensure_after_truncate_save_point_is_active_if_data_is_truncated!
|
129
|
+
if !save_point_active?(:after_data_truncate) && ar_internal_metadata_shows_test_data_is_truncated?
|
130
|
+
TestData.log.debug "Test data appears to be loaded, but the :after_data_truncate save point was rolled back (and not by this gem). Recreating the :after_data_truncate save point"
|
131
|
+
create_save_point(:after_data_truncate)
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def record_ar_internal_metadata_that_test_data_is_truncated
|
136
|
+
if ar_internal_metadata_shows_test_data_is_truncated?
|
137
|
+
TestData.log.warn "Attempted to record that test data is truncated in ar_internal_metadata, but record already existed. Perhaps a previous test run committed the truncation of your test data?"
|
138
|
+
else
|
139
|
+
ActiveRecord::InternalMetadata.create!(key: "test_data:truncated", value: "true")
|
140
|
+
end
|
141
|
+
end
|
142
|
+
|
143
|
+
def ar_internal_metadata_shows_test_data_is_truncated?
|
144
|
+
ActiveRecord::InternalMetadata.find_by(key: "test_data:truncated")&.value == "true"
|
145
|
+
end
|
146
|
+
|
147
|
+
def ensure_custom_save_point_is_active_if_memo_exists!(name)
|
148
|
+
if !save_point_active?(name) && ar_internal_metadata_shows_custom_operation_was_persisted?(name)
|
149
|
+
TestData.log.debug "#{name} appears to have been loaded by test_data, but the #{name} save point was rolled back (and not by this gem). Recreating the #{name} save point"
|
150
|
+
create_save_point(name)
|
151
|
+
end
|
152
|
+
end
|
153
|
+
|
154
|
+
def ar_internal_metadata_shows_custom_operation_was_persisted?(name)
|
155
|
+
ActiveRecord::InternalMetadata.find_by(key: "test_data:#{name}")&.value == "true"
|
156
|
+
end
|
157
|
+
|
158
|
+
def record_ar_internal_metadata_of_custom_save_point(name)
|
159
|
+
if ar_internal_metadata_shows_custom_operation_was_persisted?(name)
|
160
|
+
TestData.log.warn "Attempted to record that test_data had loaded #{name} in ar_internal_metadata, but record already existed. Perhaps a previous test run committed it?"
|
161
|
+
else
|
162
|
+
ActiveRecord::InternalMetadata.create!(key: "test_data:#{name}", value: "true")
|
163
|
+
end
|
164
|
+
end
|
165
|
+
|
166
|
+
def save_point_active?(name)
|
167
|
+
purge_closed_save_points!
|
168
|
+
!!@save_points.find { |sp| sp.name == name }&.active?
|
169
|
+
end
|
170
|
+
|
171
|
+
def create_save_point(name)
|
172
|
+
raise Error.new("Could not create test_data savepoint '#{name}', because it was already active!") if save_point_active?(name)
|
173
|
+
@save_points << SavePoint.new(name)
|
174
|
+
end
|
175
|
+
|
176
|
+
def rollback_save_point(name)
|
177
|
+
@save_points.find { |sp| sp.name == name }&.rollback!
|
178
|
+
purge_closed_save_points!
|
179
|
+
end
|
180
|
+
|
181
|
+
def purge_closed_save_points!
|
182
|
+
@save_points = @save_points.select { |save_point|
|
183
|
+
save_point.active?
|
184
|
+
}
|
185
|
+
end
|
186
|
+
end
|
187
|
+
end
|
data/lib/test_data/rake.rb
CHANGED
@@ -22,13 +22,15 @@ end
|
|
22
22
|
|
23
23
|
desc "Verifies test_data environment looks good"
|
24
24
|
task "test_data:verify_config" do
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
25
|
+
TestData.log.with_plain_writer do
|
26
|
+
config = TestData::VerifiesConfiguration.new.call
|
27
|
+
unless config.looks_good?
|
28
|
+
TestData.log.warn "\nThe test_data gem is not configured correctly. Try running: rake test_data:configure\n\n"
|
29
|
+
config.problems.each do |problem|
|
30
|
+
TestData.log.warn " - #{problem}"
|
31
|
+
end
|
32
|
+
fail
|
30
33
|
end
|
31
|
-
fail
|
32
34
|
end
|
33
35
|
end
|
34
36
|
|
@@ -48,17 +50,26 @@ task "test_data:initialize" => ["test_data:verify_config", :environment] do
|
|
48
50
|
ActiveRecord::Tasks::DatabaseTasks.load_schema_current(ActiveRecord::Base.schema_format, ENV["SCHEMA"], "test_data")
|
49
51
|
ActiveRecord::Tasks::DatabaseTasks.load_seed
|
50
52
|
end
|
53
|
+
|
54
|
+
TestData.log.info <<~MSG
|
55
|
+
Your test_data environment and database are ready for use! You can now run your server (or any command) to create some test data like so:
|
56
|
+
|
57
|
+
$ RAILS_ENV=test_data bin/rails server
|
58
|
+
|
59
|
+
MSG
|
51
60
|
end
|
52
61
|
|
53
62
|
desc "Initialize test_data Rails environment & configure database"
|
54
63
|
task "test_data:install" => ["test_data:configure", "test_data:initialize"]
|
55
64
|
|
56
65
|
desc "Dumps the interactive test_data database"
|
57
|
-
task "test_data:dump" => "test_data:verify_config" do
|
66
|
+
task "test_data:dump" => ["test_data:verify_config", :environment] do
|
67
|
+
next run_in_test_data_env("test_data:dump") if wrong_env?
|
68
|
+
|
58
69
|
TestData::DumpsDatabase.new.call
|
59
70
|
end
|
60
71
|
|
61
|
-
desc "
|
72
|
+
desc "Loads the schema and data SQL dumps into the test_data database"
|
62
73
|
task "test_data:load" => ["test_data:verify_config", :environment] do
|
63
74
|
next run_in_test_data_env("test_data:load") if wrong_env?
|
64
75
|
|
@@ -71,7 +82,7 @@ task "test_data:load" => ["test_data:verify_config", :environment] do
|
|
71
82
|
TestData::LoadsDatabaseDumps.new.call
|
72
83
|
|
73
84
|
if ActiveRecord::Base.connection.migration_context.needs_migration?
|
74
|
-
warn "
|
85
|
+
TestData.log.warn "There are pending migrations for database '#{TestData.config.database_name}'. To run them, run:\n\n $ RAILS_ENV=test_data bin/rake db:migrate\n\n"
|
75
86
|
end
|
76
87
|
end
|
77
88
|
|
@@ -0,0 +1,34 @@
|
|
1
|
+
module TestData
|
2
|
+
class SavePoint
|
3
|
+
attr_reader :name, :transaction
|
4
|
+
|
5
|
+
def initialize(name)
|
6
|
+
@name = name
|
7
|
+
@transaction = connection.begin_transaction(joinable: false, _lazy: false)
|
8
|
+
end
|
9
|
+
|
10
|
+
def active?
|
11
|
+
!@transaction.state.finalized?
|
12
|
+
end
|
13
|
+
|
14
|
+
def rollback!
|
15
|
+
warn_if_not_rollbackable!
|
16
|
+
while active?
|
17
|
+
connection.rollback_transaction
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
private
|
22
|
+
|
23
|
+
def connection
|
24
|
+
ActiveRecord::Base.connection
|
25
|
+
end
|
26
|
+
|
27
|
+
def warn_if_not_rollbackable!
|
28
|
+
return if active?
|
29
|
+
TestData.log.warn(
|
30
|
+
"Attempted to roll back transaction save point '#{name}', but its state was #{@transaction.state}"
|
31
|
+
)
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
@@ -0,0 +1,31 @@
|
|
1
|
+
module TestData
|
2
|
+
def self.statistics
|
3
|
+
@statistics ||= Statistics.new
|
4
|
+
end
|
5
|
+
|
6
|
+
class Statistics
|
7
|
+
attr_reader :load_count, :truncate_count, :load_rails_fixtures_count
|
8
|
+
|
9
|
+
def initialize
|
10
|
+
reset
|
11
|
+
end
|
12
|
+
|
13
|
+
def count_load!
|
14
|
+
@load_count += 1
|
15
|
+
end
|
16
|
+
|
17
|
+
def count_truncate!
|
18
|
+
@truncate_count += 1
|
19
|
+
end
|
20
|
+
|
21
|
+
def count_load_rails_fixtures!
|
22
|
+
@load_rails_fixtures_count += 1
|
23
|
+
end
|
24
|
+
|
25
|
+
def reset
|
26
|
+
@load_count = 0
|
27
|
+
@truncate_count = 0
|
28
|
+
@load_rails_fixtures_count = 0
|
29
|
+
end
|
30
|
+
end
|
31
|
+
end
|