test_data 0.0.1 → 0.0.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 (43) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +45 -0
  3. data/CHANGELOG.md +8 -1
  4. data/Gemfile.lock +5 -3
  5. data/README.md +961 -17
  6. data/example/Gemfile +3 -0
  7. data/example/Gemfile.lock +32 -3
  8. data/example/README.md +2 -22
  9. data/example/config/credentials.yml.enc +2 -1
  10. data/example/config/database.yml +2 -0
  11. data/example/spec/rails_helper.rb +64 -0
  12. data/example/spec/requests/boops_spec.rb +21 -0
  13. data/example/spec/spec_helper.rb +94 -0
  14. data/example/test/factories.rb +4 -0
  15. data/example/test/integration/better_mode_switching_demo_test.rb +45 -0
  16. data/example/test/integration/boops_that_boop_boops_test.rb +17 -0
  17. data/example/test/integration/dont_dump_tables_test.rb +7 -0
  18. data/example/test/integration/load_rollback_truncate_test.rb +195 -0
  19. data/example/test/integration/mode_switching_demo_test.rb +48 -0
  20. data/example/test/integration/parallel_boops_with_fixtures_test.rb +14 -0
  21. data/example/test/integration/parallel_boops_without_fixtures_test.rb +13 -0
  22. data/example/test/integration/transaction_committing_boops_test.rb +25 -0
  23. data/example/test/test_helper.rb +3 -26
  24. data/lib/generators/test_data/database_yaml_generator.rb +1 -1
  25. data/lib/generators/test_data/environment_file_generator.rb +0 -14
  26. data/lib/generators/test_data/initializer_generator.rb +38 -0
  27. data/lib/generators/test_data/webpacker_yaml_generator.rb +1 -1
  28. data/lib/test_data.rb +5 -0
  29. data/lib/test_data/config.rb +25 -2
  30. data/lib/test_data/configurators.rb +1 -0
  31. data/lib/test_data/configurators/initializer.rb +25 -0
  32. data/lib/test_data/dumps_database.rb +31 -4
  33. data/lib/test_data/loads_database_dumps.rb +7 -7
  34. data/lib/test_data/log.rb +58 -0
  35. data/lib/test_data/rake.rb +7 -5
  36. data/lib/test_data/save_point.rb +34 -0
  37. data/lib/test_data/statistics.rb +26 -0
  38. data/lib/test_data/transactional_data_loader.rb +145 -32
  39. data/lib/test_data/verifies_dumps_are_loadable.rb +4 -4
  40. data/lib/test_data/version.rb +1 -1
  41. data/script/reset_example_app +17 -0
  42. data/script/test +54 -13
  43. metadata +19 -2
@@ -0,0 +1,48 @@
1
+ require "test_helper"
2
+
3
+ class ModeSwitchingTestCase < ActiveSupport::TestCase
4
+ def self.test_data_mode(mode)
5
+ if mode == :factory_bot
6
+ require "factory_bot_rails"
7
+ include FactoryBot::Syntax::Methods
8
+
9
+ setup do
10
+ TestData.rollback(:before_data_load)
11
+ ActiveRecord::Base.connection.begin_transaction(joinable: false, _lazy: false)
12
+ end
13
+
14
+ teardown do
15
+ ActiveRecord::Base.connection.rollback_transaction
16
+ end
17
+
18
+ elsif mode == :test_data
19
+ self.use_transactional_tests = false
20
+
21
+ setup do
22
+ TestData.load
23
+ end
24
+
25
+ teardown do
26
+ TestData.rollback
27
+ end
28
+ end
29
+ end
30
+ end
31
+
32
+ class FactoryModeTest < ModeSwitchingTestCase
33
+ test_data_mode :factory_bot
34
+
35
+ def test_boops
36
+ create(:boop)
37
+
38
+ assert_equal 1, Boop.count
39
+ end
40
+ end
41
+
42
+ class TestDataModeTest < ModeSwitchingTestCase
43
+ test_data_mode :test_data
44
+
45
+ def test_boops
46
+ assert_equal 10, Boop.count
47
+ end
48
+ end
@@ -1,5 +1,19 @@
1
1
  require "test_helper"
2
2
 
