test_data 0.0.2 → 0.2.2

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.
Files changed (56) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -5
  3. data/.standard.yml +2 -0
  4. data/CHANGELOG.md +41 -0
  5. data/Gemfile.lock +16 -16
  6. data/LICENSE.txt +1 -6
  7. data/README.md +864 -501
  8. data/example/.gitignore +1 -4
  9. data/example/Gemfile.lock +74 -74
  10. data/example/config/application.rb +3 -0
  11. data/example/config/credentials.yml.enc +1 -2
  12. data/example/spec/rails_helper.rb +1 -1
  13. data/example/spec/requests/boops_spec.rb +1 -5
  14. data/example/spec/requests/rails_fixtures_override_spec.rb +106 -0
  15. data/example/test/integration/better_mode_switching_demo_test.rb +6 -10
  16. data/example/test/integration/fixture_load_count_test.rb +82 -0
  17. data/example/test/integration/load_rollback_truncate_test.rb +40 -45
  18. data/example/test/integration/mode_switching_demo_test.rb +4 -14
  19. data/example/test/integration/parallel_boops_with_fixtures_test.rb +2 -6
  20. data/example/test/integration/parallel_boops_without_fixtures_test.rb +2 -6
  21. data/example/test/integration/rails_fixtures_double_load_test.rb +10 -0
  22. data/example/test/integration/rails_fixtures_override_test.rb +110 -0
  23. data/example/test/integration/test_data_hooks_test.rb +89 -0
  24. data/example/test/integration/transaction_committing_boops_test.rb +5 -3
  25. data/example/test/test_helper.rb +2 -6
  26. data/lib/generators/test_data/cable_yaml_generator.rb +18 -0
  27. data/lib/generators/test_data/database_yaml_generator.rb +2 -3
  28. data/lib/generators/test_data/environment_file_generator.rb +7 -0
  29. data/lib/generators/test_data/initializer_generator.rb +20 -7
  30. data/lib/generators/test_data/secrets_yaml_generator.rb +19 -0
  31. data/lib/generators/test_data/webpacker_yaml_generator.rb +3 -2
  32. data/lib/test_data.rb +37 -1
  33. data/lib/test_data/active_record_ext.rb +11 -0
  34. data/lib/test_data/config.rb +33 -3
  35. data/lib/test_data/configurators.rb +2 -0
  36. data/lib/test_data/configurators/cable_yaml.rb +25 -0
  37. data/lib/test_data/configurators/environment_file.rb +3 -2
  38. data/lib/test_data/configurators/initializer.rb +3 -2
  39. data/lib/test_data/configurators/secrets_yaml.rb +25 -0
  40. data/lib/test_data/configurators/webpacker_yaml.rb +4 -3
  41. data/lib/test_data/custom_loaders/abstract_base.rb +25 -0
  42. data/lib/test_data/custom_loaders/rails_fixtures.rb +45 -0
  43. data/lib/test_data/dumps_database.rb +24 -1
  44. data/lib/test_data/generator_support.rb +3 -0
  45. data/lib/test_data/inserts_test_data.rb +25 -0
  46. data/lib/test_data/loads_database_dumps.rb +1 -1
  47. data/lib/test_data/log.rb +19 -1
  48. data/lib/test_data/{transactional_data_loader.rb → manager.rb} +78 -81
  49. data/lib/test_data/rake.rb +16 -7
  50. data/lib/test_data/statistics.rb +6 -1
  51. data/lib/test_data/truncates_test_data.rb +31 -0
  52. data/lib/test_data/version.rb +1 -1
  53. data/script/reset_example_app +1 -0
  54. data/script/test +31 -6
  55. data/test_data.gemspec +1 -1
  56. metadata +20 -4
