test_data 0.1.0 → 0.3.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 (48) 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 +38 -1
  5. data/Gemfile.lock +15 -15
  6. data/LICENSE.txt +1 -1
  7. data/README.md +701 -712
  8. data/example/.gitignore +1 -4
  9. data/example/Gemfile.lock +1 -1
  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 +2 -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 +1 -5
  20. data/example/test/integration/parallel_boops_without_fixtures_test.rb +1 -5
  21. data/example/test/integration/rails_fixtures_double_load_test.rb +2 -2
  22. data/example/test/integration/rails_fixtures_override_test.rb +18 -35
  23. data/example/test/integration/test_data_hooks_test.rb +89 -0
  24. data/example/test/integration/transaction_committing_boops_test.rb +1 -10
  25. data/example/test/test_helper.rb +1 -5
  26. data/lib/generators/test_data/environment_file_generator.rb +4 -0
  27. data/lib/generators/test_data/initializer_generator.rb +19 -13
  28. data/lib/test_data/config.rb +30 -12
  29. data/lib/test_data/custom_loaders/abstract_base.rb +25 -0
  30. data/lib/test_data/custom_loaders/rails_fixtures.rb +45 -0
  31. data/lib/test_data/detects_database_existence.rb +19 -0
  32. data/lib/test_data/determines_databases_associated_dump_time.rb +13 -0
  33. data/lib/test_data/determines_when_sql_dump_was_made.rb +24 -0
  34. data/lib/test_data/dumps_database.rb +3 -0
  35. data/lib/test_data/inserts_test_data.rb +25 -0
  36. data/lib/test_data/manager.rb +187 -0
  37. data/lib/test_data/railtie.rb +4 -0
  38. data/lib/test_data/rake.rb +41 -12
  39. data/lib/test_data/records_dump_metadata.rb +9 -0
  40. data/lib/test_data/truncates_test_data.rb +31 -0
  41. data/lib/test_data/version.rb +1 -1
  42. data/lib/test_data/warns_if_database_is_newer_than_dump.rb +32 -0
  43. data/lib/test_data/warns_if_dump_is_newer_than_database.rb +36 -0
  44. data/lib/test_data.rb +43 -1
  45. data/script/reset_example_app +1 -0
  46. data/script/test +54 -6
  47. metadata +17 -3
  48. data/lib/test_data/transactional_data_loader.rb +0 -300
@@ -0,0 +1,9 @@
1
+ module TestData
2
+ class RecordsDumpMetadata
3
+ def call
4
+ ActiveRecord::InternalMetadata
5
+ .find_or_initialize_by(key: "test_data:last_dumped_at")
6
+ .update!(value: Time.now.utc.inspect)
7
+ end
8
+ end
9
+ 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
@@ -1,3 +1,3 @@
1
1
  module TestData
2
- VERSION = "0.1.0"
2
+ VERSION = "0.3.0"
3
3
  end
