test_data 0.1.0 → 0.2.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.
Files changed (33) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +1 -5
  3. data/CHANGELOG.md +17 -1
  4. data/Gemfile.lock +1 -1
  5. data/LICENSE.txt +1 -1
  6. data/README.md +102 -102
  7. data/example/Gemfile.lock +1 -1
  8. data/example/config/credentials.yml.enc +1 -2
  9. data/example/spec/rails_helper.rb +1 -1
  10. data/example/spec/requests/boops_spec.rb +1 -5
  11. data/example/spec/requests/rails_fixtures_override_spec.rb +84 -0
  12. data/example/test/integration/better_mode_switching_demo_test.rb +2 -10
  13. data/example/test/integration/load_rollback_truncate_test.rb +40 -45
  14. data/example/test/integration/mode_switching_demo_test.rb +4 -14
  15. data/example/test/integration/parallel_boops_with_fixtures_test.rb +1 -5
  16. data/example/test/integration/parallel_boops_without_fixtures_test.rb +1 -5
  17. data/example/test/integration/rails_fixtures_double_load_test.rb +2 -2
  18. data/example/test/integration/rails_fixtures_override_test.rb +18 -35
  19. data/example/test/integration/transaction_committing_boops_test.rb +1 -10
  20. data/example/test/test_helper.rb +1 -5
  21. data/lib/generators/test_data/environment_file_generator.rb +4 -0
  22. data/lib/generators/test_data/initializer_generator.rb +1 -7
  23. data/lib/test_data.rb +32 -1
  24. data/lib/test_data/config.rb +1 -11
  25. data/lib/test_data/custom_loaders/abstract_base.rb +25 -0
  26. data/lib/test_data/custom_loaders/rails_fixtures.rb +40 -0
  27. data/lib/test_data/inserts_test_data.rb +25 -0
  28. data/lib/test_data/manager.rb +185 -0
  29. data/lib/test_data/truncates_test_data.rb +31 -0
  30. data/lib/test_data/version.rb +1 -1
  31. data/script/test +1 -0
  32. metadata +8 -3
  33. data/lib/test_data/transactional_data_loader.rb +0 -300
@@ -1,13 +1,11 @@
1
1
  require "test_helper"
2
2
 
3
- TestData.config.use_transactional_data_loader = false
4
-
5
3
  class TransactionCommittingTestCase < ActiveSupport::TestCase
6
4
  self.use_transactional_tests = false
7
5
 
8
6
  setup do
9
7
  Noncommittal.stop!
10
- TestData.load
8
+ TestData.insert_test_data_dump
11
9
  end
12
10
 
13
11
  teardown do
@@ -26,11 +24,4 @@ class TransactionCommittingBoopsTest < TransactionCommittingTestCase
26
24
  def test_finds_the_boops_via_another_process
27
25
  assert_equal 15, `RAILS_ENV=test bin/rails runner "puts Boop.count"`.chomp.to_i
28
26
  end
29
-
30
- def test_cant_have_it_both_ways
31
- error = assert_raise(TestData::Error) do
32
- TestData.config.use_transactional_data_loader = true
33
- end
34
- assert_match "There is already a non-transactional data loader", error.message
35
- end
36
27
  end
@@ -9,10 +9,6 @@ class SerializedNonTransactionalTestCase < ActiveSupport::TestCase
9
9
  self.use_transactional_tests = false
10
10
 
11
11
  setup do
12
- TestData.load
13
- end
14
-
15
- teardown do
16
- TestData.rollback
12
+ TestData.uses_test_data
17
13
  end
18
14
  end
@@ -9,6 +9,10 @@ module TestData
9
9
  require_relative "development"
10
10
 
11
11
  Rails.application.configure do
12
+ # Rails creates secret key base for only "development" and "test"
13
+ # For more info, see: https://github.com/testdouble/test_data/issues/2
14
+ self.secrets.secret_key_base ||= Rails.application.send(:generate_development_secret)
15
+
12
16
  # Don't persist schema.rb or structure.sql after test_data is migrated
13
17
  config.active_record.dump_schema_after_migration = false
14
18
  end
@@ -24,17 +24,11 @@ module TestData
24
24
  # Tables whose data should be excluded from SQL dumps (still dumps their schema DDL)
25
25
  # config.dont_dump_these_tables = []
26
26
 
27
- # Tables whose data should be truncated by TestData.truncate
27
+ # Tables whose data should be truncated by TestData.uses_clean_slate
28
28
  # If left as `nil`, all tables inserted into by the SQL file at
29
29
  # `data_dump_path` will be truncated
