test_data 0.0.1 → 0.2.1

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 (65) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ruby.yml +41 -0
  3. data/.standard.yml +2 -0
  4. data/CHANGELOG.md +43 -0
  5. data/Gemfile.lock +17 -15
  6. data/LICENSE.txt +1 -6
  7. data/README.md +1232 -17
  8. data/example/.gitignore +1 -4
  9. data/example/Gemfile +3 -0
  10. data/example/Gemfile.lock +100 -71
  11. data/example/README.md +2 -22
  12. data/example/config/application.rb +3 -0
  13. data/example/config/credentials.yml.enc +1 -1
  14. data/example/config/database.yml +2 -0
  15. data/example/spec/rails_helper.rb +64 -0
  16. data/example/spec/requests/boops_spec.rb +17 -0
  17. data/example/spec/requests/rails_fixtures_override_spec.rb +106 -0
  18. data/example/spec/spec_helper.rb +94 -0
  19. data/example/test/factories.rb +4 -0
  20. data/example/test/integration/better_mode_switching_demo_test.rb +41 -0
  21. data/example/test/integration/boops_that_boop_boops_test.rb +17 -0
  22. data/example/test/integration/dont_dump_tables_test.rb +7 -0
  23. data/example/test/integration/load_rollback_truncate_test.rb +190 -0
  24. data/example/test/integration/mode_switching_demo_test.rb +38 -0
  25. data/example/test/integration/parallel_boops_with_fixtures_test.rb +10 -0
  26. data/example/test/integration/parallel_boops_without_fixtures_test.rb +9 -0
  27. data/example/test/integration/rails_fixtures_double_load_test.rb +10 -0
  28. data/example/test/integration/rails_fixtures_override_test.rb +110 -0
  29. data/example/test/integration/test_data_hooks_test.rb +89 -0
  30. data/example/test/integration/transaction_committing_boops_test.rb +27 -0
  31. data/example/test/test_helper.rb +4 -31
  32. data/lib/generators/test_data/cable_yaml_generator.rb +18 -0
  33. data/lib/generators/test_data/database_yaml_generator.rb +3 -4
  34. data/lib/generators/test_data/environment_file_generator.rb +7 -14
  35. data/lib/generators/test_data/initializer_generator.rb +51 -0
  36. data/lib/generators/test_data/secrets_yaml_generator.rb +19 -0
  37. data/lib/generators/test_data/webpacker_yaml_generator.rb +4 -3
  38. data/lib/test_data.rb +42 -1
  39. data/lib/test_data/active_record_ext.rb +11 -0
  40. data/lib/test_data/config.rb +57 -4
  41. data/lib/test_data/configurators.rb +3 -0
  42. data/lib/test_data/configurators/cable_yaml.rb +25 -0
  43. data/lib/test_data/configurators/environment_file.rb +3 -2
  44. data/lib/test_data/configurators/initializer.rb +26 -0
  45. data/lib/test_data/configurators/secrets_yaml.rb +25 -0
  46. data/lib/test_data/configurators/webpacker_yaml.rb +4 -3
  47. data/lib/test_data/custom_loaders/abstract_base.rb +25 -0
  48. data/lib/test_data/custom_loaders/rails_fixtures.rb +42 -0
  49. data/lib/test_data/dumps_database.rb +55 -5
  50. data/lib/test_data/generator_support.rb +3 -0
  51. data/lib/test_data/inserts_test_data.rb +25 -0
  52. data/lib/test_data/loads_database_dumps.rb +8 -8
  53. data/lib/test_data/log.rb +76 -0
  54. data/lib/test_data/manager.rb +187 -0
  55. data/lib/test_data/rake.rb +20 -9
  56. data/lib/test_data/save_point.rb +34 -0
  57. data/lib/test_data/statistics.rb +31 -0
  58. data/lib/test_data/truncates_test_data.rb +31 -0
  59. data/lib/test_data/verifies_dumps_are_loadable.rb +4 -4
  60. data/lib/test_data/version.rb +1 -1
  61. data/script/reset_example_app +18 -0
  62. data/script/test +78 -13
  63. data/test_data.gemspec +1 -1
  64. metadata +36 -4
  65. data/lib/test_data/transactional_data_loader.rb +0 -77