@@ -0,0 +1,32 @@
1
+ module TestData
2
+ class WarnsIfDatabaseIsNewerThanDump
3
+ def initialize
4
+ @config = TestData.config
5
+ @determines_when_sql_dump_was_made = DeterminesWhenSqlDumpWasMade.new
6
+ @determines_databases_associated_dump_time = DeterminesDatabasesAssociatedDumpTime.new
7
+ end
8
+
9
+ def call
10
+ return unless Rails.env.test_data?
11
+ sql_dumped_at = @determines_when_sql_dump_was_made.call
12
+ database_dumped_at = @determines_databases_associated_dump_time.call
13
+
14
+ if database_dumped_at.present? &&
15
+ sql_dumped_at.present? &&
16
+ database_dumped_at > sql_dumped_at
17
+ TestData.log.warn <<~MSG
18
+ Your local test_data database '#{@config.database_name}' is associated
19
+ with a SQL dump that was NEWER than the current dumps located in
20
+ '#{File.dirname(@config.data_dump_path)}':
21
+
22
+ SQL Dump: #{sql_dumped_at.localtime}
23
+ Database: #{database_dumped_at.localtime}
24
+
25
+ If you're not intentionally resetting your local test_data database to an earlier
26
+ version, you may want to take a closer look before taking any destructive actions.
27
+
28
+ MSG
29
+ end
30
+ end
31
+ end
32
+ end
@@ -0,0 +1,36 @@
1
+ module TestData
2
+ class WarnsIfDumpIsNewerThanDatabase
3
+ def initialize
4
+ @config = TestData.config
5
+ @determines_when_sql_dump_was_made = DeterminesWhenSqlDumpWasMade.new
6
+ @determines_databases_associated_dump_time = DeterminesDatabasesAssociatedDumpTime.new
7
+ end
8
+
9
+ def call
10
+ return unless Rails.env.test_data?
11
+ sql_dumped_at = @determines_when_sql_dump_was_made.call
12
+ database_dumped_at = @determines_databases_associated_dump_time.call
13
+
14
+ if sql_dumped_at.present? &&
15
+ database_dumped_at.present? &&
16
+ sql_dumped_at > database_dumped_at
17
+ TestData.log.warn <<~MSG
18
+ The SQL dumps in '#{File.dirname(@config.data_dump_path)}' were created
19
+ after your local test_data database '#{@config.database_name}' was last dumped.
20
+
21
+ SQL Dump: #{sql_dumped_at.localtime}
22
+ Database: #{database_dumped_at.localtime}
23
+
24
+ To avoid potential data loss, you may want to consider dropping '#{@config.database_name}'
25
+ and loading the SQL dumps before making changes to the test data in this database
26
+ or performing another dump.
27
+
28
+ To do this, kill any processes with RAILS_ENV=test_data and then run:
29
+
30
+ $ bin/rake test_data:reinitialize
31
+
32
+ MSG
33
+ end
34
+ end
35
+ end
36
+ end
data/lib/test_data.rb CHANGED
@@ -9,22 +9,64 @@ 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"
15
+ require_relative "test_data/detects_database_existence"
16
+ require_relative "test_data/determines_when_sql_dump_was_made"
17
+ require_relative "test_data/determines_databases_associated_dump_time"
13
18
  require_relative "test_data/dumps_database"
14
19
  require_relative "test_data/error"
20
+ require_relative "test_data/inserts_test_data"
15
21
  require_relative "test_data/installs_configuration"
16
22
  require_relative "test_data/loads_database_dumps"
17
23
  require_relative "test_data/log"
24
+ require_relative "test_data/manager"
18
25
  require_relative "test_data/railtie"
26
+ require_relative "test_data/records_dump_metadata"
19
27
  require_relative "test_data/save_point"
20
28
  require_relative "test_data/statistics"
21
- require_relative "test_data/transactional_data_loader"
29
+ require_relative "test_data/truncates_test_data"
22
30
  require_relative "test_data/verifies_configuration"
23
31
  require_relative "test_data/verifies_dumps_are_loadable"
24
32
  require_relative "test_data/version"
33
+ require_relative "test_data/warns_if_dump_is_newer_than_database"
34
+ require_relative "test_data/warns_if_database_is_newer_than_dump"
25
35
  require_relative "generators/test_data/environment_file_generator"
26
36
  require_relative "generators/test_data/initializer_generator"
27
37
  require_relative "generators/test_data/cable_yaml_generator"
28
38
  require_relative "generators/test_data/database_yaml_generator"
29
39
  require_relative "generators/test_data/secrets_yaml_generator"
30
40
  require_relative "generators/test_data/webpacker_yaml_generator"