@@ -0,0 +1,25 @@
1
+ module TestData
2
+ module CustomLoaders
3
+ class AbstractBase
4
+ def name
5
+ raise Error.new("#name must be defined by CustomLoader subclass")
6
+ end
7
+
8
+ def load_requested(**options)
9
+ end
10
+
11
+ def loaded?(**options)
12
+ # Check to see if the requested data is already loaded (if possible and
13
+ # detectable)
14
+ #
15
+ # Return true to prevent #load from being called, potentially avoiding an
16
+ # expensive operation
17
+ false
18
+ end
19
+
20
+ def load(**options)
21
+ raise Error.new("#load must be defined by CustomLoader subclass")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,45 @@
1
+ module TestData
2
+ module CustomLoaders
3
+ class RailsFixtures < AbstractBase
4
+ def initialize
5
+ @config = TestData.config
6
+ @statistics = TestData.statistics
7
+ @already_loaded_rails_fixtures = {}
8
+ end
9
+
10
+ def name
11
+ :rails_fixtures
12
+ end
13
+
14
+ def validate!(test_instance:)
15
+ if !test_instance.respond_to?(:setup_fixtures)
16
+ raise Error.new("'TestData.uses_rails_fixtures(self)' must be passed a test instance that has had ActiveRecord::TestFixtures mixed-in (e.g. `TestData.uses_rails_fixtures(self)` in an ActiveSupport::TestCase `setup` block), but the provided argument does not respond to 'setup_fixtures'")
17
+ elsif !test_instance.respond_to?(:__test_data_gem_setup_fixtures)
18
+ raise Error.new("'TestData.uses_rails_fixtures(self)' depends on Rails' default fixture-loading behavior being disabled by calling 'TestData.prevent_rails_fixtures_from_loading_automatically!' as early as possible (e.g. near the top of your test_helper.rb), but it looks like it was never called.")
19
+ end
20
+ end
21
+
22
+ def load_requested(test_instance:)
23
+ ActiveRecord::FixtureSet.reset_cache
24
+ test_instance.instance_variable_set(:@loaded_fixtures,
25
+ @already_loaded_rails_fixtures.slice(*test_instance.class.fixture_table_names))
26
+ test_instance.instance_variable_set(:@fixture_cache, {})
27
+ end
28
+
29
+ def loaded?(test_instance:)
30
+ test_instance.class.fixture_table_names.all? { |table_name|
31
+ @already_loaded_rails_fixtures.key?(table_name)
32
+ }
33
+ end
34
+
35
+ def load(test_instance:)
36
+ test_instance.pre_loaded_fixtures = false
37
+ test_instance.use_transactional_tests = false
38
+ test_instance.__test_data_gem_setup_fixtures
39
+ @already_loaded_rails_fixtures = test_instance.instance_variable_get(:@loaded_fixtures)
40
+ @statistics.count_load_rails_fixtures!
41
+ @config.after_rails_fixture_load_hook.call
42
+ end
43
+ end
44
+ end
45
+ end
@@ -40,12 +40,14 @@ module TestData
40
40
  def dump(type:, database_name:, relative_path:, full_path:, name: type, flags: "")
41
41
  dump_pathname = Pathname.new(full_path)
42
42
  FileUtils.mkdir_p(File.dirname(dump_pathname))
43
+ before_size = File.size?(dump_pathname)
43
44
  if execute("pg_dump #{database_name} --no-tablespaces --no-owner --inserts --#{type}-only #{flags} -f #{dump_pathname}")
44
45
  prepend_set_replication_role!(full_path) if type == :data
45
46
 
46
47
  TestData.log.info "Dumped '#{database_name}' #{name} to '#{relative_path}'"
48
+ log_size_info_and_warnings(before_size: before_size, after_size: File.size(dump_pathname))
47
49
  else
48
- raise "Failed while attempting to dump '#{database_name}' #{name} to '#{relative_path}'"
50
+ raise Error.new("Failed while attempting to dump '#{database_name}' #{name} to '#{relative_path}'")
49
51
  end
50
52
  end
51
53
 
@@ -72,5 +74,26 @@ module TestData
72
74
  COMMAND
73
75
  TestData.log.debug("Prepended replication role instruction to '#{data_dump_path}'")
74
76
  end