data/lib/test_data.rb CHANGED
@@ -1,20 +1,61 @@
1
+ require_relative "test_data/active_record_ext"
1
2
  require_relative "test_data/active_support_ext"
2
3
  require_relative "test_data/config"
3
4
  require_relative "test_data/configuration_verification"
4
5
  require_relative "test_data/configurators"
5
6
  require_relative "test_data/configurators/environment_file"
7
+ require_relative "test_data/configurators/initializer"
8
+ require_relative "test_data/configurators/cable_yaml"
6
9
  require_relative "test_data/configurators/database_yaml"
10
+ require_relative "test_data/configurators/secrets_yaml"
7
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"
8
14
  require_relative "test_data/detects_database_emptiness"
9
15
  require_relative "test_data/dumps_database"
10
16
  require_relative "test_data/error"
17
+ require_relative "test_data/inserts_test_data"
11
18
  require_relative "test_data/installs_configuration"
12
19
  require_relative "test_data/loads_database_dumps"
20
+ require_relative "test_data/log"
13
21
  require_relative "test_data/railtie"
14
- require_relative "test_data/transactional_data_loader"
22
+ require_relative "test_data/save_point"
23
+ require_relative "test_data/statistics"
24
+ require_relative "test_data/manager"
25
+ require_relative "test_data/truncates_test_data"
15
26
  require_relative "test_data/verifies_configuration"
16
27
  require_relative "test_data/verifies_dumps_are_loadable"
17
28
  require_relative "test_data/version"
18
29
  require_relative "generators/test_data/environment_file_generator"
30
+ require_relative "generators/test_data/initializer_generator"
31
+ require_relative "generators/test_data/cable_yaml_generator"
19
32
  require_relative "generators/test_data/database_yaml_generator"
33
+ require_relative "generators/test_data/secrets_yaml_generator"
20
34
  require_relative "generators/test_data/webpacker_yaml_generator"
35
+
36
+ module TestData
37
+ def self.uninitialize
38
+ @manager ||= Manager.new
39
+ @manager.rollback_to_before_data_load
40
+ end
41
+
42
+ def self.uses_test_data
43
+ @manager ||= Manager.new
44
+ @manager.load
45
+ end
46
+
47
+ def self.uses_clean_slate
48
+ @manager ||= Manager.new
49
+ @manager.truncate
50
+ end
51
+
52
+ def self.uses_rails_fixtures(test_instance)
53
+ @rails_fixtures_loader ||= CustomLoaders::RailsFixtures.new
54
+ @manager ||= Manager.new
55
+ @manager.load_custom_data(@rails_fixtures_loader, test_instance: test_instance)
56
+ end
57
+
58
+ def self.insert_test_data_dump
59
+ InsertsTestData.new.call
60
+ end
61
+ end
@@ -0,0 +1,11 @@
1
+ module TestData
2
+ def self.prevent_rails_fixtures_from_loading_automatically!
3
+ ActiveRecord::TestFixtures.define_method(:__test_data_gem_setup_fixtures,
4
+ ActiveRecord::TestFixtures.instance_method(:setup_fixtures))
5
+ ActiveRecord::TestFixtures.remove_method(:setup_fixtures)
6
+ ActiveRecord::TestFixtures.define_method(:setup_fixtures, ->(config = nil) {})
7
+
8
+ ActiveRecord::TestFixtures.remove_method(:teardown_fixtures)
9
+ ActiveRecord::TestFixtures.define_method(:teardown_fixtures, -> {})
10
+ end
11
+ end
@@ -18,9 +18,31 @@ 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.uses_clean_slate 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
- attr_reader :pwd, :database_yaml_path
44
+ attr_reader :pwd, :cable_yaml_path, :database_yaml_path, :secrets_yaml_path,
45
+ :after_test_data_load_hook, :after_test_data_truncate_hook, :after_rails_fixture_load_hook
24
46
 
25
47
  def self.full_path_reader(*relative_path_readers)
26
48
  relative_path_readers.each do |relative_path_reader|