41
+
42
+ module TestData
43
+ def self.uninitialize
44
+ @manager ||= Manager.new
45
+ @manager.rollback_to_before_data_load
46
+ nil
47
+ end
48
+
49
+ def self.uses_test_data
50
+ @manager ||= Manager.new
51
+ @manager.load
52
+ nil
53
+ end
54
+
55
+ def self.uses_clean_slate
56
+ @manager ||= Manager.new
57
+ @manager.truncate
58
+ nil
59
+ end
60
+
61
+ def self.uses_rails_fixtures(test_instance)
62
+ @rails_fixtures_loader ||= CustomLoaders::RailsFixtures.new
63
+ @manager ||= Manager.new
64
+ @manager.load_custom_data(@rails_fixtures_loader, test_instance: test_instance)
65
+ nil
66
+ end
67
+
68
+ def self.insert_test_data_dump
69
+ InsertsTestData.new.call
70
+ nil
71
+ end
72
+ end
@@ -12,6 +12,7 @@ dropdb example_test_data 2>/dev/null || true
12
12
 
13
13
  # Reset files:
14
14
  git checkout app/models/boop.rb
15
+ git checkout config/application.rb
15
16
  git checkout config/database.yml
16
17
  git checkout db/schema.rb
17
18
  git clean -xdf .
data/script/test CHANGED
@@ -28,11 +28,45 @@ bin/rails test test/integration/better_mode_switching_demo_test.rb
28
28
  bin/rails test test/integration/parallel_boops_with_fixtures_test.rb
29
29
  bin/rails test test/integration/parallel_boops_without_fixtures_test.rb
30
30
 
