test-prof 0.12.0 → 1.0.7
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/CHANGELOG.md +155 -463
- data/README.md +9 -9
- data/config/default.yml +0 -15
- data/config/rubocop-rspec.yml +6 -0
- data/lib/test_prof/any_fixture/dump/base_adapter.rb +43 -0
- data/lib/test_prof/any_fixture/dump/digest.rb +29 -0
- data/lib/test_prof/any_fixture/dump/postgresql.rb +91 -0
- data/lib/test_prof/any_fixture/dump/sqlite.rb +42 -0
- data/lib/test_prof/any_fixture/dump.rb +212 -0
- data/lib/test_prof/any_fixture.rb +117 -8
- data/lib/test_prof/before_all/adapters/active_record.rb +15 -5
- data/lib/test_prof/before_all.rb +11 -2
- data/lib/test_prof/cops/rspec/aggregate_examples/its.rb +1 -1
- data/lib/test_prof/cops/rspec/aggregate_examples/line_range_helpers.rb +1 -1
- data/lib/test_prof/cops/rspec/aggregate_examples/matchers_with_side_effects.rb +1 -1
- data/lib/test_prof/cops/rspec/aggregate_examples/metadata_helpers.rb +1 -1
- data/lib/test_prof/cops/rspec/aggregate_examples/node_matchers.rb +1 -1
- data/lib/test_prof/cops/rspec/aggregate_examples.rb +1 -1
- data/lib/test_prof/event_prof/custom_events.rb +1 -1
- data/lib/test_prof/event_prof/instrumentations/active_support.rb +1 -1
- data/lib/test_prof/event_prof/profiler.rb +3 -4
- data/lib/test_prof/factory_prof/nate_heckler.rb +16 -0
- data/lib/test_prof/factory_prof/printers/flamegraph.rb +1 -1
- data/lib/test_prof/factory_prof/printers/nate_heckler.rb +26 -0
- data/lib/test_prof/factory_prof/printers/simple.rb +6 -2
- data/lib/test_prof/factory_prof.rb +24 -4
- data/lib/test_prof/logging.rb +18 -12
- data/lib/test_prof/recipes/logging.rb +1 -1
- data/lib/test_prof/recipes/minitest/before_all.rb +126 -24
- data/lib/test_prof/recipes/rspec/any_fixture.rb +1 -1
- data/lib/test_prof/recipes/rspec/before_all.rb +11 -3
- data/lib/test_prof/recipes/rspec/let_it_be.rb +6 -8
- data/lib/test_prof/rspec_dissect.rb +2 -2
- data/lib/test_prof/rspec_stamp.rb +1 -1
- data/lib/test_prof/rubocop.rb +0 -1
- data/lib/test_prof/ruby_prof.rb +11 -9
- data/lib/test_prof/tag_prof/result.rb +1 -1
- data/lib/test_prof/tag_prof/rspec.rb +3 -5
- data/lib/test_prof/utils/sized_ordered_set.rb +2 -2
- data/lib/test_prof/version.rb +1 -1
- data/lib/test_prof.rb +17 -4
- metadata +19 -14
- data/lib/test_prof/cops/rspec/aggregate_failures.rb +0 -26
- data/lib/test_prof/ext/active_record_3.rb +0 -27
- data/lib/test_prof/recipes/active_record_one_love.rb +0 -6
- data/lib/test_prof/recipes/active_record_shared_connection.rb +0 -77
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
[](https://cultofmartians.com)
|
2
|
-
[](https://rubygems.org/gems/test-prof) [](https://rubygems.org/gems/test-prof) [](https://github.com/test-prof/test-prof/actions)
|
3
|
+
[](https://github.com/test-prof/test-prof/actions)
|
4
|
+
[](https://www.codetriage.com/test-prof/test-prof)
|
5
5
|
[](https://test-prof.evilmartians.io)
|
6
6
|
|
7
7
|
# Ruby Tests Profiling Toolbox
|
@@ -32,7 +32,7 @@ TestProf toolbox aims to help you identify bottlenecks in your test suite. It co
|
|
32
32
|
📑 [Documentation](https://test-prof.evilmartians.io)
|
33
33
|
|
34
34
|
<p align="center">
|
35
|
-
<a href="http://bit.ly/test-prof-map">
|
35
|
+
<a href="http://bit.ly/test-prof-map-v1">
|
36
36
|
<img src="./docs/assets/images/coggle.png" alt="TestProf map" width="738">
|
37
37
|
</a>
|
38
38
|
</p>
|
@@ -51,7 +51,7 @@ TestProf toolbox aims to help you identify bottlenecks in your test suite. It co
|
|
51
51
|
- [CodeTriage](https://github.com/codetriage/codetriage)
|
52
52
|
- [Dev.to](https://github.com/thepracticaldev/dev.to)
|
53
53
|
- [Open Project](https://github.com/opf/openproject)
|
54
|
-
- [...and others](https://github.com/
|
54
|
+
- [...and others](https://github.com/test-prof/test-prof/issues/73)
|
55
55
|
|
56
56
|
## Resources
|
57
57
|
|
@@ -75,7 +75,7 @@ Add `test-prof` gem to your application:
|
|
75
75
|
|
76
76
|
```ruby
|
77
77
|
group :test do
|
78
|
-
gem "test-prof"
|
78
|
+
gem "test-prof", "~> 1.0"
|
79
79
|
end
|
80
80
|
```
|
81
81
|
|
@@ -83,7 +83,7 @@ And that's it)
|
|
83
83
|
|
84
84
|
Supported Ruby versions:
|
85
85
|
|
86
|
-
- Ruby (MRI) >= 2.
|
86
|
+
- Ruby (MRI) >= 2.5.0 (**NOTE:** for Ruby 2.2 use TestProf < 0.7.0, Ruby 2.3 use TestProf ~> 0.7.0, Ruby 2.4 use TestProf <0.12.0)
|
87
87
|
|
88
88
|
- JRuby >= 9.1.0.0 (**NOTE:** refinements-dependent features might require 9.2.7+)
|
89
89
|
|
@@ -95,9 +95,9 @@ Check out our [docs][].
|
|
95
95
|
|
96
96
|
## What's next?
|
97
97
|
|
98
|
-
Have an idea? [Propose](https://github.com/
|
98
|
+
Have an idea? [Propose](https://github.com/test-prof/test-prof/issues/new) a feature request!
|
99
99
|
|
100
|
-
Already using TestProf? [Share your story!](https://github.com/
|
100
|
+
Already using TestProf? [Share your story!](https://github.com/test-prof/test-prof/issues/73)
|
101
101
|
|
102
102
|
## License
|
103
103
|
|
data/config/default.yml
CHANGED
@@ -18,18 +18,3 @@ RSpec/AggregateExamples:
|
|
18
18
|
- validate_length_of
|
19
19
|
- validate_inclusion_of
|
20
20
|
- validates_exclusion_of
|
21
|
-
|
22
|
-
# TODO: remove this one we hit 1.0
|
23
|
-
RSpec/AggregateFailures:
|
24
|
-
Description: Checks if example group contains two or more aggregatable examples.
|
25
|
-
Enabled: false
|
26
|
-
StyleGuide: https://rspec.rubystyle.guide/#expectation-per-example
|
27
|
-
AddAggregateFailuresMetadata: true
|
28
|
-
MatchersWithSideEffects:
|
29
|
-
- allow_value
|
30
|
-
- allow_values
|
31
|
-
- validate_presence_of
|
32
|
-
- validate_absence_of
|
33
|
-
- validate_length_of
|
34
|
-
- validate_inclusion_of
|
35
|
-
- validates_exclusion_of
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module TestProf
|
4
|
+
module AnyFixture
|
5
|
+
class Dump
|
6
|
+
class BaseAdapter
|
7
|
+
def reset_sequence!(_table_name, _start)
|
8
|
+
end
|
9
|
+
|
10
|
+
def compile_sql(sql, _binds)
|
11
|
+
sql
|
12
|
+
end
|
13
|
+
|
14
|
+
def setup_env
|
15
|
+
end
|
16
|
+
|
17
|
+
def teardown_env
|
18
|
+
end
|
19
|
+
|
20
|
+
def import(_path)
|
21
|
+
false
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
|
26
|
+
def while_disconnected
|
27
|
+
conn.disconnect!
|
28
|
+
yield
|
29
|
+
ensure
|
30
|
+
conn.reconnect!
|
31
|
+
end
|
32
|
+
|
33
|
+
def conn
|
34
|
+
ActiveRecord::Base.connection
|
35
|
+
end
|
36
|
+
|
37
|
+
def execute(query)
|
38
|
+
conn.execute(query)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "digest/sha1"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module AnyFixture
|
7
|
+
class Dump
|
8
|
+
module Digest
|
9
|
+
module_function
|
10
|
+
|
11
|
+
def call(*paths)
|
12
|
+
files = (AnyFixture.config.default_dump_watch_paths + paths).each_with_object([]) do |path_or_glob, acc|
|
13
|
+
if File.file?(path_or_glob)
|
14
|
+
acc << path_or_glob
|
15
|
+
else
|
16
|
+
acc = acc.concat Dir[path_or_glob]
|
17
|
+
end
|
18
|
+
acc
|
19
|
+
end
|
20
|
+
|
21
|
+
return if files.empty?
|
22
|
+
|
23
|
+
file_ids = files.sort.map { |f| "#{File.basename(f)}/#{::Digest::SHA1.file(f).hexdigest}" }
|
24
|
+
::Digest::SHA1.hexdigest(file_ids.join("/"))
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
@@ -0,0 +1,91 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/any_fixture/dump/base_adapter"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module AnyFixture
|
7
|
+
class Dump
|
8
|
+
class PostgreSQL < BaseAdapter
|
9
|
+
UUID_FUNCTIONS = %w[
|
10
|
+
gen_random_uuid
|
11
|
+
uuid_generate_v4
|
12
|
+
]
|
13
|
+
|
14
|
+
def reset_sequence!(table_name, start)
|
15
|
+
_pk, sequence = conn.pk_and_sequence_for(table_name)
|
16
|
+
return unless sequence
|
17
|
+
|
18
|
+
sequence_name = "#{sequence.schema}.#{sequence.identifier}"
|
19
|
+
|
20
|
+
execute <<~SQL
|
21
|
+
ALTER SEQUENCE #{sequence_name} RESTART WITH #{start}; -- any_fixture:dump
|
22
|
+
SQL
|
23
|
+
end
|
24
|
+
|
25
|
+
def compile_sql(sql, binds)
|
26
|
+
sql.gsub(/\$\d+/) { binds.shift }
|
27
|
+
end
|
28
|
+
|
29
|
+
def import(path)
|
30
|
+
# Test if psql is installed
|
31
|
+
`psql --version`
|
32
|
+
|
33
|
+
tasks = ActiveRecord::Tasks::PostgreSQLDatabaseTasks.new(conn.pool.spec.config.with_indifferent_access)
|
34
|
+
|
35
|
+
while_disconnected do
|
36
|
+
tasks.structure_load(path, "--output=/dev/null")
|
37
|
+
end
|
38
|
+
|
39
|
+
true
|
40
|
+
rescue Errno::ENOENT
|
41
|
+
false
|
42
|
+
end
|
43
|
+
|
44
|
+
def setup_env
|
45
|
+
# Mock UUID generating functions to provide consistent results
|
46
|
+
quoted_functions = UUID_FUNCTIONS.map { |func| "'#{func}'" }.join(", ")
|
47
|
+
|
48
|
+
@uuid_funcs = execute <<~SQL
|
49
|
+
SELECT
|
50
|
+
pp.proname, pn.nspname,
|
51
|
+
pg_get_functiondef(pp.oid) AS definition
|
52
|
+
FROM pg_proc pp
|
53
|
+
JOIN pg_namespace pn
|
54
|
+
ON pn.oid = pp.pronamespace
|
55
|
+
WHERE pp.proname in (#{quoted_functions})
|
56
|
+
ORDER BY pp.oid;
|
57
|
+
SQL
|
58
|
+
|
59
|
+
uuid_funcs.each do |(func, ns, _)|
|
60
|
+
execute <<~SQL
|
61
|
+
CREATE OR REPLACE FUNCTION #{ns}.#{func}()
|
62
|
+
RETURNS UUID
|
63
|
+
LANGUAGE SQL
|
64
|
+
AS $$
|
65
|
+
SELECT md5(random()::TEXT)::UUID;
|
66
|
+
$$; -- any_fixture:dump
|
67
|
+
SQL
|
68
|
+
end
|
69
|
+
|
70
|
+
execute <<~SQL
|
71
|
+
SELECT setseed(#{rand}); -- any_fixture:dump
|
72
|
+
SQL
|
73
|
+
end
|
74
|
+
|
75
|
+
def teardown_env
|
76
|
+
uuid_funcs.each do |(func, ns, definition)|
|
77
|
+
execute "#{definition}; -- any_fixture:dump"
|
78
|
+
end
|
79
|
+
end
|
80
|
+
|
81
|
+
private
|
82
|
+
|
83
|
+
attr_reader :uuid_funcs
|
84
|
+
|
85
|
+
def execute(query)
|
86
|
+
super.values
|
87
|
+
end
|
88
|
+
end
|
89
|
+
end
|
90
|
+
end
|
91
|
+
end
|
@@ -0,0 +1,42 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/any_fixture/dump/base_adapter"
|
4
|
+
|
5
|
+
module TestProf
|
6
|
+
module AnyFixture
|
7
|
+
class Dump
|
8
|
+
class SQLite < BaseAdapter
|
9
|
+
def reset_sequence!(table_name, start)
|
10
|
+
execute <<~SQL.chomp
|
11
|
+
DELETE FROM sqlite_sequence WHERE name=#{table_name}
|
12
|
+
SQL
|
13
|
+
|
14
|
+
execute <<~SQL.chomp
|
15
|
+
INSERT INTO sqlite_sequence (name, seq)
|
16
|
+
VALUES (#{table_name}, #{start})
|
17
|
+
SQL
|
18
|
+
end
|
19
|
+
|
20
|
+
def compile_sql(sql, binds)
|
21
|
+
sql.gsub(/\?/) { binds.shift }
|
22
|
+
end
|
23
|
+
|
24
|
+
def import(path)
|
25
|
+
db = conn.pool.spec.config[:database]
|
26
|
+
return false if %r{:memory:}.match?(db)
|
27
|
+
|
28
|
+
# Check that sqlite3 is installed
|
29
|
+
`sqlite3 --version`
|
30
|
+
|
31
|
+
while_disconnected do
|
32
|
+
`sqlite3 #{db} < "#{path}"`
|
33
|
+
end
|
34
|
+
|
35
|
+
true
|
36
|
+
rescue Errno::ENOENT
|
37
|
+
false
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
@@ -0,0 +1,212 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "test_prof/any_fixture/dump/digest"
|
4
|
+
|
5
|
+
require "set"
|
6
|
+
|
7
|
+
module TestProf
|
8
|
+
module AnyFixture
|
9
|
+
MODIFY_RXP = /^(INSERT INTO|UPDATE|DELETE FROM) (\S+)/i.freeze
|
10
|
+
ANY_FIXTURE_RXP = /(\/\*|--).*\bany_fixture:dump/.freeze
|
11
|
+
ANY_FIXTURE_IGNORE_RXP = /(\/\*|--).*\bany_fixture:ignore/.freeze
|
12
|
+
|
13
|
+
using(Module.new do
|
14
|
+
refine Object do
|
15
|
+
def to_digest
|
16
|
+
to_s
|
17
|
+
end
|
18
|
+
end
|
19
|
+
|
20
|
+
refine NilClass do
|
21
|
+
def to_digest
|
22
|
+
nil
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
refine Hash do
|
27
|
+
def to_digest
|
28
|
+
map { |k, v| [k.to_digest, v.to_digest].compact.join("_") }
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
refine Array do
|
33
|
+
def to_digest
|
34
|
+
map { |v| v.to_digest }.compact.join("-")
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end)
|
38
|
+
|
39
|
+
class Dump
|
40
|
+
class Subscriber
|
41
|
+
attr_reader :path, :tmp_path
|
42
|
+
|
43
|
+
def initialize(path, adapter)
|
44
|
+
@path = path
|
45
|
+
@adapter = adapter
|
46
|
+
@tmp_path = path + ".tmp"
|
47
|
+
@reset_pk = Set.new
|
48
|
+
end
|
49
|
+
|
50
|
+
def start(_event, _id, payload)
|
51
|
+
sql = payload.fetch(:sql)
|
52
|
+
return if sql.match?(ANY_FIXTURE_IGNORE_RXP)
|
53
|
+
|
54
|
+
matches = sql.match(MODIFY_RXP)
|
55
|
+
return unless matches
|
56
|
+
|
57
|
+
reset_pk!(matches[2]) if /insert/i.match?(matches[1])
|
58
|
+
end
|
59
|
+
|
60
|
+
def finish(_event, _id, payload)
|
61
|
+
sql = payload.fetch(:sql)
|
62
|
+
return unless trackable_sql?(sql)
|
63
|
+
|
64
|
+
sql = payload[:binds].any? ? adapter.compile_sql(sql, quoted(payload[:binds])) : +sql
|
65
|
+
|
66
|
+
sql.tr!("\n", " ")
|
67
|
+
|
68
|
+
file.write(sql + ";\n")
|
69
|
+
end
|
70
|
+
|
71
|
+
def commit
|
72
|
+
return unless defined?(:@file)
|
73
|
+
|
74
|
+
file.close
|
75
|
+
|
76
|
+
FileUtils.mv(tmp_path, path)
|
77
|
+
end
|
78
|
+
|
79
|
+
private
|
80
|
+
|
81
|
+
attr_reader :reset_pk, :adapter
|
82
|
+
|
83
|
+
def file
|
84
|
+
@file ||= File.open(tmp_path, "w")
|
85
|
+
end
|
86
|
+
|
87
|
+
def reset_pk!(table_name)
|
88
|
+
return if /sqlite_sequence/.match?(table_name)
|
89
|
+
|
90
|
+
return if reset_pk.include?(table_name)
|
91
|
+
|
92
|
+
adapter.reset_sequence!(table_name, AnyFixture.config.dump_sequence_random_start)
|
93
|
+
reset_pk << table_name
|
94
|
+
end
|
95
|
+
|
96
|
+
def trackable_sql?(sql)
|
97
|
+
return false if sql.match?(ANY_FIXTURE_IGNORE_RXP)
|
98
|
+
|
99
|
+
sql.match?(MODIFY_RXP) || sql.match?(ANY_FIXTURE_RXP) || sql.match?(AnyFixture.config.dump_matching_queries)
|
100
|
+
end
|
101
|
+
|
102
|
+
def quoted(val)
|
103
|
+
if val.is_a?(Array)
|
104
|
+
val.map { |v| quoted(v) }
|
105
|
+
elsif val.is_a?(ActiveModel::Attribute)
|
106
|
+
quoted(val.value_for_database)
|
107
|
+
else
|
108
|
+
ActiveRecord::Base.connection.quote(val)
|
109
|
+
end
|
110
|
+
end
|
111
|
+
end
|
112
|
+
|
113
|
+
attr_reader :name, :digest, :path, :subscriber, :success
|
114
|
+
alias_method :success?, :success
|
115
|
+
|
116
|
+
def initialize(name, watch: [], cache_key: nil)
|
117
|
+
@name = name
|
118
|
+
@digest = [
|
119
|
+
Digest.call(*watch),
|
120
|
+
cache_key.to_digest
|
121
|
+
].compact.join("-")
|
122
|
+
|
123
|
+
@path = build_path(name, digest)
|
124
|
+
|
125
|
+
@success = false
|
126
|
+
|
127
|
+
@adapter =
|
128
|
+
case ActiveRecord::Base.connection.adapter_name
|
129
|
+
when /sqlite/i
|
130
|
+
require "test_prof/any_fixture/dump/sqlite"
|
131
|
+
SQLite.new
|
132
|
+
when /postgresql/i
|
133
|
+
require "test_prof/any_fixture/dump/postgresql"
|
134
|
+
PostgreSQL.new
|
135
|
+
else
|
136
|
+
raise ArgumentError,
|
137
|
+
"Your current database adapter (#{ActiveRecord::Base.connection.adapter_name}) " \
|
138
|
+
"is currently not supported. So far, we only support SQLite and PostgreSQL"
|
139
|
+
end
|
140
|
+
|
141
|
+
@subscriber = Subscriber.new(path, adapter)
|
142
|
+
end
|
143
|
+
|
144
|
+
def exists?
|
145
|
+
File.exist?(path)
|
146
|
+
end
|
147
|
+
|
148
|
+
def force?
|
149
|
+
AnyFixture.config.force_matching_dumps.match?(name)
|
150
|
+
end
|
151
|
+
|
152
|
+
def load
|
153
|
+
return import_via_active_record unless AnyFixture.config.import_dump_via_cli?
|
154
|
+
|
155
|
+
adapter.import(path) || import_via_active_record
|
156
|
+
end
|
157
|
+
|
158
|
+
def commit!
|
159
|
+
subscriber.commit
|
160
|
+
end
|
161
|
+
|
162
|
+
def within_prepared_env(before: nil, after: nil, import: false)
|
163
|
+
run_before_callbacks(callback: before, dump: self, import: false)
|
164
|
+
yield.tap do
|
165
|
+
@success = true
|
166
|
+
end
|
167
|
+
ensure
|
168
|
+
run_after_callbacks(callback: after, dump: self, import: false)
|
169
|
+
end
|
170
|
+
|
171
|
+
private
|
172
|
+
|
173
|
+
attr_reader :adapter
|
174
|
+
|
175
|
+
def import_via_active_record
|
176
|
+
conn = ActiveRecord::Base.connection
|
177
|
+
|
178
|
+
File.open(path).each_line do |query|
|
179
|
+
next if query.empty?
|
180
|
+
|
181
|
+
conn.execute query
|
182
|
+
end
|
183
|
+
end
|
184
|
+
|
185
|
+
def build_path(name, digest)
|
186
|
+
dir = TestProf.artifact_path(
|
187
|
+
File.join(AnyFixture.config.dumps_dir)
|
188
|
+
)
|
189
|
+
|
190
|
+
FileUtils.mkdir_p(dir)
|
191
|
+
|
192
|
+
File.join(dir, "#{name}-#{digest}.sql")
|
193
|
+
end
|
194
|
+
|
195
|
+
def run_before_callbacks(callback:, **options)
|
196
|
+
# First, call config-defined setup callbacks
|
197
|
+
AnyFixture.config.before_dump.each { |clbk| clbk.call(**options) }
|
198
|
+
# Then, adapter-defined callbacks
|
199
|
+
adapter.setup_env unless options[:import]
|
200
|
+
# Finally, user-provided callback
|
201
|
+
callback&.call(**options)
|
202
|
+
end
|
203
|
+
|
204
|
+
def run_after_callbacks(callback:, **options)
|
205
|
+
# The order is vice versa to setup
|
206
|
+
callback&.call(**options)
|
207
|
+
adapter.teardown_env unless options[:import]
|
208
|
+
AnyFixture.config.after_dump.each { |clbk| clbk.call(**options) }
|
209
|
+
end
|
210
|
+
end
|
211
|
+
end
|
212
|
+
end
|
@@ -1,14 +1,67 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
3
|
require "test_prof/ext/float_duration"
|
4
|
+
require "test_prof/any_fixture/dump"
|
4
5
|
|
5
6
|
module TestProf
|
6
7
|
# Make DB fixtures from blocks.
|
7
8
|
module AnyFixture
|
8
|
-
INSERT_RXP = /^INSERT INTO (
|
9
|
+
INSERT_RXP = /^INSERT INTO (\S+)/.freeze
|
9
10
|
|
10
11
|
using FloatDuration
|
11
12
|
|
13
|
+
# AnyFixture configuration
|
14
|
+
class Configuration
|
15
|
+
attr_accessor :reporting_enabled, :dumps_dir, :dump_sequence_start,
|
16
|
+
:import_dump_via_cli, :dump_matching_queries, :force_matching_dumps
|
17
|
+
attr_reader :default_dump_watch_paths
|
18
|
+
|
19
|
+
alias_method :reporting_enabled?, :reporting_enabled
|
20
|
+
alias_method :import_dump_via_cli?, :import_dump_via_cli
|
21
|
+
|
22
|
+
def initialize
|
23
|
+
@reporting_enabled = ENV["ANYFIXTURE_REPORT"] == "1"
|
24
|
+
@dumps_dir = "any_dumps"
|
25
|
+
@default_dump_watch_paths = %w[
|
26
|
+
db/schema.rb
|
27
|
+
db/structure.sql
|
28
|
+
]
|
29
|
+
@dump_sequence_start = 123_654
|
30
|
+
@dump_matching_queries = /^$/
|
31
|
+
@import_dump_via_cli = ENV["ANYFIXTURE_IMPORT_DUMP_CLI"] == "1"
|
32
|
+
@before_dump = []
|
33
|
+
@after_dump = []
|
34
|
+
@force_matching_dumps =
|
35
|
+
if ENV["ANYFIXTURE_FORCE_DUMP"] == "1"
|
36
|
+
/.*/
|
37
|
+
elsif ENV["ANYFIXTURE_FORCE_DUMP"]
|
38
|
+
/#{ENV["ANYFIXTURE_FORCE_DUMP"]}/
|
39
|
+
else
|
40
|
+
/^$/
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def before_dump(&block)
|
45
|
+
if block
|
46
|
+
@before_dump << block
|
47
|
+
else
|
48
|
+
@before_dump
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def after_dump(&block)
|
53
|
+
if block
|
54
|
+
@after_dump << block
|
55
|
+
else
|
56
|
+
@after_dump
|
57
|
+
end
|
58
|
+
end
|
59
|
+
|
60
|
+
def dump_sequence_random_start
|
61
|
+
rand(dump_sequence_start..(dump_sequence_start * 2))
|
62
|
+
end
|
63
|
+
end
|
64
|
+
|
12
65
|
class Cache # :nodoc:
|
13
66
|
attr_reader :store, :stats
|
14
67
|
|
@@ -40,22 +93,74 @@ module TestProf
|
|
40
93
|
class << self
|
41
94
|
include Logging
|
42
95
|
|
43
|
-
|
96
|
+
def config
|
97
|
+
@config ||= Configuration.new
|
98
|
+
end
|
99
|
+
|
100
|
+
def configure
|
101
|
+
yield config
|
102
|
+
end
|
103
|
+
|
104
|
+
# Backward compatibility
|
105
|
+
def reporting_enabled=(val)
|
106
|
+
warn "AnyFixture.reporting_enabled is deprecated and will be removed in 1.1. Use AnyFixture.config.reporting_enabled instead"
|
107
|
+
config.reporting_enabled = val
|
108
|
+
end
|
44
109
|
|
45
|
-
def reporting_enabled
|
46
|
-
reporting_enabled
|
110
|
+
def reporting_enabled
|
111
|
+
warn "AnyFixture.reporting_enabled is deprecated and will be removed in 1.1. Use AnyFixture.config.reporting_enabled instead"
|
112
|
+
config.reporting_enabled
|
47
113
|
end
|
48
114
|
|
115
|
+
alias_method :reporting_enabled?, :reporting_enabled
|
116
|
+
|
49
117
|
# Register a block of code as a fixture,
|
50
118
|
# returns the result of the block execution
|
51
119
|
def register(id)
|
52
|
-
|
120
|
+
cached(id) do
|
53
121
|
ActiveSupport::Notifications.subscribed(method(:subscriber), "sql.active_record") do
|
54
122
|
yield
|
55
123
|
end
|
56
124
|
end
|
57
125
|
end
|
58
126
|
|
127
|
+
def cached(id)
|
128
|
+
cache.fetch(id) { yield }
|
129
|
+
end
|
130
|
+
|
131
|
+
# Create and register new SQL dump.
|
132
|
+
# Use `watch` to provide additional paths to watch for
|
133
|
+
# dump re-generation
|
134
|
+
def register_dump(name, clean: true, **options)
|
135
|
+
called_from = caller_locations(1, 1).first.path
|
136
|
+
watch = options.delete(:watch) || [called_from]
|
137
|
+
cache_key = options.delete(:cache_key)
|
138
|
+
skip = options.delete(:skip_if)
|
139
|
+
|
140
|
+
id = "sql/#{name}"
|
141
|
+
|
142
|
+
register_method = clean ? :register : :cached
|
143
|
+
|
144
|
+
public_send(register_method, id) do
|
145
|
+
dump = Dump.new(name, watch: watch, cache_key: cache_key)
|
146
|
+
|
147
|
+
unless dump.force?
|
148
|
+
next if skip&.call(dump: dump)
|
149
|
+
|
150
|
+
next dump.within_prepared_env(import: true, **options) { dump.load } if dump.exists?
|
151
|
+
end
|
152
|
+
|
153
|
+
subscriber = ActiveSupport::Notifications.subscribe("sql.active_record", dump.subscriber)
|
154
|
+
res = dump.within_prepared_env(**options) { yield }
|
155
|
+
|
156
|
+
dump.commit!
|
157
|
+
|
158
|
+
res
|
159
|
+
ensure
|
160
|
+
ActiveSupport::Notifications.unsubscribe(subscriber) if subscriber
|
161
|
+
end
|
162
|
+
end
|
163
|
+
|
59
164
|
# Clean all affected tables (but do not reset cache)
|
60
165
|
def clean
|
61
166
|
disable_referential_integrity do
|
@@ -76,7 +181,13 @@ module TestProf
|
|
76
181
|
|
77
182
|
def subscriber(_event, _start, _finish, _id, data)
|
78
183
|
matches = data.fetch(:sql).match(INSERT_RXP)
|
79
|
-
|
184
|
+
return unless matches
|
185
|
+
|
186
|
+
table_name = matches[1]
|
187
|
+
|
188
|
+
return if /sqlite_sequence/.match?(table_name)
|
189
|
+
|
190
|
+
tables_cache[table_name] = true
|
80
191
|
end
|
81
192
|
|
82
193
|
def report_stats
|
@@ -148,7 +259,5 @@ module TestProf
|
|
148
259
|
connection.disable_referential_integrity { yield }
|
149
260
|
end
|
150
261
|
end
|
151
|
-
|
152
|
-
self.reporting_enabled = ENV["ANYFIXTURE_REPORT"] == "1"
|
153
262
|
end
|
154
263
|
end
|