n_plus_one_control 0.6.1 → 0.7.0
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 +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: []
|