n_plus_one_control 0.6.1 → 0.7.0
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +4 -4
- data/CHANGELOG.md +26 -0
- data/LICENSE.txt +1 -1
- data/README.md +53 -5
- data/lib/n_plus_one_control/executor.rb +1 -0
- data/lib/n_plus_one_control/minitest.rb +7 -2
- data/lib/n_plus_one_control/rspec/dsl.rb +1 -1
- data/lib/n_plus_one_control/rspec/matchers/perform_constant_number_of_queries.rb +12 -6
- data/lib/n_plus_one_control/rspec/matchers/perform_linear_number_of_queries.rb +1 -3
- data/lib/n_plus_one_control/version.rb +1 -1
- data/lib/n_plus_one_control.rb +5 -3
- metadata +15 -15
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0fed1df4b3bee8844b57b4d4201802361b5871d93db3fdb0132a4073fd56fc3a
|
4
|
+
data.tar.gz: 56649719e9b94ed7eb05ccf3c6236399a6a2a9394ce37574a8e58f079b1ff3b4
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: cae71e3487fb5210bd6dd76ae0cf7ee1bb310748a3e3a9cd95d893dc2eb99c6fa90c8482366897a7a2e6c8ed479780d337ef2e1284c266fef00e7b67f5f42dad
|
7
|
+
data.tar.gz: 270f6a75e499ffecd886d98739ccaa5f15ff60a7aebc1ebecc10cc0b2cfdd831137e482897d6ed217f0e60306e2b0350ef22d781a0475ad1a7dfda7563d6fc35
|
data/CHANGELOG.md
CHANGED
@@ -2,6 +2,31 @@
|
|
2
2
|
|
3
3
|
## master
|
4
4
|
|
5
|
+
## 0.7.0 (2023-02-17)
|
6
|
+
|
7
|
+
- Added ability to specify the exact number of expected queries when using constant matchers. ([@akostadinov][], [@palkan][])
|
8
|
+
|
9
|
+
For RSpec, you can add the `.exactly` modifier:
|
10
|
+
|
11
|
+
```ruby
|
12
|
+
expect { get :index }.to perform_constant_number_of_queries.exactly(1)
|
13
|
+
```
|
14
|
+
|
15
|
+
For Minitest, you can provide the expected number of queries as the first argument:
|
16
|
+
|
17
|
+
```ruby
|
18
|
+
assert_perform_constant_number_of_queries(0, **options) do
|
19
|
+
get :index
|
20
|
+
end
|
21
|
+
```
|
22
|
+
|
23
|
+
- **Require Ruby 2.7+**.
|
24
|
+
|
25
|
+
## 0.6.2 (2021-10-26)
|
26
|
+
|
27
|
+
- Fix .ignore setting (.ignore setting was ignored by the Collector ;-))
|
28
|
+
- Fix rspec matchers to allow expectations inside execution block
|
29
|
+
|
5
30
|
## 0.6.1 (2021-03-05)
|
6
31
|
|
7
32
|
- Ruby 3.0 compatibility. ([@palkan][])
|
@@ -39,3 +64,4 @@ Could be specified via `NPLUSONE_BACKTRACE` env var.
|
|
39
64
|
[@palkan]: https://github.com/palkan
|
40
65
|
[@caalberts]: https://github.com/caalberts
|
41
66
|
[@andrewhampton]: https://github.com/andrewhampton
|
67
|
+
[@akostadinov]: https://github.com/akostadinov
|
data/LICENSE.txt
CHANGED
@@ -1,6 +1,6 @@
|
|
1
1
|
The MIT License (MIT)
|
2
2
|
|
3
|
-
Copyright (c) 2017-
|
3
|
+
Copyright (c) 2017-2023 Vladimir Dementyev
|
4
4
|
|
5
5
|
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
6
|
of this software and associated documentation files (the "Software"), to deal
|
data/README.md
CHANGED
@@ -17,6 +17,8 @@ NPlusOneControl works differently. It evaluates the code under consideration sev
|
|
17
17
|
|
18
18
|
So, it's for _performance_ testing and not _feature_ testing.
|
19
19
|
|
20
|
+
> Read also ["Squash N+1 queries early with n_plus_one_control test matchers for Ruby and Rails"](https://evilmartians.com/chronicles/squash-n-plus-one-queries-early-with-n-plus-one-control-test-matchers-for-ruby-and-rails).
|
21
|
+
|
20
22
|
### Why not just use [`bullet`](https://github.com/flyerhzm/bullet)?
|
21
23
|
|
22
24
|
Of course, it's possible to use Bullet in tests (see more [here](https://evilmartians.com/chronicles/fighting-the-hydra-of-n-plus-one-queries)), but it's not a _silver bullet_: there can be both false positives and true negatives.
|
@@ -72,12 +74,33 @@ end
|
|
72
74
|
|
73
75
|
```ruby
|
74
76
|
# BAD – won't work!
|
75
|
-
|
76
77
|
subject { get :index }
|
77
78
|
|
78
79
|
specify do
|
79
80
|
expect { subject }.to perform_constant_number_of_queries
|
80
81
|
end
|
82
|
+
|
83
|
+
# GOOD
|
84
|
+
specify do
|
85
|
+
expect { get :index }.to perform_constant_number_of_queries
|
86
|
+
end
|
87
|
+
|
88
|
+
# BAD — the `page` record would be removed from the database
|
89
|
+
# but still present in RSpec (due to `let`'s memoization)
|
90
|
+
let(:page) { create(:page) }
|
91
|
+
|
92
|
+
populate { |n| create_list(:comment, n, page: page) }
|
93
|
+
|
94
|
+
specify do
|
95
|
+
expect { get :show, params: {id: page.id} }.to perform_constant_number_of_queries
|
96
|
+
end
|
97
|
+
|
98
|
+
# GOOD
|
99
|
+
# Ensure the record is created before `populate`
|
100
|
+
let!(:page) { create(:page) }
|
101
|
+
|
102
|
+
populate { |n| create_list(:comment, n, page: page) }
|
103
|
+
# ...
|
81
104
|
```
|
82
105
|
|
83
106
|
Availables modifiers:
|
@@ -89,6 +112,9 @@ expect { get :index }.to perform_constant_number_of_queries.matching(/INSERT/)
|
|
89
112
|
|
90
113
|
# You can also provide custom scale factors
|
91
114
|
expect { get :index }.to perform_constant_number_of_queries.with_scale_factors(10, 100)
|
115
|
+
|
116
|
+
# You can specify the exact number of expected queries
|
117
|
+
expect { get :index }.to perform_constant_number_of_queries.exactly(1)
|
92
118
|
```
|
93
119
|
|
94
120
|
#### Using scale factor in spec
|
@@ -111,6 +137,20 @@ context "N+1", :n_plus_one do
|
|
111
137
|
end
|
112
138
|
```
|
113
139
|
|
140
|
+
### Expectations in execution block
|
141
|
+
|
142
|
+
Both rspec matchers allows you to put additional expectations inside execution block to ensure that tested piece of code actually does what expected.
|
143
|
+
|
144
|
+
```ruby
|
145
|
+
context "N+1", :n_plus_one do
|
146
|
+
specify do
|
147
|
+
expect do
|
148
|
+
expect(my_query).to eq(actuall_results)
|
149
|
+
end.to perform_constant_number_of_queries
|
150
|
+
end
|
151
|
+
end
|
152
|
+
```
|
153
|
+
|
114
154
|
#### Other available matchers
|
115
155
|
|
116
156
|
`perform_linear_number_of_queries(slope: 1)` allows you to test that a query generates linear number of queries with the given slope.
|
@@ -152,7 +192,7 @@ You can also use `assert_perform_linear_number_of_queries` to test for linear qu
|
|
152
192
|
```ruby
|
153
193
|
def test_no_n_plus_one_error
|
154
194
|
populate = ->(n) { create_list(:post, n) }
|
155
|
-
|
195
|
+
|
156
196
|
assert_perform_linear_number_of_queries(slope: 1, populate: populate) do
|
157
197
|
Post.find_each { |p| p.user.name }
|
158
198
|
end
|
@@ -177,6 +217,14 @@ assert_perform_constant_number_of_queries(
|
|
177
217
|
end
|
178
218
|
```
|
179
219
|
|
220
|
+
For the constant matcher, you can also specify the expected number of queries as the first argument:
|
221
|
+
|
222
|
+
```ruby
|
223
|
+
assert_perform_constant_number_of_queries(2, populate: populate) do
|
224
|
+
get :index
|
225
|
+
end
|
226
|
+
```
|
227
|
+
|
180
228
|
It's possible to specify a filter via `NPLUSONE_FILTER` env var, e.g.:
|
181
229
|
|
182
230
|
```ruby
|
@@ -256,7 +304,7 @@ end
|
|
256
304
|
If your `warmup` and testing procs are identical, you can use:
|
257
305
|
|
258
306
|
```ruby
|
259
|
-
|
307
|
+
expect { get :index }.to perform_constant_number_of_queries.with_warming_up # RSpec only
|
260
308
|
```
|
261
309
|
|
262
310
|
### Configuration
|
@@ -288,7 +336,7 @@ NPlusOneControl.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
|
|
288
336
|
# but can also track rom-rb events ('sql.rom') as well.
|
289
337
|
NPlusOneControl.event = "sql.active_record"
|
290
338
|
|
291
|
-
# configure transactional
|
339
|
+
# configure transactional behaviour for populate method
|
292
340
|
# in case of use multiple database connections
|
293
341
|
NPlusOneControl::Executor.tap do |executor|
|
294
342
|
connections = ActiveRecord::Base.connection_handler.connection_pool_list.map(&:connection)
|
@@ -312,7 +360,7 @@ NPlusOneControl.backtrace_cleaner = ->(locations_array) { do_some_filtering(loca
|
|
312
360
|
NPlusOneControl.backtrace_length = 1
|
313
361
|
|
314
362
|
# Sometime queries could be too large to provide any meaningful insight.
|
315
|
-
# You can configure an output length limit for quries in verbose mode by setting the
|
363
|
+
# You can configure an output length limit for quries in verbose mode by setting the following option
|
316
364
|
# NOTE: It could be specified via NPLUSONE_TRUNCATE env var
|
317
365
|
NPlusOneControl.truncate_query_size = 100
|
318
366
|
```
|
@@ -21,6 +21,7 @@ module NPlusOneControl
|
|
21
21
|
|
22
22
|
def callback(_name, _start, _finish, _message_id, values) # rubocop:disable Metrics/CyclomaticComplexity,Metrics/LineLength
|
23
23
|
return if %w[CACHE SCHEMA].include? values[:name]
|
24
|
+
return if values[:sql].match?(NPlusOneControl.ignore)
|
24
25
|
|
25
26
|
return unless @pattern.nil? || (values[:sql] =~ @pattern)
|
26
27
|
|
@@ -6,6 +6,7 @@ module NPlusOneControl
|
|
6
6
|
# Minitest assertions
|
7
7
|
module MinitestHelper
|
8
8
|
def assert_perform_constant_number_of_queries(
|
9
|
+
exact = nil,
|
9
10
|
populate: nil,
|
10
11
|
matching: nil,
|
11
12
|
scale_factors: nil,
|
@@ -26,7 +27,11 @@ module NPlusOneControl
|
|
26
27
|
|
27
28
|
counts = queries.map(&:last).map(&:size)
|
28
29
|
|
29
|
-
|
30
|
+
if exact
|
31
|
+
assert counts.all? { _1 == exact }, NPlusOneControl.failure_message(:number_of_queries, queries)
|
32
|
+
else
|
33
|
+
assert counts.max == counts.min, NPlusOneControl.failure_message(:constant_queries, queries)
|
34
|
+
end
|
30
35
|
end
|
31
36
|
|
32
37
|
def assert_perform_linear_number_of_queries(
|
@@ -59,7 +64,7 @@ module NPlusOneControl
|
|
59
64
|
private
|
60
65
|
|
61
66
|
def warming_up(warmup)
|
62
|
-
(warmup || methods.include?(:warmup) ? method(:warmup) : nil)&.call
|
67
|
+
(warmup || (methods.include?(:warmup) ? method(:warmup) : nil))&.call
|
63
68
|
end
|
64
69
|
|
65
70
|
def population_method
|
@@ -6,7 +6,7 @@ module NPlusOneControl
|
|
6
6
|
module DSL
|
7
7
|
# Extends RSpec ExampleGroup with populate & warmup methods
|
8
8
|
module ClassMethods
|
9
|
-
# Setup warmup block,
|
9
|
+
# Setup warmup block, which will run before matching
|
10
10
|
# for example, if using cache, then later queries
|
11
11
|
# will perform less DB queries than first
|
12
12
|
def warmup(&block)
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# rubocop:disable Metrics/BlockLength
|
4
3
|
::RSpec::Matchers.define :perform_constant_number_of_queries do
|
5
4
|
supports_block_expectations
|
6
5
|
|
@@ -12,11 +11,15 @@
|
|
12
11
|
@pattern = pattern
|
13
12
|
end
|
14
13
|
|
14
|
+
chain :exactly do |pattern|
|
15
|
+
@exactly = pattern
|
16
|
+
end
|
17
|
+
|
15
18
|
chain :with_warming_up do
|
16
19
|
@warmup = true
|
17
20
|
end
|
18
21
|
|
19
|
-
match do |actual, *_args|
|
22
|
+
match(notify_expectation_failures: true) do |actual, *_args|
|
20
23
|
raise ArgumentError, "Block is required" unless actual.is_a? Proc
|
21
24
|
|
22
25
|
raise "Missing tag :n_plus_one" unless
|
@@ -32,20 +35,23 @@
|
|
32
35
|
@matcher_execution_context.executor = NPlusOneControl::Executor.new(
|
33
36
|
population: populate,
|
34
37
|
matching: pattern,
|
35
|
-
scale_factors: @factors
|
38
|
+
scale_factors: @exactly ? [1] : @factors
|
36
39
|
)
|
37
40
|
|
38
41
|
@queries = @matcher_execution_context.executor.call(&actual)
|
39
42
|
|
40
43
|
counts = @queries.map(&:last).map(&:size)
|
41
44
|
|
42
|
-
|
45
|
+
if @exactly
|
46
|
+
counts.all? { _1 == @exactly }
|
47
|
+
else
|
48
|
+
counts.max == counts.min
|
49
|
+
end
|
43
50
|
end
|
44
51
|
|
45
52
|
match_when_negated do |_actual|
|
46
53
|
raise "This matcher doesn't support negation"
|
47
54
|
end
|
48
55
|
|
49
|
-
failure_message { |_actual| NPlusOneControl.failure_message(:constant_queries, @queries) }
|
56
|
+
failure_message { |_actual| NPlusOneControl.failure_message(@exactly ? :number_of_queries : :constant_queries, @queries) }
|
50
57
|
end
|
51
|
-
# rubocop:enable Metrics/BlockLength
|
@@ -1,6 +1,5 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
-
# rubocop:disable Metrics/BlockLength
|
4
3
|
::RSpec::Matchers.define :perform_linear_number_of_queries do |slope: 1|
|
5
4
|
supports_block_expectations
|
6
5
|
|
@@ -16,7 +15,7 @@
|
|
16
15
|
@warmup = true
|
17
16
|
end
|
18
17
|
|
19
|
-
match do |actual, *_args|
|
18
|
+
match(notify_expectation_failures: true) do |actual, *_args|
|
20
19
|
raise ArgumentError, "Block is required" unless actual.is_a? Proc
|
21
20
|
|
22
21
|
raise "Missing tag :n_plus_one" unless
|
@@ -50,4 +49,3 @@
|
|
50
49
|
|
51
50
|
failure_message { |_actual| NPlusOneControl.failure_message(:linear_queries, @queries) }
|
52
51
|
end
|
53
|
-
# rubocop:enable Metrics/BlockLength
|
data/lib/n_plus_one_control.rb
CHANGED
@@ -25,7 +25,8 @@ module NPlusOneControl
|
|
25
25
|
|
26
26
|
FAILURE_MESSAGES = {
|
27
27
|
constant_queries: "Expected to make the same number of queries",
|
28
|
-
linear_queries: "Expected to make linear number of queries"
|
28
|
+
linear_queries: "Expected to make linear number of queries",
|
29
|
+
number_of_queries: "Expected to make the specified number of queries"
|
29
30
|
}
|
30
31
|
|
31
32
|
def failure_message(type, queries) # rubocop:disable Metrics/MethodLength
|
@@ -59,9 +60,10 @@ module NPlusOneControl
|
|
59
60
|
end
|
60
61
|
|
61
62
|
before.keys.each do |k|
|
62
|
-
next if before[k] == after
|
63
|
+
next if before[k] == after&.fetch(k, nil)
|
63
64
|
|
64
|
-
|
65
|
+
after_value = after ? " != #{after[k]}" : ""
|
66
|
+
msg << "#{k}: #{before[k]}#{after_value}\n"
|
65
67
|
end
|
66
68
|
|
67
69
|
msg
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: n_plus_one_control
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.
|
4
|
+
version: 0.7.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- palkan
|
8
|
-
autorequire:
|
8
|
+
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2023-02-17 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
@@ -67,19 +67,19 @@ dependencies:
|
|
67
67
|
- !ruby/object:Gem::Version
|
68
68
|
version: '5.9'
|
69
69
|
- !ruby/object:Gem::Dependency
|
70
|
-
name:
|
70
|
+
name: factory_bot
|
71
71
|
requirement: !ruby/object:Gem::Requirement
|
72
72
|
requirements:
|
73
73
|
- - "~>"
|
74
74
|
- !ruby/object:Gem::Version
|
75
|
-
version:
|
75
|
+
version: '6.0'
|
76
76
|
type: :development
|
77
77
|
prerelease: false
|
78
78
|
version_requirements: !ruby/object:Gem::Requirement
|
79
79
|
requirements:
|
80
80
|
- - "~>"
|
81
81
|
- !ruby/object:Gem::Version
|
82
|
-
version:
|
82
|
+
version: '6.0'
|
83
83
|
description: "\n RSpec and Minitest matchers to prevent N+1 queries problem.\n\n
|
84
84
|
\ Evaluates code under consideration several times with different scale factors\n
|
85
85
|
\ to make sure that the number of DB queries behaves as expected (i.e. O(1) instead
|
@@ -103,16 +103,16 @@ files:
|
|
103
103
|
- lib/n_plus_one_control/rspec/matchers/perform_constant_number_of_queries.rb
|
104
104
|
- lib/n_plus_one_control/rspec/matchers/perform_linear_number_of_queries.rb
|
105
105
|
- lib/n_plus_one_control/version.rb
|
106
|
-
homepage:
|
106
|
+
homepage: https://github.com/palkan/n_plus_one_control
|
107
107
|
licenses:
|
108
108
|
- MIT
|
109
109
|
metadata:
|
110
|
-
bug_tracker_uri:
|
110
|
+
bug_tracker_uri: https://github.com/palkan/n_plus_one_control/issues
|
111
111
|
changelog_uri: https://github.com/palkan/n_plus_one_control/blob/master/CHANGELOG.md
|
112
|
-
documentation_uri:
|
113
|
-
homepage_uri:
|
114
|
-
source_code_uri:
|
115
|
-
post_install_message:
|
112
|
+
documentation_uri: https://github.com/palkan/n_plus_one_control
|
113
|
+
homepage_uri: https://github.com/palkan/n_plus_one_control
|
114
|
+
source_code_uri: https://github.com/palkan/n_plus_one_control
|
115
|
+
post_install_message:
|
116
116
|
rdoc_options: []
|
117
117
|
require_paths:
|
118
118
|
- lib
|
@@ -120,15 +120,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
|
|
120
120
|
requirements:
|
121
121
|
- - ">="
|
122
122
|
- !ruby/object:Gem::Version
|
123
|
-
version: 2.
|
123
|
+
version: 2.7.0
|
124
124
|
required_rubygems_version: !ruby/object:Gem::Requirement
|
125
125
|
requirements:
|
126
126
|
- - ">="
|
127
127
|
- !ruby/object:Gem::Version
|
128
128
|
version: '0'
|
129
129
|
requirements: []
|
130
|
-
rubygems_version: 3.
|
131
|
-
signing_key:
|
130
|
+
rubygems_version: 3.4.6
|
131
|
+
signing_key:
|
132
132
|
specification_version: 4
|
133
133
|
summary: RSpec and Minitest matchers to prevent N+1 queries problem
|
134
134
|
test_files: []
|