test_data 0.0.1 → 0.0.2

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