@@ -30,15 +52,46 @@ module TestData
30
52
  end
31
53
  end
32
54
 
33
- full_path_reader :schema_dump_path, :data_dump_path, :non_test_data_dump_path, :database_yaml_path
55
+ full_path_reader :schema_dump_path, :data_dump_path, :non_test_data_dump_path, :cable_yaml_path, :database_yaml_path, :secrets_yaml_path
34
56
 
35
57
  def initialize(pwd:)
36
58
  @pwd = pwd
37
59
  @schema_dump_path = "test/support/test_data/schema.sql"
38
60
  @data_dump_path = "test/support/test_data/data.sql"
39
61
  @non_test_data_dump_path = "test/support/test_data/non_test_data.sql"
62
+ @cable_yaml_path = "config/cable.yml"
40
63
  @database_yaml_path = "config/database.yml"
41
- @non_test_data_tables = ["ar_internal_metadata", "schema_migrations"]
64
+ @secrets_yaml_path = "config/secrets.yml"
65
+ @non_test_data_tables = []
66
+ @dont_dump_these_tables = []
67
+ @truncate_these_test_data_tables = nil
68
+ @after_test_data_load_hook = -> {}
69
+ @after_test_data_truncate_hook = -> {}
70
+ @after_rails_fixture_load_hook = -> {}
71
+ end
72
+
73
+ def after_test_data_load(callable = nil, &blk)
74
+ hook = callable || blk
75
+ if !hook.respond_to?(:call)
76
+ raise Error.new("after_test_data_load must be passed a callable (e.g. a Proc) or called with a block")
77
+ end
78
+ @after_test_data_load_hook = hook
79
+ end
80
+
81
+ def after_test_data_truncate(callable = nil, &blk)
82
+ hook = callable || blk
83
+ if !hook.respond_to?(:call)
84
+ raise Error.new("after_test_data_truncate must be passed a callable (e.g. a Proc) or called with a block")
85
+ end
86
+ @after_test_data_truncate_hook = hook
87
+ end
88
+
89
+ def after_rails_fixture_load(callable = nil, &blk)
90
+ hook = callable || blk
91
+ if !hook.respond_to?(:call)
92
+ raise Error.new("after_rails_fixture_load must be passed a callable (e.g. a Proc) or called with a block")
93
+ end
94
+ @after_rails_fixture_load_hook = hook
42
95
  end
43
96
 
44
97
  def database_yaml
@@ -3,7 +3,10 @@ module TestData
3
3
  def self.all
4
4
  [
5
5
  EnvironmentFile,
6
+ Initializer,
7
+ CableYaml,
6
8
  DatabaseYaml,
9
+ SecretsYaml,
7
10
  WebpackerYaml
8
11
  ].map(&:new)
9
12
  end
@@ -0,0 +1,25 @@
1
+ module TestData
2
+ module Configurators
3
+ class CableYaml
4
+ def initialize
5
+ @generator = CableYamlGenerator.new
6
+ @config = TestData.config
7
+ end
8
+
9
+ def verify
10
+ if !File.exist?(@config.cable_yaml_full_path) ||
11
+ YAML.load_file(@config.cable_yaml_full_path).key?("test_data")
12
+ ConfigurationVerification.new(looks_good?: true)
13
+ else
14
+ ConfigurationVerification.new(problems: [
15
+ "'#{@config.cable_yaml_path}' exists but does not contain a 'test_data' section"
16
+ ])
17
+ end
18
+ end
19
+
20
+ def configure
21
+ @generator.call
22
+ end
23
+ end
24
+ end
25
+ end
@@ -7,12 +7,13 @@ module TestData
7
7
  end
8
8
 
9
9
  def verify
10
- pathname = Pathname.new("#{@config.pwd}/config/environments/test_data.rb")
10
+ path = "config/environments/test_data.rb"
11
+ pathname = Pathname.new("#{@config.pwd}/#{path}")
11
12
  if pathname.readable?
12
13
  ConfigurationVerification.new(looks_good?: true)
13
14
  else
14
15
  ConfigurationVerification.new(problems: [
15
- "'#{pathname}' is not readable"
16
+ "'#{path}' is not readable"
16
17
  ])
