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.
- 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
|