test_data 0.1.0 → 0.3.0

Sign up to get free protection for your applications and to get access to all the features.
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