3
+ class ParallelizedTransactionalFixturefullTestCase < ActiveSupport::TestCase
4
+ parallelize(workers: :number_of_processors)
5
+ self.use_transactional_tests = true
6
+ fixtures :all
7
+
8
+ def setup
9
+ TestData.load
10
+ end
11
+
12
+ def teardown
13
+ TestData.rollback
14
+ end
15
+ end
16
+
3
17
  class ParallelBoopsWithFixturesTest < ParallelizedTransactionalFixturefullTestCase
4
18
  100.times do |i|
5
19
  test "that boops don't change ##{i}" do
@@ -1,5 +1,18 @@
1
1
  require "test_helper"
2
2
 
3
+ class ParallelizedNonTransactionalFixturelessTestCase < ActiveSupport::TestCase
4
+ parallelize(workers: :number_of_processors)
5
+ self.use_transactional_tests = false
6
+
7
+ def setup
8
+ TestData.load
9
+ end
10
+
11
+ def teardown
12
+ TestData.rollback
13
+ end
14
+ end
15
+
3
16
  class ParallelBoopsWithoutFixturesTest < ParallelizedNonTransactionalFixturelessTestCase
4
17
  100.times do |i|
5
18
  test "that boops don't change ##{i}" do
@@ -0,0 +1,25 @@
1
+ require "test_helper"
2
+
3
+ class TransactionCommittingTestCase < ActiveSupport::TestCase
4
+ self.use_transactional_tests = false
5
+
6
+ def setup
7
+ Noncommittal.stop!
8
+ TestData.load(transactions: false)
9
+ end
10
+
11
+ def teardown
12
+ Boop.delete_all
13
+ Noncommittal.start!
14
+ end
15
+ end
16
+
17
+ class TransactionCommittingBoopsTest < TransactionCommittingTestCase
18
+ def test_finds_the_boops
19
+ assert_equal 15, Boop.count
20
+ end
21
+
22
+ def test_finds_the_boops_via_another_process
23
+ assert_equal 15, `RAILS_ENV=test bin/rails runner "puts Boop.count"`.chomp.to_i
24
+ end
25
+ end
@@ -2,37 +2,14 @@ ENV["RAILS_ENV"] ||= "test"
2
2
  require_relative "../config/environment"
3
3
  require "rails/test_help"
4
4
 
5
+ Noncommittal.start!
6
+
5
7
  class SerializedNonTransactionalTestCase < ActiveSupport::TestCase
6
8
  parallelize(workers: 1)
7
9
  self.use_transactional_tests = false
8
10
 
9
11
  def setup
10
- TestData.load_data_dump
11
- end
12
-
13
- def teardown
14
- TestData.rollback
15
- end
16
- end
17
-
18
- class ParallelizedTransactionalFixturefullTestCase < ActiveSupport::TestCase
19
- parallelize(workers: :number_of_processors)
20
- self.use_transactional_tests = true
21
- fixtures :all
22
-
23
- def setup
24
- TestData.load_data_dump
25
- end
26
-
27
- # use_transactional_tests will cause a single rollback on teardown
28
- end
29
-
30
- class ParallelizedNonTransactionalFixturelessTestCase < ActiveSupport::TestCase
31
- parallelize(workers: :number_of_processors)
32
- self.use_transactional_tests = false
33
-
34
- def setup
35
- TestData.load_data_dump
12
+ TestData.load
36
13
  end
37
14
 
38
15
  def teardown
@@ -6,7 +6,7 @@ module TestData
6
6
 
7
7
  def call
8
8
  if Configurators::DatabaseYaml.new.verify.looks_good?
9
- warn "'test_data' section already defined in config/database.yml"
9
+ TestData.log.info "'test_data' section already defined in config/database.yml"
10
10
  else
11
11
  app_name = Rails.application.railtie_name.chomp("_application")
12
12
  inject_into_file "config/database.yml", before: BEFORE_TEST_DATABASE_STANZA_REGEX do
@@ -6,20 +6,6 @@ module TestData
6
6
  create_file "config/environments/test_data.rb", <<~RUBY
7
7
  require_relative "development"
8
8
 
9
- TestData.config do |config|
10
- # Where to store SQL dumps of the test_data database schema
11
- # config.schema_dump_path = "test/support/test_data/schema.sql"
12
-
13
- # Where to store SQL dumps of the test_data database test data
14
- # config.data_dump_path = "test/support/test_data/data.sql"
15
-
16
- # Where to store SQL dumps of the test_data database non-test data
17
- # config.non_test_data_dump_path = "test/support/test_data/non_test_data.sql"
18
-
19
- # Tables whose data shouldn't be loaded into tests
20
- # config.non_test_data_tables = ["ar_internal_metadata", "schema_migrations"]
21
- end
22
-
23
9
  Rails.application.configure do