30
30
  # config.truncate_these_test_data_tables = nil
31
31
 
32
- # Perform TestData.load and TestData.truncate inside nested
33
- # transactions for increased test isolation and speed. Setting this
34
- # to false will disable several features that depend on transactions
35
- # being used
36
- # config.use_transactional_data_loader = true
37
-
38
32
  # Log level (valid values: [:debug, :info, :warn, :error, :quiet])
39
33
  # Can also be set with env var TEST_DATA_LOG_LEVEL
40
34
  # config.log_level = :info
data/lib/test_data.rb CHANGED
@@ -9,16 +9,20 @@ require_relative "test_data/configurators/cable_yaml"
9
9
  require_relative "test_data/configurators/database_yaml"
10
10
  require_relative "test_data/configurators/secrets_yaml"
11
11
  require_relative "test_data/configurators/webpacker_yaml"
12
+ require_relative "test_data/custom_loaders/abstract_base"
13
+ require_relative "test_data/custom_loaders/rails_fixtures"
12
14
  require_relative "test_data/detects_database_emptiness"
13
15
  require_relative "test_data/dumps_database"
14
16
  require_relative "test_data/error"
17
+ require_relative "test_data/inserts_test_data"
15
18
  require_relative "test_data/installs_configuration"
16
19
  require_relative "test_data/loads_database_dumps"
17
20
  require_relative "test_data/log"
18
21
  require_relative "test_data/railtie"
19
22
  require_relative "test_data/save_point"
20
23
  require_relative "test_data/statistics"
21
- require_relative "test_data/transactional_data_loader"
24
+ require_relative "test_data/manager"
25
+ require_relative "test_data/truncates_test_data"
22
26
  require_relative "test_data/verifies_configuration"
23
27
  require_relative "test_data/verifies_dumps_are_loadable"
24
28
  require_relative "test_data/version"
@@ -28,3 +32,30 @@ require_relative "generators/test_data/cable_yaml_generator"
28
32
  require_relative "generators/test_data/database_yaml_generator"
29
33
  require_relative "generators/test_data/secrets_yaml_generator"
30
34
  require_relative "generators/test_data/webpacker_yaml_generator"
35
+
36
+ module TestData
37
+ def self.uninitialize
38
+ @manager ||= Manager.new
39
+ @manager.rollback_to_before_data_load
40
+ end
41
+
42
+ def self.uses_test_data
43
+ @manager ||= Manager.new
44
+ @manager.load
45
+ end
46
+
47
+ def self.uses_clean_slate
48
+ @manager ||= Manager.new
49
+ @manager.truncate
50
+ end
51
+
52
+ def self.uses_rails_fixtures(test_instance)
53
+ @rails_fixtures_loader ||= CustomLoaders::RailsFixtures.new
54
+ @manager ||= Manager.new
55
+ @manager.load_custom_data(@rails_fixtures_loader, test_instance: test_instance)
56
+ end
57
+
58
+ def self.insert_test_data_dump
59
+ InsertsTestData.new.call
60
+ end
61
+ end
@@ -29,18 +29,9 @@ module TestData
29
29
  # Tables to exclude from all dumps
30
30
  attr_accessor :dont_dump_these_tables
31
31
 
32
- # Tables to truncate when TestData.truncate is called
32
+ # Tables to truncate when TestData.uses_clean_slate is called
33
33
  attr_accessor :truncate_these_test_data_tables
34
34
 
35
- # Perform TestData.load and TestData.truncate inside nested
36
- # transactions for increased test isolation and speed. Setting this to false
37
- # will disable several features that depend on transactions being used
38
- attr_reader :use_transactional_data_loader
39
- def use_transactional_data_loader=(use_transactions)
40
- TestData.ensure_we_dont_mix_transactional_and_non_transactional_data_loaders!(use_transactions)
41
- @use_transactional_data_loader = use_transactions
42
- end
43
-
44
35
  # Log level (valid values: [:debug, :info, :warn, :error, :quiet])
45
36
  def log_level
46
37
  TestData.log.level
@@ -73,7 +64,6 @@ module TestData
73
64
  @non_test_data_tables = []
74
65
  @dont_dump_these_tables = []
75
66
  @truncate_these_test_data_tables = nil
76
- @use_transactional_data_loader = true
77
67
  end
78
68
 
79
69
  def database_yaml
