test-prof 0.12.1 → 1.0.1
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +32 -0
- data/README.md +1 -1
- data/config/default.yml +0 -15
- data/config/rubocop-rspec.yml +6 -0
- data/lib/test_prof/any_fixture.rb +116 -7
- data/lib/test_prof/any_fixture/dump.rb +212 -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 -0
- data/lib/test_prof/before_all/adapters/active_record.rb +14 -5
- data/lib/test_prof/recipes/minitest/before_all.rb +48 -23
- data/lib/test_prof/recipes/rspec/any_fixture.rb +1 -1
- data/lib/test_prof/recipes/rspec/before_all.rb +10 -2
- data/lib/test_prof/recipes/rspec/let_it_be.rb +3 -2
- data/lib/test_prof/rubocop.rb +0 -1
- data/lib/test_prof/version.rb +1 -1
- metadata +8 -6
- 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
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 372bc2a293716277d1437df54387b0ce7a091c01a4fc3622f206afac9d299cc9
|
4
|
+
data.tar.gz: 9a21beb5616e03d3b7b8dd2a0d3a2ccd4bd7f5f6004f946ca0d7933ce654289c
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 7eccde7e36088e71e7b21fd3637609c4814497e438281cf87cdab75f836ee5a36beea363c7595b1060aeebb91997d2d3764999e62de3d28a8ed75998766de0c8
|
7
|
+
data.tar.gz: 3d4b46a7adde33b1939eb877867bae39f31a8abd5a7db9404ff8ad2631c79ca116dc0cec824da79223e7cf8df3075374b6fe3b939a5f1fc276f3f7319c573407
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,38 @@
|
|
2
2
|
|
3
3
|
## master (unrealeased)
|
4
4
|
|
5
|
+
## 1.0.1 (2021-02-12)
|
6
|
+
|
7
|
+
- Fixed AnyFixture deprecation warning.
|
8
|
+
|
9
|
+
## 1.0.0 (2021-01-21)
|
10
|
+
|
11
|
+
## 1.0.0.rc2 (2021-01-06)
|
12
|
+
|
13
|
+
- Make Rails fixtures accesible in `before_all`. ([@palkan][])
|
14
|
+
|
15
|
+
You can load and access fixtures when explicitly enabling them via `before_all(setup_fixtures: true, &block)`.
|
16
|
+
|
17
|
+
- Minitest's `before_all` is not longer experimental. ([@palkan][])
|
18
|
+
|
19
|
+
- Add `after_all` to Minitest in addition to `before_all`. ([@palkan][])
|
20
|
+
|
21
|
+
## 1.0.0.rc1 (2020-12-30)
|
22
|
+
|
23
|
+
- Remove deprecated `AggregateFailures` cop. ([@palkan][])
|
24
|
+
|
25
|
+
- Remove `ActiveRecordSharedConnection`. ([@palkan][])
|
26
|
+
|
27
|
+
- Add `AnyFixture#register_dump` to _cache_ fixtures using SQL dumps. ([@palkan][])
|
28
|
+
|
29
|
+
- Replaced `TestProf::AnyFixture.reporting_enabled = true` with `TestProf::AnyFixture.config.reporting_enabled = true`. ([@palkan][])
|
30
|
+
|
31
|
+
- Add support for RSpec aliases detection when linting specs using `let_it_be`/`before_all` with `rubocop-rspec` 2.0 ([@pirj][])
|
32
|
+
|
33
|
+
## 0.12.2 (2020-09-03)
|
34
|
+
|
35
|
+
- Execute Minitest `before_all` in the context of the current test object. ([@palkan][])
|
36
|
+
|
5
37
|
## 0.12.1 (2020-09-01)
|
6
38
|
|
7
39
|
- Minor improvements.
|
data/README.md
CHANGED
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
|
@@ -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,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?(ActiveRecord::Relation::QueryAttribute)
|
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 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
|
@@ -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
|
data/lib/test_prof/before_all.rb
CHANGED
@@ -32,6 +32,12 @@ module TestProf
|
|
32
32
|
end
|
33
33
|
end
|
34
34
|
|
35
|
+
def setup_fixtures(test_object)
|
36
|
+
raise ArgumentError, "Current adapter doesn't support #setup_fixtures" unless adapter.respond_to?(:setup_fixtures)
|
37
|
+
|
38
|
+
adapter.setup_fixtures(test_object)
|
39
|
+
end
|
40
|
+
|
35
41
|
def config
|
36
42
|
@config ||= Configuration.new
|
37
43
|
end
|
@@ -60,8 +66,11 @@ module TestProf
|
|
60
66
|
class Configuration
|
61
67
|
HOOKS = %i[begin rollback].freeze
|
62
68
|
|
69
|
+
attr_accessor :setup_fixtures
|
70
|
+
|
63
71
|
def initialize
|
64
72
|
@hooks = Hash.new { |h, k| h[k] = HooksChain.new(k) }
|
73
|
+
@setup_fixtures = false
|
65
74
|
end
|
66
75
|
|
67
76
|
# Add `before` hook for `begin` or
|
@@ -1,10 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
if ::ActiveRecord::VERSION::MAJOR < 4
|
4
|
-
require "test_prof/ext/active_record_3"
|
5
|
-
using TestProf::ActiveRecord3Transactions
|
6
|
-
end
|
7
|
-
|
8
3
|
module TestProf
|
9
4
|
module BeforeAll
|
10
5
|
module Adapters
|
@@ -23,6 +18,20 @@ module TestProf
|
|
23
18
|
end
|
24
19
|
::ActiveRecord::Base.connection.rollback_transaction
|
25
20
|
end
|
21
|
+
|
22
|
+
def setup_fixtures(test_object)
|
23
|
+
test_object.instance_eval do
|
24
|
+
@@already_loaded_fixtures ||= {}
|
25
|
+
@fixture_cache ||= {}
|
26
|
+
|
27
|
+
if @@already_loaded_fixtures[self.class]
|
28
|
+
@loaded_fixtures = @@already_loaded_fixtures[self.class]
|
29
|
+
else
|
30
|
+
@loaded_fixtures = load_fixtures(config)
|
31
|
+
@@already_loaded_fixtures[self.class] = @loaded_fixtures
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
26
35
|
end
|
27
36
|
end
|
28
37
|
end
|
@@ -8,41 +8,59 @@ module TestProf
|
|
8
8
|
# store instance variables
|
9
9
|
module Minitest # :nodoc: all
|
10
10
|
class Executor
|
11
|
-
attr_reader :active
|
11
|
+
attr_reader :active, :block, :captured_ivars, :teardown_block, :current_test_object,
|
12
|
+
:setup_fixtures
|
12
13
|
|
13
14
|
alias active? active
|
15
|
+
alias setup_fixtures? setup_fixtures
|
14
16
|
|
15
|
-
def initialize(&block)
|
17
|
+
def initialize(setup_fixtures: false, &block)
|
18
|
+
@setup_fixtures = setup_fixtures
|
16
19
|
@block = block
|
20
|
+
@captured_ivars = []
|
17
21
|
end
|
18
22
|
|
19
|
-
def
|
20
|
-
|
23
|
+
def teardown(&block)
|
24
|
+
@teardown_block = block
|
25
|
+
end
|
26
|
+
|
27
|
+
def activate!(test_object)
|
28
|
+
@current_test_object = test_object
|
29
|
+
|
30
|
+
return restore_ivars(test_object) if active?
|
21
31
|
@active = true
|
22
|
-
|
32
|
+
BeforeAll.setup_fixtures(test_object) if setup_fixtures?
|
23
33
|
BeforeAll.begin_transaction do
|
24
|
-
capture!
|
34
|
+
capture!(test_object)
|
25
35
|
end
|
26
36
|
end
|
27
37
|
|
28
|
-
def
|
29
|
-
@examples_left -= 1
|
30
|
-
return unless @examples_left.zero?
|
31
|
-
|
38
|
+
def deactivate!
|
32
39
|
@active = false
|
40
|
+
|
41
|
+
current_test_object&.instance_eval(&teardown_block) if teardown_block
|
42
|
+
|
43
|
+
@current_test_object = nil
|
33
44
|
BeforeAll.rollback_transaction
|
34
45
|
end
|
35
46
|
|
36
|
-
def capture!
|
37
|
-
|
47
|
+
def capture!(test_object)
|
48
|
+
return unless block
|
49
|
+
|
50
|
+
before_ivars = test_object.instance_variables
|
51
|
+
|
52
|
+
test_object.instance_eval(&block)
|
53
|
+
|
54
|
+
(test_object.instance_variables - before_ivars).each do |ivar|
|
55
|
+
captured_ivars << [ivar, test_object.instance_variable_get(ivar)]
|
56
|
+
end
|
38
57
|
end
|
39
58
|
|
40
|
-
def
|
41
|
-
|
42
|
-
next if ivar == :@block
|
59
|
+
def restore_ivars(test_object)
|
60
|
+
captured_ivars.each do |(ivar, val)|
|
43
61
|
test_object.instance_variable_set(
|
44
62
|
ivar,
|
45
|
-
|
63
|
+
val
|
46
64
|
)
|
47
65
|
end
|
48
66
|
end
|
@@ -57,22 +75,29 @@ module TestProf
|
|
57
75
|
module ClassMethods
|
58
76
|
attr_accessor :before_all_executor
|
59
77
|
|
60
|
-
def before_all(&block)
|
61
|
-
self.before_all_executor = Executor.new(&block)
|
78
|
+
def before_all(setup_fixtures: BeforeAll.config.setup_fixtures, &block)
|
79
|
+
self.before_all_executor = Executor.new(setup_fixtures: setup_fixtures, &block)
|
62
80
|
|
63
81
|
prepend(Module.new do
|
64
|
-
def
|
65
|
-
self.class.before_all_executor.activate!(self
|
66
|
-
self.class.before_all_executor.restore_to(self)
|
82
|
+
def before_setup
|
83
|
+
self.class.before_all_executor.activate!(self)
|
67
84
|
super
|
68
85
|
end
|
86
|
+
end)
|
69
87
|
|
70
|
-
|
88
|
+
singleton_class.prepend(Module.new do
|
89
|
+
def run(*)
|
71
90
|
super
|
72
|
-
|
91
|
+
ensure
|
92
|
+
before_all_executor&.deactivate!
|
73
93
|
end
|
74
94
|
end)
|
75
95
|
end
|
96
|
+
|
97
|
+
def after_all(&block)
|
98
|
+
self.before_all_executor ||= Executor.new
|
99
|
+
before_all_executor.teardown(&block)
|
100
|
+
end
|
76
101
|
end
|
77
102
|
end
|
78
103
|
end
|
@@ -15,7 +15,7 @@ RSpec.configure do |config|
|
|
15
15
|
config.include_context "any_fixture:clean", with_clean_fixture: true
|
16
16
|
|
17
17
|
config.after(:suite) do
|
18
|
-
TestProf::AnyFixture.report_stats if TestProf::AnyFixture.reporting_enabled?
|
18
|
+
TestProf::AnyFixture.report_stats if TestProf::AnyFixture.config.reporting_enabled?
|
19
19
|
TestProf::AnyFixture.reset
|
20
20
|
end
|
21
21
|
end
|
@@ -6,14 +6,22 @@ module TestProf
|
|
6
6
|
module BeforeAll
|
7
7
|
# Helper to wrap the whole example group into a transaction
|
8
8
|
module RSpec
|
9
|
-
def before_all(&block)
|
9
|
+
def before_all(setup_fixtures: BeforeAll.config.setup_fixtures, &block)
|
10
10
|
raise ArgumentError, "Block is required!" unless block_given?
|
11
11
|
|
12
|
-
|
12
|
+
if within_before_all?
|
13
|
+
before(:all) do
|
14
|
+
@__inspect_output = "before_all hook"
|
15
|
+
instance_eval(&block)
|
16
|
+
end
|
17
|
+
return
|
18
|
+
end
|
13
19
|
|
14
20
|
@__before_all_activated__ = true
|
15
21
|
|
16
22
|
before(:all) do
|
23
|
+
@__inspect_output = "before_all hook"
|
24
|
+
BeforeAll.setup_fixtures(self) if setup_fixtures
|
17
25
|
BeforeAll.begin_transaction do
|
18
26
|
instance_eval(&block)
|
19
27
|
end
|
@@ -108,8 +108,9 @@ module TestProf
|
|
108
108
|
|
109
109
|
LetItBe.module_for(self).module_eval do
|
110
110
|
define_method(identifier) do
|
111
|
-
# Trying to detect the context
|
112
|
-
|
111
|
+
# Trying to detect the context
|
112
|
+
# Based on https://github.com/rspec/rspec-rails/commit/7cb796db064f58da7790a92e73ab906ef50b1f34
|
113
|
+
if @__inspect_output.include?("before(:context)") || @__inspect_output.include?("before_all")
|
113
114
|
instance_variable_get(:"#{PREFIX}#{identifier}")
|
114
115
|
else
|
115
116
|
# Fallback to let definition
|
data/lib/test_prof/rubocop.rb
CHANGED
data/lib/test_prof/version.rb
CHANGED
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: test-prof
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 1.0.1
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Vladimir Dementyev
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2021-02-12 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -119,6 +119,7 @@ files:
|
|
119
119
|
- assets/tagprof.demo.html
|
120
120
|
- assets/tagprof.template.html
|
121
121
|
- config/default.yml
|
122
|
+
- config/rubocop-rspec.yml
|
122
123
|
- lib/minitest/base_reporter.rb
|
123
124
|
- lib/minitest/event_prof_formatter.rb
|
124
125
|
- lib/minitest/test_prof_plugin.rb
|
@@ -126,6 +127,11 @@ files:
|
|
126
127
|
- lib/test_prof.rb
|
127
128
|
- lib/test_prof/any_fixture.rb
|
128
129
|
- lib/test_prof/any_fixture/dsl.rb
|
130
|
+
- lib/test_prof/any_fixture/dump.rb
|
131
|
+
- lib/test_prof/any_fixture/dump/base_adapter.rb
|
132
|
+
- lib/test_prof/any_fixture/dump/digest.rb
|
133
|
+
- lib/test_prof/any_fixture/dump/postgresql.rb
|
134
|
+
- lib/test_prof/any_fixture/dump/sqlite.rb
|
129
135
|
- lib/test_prof/before_all.rb
|
130
136
|
- lib/test_prof/before_all/adapters/active_record.rb
|
131
137
|
- lib/test_prof/before_all/isolator.rb
|
@@ -136,7 +142,6 @@ files:
|
|
136
142
|
- lib/test_prof/cops/rspec/aggregate_examples/matchers_with_side_effects.rb
|
137
143
|
- lib/test_prof/cops/rspec/aggregate_examples/metadata_helpers.rb
|
138
144
|
- lib/test_prof/cops/rspec/aggregate_examples/node_matchers.rb
|
139
|
-
- lib/test_prof/cops/rspec/aggregate_failures.rb
|
140
145
|
- lib/test_prof/cops/rspec/language.rb
|
141
146
|
- lib/test_prof/event_prof.rb
|
142
147
|
- lib/test_prof/event_prof/custom_events.rb
|
@@ -148,7 +153,6 @@ files:
|
|
148
153
|
- lib/test_prof/event_prof/monitor.rb
|
149
154
|
- lib/test_prof/event_prof/profiler.rb
|
150
155
|
- lib/test_prof/event_prof/rspec.rb
|
151
|
-
- lib/test_prof/ext/active_record_3.rb
|
152
156
|
- lib/test_prof/ext/active_record_refind.rb
|
153
157
|
- lib/test_prof/ext/array_bsearch_index.rb
|
154
158
|
- lib/test_prof/ext/factory_bot_strategy.rb
|
@@ -173,8 +177,6 @@ files:
|
|
173
177
|
- lib/test_prof/factory_prof/printers/flamegraph.rb
|
174
178
|
- lib/test_prof/factory_prof/printers/simple.rb
|
175
179
|
- lib/test_prof/logging.rb
|
176
|
-
- lib/test_prof/recipes/active_record_one_love.rb
|
177
|
-
- lib/test_prof/recipes/active_record_shared_connection.rb
|
178
180
|
- lib/test_prof/recipes/logging.rb
|
179
181
|
- lib/test_prof/recipes/minitest/before_all.rb
|
180
182
|
- lib/test_prof/recipes/minitest/sample.rb
|
@@ -1,26 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module RuboCop
|
4
|
-
module Cop
|
5
|
-
module RSpec
|
6
|
-
class AggregateExamples
|
7
|
-
def self.registry
|
8
|
-
RuboCop::Cop::Cop.registry
|
9
|
-
end
|
10
|
-
end
|
11
|
-
|
12
|
-
class AggregateFailures < AggregateExamples
|
13
|
-
def initialize(*)
|
14
|
-
super
|
15
|
-
self.class.just_once { warn "`AggregateFailures` cop has been renamed to `AggregateExamples`." }
|
16
|
-
end
|
17
|
-
|
18
|
-
def self.just_once
|
19
|
-
return if @already_done
|
20
|
-
yield
|
21
|
-
@already_done = true
|
22
|
-
end
|
23
|
-
end
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
@@ -1,27 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module TestProf
|
4
|
-
# Add missing `begin_transaction` and `rollback_transaction` methods
|
5
|
-
module ActiveRecord3Transactions
|
6
|
-
refine ::ActiveRecord::ConnectionAdapters::AbstractAdapter do
|
7
|
-
def begin_transaction(joinable: true)
|
8
|
-
if open_transactions > 0
|
9
|
-
increment_open_transactions
|
10
|
-
create_savepoint
|
11
|
-
else
|
12
|
-
begin_db_transaction
|
13
|
-
end
|
14
|
-
self.transaction_joinable = joinable
|
15
|
-
end
|
16
|
-
|
17
|
-
def rollback_transaction(*)
|
18
|
-
if open_transactions > 1
|
19
|
-
rollback_to_savepoint
|
20
|
-
else
|
21
|
-
rollback_db_transaction
|
22
|
-
end
|
23
|
-
decrement_open_transactions
|
24
|
-
end
|
25
|
-
end
|
26
|
-
end
|
27
|
-
end
|
@@ -1,77 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
module TestProf
|
4
|
-
# Forces ActiveRecord to use the same connection between threads
|
5
|
-
module ActiveRecordSharedConnection # :nodoc: all
|
6
|
-
class << self
|
7
|
-
attr_reader :connection
|
8
|
-
|
9
|
-
def enable!
|
10
|
-
self.connection = ActiveRecord::Base.connection
|
11
|
-
end
|
12
|
-
|
13
|
-
def disable!
|
14
|
-
self.connection = nil
|
15
|
-
end
|
16
|
-
|
17
|
-
def ignore(&block)
|
18
|
-
raise ArgumentError, "Block is required" unless block_given?
|
19
|
-
|
20
|
-
@ignores ||= []
|
21
|
-
|
22
|
-
ignores << block
|
23
|
-
end
|
24
|
-
|
25
|
-
def ignored?(config)
|
26
|
-
!ignores.nil? && ignores.any? { |clbk| clbk.call(config) }
|
27
|
-
end
|
28
|
-
|
29
|
-
private
|
30
|
-
|
31
|
-
attr_reader :ignores
|
32
|
-
|
33
|
-
def connection=(conn)
|
34
|
-
@connection = conn
|
35
|
-
connection.singleton_class.prepend Connection
|
36
|
-
connection
|
37
|
-
end
|
38
|
-
end
|
39
|
-
|
40
|
-
module Connection
|
41
|
-
def shared_lock
|
42
|
-
@shared_lock ||= Mutex.new
|
43
|
-
end
|
44
|
-
|
45
|
-
def exec_cache(*)
|
46
|
-
shared_lock.synchronize { super }
|
47
|
-
end
|
48
|
-
|
49
|
-
def exec_no_cache(*)
|
50
|
-
shared_lock.synchronize { super }
|
51
|
-
end
|
52
|
-
|
53
|
-
def execute(*)
|
54
|
-
shared_lock.synchronize { super }
|
55
|
-
end
|
56
|
-
end
|
57
|
-
|
58
|
-
module Ext
|
59
|
-
def connection
|
60
|
-
return super if ActiveRecordSharedConnection.ignored?(connection_config)
|
61
|
-
ActiveRecordSharedConnection.connection || super
|
62
|
-
end
|
63
|
-
end
|
64
|
-
end
|
65
|
-
end
|
66
|
-
|
67
|
-
ActiveSupport.on_load(:active_record) do
|
68
|
-
if ::ActiveRecord::Base.connection.pool.respond_to?(:lock_thread=)
|
69
|
-
TestProf.log :warn, "You activated ActiveRecordSharedConnection patch for the Rails version,\n" \
|
70
|
-
"which has a built-in support for the same functionality.\n" \
|
71
|
-
"Consider removing it, 'cause this could result in unexpected behaviour.\n\n" \
|
72
|
-
"Read more in the docs: https://test-prof.evilmartians.io/#/active_record_shared_connection"
|
73
|
-
end
|
74
|
-
|
75
|
-
TestProf::ActiveRecordSharedConnection.enable!
|
76
|
-
ActiveRecord::Base.singleton_class.prepend TestProf::ActiveRecordSharedConnection::Ext
|
77
|
-
end
|