31
+ # Verify the out-of-date database warning system works (step 1)
32
+ mkdir -p tmp/dump-backup-1
33
+ cp test/support/test_data/* tmp/dump-backup-1
34
+
31
35
  # Test using the test_data env to interactively create test data
32
36
  RAILS_ENV=test_data rails runner "5.times { Boop.create! }"
33
37
  bin/rake test_data:dump
34
38
  bin/rails test test/integration/updated_boops_test.rb
35
39
 
40
+ # Verify the out-of-date database warning system works (step 2)
41
+ mkdir -p tmp/dump-backup-2
42
+ cp test/support/test_data/* tmp/dump-backup-2
43
+ cp tmp/dump-backup-1/* test/support/test_data
44
+ set +e
45
+ result=`yes "n" | bin/rake test_data:reinitialize 2>&1`
46
+ set -e
47
+ if ! echo "$result" | grep -q "\[test_data:warn\] Your local test_data database 'example_test_data' is associated"; then
48
+ echo "Expected to be warned that the SQL dump was older than the database, but there was no warning."
49
+ exit 1
50
+ fi
51
+ TEST_DATA_CONFIRM=true bin/rake test_data:reinitialize
52
+ cp tmp/dump-backup-2/* test/support/test_data
53
+ if ! RAILS_ENV=test_data bin/rails runner "" 2>&1 | grep -q "\[test_data:warn\] The SQL dumps in 'test/support/test_data' were created"; then
54
+ echo "Expected to be warned that the SQL dump was newer than the database, but there was no warning."
55
+ exit 1
56
+ fi
57
+ set +e
58
+ result=`yes "n" | bin/rake test_data:reinitialize 2>&1`
59
+ set -e
60
+ if echo "$result" | grep -q "\[test_data:warn\] Your local test_data database 'example_test_data' is associated"; then
61
+ echo "Expected NOT to be warned that the SQL dump was older than the database, but a warning was printed."
62
+ exit 1
63
+ fi
64
+ TEST_DATA_CONFIRM=true bin/rake test_data:reinitialize
65
+ if RAILS_ENV=test_data bin/rails runner "" 2>&1 | grep -q "\[test_data:warn\]"; then
66
+ echo "Expected NOT to be warned that the SQL dump was newer than the database, but a warning was printed."
67
+ exit 1
68
+ fi
69
+
36
70
  # Test a migration being added and run and an out-of-date dump being loaded
37
71
  cp ../test/fixtures/20210418220133_add_beep_to_boops.rb db/migrate
38
72
  cp ../test/fixtures/20210624180810_create_pants.rb db/migrate
@@ -51,6 +85,8 @@ bin/rails test test/integration/transaction_committing_boops_test.rb
51
85
 
52
86
  # Run a test that prevents Rails fixtures for preloading and then loads them in a transaction
53
87
  bin/rails test test/integration/rails_fixtures_override_test.rb
88
+ bundle exec rspec spec/requests/rails_fixtures_override_spec.rb
89
+ bin/rails test test/integration/fixture_load_count_test.rb
54
90
 
55
91
  # Run a test that forgets to prevent Rails fixtures but then tries to load them in a transaction
56
92
  bin/rails test test/integration/rails_fixtures_double_load_test.rb
@@ -82,18 +118,30 @@ cp ../test/fixtures/20210423190737_add_foreign_keys.rb db/migrate/
82
118
  cp ../test/fixtures/boop_with_other_boops.rb app/models/boop.rb
83
119
  RAILS_ENV=test_data bin/rake db:migrate
84
120
  bin/rake test_data:dump
85
- bin/rake db:migrate
86
- bin/rake db:test:prepare
121
+ bin/rake db:migrate db:test:prepare
87
122
  bin/rails test test/integration/boops_that_boop_boops_test.rb
88
123
 
89
124
  # Make sure it loads cleanly again
90
125
  bin/rake test_data:drop_database
91
- if [ ! bin/rake test_data:load | grep -q "ERROR" ]; then
92
- echo "Running test_data:load after adding FK constraints led to errors"
93
- exit 1
94
- fi
126
+ bin/rake test_data:load
95
127
  bin/rails test test/integration/boops_that_boop_boops_test.rb
96
128
 
129
+ # Test all the after hooks!
130
+ cp ../test/fixtures/20210729130542_add_materialized_meta_boop_view.rb db/migrate/
131
+ cp ../test/fixtures/meta_boop.rb app/models/meta_boop.rb
132
+ # Gsub config file to switch to structure.sql b/c materialized view
133
+ ruby -e '
134
+ path = "config/application.rb"
135
+ IO.write(path, File.open(path) { |f|
136
+ f.read.gsub("# config.active_record.schema_format = :sql", "config.active_record.schema_format = :sql")
137
+ })
138
+ '
139
+ rm db/schema.rb
140
+ bin/rake db:migrate db:test:prepare
141
+ RAILS_ENV=test_data bin/rake db:migrate
142
+ bin/rake test_data:dump
143
+ bin/rails test test/integration/test_data_hooks_test.rb
144
+
97
145
  # Cleanup
98
146
  cd ..
99
147
  ./script/reset_example_app
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: test_data
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.3.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Justin Searls
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2021-06-27 00:00:00.000000000 Z
11
+ date: 2021-09-13 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: railties
@@ -33,6 +33,7 @@ extra_rdoc_files: []
33
33
  files:
34
34
  - ".github/workflows/ruby.yml"
35
35
  - ".gitignore"
36
+ - ".standard.yml"
36
37
  - CHANGELOG.md
37
38
  - Gemfile
38
39
  - Gemfile.lock
@@ -93,6 +94,7 @@ files:
93
94
  - example/public/robots.txt
94
95
  - example/spec/rails_helper.rb
95
96
  - example/spec/requests/boops_spec.rb
97
+ - example/spec/requests/rails_fixtures_override_spec.rb
96
98
  - example/spec/spec_helper.rb
97
99
  - example/test/application_system_test_case.rb
98
100
  - example/test/factories.rb
@@ -101,6 +103,7 @@ files:
101
103
  - example/test/integration/better_mode_switching_demo_test.rb
102
104
  - example/test/integration/boops_that_boop_boops_test.rb
103
105
  - example/test/integration/dont_dump_tables_test.rb
106
+ - example/test/integration/fixture_load_count_test.rb
104
107
  - example/test/integration/load_rollback_truncate_test.rb
105
108
  - example/test/integration/migrated_boops_test.rb
106
109
  - example/test/integration/mode_switching_demo_test.rb
@@ -108,6 +111,7 @@ files:
108
111
  - example/test/integration/parallel_boops_without_fixtures_test.rb
109
112
  - example/test/integration/rails_fixtures_double_load_test.rb
110
113
  - example/test/integration/rails_fixtures_override_test.rb
114
+ - example/test/integration/test_data_hooks_test.rb
111
115
  - example/test/integration/transaction_committing_boops_test.rb
112
116
  - example/test/integration/updated_boops_test.rb
113
117
  - example/test/test_helper.rb
@@ -129,21 +133,31 @@ files:
129
133
  - lib/test_data/configurators/initializer.rb
130
134
  - lib/test_data/configurators/secrets_yaml.rb
131
135
  - lib/test_data/configurators/webpacker_yaml.rb
136
+ - lib/test_data/custom_loaders/abstract_base.rb
137
+ - lib/test_data/custom_loaders/rails_fixtures.rb
132
138
  - lib/test_data/detects_database_emptiness.rb
139
+ - lib/test_data/detects_database_existence.rb
140
+ - lib/test_data/determines_databases_associated_dump_time.rb
141
+ - lib/test_data/determines_when_sql_dump_was_made.rb
133
142
  - lib/test_data/dumps_database.rb
134
143
  - lib/test_data/error.rb
135
144
  - lib/test_data/generator_support.rb
145
+ - lib/test_data/inserts_test_data.rb
136
146
  - lib/test_data/installs_configuration.rb
137
147
  - lib/test_data/loads_database_dumps.rb
138
148
  - lib/test_data/log.rb
149
+ - lib/test_data/manager.rb
139
150
  - lib/test_data/railtie.rb
140
151
  - lib/test_data/rake.rb
152
+ - lib/test_data/records_dump_metadata.rb
141
153
  - lib/test_data/save_point.rb
142
154
  - lib/test_data/statistics.rb
143
- - lib/test_data/transactional_data_loader.rb
155
+ - lib/test_data/truncates_test_data.rb
144
156
  - lib/test_data/verifies_configuration.rb
145
157
  - lib/test_data/verifies_dumps_are_loadable.rb
146
158
  - lib/test_data/version.rb
159
+ - lib/test_data/warns_if_database_is_newer_than_dump.rb
160
+ - lib/test_data/warns_if_dump_is_newer_than_database.rb
147
161
  - script/reset_example_app
148
162
  - script/test
149
163
  - test_data.gemspec
@@ -1,300 +0,0 @@
1
- module TestData
2
- def self.load
3
- @data_loader ||= TestData.config.use_transactional_data_loader ? TransactionalDataLoader.new : CommittingDataLoader.new
4
- @data_loader.load
5
- end
6
-
7
- def self.ensure_we_dont_mix_transactional_and_non_transactional_data_loaders!(use_transactions)
8
- unless @data_loader.nil? || use_transactions == @data_loader.transactional?
9
- raise Error.new("There is already a #{@data_loader.transactional? ? "transactional" : "non-transactional"} data loader in use, and test_data does not support mixing both types of loaders in a single process.")
10
- end
11
- end
12
-
13
- def self.rollback(save_point_name = :after_data_load)
14
- unless TestData.config.use_transactional_data_loader
15
- raise Error.new("TestData.rollback requires config.use_transactional_data_loader = true")
16
- end
17
- @data_loader ||= TransactionalDataLoader.new
18
- case save_point_name
19
- when :before_data_load
20
- @data_loader.rollback_to_before_data_load
21
- when :after_data_load
22
- @data_loader.rollback_to_after_data_load
23
- when :after_data_truncate
24
- @data_loader.rollback_to_after_data_truncate
25
- when :after_load_rails_fixtures
26
- @data_loader.rollback_to_after_load_rails_fixtures
27
- else
28
- raise Error.new("No known save point named '#{save_point_name}'. Valid values are: [:before_data_load, :after_data_load, :after_data_truncate, :after_load_rails_fixtures]")
29
- end
30
- end
31
-
32
- def self.truncate
33
- @data_loader ||= TestData.config.use_transactional_data_loader ? TransactionalDataLoader.new : CommittingDataLoader.new
34
- @data_loader.truncate
35
- end
36
-
37
- def self.load_rails_fixtures(test_instance)
38
- if !test_instance.respond_to?(:setup_fixtures)
39
- raise Error.new("'TestData.load_rails_fixtures' must be passed a test instance that has had ActiveRecord::TestFixtures mixed-in (e.g. `TestData.load_rails_fixtures(self)` in an ActiveSupport::TestCase `setup` block), but the provided argument does not respond to 'setup_fixtures'")
40
- elsif !test_instance.respond_to?(:__test_data_gem_setup_fixtures)
41
- raise Error.new("'TestData.load_rails_fixtures' 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.")
42
- elsif !TestData.config.use_transactional_data_loader
43
- raise Error.new("'TestData.load_rails_fixtures' requires config.use_transactional_data_loader = true")
44
- end
45
- @data_loader ||= TransactionalDataLoader.new
46
- @data_loader.load_rails_fixtures(test_instance)
47
- end
48
-
49
- class AbstractDataLoader
50
- def initialize
51
- @config = TestData.config
52
- @statistics = TestData.statistics
53
- end
54
-
55
- protected
56
-
57
- def execute_data_load
58
- search_path = execute("show search_path").first["search_path"]
59
- connection.disable_referential_integrity do
60
- execute(File.read(@config.data_dump_full_path))
61
- end
62
- execute <<~SQL
63
- select pg_catalog.set_config('search_path', '#{search_path}', false)
64
- SQL
65
- @statistics.count_load!
66
- end
67
-
68
- def execute_data_truncate
69
- connection.disable_referential_integrity do
70
- execute("TRUNCATE TABLE #{tables_to_truncate.map { |t| connection.quote_table_name(t) }.join(", ")} #{"CASCADE" unless @config.truncate_these_test_data_tables.present?}")
71
- end
72
- @statistics.count_truncate!
73
- end
74
-
75
- def execute_load_rails_fixtures(test_instance)
76
- test_instance.pre_loaded_fixtures = false
77
- test_instance.use_transactional_tests = false
78
- test_instance.__test_data_gem_setup_fixtures
79
- @already_loaded_rails_fixtures[test_instance.class] = test_instance.instance_variable_get(:@loaded_fixtures)
80
- @statistics.count_load_rails_fixtures!
81
- end
82
-
83
- def tables_to_truncate
84
- if @config.truncate_these_test_data_tables.present?
85
- @config.truncate_these_test_data_tables
86
- else
87
- @tables_to_truncate ||= IO.foreach(@config.data_dump_path).grep(/^INSERT INTO/) { |line|
88
- line.match(/^INSERT INTO ([^\s]+)/)&.captures&.first
89
- }.compact.uniq
90
- end
91
- end
92
-
93
- def connection
94
- ActiveRecord::Base.connection
95
- end
96
-
97
- private
98
-
99
- def execute(sql)
100
- connection.execute(sql)
101
- end
102
- end
103
-
104
- class CommittingDataLoader < AbstractDataLoader
105
- def transactional?
106
- false
107
- end
108
-
109
- def load
110
- execute_data_load
111
- end
112
-
113
- def truncate
114
- execute_data_truncate
115
- end
116
- end
117
-
118
- class TransactionalDataLoader < AbstractDataLoader
119
- def initialize
120
- super
121
- @save_points = []
122
- @already_loaded_rails_fixtures = {}
123
- end
124
-
125
- def transactional?
126
- true
127
- end
128
-
129
- def load
130
- ensure_after_load_save_point_is_active_if_data_is_loaded!
131
- return rollback_to_after_data_load if save_point_active?(:after_data_load)
132
-
133
- create_save_point(:before_data_load)
134
- execute_data_load
135
- record_ar_internal_metadata_that_test_data_is_loaded
136
- create_save_point(:after_data_load)
137
- end
138
-
139
- def rollback_to_before_data_load
140
- if save_point_active?(:before_data_load)
141
- rollback_save_point(:before_data_load)
142
- # No need to recreate the save point -- TestData.load will if called
143
- end
144
- end
145
-
146
- def rollback_to_after_data_load
147
- if save_point_active?(:after_data_load)
148
- rollback_save_point(:after_data_load)
149
- create_save_point(:after_data_load)
150
- end
151
- end
152
-
153
- def rollback_to_after_data_truncate
154
- if save_point_active?(:after_data_truncate)
155
- rollback_save_point(:after_data_truncate)
156
- create_save_point(:after_data_truncate)
157
- end
158
- end
159
-
160
- def rollback_to_after_load_rails_fixtures
161
- if save_point_active?(:after_load_rails_fixtures)
162
- rollback_save_point(:after_load_rails_fixtures)
163
- create_save_point(:after_load_rails_fixtures)
164
- end
165
- end
166
-
167
- def truncate
168
- ensure_after_load_save_point_is_active_if_data_is_loaded!
169
- ensure_after_truncate_save_point_is_active_if_data_is_truncated!
170
- return rollback_to_after_data_truncate if save_point_active?(:after_data_truncate)
171
-
172
- if save_point_active?(:after_data_load)
173
- # If a test that uses the test data runs before a test that starts by
174
- # calling truncate, tables in the database that would NOT be truncated
175
- # may have been changed. To avoid this category of test pollution, start
176
- # the truncation by rolling back to the known clean point
177
- rollback_to_after_data_load
178
- else
179
- # Seems silly loading data when the user asked us to truncate, but
180
- # it's important that the state of the transaction stack matches the
181
- # mental model we advertise, because any _other_ test in their suite
182
- # should expect that the existence of :after_data_truncate save point
183
- # implies that it's safe to rollback to the :after_data_load save
184
- # point; since tests run in random order, it's likely to happen
185
- TestData.log.debug("TestData.truncate was called, but data was not loaded. Loading data before truncate to preserve the transaction save point ordering")
186
- load
187
- end
188
-
189
- execute_data_truncate
190
- record_ar_internal_metadata_that_test_data_is_truncated
191
- create_save_point(:after_data_truncate)
192
- end
193
-
194
- # logic is beat-for-beat the same as #truncate just one step deeper down
195
- # this rabbit hole…
196
- def load_rails_fixtures(test_instance)
197
- ensure_after_load_save_point_is_active_if_data_is_loaded!
198
- ensure_after_truncate_save_point_is_active_if_data_is_truncated!
199
- ensure_after_load_rails_fixtures_save_point_is_active_if_fixtures_are_loaded!
200
- reset_rails_fixture_caches(test_instance)
201
- return rollback_to_after_load_rails_fixtures if save_point_active?(:after_load_rails_fixtures) && @already_loaded_rails_fixtures[test_instance.class].present?
202
-
203
- if save_point_active?(:after_data_truncate)
204
- rollback_to_after_data_truncate
205
- else
206
- truncate
207
- end
208
-
209
- execute_load_rails_fixtures(test_instance)
210
- record_ar_internal_metadata_that_rails_fixtures_are_loaded
211
- create_save_point(:after_load_rails_fixtures)
212
- end
213
-
214
- private
215
-
216
- def ensure_after_load_save_point_is_active_if_data_is_loaded!
217
- if !save_point_active?(:after_data_load) && ar_internal_metadata_shows_test_data_is_loaded?
218
- 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"
219
- create_save_point(:after_data_load)
220
- end
221
- end
222
-
223
- def record_ar_internal_metadata_that_test_data_is_loaded
224
- if ar_internal_metadata_shows_test_data_is_loaded?
225
- 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?"
226
- else
227
- ActiveRecord::InternalMetadata.create!(key: "test_data:loaded", value: "true")
228
- end
229
- end
230
-
231
- def ar_internal_metadata_shows_test_data_is_loaded?
232
- ActiveRecord::InternalMetadata.find_by(key: "test_data:loaded")&.value == "true"
233
- end
234
-
235
- def ensure_after_truncate_save_point_is_active_if_data_is_truncated!
236
- if !save_point_active?(:after_data_truncate) && ar_internal_metadata_shows_test_data_is_truncated?
237
- 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"
238
- create_save_point(:after_data_truncate)
239
- end
240
- end
241
-
242
- def record_ar_internal_metadata_that_test_data_is_truncated
243
- if ar_internal_metadata_shows_test_data_is_truncated?
244
- 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?"
245
- else
246
- ActiveRecord::InternalMetadata.create!(key: "test_data:truncated", value: "true")
247
- end
248
- end
249
-
250
- def ar_internal_metadata_shows_test_data_is_truncated?
251
- ActiveRecord::InternalMetadata.find_by(key: "test_data:truncated")&.value == "true"
252
- end
253
-
254
- def ensure_after_load_rails_fixtures_save_point_is_active_if_fixtures_are_loaded!
255
- if !save_point_active?(:after_load_rails_fixtures) && ar_internal_metadata_shows_rails_fixtures_are_loaded?
256
- TestData.log.debug "Rails Fixtures appears to have been loaded by test_data, but the :after_load_rails_fixtures save point was rolled back (and not by this gem). Recreating the :after_load_rails_fixtures save point"
257
- create_save_point(:after_load_rails_fixtures)
258
- end
259
- end
260
-
261
- def record_ar_internal_metadata_that_rails_fixtures_are_loaded
262
- if ar_internal_metadata_shows_rails_fixtures_are_loaded?
263
- TestData.log.warn "Attempted to record that test_data had loaded your Rails fixtures in ar_internal_metadata, but record already existed. Perhaps a previous test run committed the loading of your Rails fixtures?"
264
- else
265
- ActiveRecord::InternalMetadata.create!(key: "test_data:rails_fixtures_loaded", value: "true")
266
- end
267
- end
268
-
269
- def ar_internal_metadata_shows_rails_fixtures_are_loaded?
270
- ActiveRecord::InternalMetadata.find_by(key: "test_data:rails_fixtures_loaded")&.value == "true"
271
- end
272
-
273
- def reset_rails_fixture_caches(test_instance)
274
- ActiveRecord::FixtureSet.reset_cache
275
- test_instance.instance_variable_set(:@loaded_fixtures, @already_loaded_rails_fixtures[test_instance.class])
276
- test_instance.instance_variable_set(:@fixture_cache, {})
277
- end
278
-
279
- def save_point_active?(name)
280
- purge_closed_save_points!
281
- !!@save_points.find { |sp| sp.name == name }&.active?
282
- end
283
-
284
- def create_save_point(name)
285
- raise Error.new("Could not create test_data savepoint '#{name}', because it was already active!") if save_point_active?(name)
286
- @save_points << SavePoint.new(name)
287
- end
288
-
289
- def rollback_save_point(name)
290
- @save_points.find { |sp| sp.name == name }&.rollback!
291
- purge_closed_save_points!
292
- end
293
-
294
- def purge_closed_save_points!
295
- @save_points = @save_points.select { |save_point|
296
- save_point.active?
297
- }
298
- end
299
- end
300
- end