@@ -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,40 @@
1
+ module TestData
2
+ module CustomLoaders
3
+ class RailsFixtures < AbstractBase
4
+ def initialize
5
+ @statistics = TestData.statistics
6
+ @already_loaded_rails_fixtures = {}
7
+ end
8
+
9
+ def name
10
+ :rails_fixtures
11
+ end
12
+
13
+ def validate!(test_instance:)
14
+ if !test_instance.respond_to?(:setup_fixtures)
15
+ 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'")
16
+ elsif !test_instance.respond_to?(:__test_data_gem_setup_fixtures)
17
+ 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.")
18
+ end
19
+ end
20
+
21
+ def load_requested(test_instance:)
22
+ ActiveRecord::FixtureSet.reset_cache
23
+ test_instance.instance_variable_set(:@loaded_fixtures, @already_loaded_rails_fixtures[test_instance.class])
24
+ test_instance.instance_variable_set(:@fixture_cache, {})
25
+ end
26
+
27
+ def loaded?(test_instance:)
28
+ @already_loaded_rails_fixtures[test_instance.class].present?
29
+ end
30
+
31
+ def load(test_instance:)
32
+ test_instance.pre_loaded_fixtures = false
33
+ test_instance.use_transactional_tests = false
34
+ test_instance.__test_data_gem_setup_fixtures
35
+ @already_loaded_rails_fixtures[test_instance.class] = test_instance.instance_variable_get(:@loaded_fixtures)
36
+ @statistics.count_load_rails_fixtures!
37
+ end
38
+ end
39
+ end
40
+ 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
@@ -0,0 +1,185 @@
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
+ record_ar_internal_metadata_that_test_data_is_loaded
18
+ create_save_point(:after_data_load)
19
+ end
20
+
21
+ def truncate
22
+ ensure_after_load_save_point_is_active_if_data_is_loaded!
23
+ ensure_after_truncate_save_point_is_active_if_data_is_truncated!
24
+ return rollback_to_after_data_truncate if save_point_active?(:after_data_truncate)
25
+
26
+ if save_point_active?(:after_data_load)
27
+ # If a test that uses the test data runs before a test that starts by
28
+ # calling truncate, tables in the database that would NOT be truncated
29
+ # may have been changed. To avoid this category of test pollution, start
30
+ # the truncation by rolling back to the known clean point
31
+ rollback_to_after_data_load
32
+ else
33
+ # Seems silly loading data when the user asked us to truncate, but
34
+ # it's important that the state of the transaction stack matches the
35
+ # mental model we advertise, because any _other_ test in their suite
36
+ # should expect that the existence of :after_data_truncate save point
37
+ # implies that it's safe to rollback to the :after_data_load save
38
+ # point; since tests run in random order, it's likely to happen
39
+ 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")
40
+ load
41
+ end
42
+
43
+ @truncates_test_data.call
44
+ record_ar_internal_metadata_that_test_data_is_truncated
45
+ create_save_point(:after_data_truncate)
46
+ end
47
+
48
+ def load_custom_data(loader, **options)
49
+ loader.validate!(**options)
50
+ snapshot_name = "user_#{loader.name}".to_sym
51
+
52
+ ensure_after_load_save_point_is_active_if_data_is_loaded!
53
+ ensure_after_truncate_save_point_is_active_if_data_is_truncated!
54
+ ensure_custom_save_point_is_active_if_memo_exists!(snapshot_name)
55
+
56
+ loader.load_requested(**options)
57
+ if save_point_active?(snapshot_name) && loader.loaded?(**options)
58
+ return rollback_to_custom_savepoint(snapshot_name)
59
+ end
60
+
61
+ if save_point_active?(:after_data_truncate)
62
+ rollback_to_after_data_truncate
63
+ else
64
+ truncate
65
+ end
66
+
67
+ loader.load(**options)
68
+ record_ar_internal_metadata_of_custom_save_point(snapshot_name)
69
+ create_save_point(snapshot_name)
70
+ end
71
+
72
+ def rollback_to_before_data_load
73
+ if save_point_active?(:before_data_load)
74
+ rollback_save_point(:before_data_load)
75
+ # No need to recreate the save point
76
+ # (TestData.uses_test_data will if called)
77
+ end
78
+ end
79
+
80
+ private
81
+
82
+ def rollback_to_after_data_load
83
+ if save_point_active?(:after_data_load)
84
+ rollback_save_point(:after_data_load)
85
+ create_save_point(:after_data_load)
86
+ end
87
+ end
88
+
89
+ def rollback_to_after_data_truncate
90
+ if save_point_active?(:after_data_truncate)
91
+ rollback_save_point(:after_data_truncate)
92
+ create_save_point(:after_data_truncate)
93
+ end
94
+ end
95
+
96
+ def rollback_to_custom_savepoint(name)
97
+ if save_point_active?(name)
98
+ rollback_save_point(name)
99
+ create_save_point(name)
100
+ end
101
+ end
102
+
103
+ def connection
104
+ ActiveRecord::Base.connection
105
+ end
106
+
107
+ def ensure_after_load_save_point_is_active_if_data_is_loaded!
108
+ if !save_point_active?(:after_data_load) && ar_internal_metadata_shows_test_data_is_loaded?
109
+ 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"
110
+ create_save_point(:after_data_load)
111
+ end
112
+ end
113
+
114
+ def record_ar_internal_metadata_that_test_data_is_loaded
115
+ if ar_internal_metadata_shows_test_data_is_loaded?
116
+ 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?"
117
+ else
118
+ ActiveRecord::InternalMetadata.create!(key: "test_data:loaded", value: "true")
119
+ end
120
+ end
121
+
122
+ def ar_internal_metadata_shows_test_data_is_loaded?
123
+ ActiveRecord::InternalMetadata.find_by(key: "test_data:loaded")&.value == "true"
124
+ end
125
+
126
+ def ensure_after_truncate_save_point_is_active_if_data_is_truncated!
127
+ if !save_point_active?(:after_data_truncate) && ar_internal_metadata_shows_test_data_is_truncated?
128
+ 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"
129
+ create_save_point(:after_data_truncate)
130
+ end
131
+ end
132
+
133
+ def record_ar_internal_metadata_that_test_data_is_truncated
134
+ if ar_internal_metadata_shows_test_data_is_truncated?
135
+ 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?"
136
+ else
137
+ ActiveRecord::InternalMetadata.create!(key: "test_data:truncated", value: "true")
138
+ end
139
+ end
140
+
141
+ def ar_internal_metadata_shows_test_data_is_truncated?
142
+ ActiveRecord::InternalMetadata.find_by(key: "test_data:truncated")&.value == "true"
143
+ end
144
+
145
+ def ensure_custom_save_point_is_active_if_memo_exists!(name)
146
+ if !save_point_active?(name) && ar_internal_metadata_shows_custom_operation_was_persisted?(name)
147
+ 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"
148
+ create_save_point(name)
149
+ end
150
+ end
151
+
152
+ def ar_internal_metadata_shows_custom_operation_was_persisted?(name)
153
+ ActiveRecord::InternalMetadata.find_by(key: "test_data:#{name}")&.value == "true"
154
+ end
155
+
156
+ def record_ar_internal_metadata_of_custom_save_point(name)
157
+ if ar_internal_metadata_shows_custom_operation_was_persisted?(name)
158
+ 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?"
159
+ else
160
+ ActiveRecord::InternalMetadata.create!(key: "test_data:#{name}", value: "true")
161
+ end
162
+ end
163
+
164
+ def save_point_active?(name)
165
+ purge_closed_save_points!
166
+ !!@save_points.find { |sp| sp.name == name }&.active?
167
+ end
168
+
169
+ def create_save_point(name)
170
+ raise Error.new("Could not create test_data savepoint '#{name}', because it was already active!") if save_point_active?(name)
171
+ @save_points << SavePoint.new(name)
172
+ end
173
+
174
+ def rollback_save_point(name)
175
+ @save_points.find { |sp| sp.name == name }&.rollback!
176
+ purge_closed_save_points!
177
+ end
178
+
179
+ def purge_closed_save_points!
180
+ @save_points = @save_points.select { |save_point|
181
+ save_point.active?
182
+ }
183
+ end
184
+ end
185
+ end
@@ -0,0 +1,31 @@
1
+ module TestData
2
+ class TruncatesTestData
3
+ def initialize
4
+ @config = TestData.config
5
+ @statistics = TestData.statistics
6
+ end
7
+
8
+ def call
9
+ connection.disable_referential_integrity do
10
+ connection.execute("TRUNCATE TABLE #{tables_to_truncate.map { |t| connection.quote_table_name(t) }.join(", ")} #{"CASCADE" unless @config.truncate_these_test_data_tables.present?}")
11
+ end
12
+ @statistics.count_truncate!
13
+ end
14
+
15
+ private
16
+
17
+ def tables_to_truncate
18
+ if @config.truncate_these_test_data_tables.present?
19
+ @config.truncate_these_test_data_tables
20
+ else
21
+ @tables_to_truncate ||= IO.foreach(@config.data_dump_path).grep(/^INSERT INTO/) { |line|
22
+ line.match(/^INSERT INTO ([^\s]+)/)&.captures&.first
23
+ }.compact.uniq
24
+ end
25
+ end
26
+
27
+ def connection
28
+ ActiveRecord::Base.connection
29
+ end
30
+ end
31
+ end