17
18
  end
18
19
  end
@@ -0,0 +1,26 @@
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
+ path = "config/initializers/test_data.rb"
11
+ pathname = Pathname.new("#{@config.pwd}/#{path}")
12
+ if pathname.readable?
13
+ ConfigurationVerification.new(looks_good?: true)
14
+ else
15
+ ConfigurationVerification.new(problems: [
16
+ "'#{path}' is not readable"
17
+ ])
18
+ end
19
+ end
20
+
21
+ def configure
22
+ @generator.call
23
+ end
24
+ end
25
+ end
26
+ end
@@ -0,0 +1,25 @@
1
+ module TestData
2
+ module Configurators
3
+ class SecretsYaml
4
+ def initialize
5
+ @generator = SecretsYamlGenerator.new
6
+ @config = TestData.config
7
+ end
8
+
9
+ def verify
10
+ if !File.exist?(@config.secrets_yaml_full_path) ||
11
+ YAML.load_file(@config.secrets_yaml_full_path).key?("test_data")
12
+ ConfigurationVerification.new(looks_good?: true)
13
+ else
14
+ ConfigurationVerification.new(problems: [
15
+ "'#{@config.secrets_yaml_path}' exists but does not contain a 'test_data' section"
16
+ ])
17
+ end
18
+ end
19
+
20
+ def configure
21
+ @generator.call
22
+ end
23
+ end
24
+ end
25
+ end
@@ -7,16 +7,17 @@ module TestData
7
7
  end
8
8
 
9
9
  def verify
10
- pathname = Pathname.new("#{@config.pwd}/config/webpacker.yml")
10
+ path = "config/webpacker.yml"
11
+ pathname = Pathname.new("#{@config.pwd}/#{path}")
11
12
  return ConfigurationVerification.new(looks_good?: true) unless pathname.readable?
12
13
  yaml = load_yaml(pathname)
13
14
  if yaml.nil?
14
15
  ConfigurationVerification.new(problems: [
15
- "'#{pathname}' is not valid YAML"
16
+ "'#{path}' is not valid YAML"
16
17
  ])
17
18
  elsif !yaml.key?("test_data")
18
19
  ConfigurationVerification.new(problems: [
19
- "'#{pathname}' does not contain a 'test_data' section"
20
+ "'#{path}' does not contain a 'test_data' section"
20
21
  ])
21
22
  else
22
23
  ConfigurationVerification.new(looks_good?: true)
