test-prof 0.11.3 → 1.0.0.rc2
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 +122 -447
- data/LICENSE.txt +1 -1
- data/README.md +9 -13
- data/config/default.yml +0 -15
- data/config/rubocop-rspec.yml +6 -0
- data/lib/minitest/test_prof_plugin.rb +3 -0
- data/lib/test_prof/any_fixture.rb +116 -7
- data/lib/test_prof/any_fixture/dump.rb +207 -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/before_all.rb +9 -4
- data/lib/test_prof/before_all/adapters/active_record.rb +14 -5
- data/lib/test_prof/cops/rspec/aggregate_examples.rb +2 -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/event_prof/instrumentations/active_support.rb +22 -4
- data/lib/test_prof/recipes/minitest/before_all.rb +48 -23
- data/lib/test_prof/recipes/minitest/sample.rb +6 -10
- data/lib/test_prof/recipes/rspec/before_all.rb +10 -10
- data/lib/test_prof/recipes/rspec/let_it_be.rb +111 -13
- data/lib/test_prof/recipes/rspec/sample.rb +4 -2
- data/lib/test_prof/rubocop.rb +0 -1
- data/lib/test_prof/stack_prof.rb +3 -0
- data/lib/test_prof/version.rb +1 -1
- metadata +23 -21
- 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/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
|
-
[](
|
2
|
-
[](https://rubygems.org/gems/test-prof) [](https://cultofmartians.com)
|
2
|
+
[](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
|
@@ -47,11 +47,11 @@ TestProf toolbox aims to help you identify bottlenecks in your test suite. It co
|
|
47
47
|
## Who uses TestProf
|
48
48
|
|
49
49
|
- [Discourse](https://github.com/discourse/discourse) reduced [~27% of their test suite time](https://twitter.com/samsaffron/status/1125602558024699904)
|
50
|
-
- [Gitlab](https://gitlab.com/gitlab-org/gitlab-ce) reduced [39% of their API tests time](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14370)
|
50
|
+
- [Gitlab](https://gitlab.com/gitlab-org/gitlab-ce) reduced [39% of their API tests time](https://gitlab.com/gitlab-org/gitlab-ce/merge_requests/14370) and [improved factories usage](https://gitlab.com/gitlab-org/gitlab/-/merge_requests/26810)
|
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
|
|
@@ -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,16 +95,12 @@ 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
|
|
104
104
|
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
105
105
|
|
106
106
|
[docs]: https://test-prof.evilmartians.io
|
107
|
-
|
108
|
-
## Security Contact
|
109
|
-
|
110
|
-
To report a security vulnerability, please use the [Tidelift security contact](https://tidelift.com/security). Tidelift will coordinate the fix and disclosure.
|
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
|
@@ -12,6 +12,7 @@ module Minitest # :nodoc:
|
|
12
12
|
opts[:top_count] = ENV["EVENT_PROF_TOP"].to_i if ENV["EVENT_PROF_TOP"]
|
13
13
|
opts[:per_example] = true if ENV["EVENT_PROF_EXAMPLES"]
|
14
14
|
opts[:fdoc] = true if ENV["FDOC"]
|
15
|
+
opts[:sample] = true if ENV["SAMPLE"] || ENV["SAMPLE_GROUPS"]
|
15
16
|
end
|
16
17
|
end
|
17
18
|
end
|
@@ -39,5 +40,7 @@ module Minitest # :nodoc:
|
|
39
40
|
|
40
41
|
reporter << TestProf::EventProfReporter.new(options[:io], options) if options[:event]
|
41
42
|
reporter << TestProf::FactoryDoctorReporter.new(options[:io], options) if options[:fdoc]
|
43
|
+
|
44
|
+
::TestProf::MinitestSample.call if options[:sample]
|
42
45
|
end
|
43
46
|
end
|
@@ -1,6 +1,7 @@
|
|
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.
|
@@ -9,6 +10,58 @@ module TestProf
|
|
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 reporting_enabled? reporting_enabled
|
20
|
+
alias 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_given?
|
46
|
+
@before_dump << block
|
47
|
+
else
|
48
|
+
@before_dump
|
49
|
+
end
|
50
|
+
end
|
51
|
+
|
52
|
+
def after_dump(&block)
|
53
|
+
if block_given?
|
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 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
|
@@ -0,0 +1,207 @@
|
|
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
|
+
@file = File.open(tmp_path, "w")
|
49
|
+
end
|
50
|
+
|
51
|
+
def start(_event, _id, payload)
|
52
|
+
sql = payload.fetch(:sql)
|
53
|
+
return if sql.match?(ANY_FIXTURE_IGNORE_RXP)
|
54
|
+
|
55
|
+
matches = sql.match(MODIFY_RXP)
|
56
|
+
return unless matches
|
57
|
+
|
58
|
+
reset_pk!(matches[2]) if /insert/i.match?(matches[1])
|
59
|
+
end
|
60
|
+
|
61
|
+
def finish(_event, _id, payload)
|
62
|
+
sql = payload.fetch(:sql)
|
63
|
+
return unless trackable_sql?(sql)
|
64
|
+
|
65
|
+
sql = payload[:binds].any? ? adapter.compile_sql(sql, quoted(payload[:binds])) : +sql
|
66
|
+
|
67
|
+
sql.tr!("\n", " ")
|
68
|
+
|
69
|
+
file.write(sql + ";\n")
|
70
|
+
end
|
71
|
+
|
72
|
+
def commit
|
73
|
+
file.close
|
74
|
+
|
75
|
+
FileUtils.mv(tmp_path, path)
|
76
|
+
end
|
77
|
+
|
78
|
+
private
|
79
|
+
|
80
|
+
attr_reader :file, :reset_pk, :adapter
|
81
|
+
|
82
|
+
def reset_pk!(table_name)
|
83
|
+
return if /sqlite_sequence/.match?(table_name)
|
84
|
+
|
85
|
+
return if reset_pk.include?(table_name)
|
86
|
+
|
87
|
+
adapter.reset_sequence!(table_name, AnyFixture.config.dump_sequence_random_start)
|
88
|
+
reset_pk << table_name
|
89
|
+
end
|
90
|
+
|
91
|
+
def trackable_sql?(sql)
|
92
|
+
return false if sql.match?(ANY_FIXTURE_IGNORE_RXP)
|
93
|
+
|
94
|
+
sql.match?(MODIFY_RXP) || sql.match?(ANY_FIXTURE_RXP) || sql.match?(AnyFixture.config.dump_matching_queries)
|
95
|
+
end
|
96
|
+
|
97
|
+
def quoted(val)
|
98
|
+
if val.is_a?(Array)
|
99
|
+
val.map { |v| quoted(v) }
|
100
|
+
elsif val.is_a?(ActiveRecord::Relation::QueryAttribute)
|
101
|
+
quoted(val.value_for_database)
|
102
|
+
else
|
103
|
+
ActiveRecord::Base.connection.quote(val)
|
104
|
+
end
|
105
|
+
end
|
106
|
+
end
|
107
|
+
|
108
|
+
attr_reader :name, :digest, :path, :subscriber, :success
|
109
|
+
alias success? success
|
110
|
+
|
111
|
+
def initialize(name, watch: [], cache_key: nil)
|
112
|
+
@name = name
|
113
|
+
@digest = [
|
114
|
+
Digest.call(*watch),
|
115
|
+
cache_key.to_digest
|
116
|
+
].compact.join("-")
|
117
|
+
|
118
|
+
@path = build_path(name, digest)
|
119
|
+
|
120
|
+
@success = false
|
121
|
+
|
122
|
+
@adapter =
|
123
|
+
case ActiveRecord::Base.connection.adapter_name
|
124
|
+
when /sqlite/i
|
125
|
+
require "test_prof/any_fixture/dump/sqlite"
|
126
|
+
SQLite.new
|
127
|
+
when /postgresql/i
|
128
|
+
require "test_prof/any_fixture/dump/postgresql"
|
129
|
+
PostgreSQL.new
|
130
|
+
else
|
131
|
+
raise ArgumentError,
|
132
|
+
"Your current database adapter (#{ActiveRecord::Base.connection.adapter_name}) " \
|
133
|
+
"is currently not supported. So far, we only support SQLite and PostgreSQL"
|
134
|
+
end
|
135
|
+
|
136
|
+
@subscriber = Subscriber.new(path, adapter)
|
137
|
+
end
|
138
|
+
|
139
|
+
def exists?
|
140
|
+
File.exist?(path)
|
141
|
+
end
|
142
|
+
|
143
|
+
def force?
|
144
|
+
AnyFixture.config.force_matching_dumps.match?(name)
|
145
|
+
end
|
146
|
+
|
147
|
+
def load
|
148
|
+
return import_via_active_record unless AnyFixture.config.import_dump_via_cli?
|
149
|
+
|
150
|
+
adapter.import(path) || import_via_active_record
|
151
|
+
end
|
152
|
+
|
153
|
+
def commit!
|
154
|
+
subscriber.commit
|
155
|
+
end
|
156
|
+
|
157
|
+
def within_prepared_env(before: nil, after: nil, import: false)
|
158
|
+
run_before_callbacks(callback: before, dump: self, import: false)
|
159
|
+
yield.tap do
|
160
|
+
@success = true
|
161
|
+
end
|
162
|
+
ensure
|
163
|
+
run_after_callbacks(callback: after, dump: self, import: false)
|
164
|
+
end
|
165
|
+
|
166
|
+
private
|
167
|
+
|
168
|
+
attr_reader :adapter
|
169
|
+
|
170
|
+
def import_via_active_record
|
171
|
+
conn = ActiveRecord::Base.connection
|
172
|
+
|
173
|
+
File.open(path).each_line do |query|
|
174
|
+
next if query.empty?
|
175
|
+
|
176
|
+
conn.execute query
|
177
|
+
end
|
178
|
+
end
|
179
|
+
|
180
|
+
def build_path(name, digest)
|
181
|
+
dir = TestProf.artifact_path(
|
182
|
+
File.join(AnyFixture.config.dumps_dir)
|
183
|
+
)
|
184
|
+
|
185
|
+
FileUtils.mkdir_p(dir)
|
186
|
+
|
187
|
+
File.join(dir, "#{name}-#{digest}.sql")
|
188
|
+
end
|
189
|
+
|
190
|
+
def run_before_callbacks(callback:, **options)
|
191
|
+
# First, call config-defined setup callbacks
|
192
|
+
AnyFixture.config.before_dump.each { |clbk| clbk.call(**options) }
|
193
|
+
# Then, adapter-defined callbacks
|
194
|
+
adapter.setup_env unless options[:import]
|
195
|
+
# Finally, user-provided callback
|
196
|
+
callback&.call(**options)
|
197
|
+
end
|
198
|
+
|
199
|
+
def run_after_callbacks(callback:, **options)
|
200
|
+
# The order is vice versa to setup
|
201
|
+
callback&.call(**options)
|
202
|
+
adapter.teardown_env unless options[:import]
|
203
|
+
AnyFixture.config.after_dump.each { |clbk| clbk.call(**options) }
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|