77
+
78
+ def log_size_info_and_warnings(before_size:, after_size:)
79
+ percent_change = percent_change(before_size, after_size)
80
+ TestData.log.info " Size: #{to_size(after_size)}#{" (#{percent_change}% #{before_size > after_size ? "decrease" : "increase"})" if percent_change}"
81
+ if after_size > 5242880
82
+ TestData.log.warn " WARNING: file size exceeds 5MB. Be sure to only persist what data you need to sufficiently test your application"
83
+ end
84
+ if before_size && (after_size - before_size) > 1048576
85
+ TestData.log.warn " WARNING: size of this dump increased by #{to_size(after_size - before_size)}. You may want to inspect the file to validate extraneous data was not committed"
86
+ end
87
+ end
88
+
89
+ def percent_change(before_size, after_size)
90
+ return unless before_size && before_size > 0 && after_size
91
+ ((before_size - after_size).abs / before_size * 100).round(2)
92
+ end
93
+
94
+ def to_size(bytes)
95
+ e = Math.log10(bytes).to_i / 3
96
+ "%.0f" % (bytes / 1000**e) + [" bytes", "KB", "MB", "GB"][e]
97
+ end
75
98
  end
76
99
  end
@@ -0,0 +1,3 @@
1
+ module TestData
2
+ BEFORE_TEST_STANZA_REGEX = /^$\n(?:^\#.*\n)*^test:/
3
+ end
@@ -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
@@ -38,7 +38,7 @@ module TestData
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
data/lib/test_data/log.rb CHANGED
@@ -6,13 +6,20 @@ module TestData
6
6
  class Log
7
7
  LEVELS = [:debug, :info, :warn, :error, :quiet]
8
8
  DEFAULT_WRITER = ->(message, level) do
9
- output = "[test_data: #{level}] #{message}"
9
+ output = "[test_data:#{level}] #{message}"
10
10
  if [:warn, :error].include?(level)
11
11
  warn output
12
12
  else
13
13
  puts output
14
14
  end
15
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
16
23
 
17
24
  attr_reader :level, :writer
18
25
 
@@ -49,6 +56,17 @@ module TestData
49
56
  end
50
57
  end
51
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
+
52
70
  private
53
71
 
54
72
  def enabled?(level)
@@ -1,69 +1,25 @@
1
1
  module TestData
2
- def self.load(transactions: true)
3
- @transactional_data_loader ||= TransactionalDataLoader.new
4
- @transactional_data_loader.load(transactions: transactions)
5
- end
6
-
7
- def self.rollback(save_point_name = :after_data_load)
8
- @transactional_data_loader ||= TransactionalDataLoader.new
9
- case save_point_name
10
- when :before_data_load
11
- @transactional_data_loader.rollback_to_before_data_load
12
- when :after_data_load
13
- @transactional_data_loader.rollback_to_after_data_load
14
- when :after_data_truncate
15
- @transactional_data_loader.rollback_to_after_data_truncate
16
- else
17
- raise Error.new("No known save point named '#{save_point_name}'. Valid values are: [:before_data_load, :after_data_load, :after_data_truncate]")
18
- end
19
- end
20
-
21
- def self.truncate(transactions: true)
22
- @transactional_data_loader ||= TransactionalDataLoader.new
23
- @transactional_data_loader.truncate(transactions: transactions)
24
- end
25
-
26
- class TransactionalDataLoader
2
+ class Manager
27
3
  def initialize
4
+ @inserts_test_data = InsertsTestData.new
5
+ @truncates_test_data = TruncatesTestData.new
28
6
  @config = TestData.config
29
7
  @statistics = TestData.statistics
30
8
  @save_points = []
31
9
  end
32
10
 
33
- def load(transactions: true)
34
- return execute_data_load unless transactions
11
+ def load
35
12
  ensure_after_load_save_point_is_active_if_data_is_loaded!
36
13
  return rollback_to_after_data_load if save_point_active?(:after_data_load)
37
14
 
38
15
  create_save_point(:before_data_load)
39
- execute_data_load
16
+ @inserts_test_data.call
17
+ @config.after_test_data_load_hook.call
40
18
  record_ar_internal_metadata_that_test_data_is_loaded
41
19
  create_save_point(:after_data_load)
42
20
  end
43
21
 
44
- def rollback_to_before_data_load
45
- if save_point_active?(:before_data_load)
46
- rollback_save_point(:before_data_load)
47
- # No need to recreate the save point -- TestData.load will if called
48
- end
49
- end
50
-
51
- def rollback_to_after_data_load
52
- if save_point_active?(:after_data_load)
53
- rollback_save_point(:after_data_load)
54
- create_save_point(:after_data_load)
55
- end
56
- end
57
-
58
- def rollback_to_after_data_truncate
59
- if save_point_active?(:after_data_truncate)
60
- rollback_save_point(:after_data_truncate)
61
- create_save_point(:after_data_truncate)
62
- end
63
- end
64
-
65
- def truncate(transactions: true)
66
- return execute_data_truncate unless transactions
22
+ def truncate
67
23
  ensure_after_load_save_point_is_active_if_data_is_loaded!
68
24
  ensure_after_truncate_save_point_is_active_if_data_is_truncated!
69
25
  return rollback_to_after_data_truncate if save_point_active?(:after_data_truncate)
@@ -81,17 +37,75 @@ module TestData
81
37
  # should expect that the existence of :after_data_truncate save point
82
38
  # implies that it's safe to rollback to the :after_data_load save
83
39
  # point; since tests run in random order, it's likely to happen
84
- TestData.log.debug("TestData.truncate was called, but data was not loaded. Loading data before truncate to preserve the documents transaction save point ordering")
85
- load(transactions: true)
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
86
42
  end
87
43
 
88
- execute_data_truncate
44
+ @truncates_test_data.call
45
+ @config.after_test_data_truncate_hook.call
89
46
  record_ar_internal_metadata_that_test_data_is_truncated
90
47
  create_save_point(:after_data_truncate)
91
48
  end
92
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
+
93
82
  private
94
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
+
95
109
  def ensure_after_load_save_point_is_active_if_data_is_loaded!
96
110
  if !save_point_active?(:after_data_load) && ar_internal_metadata_shows_test_data_is_loaded?
97
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"
@@ -130,31 +144,22 @@ module TestData
130
144
  ActiveRecord::InternalMetadata.find_by(key: "test_data:truncated")&.value == "true"
131
145
  end
132
146
 
133
- def execute_data_load
134
- search_path = execute("show search_path").first["search_path"]
135
- connection.disable_referential_integrity do
136
- execute(File.read(@config.data_dump_full_path))
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)
137
151
  end
138
- execute <<~SQL
139
- select pg_catalog.set_config('search_path', '#{search_path}', false)
140
- SQL
141
- @statistics.count_load!
142
152
  end
143
153
 
144
- def execute_data_truncate
145
- connection.disable_referential_integrity do
146
- execute("TRUNCATE TABLE #{tables_to_truncate.map { |t| connection.quote_table_name(t) }.join(", ")}")
147
- end
148
- @statistics.count_truncate!
154
+ def ar_internal_metadata_shows_custom_operation_was_persisted?(name)
155
+ ActiveRecord::InternalMetadata.find_by(key: "test_data:#{name}")&.value == "true"
149
156
  end
150
157
 
151
- def tables_to_truncate
152
- if @config.truncate_these_test_data_tables.present?
153
- @config.truncate_these_test_data_tables
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?"
154
161
  else
155
- @tables_to_truncate ||= IO.foreach(@config.data_dump_path).grep(/^INSERT INTO/) { |line|
156
- line.match(/^INSERT INTO ([^\s]+)/)&.captures&.first
157
- }.compact.uniq
162
+ ActiveRecord::InternalMetadata.create!(key: "test_data:#{name}", value: "true")
158
163
  end
159
164
  end
160
165
 
@@ -178,13 +183,5 @@ module TestData
178
183
  save_point.active?
179
184
  }
180
185
  end
181
-
182
- def execute(sql)
183
- connection.execute(sql)
184
- end
185
-
186
- def connection
187
- ActiveRecord::Base.connection
188
- end
189
186
  end
190
187
  end
@@ -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
- config = TestData::VerifiesConfiguration.new.call
26
- unless config.looks_good?
27
- TestData.log.warn "The test_data gem is not configured correctly. Try 'rake test_data:configure'?\n"
28
- config.problems.each do |problem|
29
- TestData.log.warn " - #{problem}"
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,6 +50,13 @@ 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"
@@ -73,7 +82,7 @@ task "test_data:load" => ["test_data:verify_config", :environment] do
73
82
  TestData::LoadsDatabaseDumps.new.call
74
83
 
75
84
  if ActiveRecord::Base.connection.migration_context.needs_migration?
76
- 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"
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"
77
86
  end
78
87
  end
79
88