24
10
  config.active_record.dump_schema_after_migration = false
25
11
  end
@@ -0,0 +1,38 @@
1
+ require "rails/generators"
2
+
3
+ module TestData
4
+ class InitializerGenerator < Rails::Generators::Base
5
+ def call
6
+ create_file "config/initializers/test_data.rb", <<~RUBY
7
+ return unless defined?(TestData)
8
+
9
+ TestData.config do |config|
10
+ # Where to store SQL dumps of the test_data database schema
11
+ # config.schema_dump_path = "test/support/test_data/schema.sql"
12
+
13
+ # Where to store SQL dumps of the test_data database test data
14
+ # config.data_dump_path = "test/support/test_data/data.sql"
15
+
16
+ # Where to store SQL dumps of the test_data database non-test data
17
+ # config.non_test_data_dump_path = "test/support/test_data/non_test_data.sql"
18
+
19
+ # Tables whose data shouldn't be loaded into tests.
20
+ # ("ar_internal_metadata" and "schema_migrations" are always excluded)
21
+ # config.non_test_data_tables = []
22
+
23
+ # Tables whose data should be excluded from SQL dumps (still dumps their schema DDL)
24
+ # config.dont_dump_these_tables = []
25
+
26
+ # Tables whose data should be truncated by TestData.truncate
27
+ # If left as `nil`, all tables inserted into by the SQL file at
28
+ # `data_dump_path` will be truncated
29
+ # config.truncate_these_test_data_tables = nil
30
+
31
+ # Log level (valid values: [:debug, :info, :warn, :error, :quiet])
32
+ # Can also be set with env var TEST_DATA_LOG_LEVEL
33
+ # config.log_level = :info
34
+ end
35
+ RUBY
36
+ end
37
+ end
38
+ end
@@ -7,7 +7,7 @@ module TestData
7
7
 
8
8
  def call
9
9
  if Configurators::WebpackerYaml.new.verify.looks_good?
10
- warn "'test_data' section not needed in config/webpacker.yml"
10
+ TestData.log.debug "'test_data' section not needed in config/webpacker.yml"
11
11
  else
12
12
  inject_into_file "config/webpacker.yml", after: AFTER_DEVELOPMENT_WEBPACK_STANZA_REGEX do
13
13
  " &development"
data/lib/test_data.rb CHANGED
@@ -3,6 +3,7 @@ require_relative "test_data/config"
3
3
  require_relative "test_data/configuration_verification"
4
4
  require_relative "test_data/configurators"
5
5
  require_relative "test_data/configurators/environment_file"
6
+ require_relative "test_data/configurators/initializer"
6
7
  require_relative "test_data/configurators/database_yaml"
7
8
  require_relative "test_data/configurators/webpacker_yaml"
8
9
  require_relative "test_data/detects_database_emptiness"
@@ -10,11 +11,15 @@ require_relative "test_data/dumps_database"
10
11
  require_relative "test_data/error"
11
12
  require_relative "test_data/installs_configuration"
12
13
  require_relative "test_data/loads_database_dumps"
14
+ require_relative "test_data/log"
13
15
  require_relative "test_data/railtie"
16
+ require_relative "test_data/save_point"
17
+ require_relative "test_data/statistics"
14
18
  require_relative "test_data/transactional_data_loader"
15
19
  require_relative "test_data/verifies_configuration"
16
20
  require_relative "test_data/verifies_dumps_are_loadable"
17
21
  require_relative "test_data/version"
18
22
  require_relative "generators/test_data/environment_file_generator"
23
+ require_relative "generators/test_data/initializer_generator"
19
24
  require_relative "generators/test_data/database_yaml_generator"
20
25
  require_relative "generators/test_data/webpacker_yaml_generator"
@@ -18,7 +18,28 @@ module TestData
18
18
  attr_accessor :non_test_data_dump_path
19
19
 
20
20
  # Tables to exclude from test data dumps