@@ -0,0 +1,25 @@
1
+ module TestData
2
+ module CustomLoaders
3
+ class AbstractBase
4
+ def name
5
+ raise Error.new("#name must be defined by CustomLoader subclass")
6
+ end
7
+
8
+ def load_requested(**options)
9
+ end
10
+
11
+ def loaded?(**options)
12
+ # Check to see if the requested data is already loaded (if possible and
13
+ # detectable)
14
+ #
15
+ # Return true to prevent #load from being called, potentially avoiding an
16
+ # expensive operation
17
+ false
18
+ end
19
+
20
+ def load(**options)
21
+ raise Error.new("#load must be defined by CustomLoader subclass")
22
+ end
23
+ end
24
+ end
25
+ end
@@ -0,0 +1,42 @@
1
+ module TestData
2
+ module CustomLoaders
3
+ class RailsFixtures < AbstractBase
4
+ def initialize
5
+ @config = TestData.config
6
+ @statistics = TestData.statistics
7
+ @already_loaded_rails_fixtures = {}
8
+ end
9
+
10
+ def name
11
+ :rails_fixtures
12
+ end
13
+
14
+ def validate!(test_instance:)
15
+ if !test_instance.respond_to?(:setup_fixtures)
16
+ raise Error.new("'TestData.uses_rails_fixtures(self)' must be passed a test instance that has had ActiveRecord::TestFixtures mixed-in (e.g. `TestData.uses_rails_fixtures(self)` in an ActiveSupport::TestCase `setup` block), but the provided argument does not respond to 'setup_fixtures'")
17
+ elsif !test_instance.respond_to?(:__test_data_gem_setup_fixtures)
18
+ raise Error.new("'TestData.uses_rails_fixtures(self)' depends on Rails' default fixture-loading behavior being disabled by calling 'TestData.prevent_rails_fixtures_from_loading_automatically!' as early as possible (e.g. near the top of your test_helper.rb), but it looks like it was never called.")
19
+ end
20
+ end
21
+
22
+ def load_requested(test_instance:)
23
+ ActiveRecord::FixtureSet.reset_cache
24
+ test_instance.instance_variable_set(:@loaded_fixtures, @already_loaded_rails_fixtures[test_instance.class])
25
+ test_instance.instance_variable_set(:@fixture_cache, {})
26
+ end
27
+
28
+ def loaded?(test_instance:)
29
+ @already_loaded_rails_fixtures[test_instance.class].present?
30
+ end
31
+
32
+ def load(test_instance:)
33
+ test_instance.pre_loaded_fixtures = false
34
+ test_instance.use_transactional_tests = false
35
+ test_instance.__test_data_gem_setup_fixtures
36
+ @already_loaded_rails_fixtures[test_instance.class] = test_instance.instance_variable_get(:@loaded_fixtures)
37
+ @statistics.count_load_rails_fixtures!
38
+ @config.after_rails_fixture_load_hook.call
39
+ end
40
+ end
41
+ end
42
+ 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,60 @@ 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
+ before_size = File.size?(dump_pathname)
44
+ if execute("pg_dump #{database_name} --no-tablespaces --no-owner --inserts --#{type}-only #{flags} -f #{dump_pathname}")
45
+ prepend_set_replication_role!(full_path) if type == :data
46
+
47
+ TestData.log.info "Dumped '#{database_name}' #{name} to '#{relative_path}'"
48
+ log_size_info_and_warnings(before_size: before_size, after_size: File.size(dump_pathname))
49
+ else
50
+ raise Error.new("Failed while attempting to dump '#{database_name}' #{name} to '#{relative_path}'")
51
+ end
52
+ end
53
+
54
+ def execute(command)
55
+ TestData.log.debug("Running SQL dump command:\n #{command}")
56
+ stdout, stderr, status = Open3.capture3(command)
57
+ if status == 0
58
+ TestData.log.debug(stdout)
59
+ TestData.log.debug(stderr)
60
+ true
44
61
  else
45
- raise "Failed while attempting to dump '#{database_name}' #{name} to '#{relative_path}'"
62
+ TestData.log.info(stdout)
63
+ TestData.log.error(stderr)
64
+ false
46
65
  end
47
66
  end
67
+
68
+ def prepend_set_replication_role!(data_dump_path)
69
+ system <<~COMMAND
70
+ ed -s #{data_dump_path} <<EOF
71
+ 1 s/^/set session_replication_role = replica;/
72
+ w
73
+ EOF
74
+ COMMAND
75
+ TestData.log.debug("Prepended replication role instruction to '#{data_dump_path}'")
76
+ end
77
+
78
+ def log_size_info_and_warnings(before_size:, after_size:)
79
+ percent_change = percent_change(before_size, after_size)
80
+ TestData.log.info " Size: #{to_size(after_size)}#{" (#{percent_change}% #{before_size > after_size ? "decrease" : "increase"})" if percent_change}"
81
+ if after_size > 5242880
82
+ TestData.log.warn " WARNING: file size exceeds 5MB. Be sure to only persist what data you need to sufficiently test your application"
83
+ end
84
+ if before_size && (after_size - before_size) > 1048576
85
+ TestData.log.warn " WARNING: size of this dump increased by #{to_size(after_size - before_size)}. You may want to inspect the file to validate extraneous data was not committed"
86
+ end
87
+ end
88
+
89
+ def percent_change(before_size, after_size)
90
+ return unless before_size && before_size > 0 && after_size
91
+ ((before_size - after_size).abs / before_size * 100).round(2)
92
+ end
93
+
94
+ def to_size(bytes)
95
+ e = Math.log10(bytes).to_i / 3
96
+ "%.0f" % (bytes / 1000**e) + [" bytes", "KB", "MB", "GB"][e]
97
+ end
48
98
  end
49
99
  end