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.
- checksums.yaml +4 -4
- data/.github/workflows/ruby.yml +45 -0
- data/CHANGELOG.md +8 -1
- data/Gemfile.lock +5 -3
- data/README.md +961 -17
- data/example/Gemfile +3 -0
- data/example/Gemfile.lock +32 -3
- data/example/README.md +2 -22
- data/example/config/credentials.yml.enc +2 -1
- data/example/config/database.yml +2 -0
- data/example/spec/rails_helper.rb +64 -0
- data/example/spec/requests/boops_spec.rb +21 -0
- data/example/spec/spec_helper.rb +94 -0
- data/example/test/factories.rb +4 -0
- data/example/test/integration/better_mode_switching_demo_test.rb +45 -0
- data/example/test/integration/boops_that_boop_boops_test.rb +17 -0
- data/example/test/integration/dont_dump_tables_test.rb +7 -0
- data/example/test/integration/load_rollback_truncate_test.rb +195 -0
- data/example/test/integration/mode_switching_demo_test.rb +48 -0
- data/example/test/integration/parallel_boops_with_fixtures_test.rb +14 -0
- data/example/test/integration/parallel_boops_without_fixtures_test.rb +13 -0
- data/example/test/integration/transaction_committing_boops_test.rb +25 -0
- data/example/test/test_helper.rb +3 -26
- data/lib/generators/test_data/database_yaml_generator.rb +1 -1
- data/lib/generators/test_data/environment_file_generator.rb +0 -14
- data/lib/generators/test_data/initializer_generator.rb +38 -0
- data/lib/generators/test_data/webpacker_yaml_generator.rb +1 -1
- data/lib/test_data.rb +5 -0
- data/lib/test_data/config.rb +25 -2
- data/lib/test_data/configurators.rb +1 -0
- data/lib/test_data/configurators/initializer.rb +25 -0
- data/lib/test_data/dumps_database.rb +31 -4
- data/lib/test_data/loads_database_dumps.rb +7 -7
- data/lib/test_data/log.rb +58 -0
- data/lib/test_data/rake.rb +7 -5
- data/lib/test_data/save_point.rb +34 -0
- data/lib/test_data/statistics.rb +26 -0
- data/lib/test_data/transactional_data_loader.rb +145 -32
- data/lib/test_data/verifies_dumps_are_loadable.rb +4 -4
- data/lib/test_data/version.rb +1 -1
- data/script/reset_example_app +17 -0
- data/script/test +54 -13
- 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
|
data/example/test/test_helper.rb
CHANGED
@@ -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.
|
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
|
-
|
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
|
-
|
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"
|
data/lib/test_data/config.rb
CHANGED
@@ -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
|
-
|
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 = [
|
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
|
@@ -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
|
43
|
-
|
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.
|
22
|
-
full_path: @config.
|
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: "
|
26
|
+
name: "test data",
|
27
27
|
database_name: @config.database_name,
|
28
|
-
relative_path: @config.
|
29
|
-
full_path: @config.
|
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
|
-
|
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
|