21
- attr_accessor :non_test_data_tables
21
+ attr_writer :non_test_data_tables
22
+ def non_test_data_tables
23
+ (@non_test_data_tables + [
24
+ ActiveRecord::Base.connection.schema_migration.table_name,
25
+ ActiveRecord::InternalMetadata.table_name
26
+ ]).uniq
27
+ end
28
+
29
+ # Tables to exclude from all dumps
30
+ attr_accessor :dont_dump_these_tables
31
+
32
+ # Tables to truncate when TestData.truncate is called
33
+ attr_accessor :truncate_these_test_data_tables
34
+
35
+ # Log level (valid values: [:debug, :info, :warn, :error, :quiet])
36
+ def log_level
37
+ TestData.log.level
38
+ end
39
+
40
+ def log_level=(level)
41
+ TestData.log.level = level
42
+ end
22
43
 
23
44
  attr_reader :pwd, :database_yaml_path
24
45
 
@@ -38,7 +59,9 @@ module TestData
38
59
  @data_dump_path = "test/support/test_data/data.sql"
39
60
  @non_test_data_dump_path = "test/support/test_data/non_test_data.sql"
40
61
  @database_yaml_path = "config/database.yml"
41
- @non_test_data_tables = ["ar_internal_metadata", "schema_migrations"]
62
+ @non_test_data_tables = []
63
+ @dont_dump_these_tables = []
64
+ @truncate_these_test_data_tables = nil
42
65
  end
43
66
 
44
67
  def database_yaml
@@ -3,6 +3,7 @@ module TestData
3
3
  def self.all
4
4
  [
5
5
  EnvironmentFile,
6
+ Initializer,
6
7
  DatabaseYaml,
7
8
  WebpackerYaml
8
9
  ].map(&:new)
@@ -0,0 +1,25 @@
1
+ module TestData
2
+ module Configurators
3
+ class Initializer
4
+ def initialize
5
+ @generator = InitializerGenerator.new
6
+ @config = TestData.config
7
+ end
8
+
9
+ def verify
10
+ pathname = Pathname.new("#{@config.pwd}/config/initializers/test_data.rb")
11
+ if pathname.readable?
12
+ ConfigurationVerification.new(looks_good?: true)
13
+ else
14
+ ConfigurationVerification.new(problems: [
15
+ "'#{pathname}' is not readable"
16
+ ])
17
+ end
18
+ end
19
+
20
+ def configure
21
+ @generator.call
22
+ end
23
+ end
24
+ end
25
+ end
@@ -1,5 +1,6 @@
1
1
  require "pathname"
2
2
  require "fileutils"
3
+ require "open3"
3
4
 
4
5
  module TestData
5
6
  class DumpsDatabase
@@ -21,7 +22,7 @@ module TestData
21
22
  database_name: @config.database_name,
22
23
  relative_path: @config.data_dump_path,
23
24
  full_path: @config.data_dump_full_path,
24
- flags: @config.non_test_data_tables.map { |t| "-T #{t}" }.join(" ")
25
+ flags: (@config.non_test_data_tables + @config.dont_dump_these_tables).uniq.map { |t| "-T #{t} -T #{t}_id_seq" }.join(" ")
25
26
  )
26
27
 
27
28
  dump(
@@ -30,7 +31,7 @@ module TestData
30
31
  database_name: @config.database_name,
31
32
  relative_path: @config.non_test_data_dump_path,
32
33
  full_path: @config.non_test_data_dump_full_path,
33
- flags: @config.non_test_data_tables.map { |t| "-t #{t}" }.join(" ")
34
+ flags: (@config.non_test_data_tables - @config.dont_dump_these_tables).uniq.map { |t| "-t #{t}" }.join(" ")
34
35
  )
35
36
  end
36
37
 
@@ -39,11 +40,37 @@ module TestData
39
40
  def dump(type:, database_name:, relative_path:, full_path:, name: type, flags: "")
40
41
  dump_pathname = Pathname.new(full_path)
41
42
  FileUtils.mkdir_p(File.dirname(dump_pathname))
42
- if system "pg_dump #{database_name} --no-tablespaces --no-owner --inserts --#{type}-only #{flags} -f #{dump_pathname}"
43
- puts "Dumped database '#{database_name}' #{name} to '#{relative_path}'"
43
+ if execute("pg_dump #{database_name} --no-tablespaces --no-owner --inserts --#{type}-only #{flags} -f #{dump_pathname}")
44
+ prepend_set_replication_role!(full_path) if type == :data
45
+
46
+ TestData.log.info "Dumped '#{database_name}' #{name} to '#{relative_path}'"
44
47
  else
45
48
  raise "Failed while attempting to dump '#{database_name}' #{name} to '#{relative_path}'"
46
49
  end
47
50
  end
51
+
52
+ def execute(command)
53
+ TestData.log.debug("Running SQL dump command:\n #{command}")
54
+ stdout, stderr, status = Open3.capture3(command)
55
+ if status == 0
56
+ TestData.log.debug(stdout)
57
+ TestData.log.debug(stderr)
58
+ true
59
+ else
60
+ TestData.log.info(stdout)
61
+ TestData.log.error(stderr)
62
+ false
63
+ end
64
+ end
65
+
66
+ def prepend_set_replication_role!(data_dump_path)
67
+ system <<~COMMAND
68
+ ed -s #{data_dump_path} <<EOF
69
+ 1 s/^/set session_replication_role = replica;/
70
+ w
71
+ EOF
72
+ COMMAND
73
+ TestData.log.debug("Prepended replication role instruction to '#{data_dump_path}'")
74
+ end
48
75
  end
49
76
  end
@@ -16,17 +16,17 @@ module TestData
16
16
  )
17
17
 
18
18
  load_dump(
19
- name: "test data",
19
+ name: "non-test data",
20
20
  database_name: @config.database_name,
21
- relative_path: @config.data_dump_path,
22
- full_path: @config.data_dump_full_path
21
+ relative_path: @config.non_test_data_dump_path,
22
+ full_path: @config.non_test_data_dump_full_path
23
23
  )
24
24
 
25
25
  load_dump(
26
- name: "non-test data",
26
+ name: "test data",
27
27
  database_name: @config.database_name,
28
- relative_path: @config.non_test_data_dump_path,
29
- full_path: @config.non_test_data_dump_full_path
28
+ relative_path: @config.data_dump_path,
29
+ full_path: @config.data_dump_full_path
30
30
  )
31
31
  end
32
32
 
@@ -36,7 +36,7 @@ module TestData
36
36
  dump_pathname = Pathname.new(full_path)
37
37
  FileUtils.mkdir_p(File.dirname(dump_pathname))
38
38
  if system "psql -q -d #{database_name} < #{dump_pathname}"
39
- puts "Loaded #{name} from '#{relative_path}' into database '#{database_name}' "
39
+ TestData.log.info "Loaded #{name} from '#{relative_path}' into database '#{database_name}' "
40
40
  else
41
41
  raise "Failed while attempting to load #{name} from '#{relative_path}' into database '#{database_name}'"
42
42
  end
@@ -0,0 +1,58 @@
1
+ module TestData
2
+ def self.log
3
+ @log ||= Log.new
4
+ end
5
+
6
+ class Log
7
+ LEVELS = [:debug, :info, :warn, :error, :quiet]
8
+ DEFAULT_WRITER = ->(message, level) do
9
+ output = "[test_data: #{level}] #{message}"
10
+ if [:warn, :error].include?(level)
11
+ warn output
12
+ else
13
+ puts output
14
+ end
15
+ end
16
+
17
+ attr_reader :level, :writer
18
+
19
+ def initialize
20
+ reset
21
+ end
22
+
23
+ LEVELS[0...4].each do |level|
24
+ define_method level.to_s do |message|
25
+ next unless message.strip.present?
26
+
27
+ @writer.call(message, level) if enabled?(level)
28
+ end
29
+ end
30
+
31
+ def reset
32
+ self.level = ENV["TEST_DATA_LOG_LEVEL"]&.to_sym || :info
33
+ @writer = DEFAULT_WRITER
34
+ end
35
+
36
+ def level=(level)
37
+ if LEVELS.include?(level)
38
+ @level = level
39
+ else
40
+ raise Error.new("Not a valid level")
41
+ end
42
+ end
43
+
44
+ def writer=(writer)
45
+ if writer.respond_to?(:call)
46
+ @writer = writer
47
+ else
48
+ raise Error.new("Log writer must be callable")
49
+ end
50
+ end
51
+
52
+ private
53
+
54
+ def enabled?(level)
55
+ LEVELS.index(level) >= LEVELS.index(@level)
56
+ end
57
+ end
58
+ end