n_plus_one_control 0.0.3
Sign up to get free protection for your applications and to get access to all the features.
- checksums.yaml +7 -0
- data/.gitignore +9 -0
- data/.rspec +2 -0
- data/.rubocop.yml +71 -0
- data/.travis.yml +5 -0
- data/Gemfile +4 -0
- data/LICENSE.txt +21 -0
- data/README.md +204 -0
- data/Rakefile +13 -0
- data/bin/console +14 -0
- data/bin/setup +8 -0
- data/lib/n_plus_one_control.rb +35 -0
- data/lib/n_plus_one_control/executor.rb +55 -0
- data/lib/n_plus_one_control/minitest.rb +29 -0
- data/lib/n_plus_one_control/rspec.rb +15 -0
- data/lib/n_plus_one_control/rspec/context.rb +19 -0
- data/lib/n_plus_one_control/rspec/dsl.rb +16 -0
- data/lib/n_plus_one_control/rspec/matcher.rb +43 -0
- data/lib/n_plus_one_control/version.rb +5 -0
- data/n_plus_one_control.gemspec +46 -0
- data/spec/n_plus_one_control/executor_spec.rb +65 -0
- data/spec/n_plus_one_control/rspec_spec.rb +84 -0
- data/spec/n_plus_one_control_spec.rb +9 -0
- data/spec/spec_helper.rb +30 -0
- data/spec/support/post.rb +19 -0
- data/spec/support/user.rb +17 -0
- data/tests/minitest_test.rb +63 -0
- data/tests/test_helper.rb +32 -0
- metadata +203 -0
checksums.yaml
ADDED
@@ -0,0 +1,7 @@
|
|
1
|
+
---
|
2
|
+
SHA1:
|
3
|
+
metadata.gz: c24a275d72a208eda2b17b7eb6200eead0dc372a
|
4
|
+
data.tar.gz: 711bccd2b8d3cd2ebc337a8d9ebfd407933be10d
|
5
|
+
SHA512:
|
6
|
+
metadata.gz: e35efece4fd758e688bf3d294b26a7071e8d38a2ecfcda7fa3942130398a1578d8c539da171afb529dd4259fff692d42433197169c99b89dc2f12318a53efe7c
|
7
|
+
data.tar.gz: 83403b68d17b11f8c5261045d599ea64e5e33e1c1e7f4c1d1db345a12ef0b36dd3543866485e601e364a164104c678594b5452188d983fe5208d4e6499b8b8c3
|
data/.gitignore
ADDED
data/.rspec
ADDED
data/.rubocop.yml
ADDED
@@ -0,0 +1,71 @@
|
|
1
|
+
AllCops:
|
2
|
+
Include:
|
3
|
+
- 'lib/**/*.rb'
|
4
|
+
- 'lib/**/*.rake'
|
5
|
+
- 'spec/**/*.rb'
|
6
|
+
Exclude:
|
7
|
+
- 'bin/**/*'
|
8
|
+
- 'spec/dummy/**/*'
|
9
|
+
- 'tmp/**/*'
|
10
|
+
- 'Rakefile'
|
11
|
+
- 'Gemfile'
|
12
|
+
- '*.gemspec'
|
13
|
+
DisplayCopNames: true
|
14
|
+
StyleGuideCopsOnly: false
|
15
|
+
TargetRubyVersion: 2.4
|
16
|
+
|
17
|
+
Rails:
|
18
|
+
Enabled: false
|
19
|
+
|
20
|
+
Style/AccessorMethodName:
|
21
|
+
Enabled: false
|
22
|
+
|
23
|
+
Style/TrivialAccessors:
|
24
|
+
Enabled: false
|
25
|
+
|
26
|
+
Style/Documentation:
|
27
|
+
Exclude:
|
28
|
+
- 'spec/**/*.rb'
|
29
|
+
- 'tests/**/*.rb'
|
30
|
+
|
31
|
+
Style/StringLiterals:
|
32
|
+
Enabled: false
|
33
|
+
|
34
|
+
Style/RegexpLiteral:
|
35
|
+
Enabled: false
|
36
|
+
|
37
|
+
Style/SpaceInsideStringInterpolation:
|
38
|
+
EnforcedStyle: no_space
|
39
|
+
|
40
|
+
Style/ClassAndModuleChildren:
|
41
|
+
Enabled: false
|
42
|
+
|
43
|
+
Style/BlockDelimiters:
|
44
|
+
Exclude:
|
45
|
+
- 'spec/**/*.rb'
|
46
|
+
|
47
|
+
Lint/AmbiguousRegexpLiteral:
|
48
|
+
Enabled: false
|
49
|
+
|
50
|
+
|
51
|
+
Metrics/MethodLength:
|
52
|
+
Exclude:
|
53
|
+
- 'spec/**/*.rb'
|
54
|
+
|
55
|
+
Metrics/AbcSize:
|
56
|
+
Max: 20
|
57
|
+
|
58
|
+
Metrics/LineLength:
|
59
|
+
Max: 100
|
60
|
+
Exclude:
|
61
|
+
- 'spec/**/*.rb'
|
62
|
+
|
63
|
+
Metrics/BlockLength:
|
64
|
+
Exclude:
|
65
|
+
- 'spec/**/*.rb'
|
66
|
+
|
67
|
+
Rails/Date:
|
68
|
+
Enabled: false
|
69
|
+
|
70
|
+
Rails/TimeZone:
|
71
|
+
Enabled: false
|
data/.travis.yml
ADDED
data/Gemfile
ADDED
data/LICENSE.txt
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
The MIT License (MIT)
|
2
|
+
|
3
|
+
Copyright (c) 2017 palkan
|
4
|
+
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
7
|
+
in the Software without restriction, including without limitation the rights
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
10
|
+
furnished to do so, subject to the following conditions:
|
11
|
+
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
13
|
+
all copies or substantial portions of the Software.
|
14
|
+
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
@@ -0,0 +1,204 @@
|
|
1
|
+
# N + 1 Control
|
2
|
+
|
3
|
+
RSpec and Minitest matchers to prevent the N+1 queries problem.
|
4
|
+
|
5
|
+
### Why yet another gem to assert DB queries?
|
6
|
+
|
7
|
+
Unlike other libraries (such as [db-query-matchers](https://github.com/brigade/db-query-matchers), [rspec-sqlimit](https://github.com/nepalez/rspec-sqlimit), etc), with `n_plus_one_control` you don't have to specify exact expectations to control your code behaviour (e.g. `expect { subject }.to query(2).times`).
|
8
|
+
|
9
|
+
Such expectations are rather hard to maintain, 'cause there is a big chance of adding more queries, not related to the system under test.
|
10
|
+
|
11
|
+
NPlusOneControl works differently. It evaluates the code under consideration several times with different scale factors to make sure that the number of DB queries behaves as expected (i.e. O(1) instead of O(N)).
|
12
|
+
|
13
|
+
### Why not just use [`bullet`](https://github.com/flyerhzm/bullet)?
|
14
|
+
|
15
|
+
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.
|
16
|
+
|
17
|
+
This gem was born after I've found myself not able to verify with a test yet another N+1 problem.
|
18
|
+
|
19
|
+
## Installation
|
20
|
+
|
21
|
+
Add this line to your application's Gemfile:
|
22
|
+
|
23
|
+
```ruby
|
24
|
+
group :test do
|
25
|
+
gem 'n_plus_one_control'
|
26
|
+
end
|
27
|
+
```
|
28
|
+
|
29
|
+
And then execute:
|
30
|
+
|
31
|
+
$ bundle
|
32
|
+
|
33
|
+
## Usage
|
34
|
+
|
35
|
+
### RSpec
|
36
|
+
|
37
|
+
First, add NPlusOneControl to your `spec_helper.rb`:
|
38
|
+
|
39
|
+
```ruby
|
40
|
+
# spec_helper.rb
|
41
|
+
...
|
42
|
+
|
43
|
+
require "n_plus_one_control/rspec"
|
44
|
+
```
|
45
|
+
|
46
|
+
Then:
|
47
|
+
|
48
|
+
```ruby
|
49
|
+
# Wrap example into a context with :n_plus_one tag
|
50
|
+
context "N+1", :n_plus_one do
|
51
|
+
# Define `populate` callbacks which is responsible for data
|
52
|
+
# generation (and whatever else).
|
53
|
+
#
|
54
|
+
# It accepts one argument – the scale factor (read below)
|
55
|
+
populate { |n| create_list(:post, n) }
|
56
|
+
|
57
|
+
specify do
|
58
|
+
expect { get :index }.to perform_constant_number_of_queries
|
59
|
+
end
|
60
|
+
end
|
61
|
+
```
|
62
|
+
|
63
|
+
**NOTE:** do not use memoized values within the expectation block!
|
64
|
+
|
65
|
+
```ruby
|
66
|
+
# BAD – won't work!
|
67
|
+
|
68
|
+
subject { get :index }
|
69
|
+
|
70
|
+
specify do
|
71
|
+
expect { subject }.to perform_constant_number_of_queries
|
72
|
+
end
|
73
|
+
```
|
74
|
+
|
75
|
+
Availables modifiers:
|
76
|
+
|
77
|
+
```ruby
|
78
|
+
# You can specify the RegExp to filter queries.
|
79
|
+
# By default, it only considers SELECT queries.
|
80
|
+
expect { ... }.to perform_constant_number_of_queries.matching(/INSERT/)
|
81
|
+
|
82
|
+
# You can also provide custom scale factors
|
83
|
+
expect { ... }.to perform_constant_number_of_queries.with_scale_factors(10, 100)
|
84
|
+
```
|
85
|
+
|
86
|
+
### Minitest
|
87
|
+
|
88
|
+
First, add NPlusOneControl to your `test_helper.rb`:
|
89
|
+
|
90
|
+
```ruby
|
91
|
+
# test_helper.rb
|
92
|
+
...
|
93
|
+
|
94
|
+
require "n_plus_one_control/minitest"
|
95
|
+
```
|
96
|
+
|
97
|
+
Then use `assert_perform_constant_number_of_queries` assertion method:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
def test_no_n_plus_one_error
|
101
|
+
populate = ->(n) { create_list(:post, n) }
|
102
|
+
|
103
|
+
assert_perform_constant_number_of_queries(populate: populate) do
|
104
|
+
get :index
|
105
|
+
end
|
106
|
+
end
|
107
|
+
```
|
108
|
+
|
109
|
+
You can also specify custom scale factors or filter patterns:
|
110
|
+
|
111
|
+
```ruby
|
112
|
+
assert_perform_constant_number_of_queries(
|
113
|
+
populate: populate,
|
114
|
+
scale_factors: [2, 5, 10]
|
115
|
+
) do
|
116
|
+
get :index
|
117
|
+
end
|
118
|
+
|
119
|
+
assert_perform_constant_number_of_queries(
|
120
|
+
populate: populate,
|
121
|
+
matching: /INSERT/
|
122
|
+
) do
|
123
|
+
do_some_havey_stuff
|
124
|
+
end
|
125
|
+
```
|
126
|
+
|
127
|
+
You can also specify `populate` as a test class instance method:
|
128
|
+
|
129
|
+
```ruby
|
130
|
+
def populate(n)
|
131
|
+
create_list(:post, n)
|
132
|
+
end
|
133
|
+
|
134
|
+
def test_no_n_plus_one_error
|
135
|
+
assert_perform_constant_number_of_queries do
|
136
|
+
get :index
|
137
|
+
end
|
138
|
+
end
|
139
|
+
```
|
140
|
+
|
141
|
+
### Configuration
|
142
|
+
|
143
|
+
There are some global configuration parameters (and their corresponding defaults):
|
144
|
+
|
145
|
+
```ruby
|
146
|
+
# Default scale factors to use.
|
147
|
+
# We use the smallest possible but representative scale factors by default.
|
148
|
+
self.default_scale_factors = [2, 3]
|
149
|
+
|
150
|
+
# Print performed queries if true in the case of failure
|
151
|
+
self.verbose = false
|
152
|
+
|
153
|
+
# Ignore matching queries
|
154
|
+
self.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
|
155
|
+
|
156
|
+
# ActiveSupport notifications event to track queries.
|
157
|
+
# We track ActiveRecord event by default,
|
158
|
+
# but can also track rom-rb events ('sql.rom') as well.
|
159
|
+
self.event = 'sql.active_record'
|
160
|
+
```
|
161
|
+
|
162
|
+
## How does it work?
|
163
|
+
|
164
|
+
Take a look at our [Executor](https://github.com/palkan/test-prof/tree/master/lib/n_plus_one_control/executor.rb) to figure out what's under the hood.
|
165
|
+
|
166
|
+
## What's next?
|
167
|
+
|
168
|
+
It may be useful to provide more matchers/assertions, for example:
|
169
|
+
|
170
|
+
```ruby
|
171
|
+
|
172
|
+
# Actually, that means that it is N+1))
|
173
|
+
assert_linear_number_of_queries { ... }
|
174
|
+
|
175
|
+
# But we can tune it with `coef` and handle such cases as selecting in batches
|
176
|
+
assert_linear_number_of_queries(coef: 0.1) do
|
177
|
+
Post.find_in_batches { ... }
|
178
|
+
end
|
179
|
+
|
180
|
+
# probably, also make sense to add another curve types
|
181
|
+
assert_logarithmic_number_of_queries { ... }
|
182
|
+
```
|
183
|
+
|
184
|
+
If you want to discuss or implement any of these, feel free to open an [issue]() or propose a [pull request]().
|
185
|
+
|
186
|
+
## Development
|
187
|
+
|
188
|
+
```sh
|
189
|
+
# install deps
|
190
|
+
bundle install
|
191
|
+
|
192
|
+
# run tests
|
193
|
+
bundle exec rake
|
194
|
+
```
|
195
|
+
|
196
|
+
## Contributing
|
197
|
+
|
198
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/palkan/n_plus_one_control.
|
199
|
+
|
200
|
+
|
201
|
+
## License
|
202
|
+
|
203
|
+
The gem is available as open source under the terms of the [MIT License](http://opensource.org/licenses/MIT).
|
204
|
+
|
data/Rakefile
ADDED
@@ -0,0 +1,13 @@
|
|
1
|
+
require "bundler/gem_tasks"
|
2
|
+
require "rspec/core/rake_task"
|
3
|
+
require "rubocop/rake_task"
|
4
|
+
require "rake/testtask"
|
5
|
+
|
6
|
+
Rake::TestTask.new do |t|
|
7
|
+
t.test_files = FileList['tests/**/*_test.rb']
|
8
|
+
end
|
9
|
+
|
10
|
+
RuboCop::RakeTask.new
|
11
|
+
RSpec::Core::RakeTask.new(:spec)
|
12
|
+
|
13
|
+
task :default => [:spec, :test, :rubocop]
|
data/bin/console
ADDED
@@ -0,0 +1,14 @@
|
|
1
|
+
#!/usr/bin/env ruby
|
2
|
+
|
3
|
+
require "bundler/setup"
|
4
|
+
require "n_plus_one_control"
|
5
|
+
|
6
|
+
# You can add fixtures and/or initialization code here to make experimenting
|
7
|
+
# with your gem easier. You can also use a different console, if you like.
|
8
|
+
|
9
|
+
# (If you use this, don't forget to add pry to your Gemfile!)
|
10
|
+
# require "pry"
|
11
|
+
# Pry.start
|
12
|
+
|
13
|
+
require "irb"
|
14
|
+
IRB.start
|
data/bin/setup
ADDED
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "n_plus_one_control/version"
|
4
|
+
require "n_plus_one_control/executor"
|
5
|
+
|
6
|
+
# RSpec and Minitest matchers to prevent N+1 queries problem.
|
7
|
+
module NPlusOneControl
|
8
|
+
class << self
|
9
|
+
attr_accessor :default_scale_factors, :verbose, :ignore, :event
|
10
|
+
|
11
|
+
def failure_message(queries)
|
12
|
+
msg = ["Expected to make the same number of queries, but got:\n"]
|
13
|
+
queries.each do |(scale, data)|
|
14
|
+
msg << " #{data.size} for N=#{scale}\n"
|
15
|
+
msg << data.map { |sql| " #{sql}\n" }.join.to_s if verbose
|
16
|
+
end
|
17
|
+
msg.join
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Scale factors to use.
|
22
|
+
# Use the smallest possible but representative scale factors by default.
|
23
|
+
self.default_scale_factors = [2, 3]
|
24
|
+
|
25
|
+
# Print performed queries if true
|
26
|
+
self.verbose = false
|
27
|
+
|
28
|
+
# Ignore matching queries
|
29
|
+
self.ignore = /^(BEGIN|COMMIT|SAVEPOINT|RELEASE)/
|
30
|
+
|
31
|
+
# ActiveSupport notifications event to track queries.
|
32
|
+
# We track ActiveRecord event by default,
|
33
|
+
# but can also track rom-rb events ('sql.rom') as well.
|
34
|
+
self.event = 'sql.active_record'
|
35
|
+
end
|
@@ -0,0 +1,55 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NPlusOneControl
|
4
|
+
# Runs code for every scale factor
|
5
|
+
# and returns collected queries.
|
6
|
+
module Executor
|
7
|
+
# Subscribes to ActiveSupport notifications and collect matching queries.
|
8
|
+
class Collector
|
9
|
+
def initialize(pattern)
|
10
|
+
@pattern = pattern
|
11
|
+
end
|
12
|
+
|
13
|
+
def call
|
14
|
+
@queries = []
|
15
|
+
ActiveSupport::Notifications
|
16
|
+
.subscribed(method(:callback), NPlusOneControl.event) do
|
17
|
+
yield
|
18
|
+
end
|
19
|
+
@queries
|
20
|
+
end
|
21
|
+
|
22
|
+
def callback(_name, _start, _finish, _message_id, values)
|
23
|
+
return if %w[CACHE SCHEMA].include? values[:name]
|
24
|
+
@queries << values[:sql] if @pattern.nil? || (values[:sql] =~ @pattern)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
class << self
|
29
|
+
def call(population:, scale_factors: nil, matching: nil)
|
30
|
+
raise ArgumentError, "Block is required!" unless block_given?
|
31
|
+
|
32
|
+
results = []
|
33
|
+
collector = Collector.new(matching)
|
34
|
+
|
35
|
+
(scale_factors || NPlusOneControl.default_scale_factors).each do |scale|
|
36
|
+
with_transaction do
|
37
|
+
population.call(scale)
|
38
|
+
results << [scale, collector.call { yield }]
|
39
|
+
end
|
40
|
+
end
|
41
|
+
results
|
42
|
+
end
|
43
|
+
|
44
|
+
private
|
45
|
+
|
46
|
+
def with_transaction
|
47
|
+
return yield unless defined?(ActiveRecord)
|
48
|
+
ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
49
|
+
yield
|
50
|
+
ensure
|
51
|
+
ActiveRecord::Base.connection.rollback_transaction
|
52
|
+
end
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
@@ -0,0 +1,29 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "n_plus_one_control"
|
4
|
+
|
5
|
+
module NPlusOneControl
|
6
|
+
# Minitest assertions
|
7
|
+
module MinitestHelper
|
8
|
+
def assert_perform_constant_number_of_queries(
|
9
|
+
populate: nil,
|
10
|
+
matching: nil,
|
11
|
+
scale_factors: nil
|
12
|
+
)
|
13
|
+
|
14
|
+
raise ArgumentError, "Block is required" unless block_given?
|
15
|
+
|
16
|
+
queries = NPlusOneControl::Executor.call(
|
17
|
+
population: populate || method(:populate),
|
18
|
+
matching: matching || /^SELECT/i,
|
19
|
+
scale_factors: scale_factors || NPlusOneControl.default_scale_factors
|
20
|
+
) { yield }
|
21
|
+
|
22
|
+
counts = queries.map(&:last).map(&:size)
|
23
|
+
|
24
|
+
assert counts.max == counts.min, NPlusOneControl.failure_message(queries)
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
Minitest::Test.include NPlusOneControl::MinitestHelper
|
@@ -0,0 +1,15 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "n_plus_one_control"
|
4
|
+
require "n_plus_one_control/rspec/dsl"
|
5
|
+
require "n_plus_one_control/rspec/matcher"
|
6
|
+
require "n_plus_one_control/rspec/context"
|
7
|
+
|
8
|
+
module NPlusOneControl
|
9
|
+
module RSpec # :nodoc:
|
10
|
+
end
|
11
|
+
end
|
12
|
+
|
13
|
+
::RSpec.configure do |config|
|
14
|
+
config.extend NPlusOneControl::RSpec::DSL, n_plus_one: true
|
15
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
::RSpec.shared_context "n_plus_one_control", n_plus_one: true do
|
4
|
+
# Helper to access populate block from within example/matcher
|
5
|
+
let(:n_plus_one_populate) do |ex|
|
6
|
+
if ex.example_group.populate.nil?
|
7
|
+
raise(
|
8
|
+
<<~MSG
|
9
|
+
Populate block is missing!
|
10
|
+
|
11
|
+
Please provide populate callback, e.g.:
|
12
|
+
|
13
|
+
populate { |n| n.times { create_some_stuff } }
|
14
|
+
MSG
|
15
|
+
)
|
16
|
+
end
|
17
|
+
->(n) { ex.instance_exec(n, &ex.example_group.populate) }
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,16 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module NPlusOneControl
|
4
|
+
module RSpec
|
5
|
+
# Extends RSpec ExampleGroup with populate method
|
6
|
+
module DSL
|
7
|
+
# Setup populate callback, which is used
|
8
|
+
# to prepare data for each run.
|
9
|
+
def populate
|
10
|
+
return @populate unless block_given?
|
11
|
+
|
12
|
+
@populate = Proc.new
|
13
|
+
end
|
14
|
+
end
|
15
|
+
end
|
16
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
# rubocop:disable Metrics/BlockLength
|
4
|
+
::RSpec::Matchers.define :perform_constant_number_of_queries do
|
5
|
+
supports_block_expectations
|
6
|
+
|
7
|
+
chain :with_scale_factors do |*factors|
|
8
|
+
@factors = factors
|
9
|
+
end
|
10
|
+
|
11
|
+
chain :matching do |pattern|
|
12
|
+
@pattern = pattern
|
13
|
+
end
|
14
|
+
|
15
|
+
match do |actual, *_args|
|
16
|
+
raise ArgumentError, "Block is required" unless actual.is_a? Proc
|
17
|
+
|
18
|
+
raise "Missing tag :n_plus_one" unless
|
19
|
+
@matcher_execution_context.respond_to?(:n_plus_one_populate)
|
20
|
+
|
21
|
+
populate = @matcher_execution_context.n_plus_one_populate
|
22
|
+
|
23
|
+
# by default we're looking for select queries
|
24
|
+
pattern = @pattern || /^SELECT/i
|
25
|
+
|
26
|
+
@queries = NPlusOneControl::Executor.call(
|
27
|
+
population: populate,
|
28
|
+
matching: pattern,
|
29
|
+
scale_factors: @factors,
|
30
|
+
&actual
|
31
|
+
)
|
32
|
+
|
33
|
+
counts = @queries.map(&:last).map(&:size)
|
34
|
+
|
35
|
+
counts.max == counts.min
|
36
|
+
end
|
37
|
+
|
38
|
+
match_when_negated do |_actual|
|
39
|
+
raise "This matcher doesn't support negation"
|
40
|
+
end
|
41
|
+
|
42
|
+
failure_message { |_actual| NPlusOneControl.failure_message(@queries) }
|
43
|
+
end
|
@@ -0,0 +1,46 @@
|
|
1
|
+
# coding: utf-8
|
2
|
+
lib = File.expand_path('../lib', __FILE__)
|
3
|
+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
|
4
|
+
require 'n_plus_one_control/version'
|
5
|
+
|
6
|
+
Gem::Specification.new do |spec|
|
7
|
+
spec.name = "n_plus_one_control"
|
8
|
+
spec.version = NPlusOneControl::VERSION
|
9
|
+
spec.authors = ["palkan"]
|
10
|
+
spec.email = ["dementiev.vm@gmail.com"]
|
11
|
+
|
12
|
+
spec.summary = "RSpec and Minitest matchers to prevent N+1 queries problem"
|
13
|
+
spec.description = %{
|
14
|
+
RSpec and Minitest matchers to prevent N+1 queries problem.
|
15
|
+
|
16
|
+
Evaluates code under consideration several times with different scale factors
|
17
|
+
to make sure that the number of DB queries behaves as expected (i.e. O(1) instead of O(N)).
|
18
|
+
|
19
|
+
Example:
|
20
|
+
|
21
|
+
```ruby
|
22
|
+
context "N+1", :n_plus_one do
|
23
|
+
populate { |n| create_list(:post, n) }
|
24
|
+
|
25
|
+
specify do
|
26
|
+
expect { get :index }.to perform_constant_number_of_queries
|
27
|
+
end
|
28
|
+
end
|
29
|
+
```
|
30
|
+
}
|
31
|
+
spec.homepage = "http://github.com/palkan/n_plus_one_control"
|
32
|
+
spec.license = "MIT"
|
33
|
+
|
34
|
+
spec.files = `git ls-files`.split($/)
|
35
|
+
spec.require_paths = ["lib"]
|
36
|
+
|
37
|
+
spec.add_development_dependency "bundler", "~> 1.10"
|
38
|
+
spec.add_development_dependency "rake", "~> 10.0"
|
39
|
+
spec.add_development_dependency "rspec", "~> 3.5"
|
40
|
+
spec.add_development_dependency "minitest", "~> 5.9"
|
41
|
+
spec.add_development_dependency "factory_girl", "~> 4.8.0"
|
42
|
+
spec.add_development_dependency "rubocop", "~> 0.49"
|
43
|
+
spec.add_development_dependency "activerecord", "~> 5.1"
|
44
|
+
spec.add_development_dependency "sqlite3"
|
45
|
+
spec.add_development_dependency "pry-byebug"
|
46
|
+
end
|
@@ -0,0 +1,65 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe NPlusOneControl::Executor do
|
6
|
+
let(:populate) do
|
7
|
+
->(n) { create_list(:post, n) }
|
8
|
+
end
|
9
|
+
|
10
|
+
let(:observable) do
|
11
|
+
-> { Post.find_each(&:user) }
|
12
|
+
end
|
13
|
+
|
14
|
+
it "raises when block is missing" do
|
15
|
+
expect { described_class.call(population: populate) }
|
16
|
+
.to raise_error(ArgumentError, "Block is required!")
|
17
|
+
end
|
18
|
+
|
19
|
+
it "raises when populate is missing" do
|
20
|
+
expect { described_class.call(&observable) }
|
21
|
+
.to raise_error(ArgumentError, /population/)
|
22
|
+
end
|
23
|
+
|
24
|
+
it "returns correct counts for default scales" do
|
25
|
+
result = described_class.call(
|
26
|
+
population: populate,
|
27
|
+
&observable
|
28
|
+
)
|
29
|
+
|
30
|
+
expect(result.size).to eq 2
|
31
|
+
expect(result.first[0]).to eq 2
|
32
|
+
expect(result.first[1].size).to eq 3
|
33
|
+
expect(result.last[0]).to eq 3
|
34
|
+
expect(result.last[1].size).to eq 4
|
35
|
+
end
|
36
|
+
|
37
|
+
it "returns correct counts for custom scales" do
|
38
|
+
result = described_class.call(
|
39
|
+
population: populate,
|
40
|
+
scale_factors: [5, 10, 100],
|
41
|
+
&observable
|
42
|
+
)
|
43
|
+
|
44
|
+
expect(result.size).to eq 3
|
45
|
+
expect(result.first[0]).to eq 5
|
46
|
+
expect(result.first[1].size).to eq 6
|
47
|
+
expect(result.second[0]).to eq 10
|
48
|
+
expect(result.second[1].size).to eq 11
|
49
|
+
expect(result.last[0]).to eq 100
|
50
|
+
expect(result.last[1].size).to eq 101
|
51
|
+
end
|
52
|
+
|
53
|
+
it "returns correct counts with custom match" do
|
54
|
+
result = described_class.call(
|
55
|
+
population: populate,
|
56
|
+
matching: /users/,
|
57
|
+
&observable
|
58
|
+
)
|
59
|
+
|
60
|
+
expect(result.first[0]).to eq 2
|
61
|
+
expect(result.first[1].size).to eq 2
|
62
|
+
expect(result.last[0]).to eq 3
|
63
|
+
expect(result.last[1].size).to eq 3
|
64
|
+
end
|
65
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "spec_helper"
|
4
|
+
|
5
|
+
describe NPlusOneControl::RSpec do
|
6
|
+
context "when no N+1", :n_plus_one do
|
7
|
+
populate { |n| create_list(:post, n) }
|
8
|
+
|
9
|
+
specify do
|
10
|
+
expect { Post.preload(:user).find_each { |p| p.user.name } }
|
11
|
+
.to perform_constant_number_of_queries
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
context "when has N+1", :n_plus_one do
|
16
|
+
populate { |n| create_list(:post, n) }
|
17
|
+
|
18
|
+
specify do
|
19
|
+
expect do
|
20
|
+
expect { Post.find_each { |p| p.user.name } }
|
21
|
+
.to perform_constant_number_of_queries
|
22
|
+
end.to raise_error(RSpec::Expectations::ExpectationNotMetError)
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
context "when context is missing" do
|
27
|
+
specify do
|
28
|
+
expect do
|
29
|
+
expect { subject }.to perform_constant_number_of_queries
|
30
|
+
end.to raise_error(/missing tag/i)
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
context "when populate is missing", :n_plus_one do
|
35
|
+
specify do
|
36
|
+
expect do
|
37
|
+
expect { subject }.to perform_constant_number_of_queries
|
38
|
+
end.to raise_error(/please provide populate/i)
|
39
|
+
end
|
40
|
+
end
|
41
|
+
|
42
|
+
context "when negated" do
|
43
|
+
specify do
|
44
|
+
expect do
|
45
|
+
expect { subject }.not_to perform_constant_number_of_queries
|
46
|
+
end.to raise_error(/support negation/i)
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
context "when verbose", :n_plus_one do
|
51
|
+
populate { |n| create_list(:post, n) }
|
52
|
+
|
53
|
+
around(:each) do |ex|
|
54
|
+
NPlusOneControl.verbose = true
|
55
|
+
ex.run
|
56
|
+
NPlusOneControl.verbose = false
|
57
|
+
end
|
58
|
+
|
59
|
+
specify do
|
60
|
+
expect do
|
61
|
+
expect { Post.find_each { |p| p.user.name } }
|
62
|
+
.to perform_constant_number_of_queries
|
63
|
+
end.to raise_error(RSpec::Expectations::ExpectationNotMetError, /select .+ from/i)
|
64
|
+
end
|
65
|
+
end
|
66
|
+
|
67
|
+
context "with scale_factors", :n_plus_one do
|
68
|
+
populate { |n| create_list(:post, n) }
|
69
|
+
|
70
|
+
specify do
|
71
|
+
expect { Post.find_each { |p| p.user.name } }
|
72
|
+
.to perform_constant_number_of_queries.with_scale_factors(1, 1)
|
73
|
+
end
|
74
|
+
end
|
75
|
+
|
76
|
+
context "with matching", :n_plus_one do
|
77
|
+
populate { |n| create_list(:post, n) }
|
78
|
+
|
79
|
+
specify do
|
80
|
+
expect { Post.find_each { |p| p.user.name } }
|
81
|
+
.to perform_constant_number_of_queries.matching(/posts/)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
data/spec/spec_helper.rb
ADDED
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
$LOAD_PATH.unshift File.expand_path("../../lib", __FILE__)
|
4
|
+
require "n_plus_one_control/rspec"
|
5
|
+
require "benchmark"
|
6
|
+
require "active_record"
|
7
|
+
require "factory_girl"
|
8
|
+
require "pry-byebug"
|
9
|
+
|
10
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
11
|
+
|
12
|
+
Dir["#{File.dirname(__FILE__)}/support/**/*.rb"].each { |f| require f }
|
13
|
+
|
14
|
+
RSpec.configure do |config|
|
15
|
+
config.mock_with :rspec
|
16
|
+
|
17
|
+
config.order = :random
|
18
|
+
config.filter_run focus: true
|
19
|
+
config.run_all_when_everything_filtered = true
|
20
|
+
|
21
|
+
config.include FactoryGirl::Syntax::Methods
|
22
|
+
|
23
|
+
config.before(:each) do
|
24
|
+
ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
25
|
+
end
|
26
|
+
|
27
|
+
config.after(:each) do
|
28
|
+
ActiveRecord::Base.connection.rollback_transaction
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,19 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ActiveRecord::Schema.define do
|
4
|
+
create_table :posts do |t|
|
5
|
+
t.string :title
|
6
|
+
t.integer :user_id
|
7
|
+
end
|
8
|
+
end
|
9
|
+
|
10
|
+
class Post < ActiveRecord::Base
|
11
|
+
belongs_to :user
|
12
|
+
end
|
13
|
+
|
14
|
+
FactoryGirl.define do
|
15
|
+
factory :post do
|
16
|
+
title "Title"
|
17
|
+
user
|
18
|
+
end
|
19
|
+
end
|
@@ -0,0 +1,17 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
ActiveRecord::Schema.define do
|
4
|
+
create_table :users do |t|
|
5
|
+
t.string :name
|
6
|
+
end
|
7
|
+
end
|
8
|
+
|
9
|
+
class User < ActiveRecord::Base
|
10
|
+
has_many :posts
|
11
|
+
end
|
12
|
+
|
13
|
+
FactoryGirl.define do
|
14
|
+
factory :user do
|
15
|
+
name "John"
|
16
|
+
end
|
17
|
+
end
|
@@ -0,0 +1,63 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require_relative "test_helper"
|
4
|
+
|
5
|
+
class TestMinitest < Minitest::Test
|
6
|
+
def test_no_n_plus_one_error
|
7
|
+
populate = ->(n) { create_list(:post, n) }
|
8
|
+
|
9
|
+
assert_perform_constant_number_of_queries(populate: populate) do
|
10
|
+
Post.preload(:user).find_each { |p| p.user.name }
|
11
|
+
end
|
12
|
+
end
|
13
|
+
|
14
|
+
def test_with_n_plus_one_error
|
15
|
+
populate = ->(n) { create_list(:post, n) }
|
16
|
+
|
17
|
+
e = assert_raises Minitest::Assertion do
|
18
|
+
assert_perform_constant_number_of_queries(populate: populate) do
|
19
|
+
Post.find_each { |p| p.user.name }
|
20
|
+
end
|
21
|
+
end
|
22
|
+
|
23
|
+
assert_match "Expected to make the same number of queries", e.message
|
24
|
+
assert_match "3 for N=2", e.message
|
25
|
+
assert_match "4 for N=3", e.message
|
26
|
+
end
|
27
|
+
|
28
|
+
def test_no_n_plus_one_error_with_scale_factors
|
29
|
+
populate = ->(n) { create_list(:post, n) }
|
30
|
+
|
31
|
+
assert_perform_constant_number_of_queries(
|
32
|
+
populate: populate,
|
33
|
+
scale_factors: [1, 1]
|
34
|
+
) do
|
35
|
+
Post.find_each { |p| p.user.name }
|
36
|
+
end
|
37
|
+
end
|
38
|
+
|
39
|
+
def test_no_n_plus_one_error_with_matching
|
40
|
+
populate = ->(n) { create_list(:post, n) }
|
41
|
+
|
42
|
+
assert_perform_constant_number_of_queries(
|
43
|
+
populate: populate,
|
44
|
+
matching: /posts/
|
45
|
+
) do
|
46
|
+
Post.find_each { |p| p.user.name }
|
47
|
+
end
|
48
|
+
end
|
49
|
+
|
50
|
+
def populate(n)
|
51
|
+
create_list(:post, n)
|
52
|
+
end
|
53
|
+
|
54
|
+
def test_fallback_to_populate_method
|
55
|
+
e = assert_raises Minitest::Assertion do
|
56
|
+
assert_perform_constant_number_of_queries do
|
57
|
+
Post.find_each { |p| p.user.name }
|
58
|
+
end
|
59
|
+
end
|
60
|
+
|
61
|
+
assert_match "Expected to make the same number of queries", e.message
|
62
|
+
end
|
63
|
+
end
|
@@ -0,0 +1,32 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "minitest/autorun"
|
4
|
+
require "minitest/pride"
|
5
|
+
|
6
|
+
$LOAD_PATH << File.expand_path("../../lib", __FILE__)
|
7
|
+
Thread.abort_on_exception = true
|
8
|
+
|
9
|
+
require "n_plus_one_control/minitest"
|
10
|
+
require "benchmark"
|
11
|
+
require "active_record"
|
12
|
+
require "factory_girl"
|
13
|
+
require "pry-byebug"
|
14
|
+
|
15
|
+
ActiveRecord::Base.establish_connection(adapter: 'sqlite3', database: ':memory:')
|
16
|
+
|
17
|
+
Dir["#{File.dirname(__FILE__)}/../spec/support/**/*.rb"].each { |f| require f }
|
18
|
+
|
19
|
+
module TransactionalTests
|
20
|
+
def setup
|
21
|
+
ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
22
|
+
super
|
23
|
+
end
|
24
|
+
|
25
|
+
def teardown
|
26
|
+
super
|
27
|
+
ActiveRecord::Base.connection.rollback_transaction
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
Minitest::Test.prepend TransactionalTests
|
32
|
+
Minitest::Test.include FactoryGirl::Syntax::Methods
|
metadata
ADDED
@@ -0,0 +1,203 @@
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
2
|
+
name: n_plus_one_control
|
3
|
+
version: !ruby/object:Gem::Version
|
4
|
+
version: 0.0.3
|
5
|
+
platform: ruby
|
6
|
+
authors:
|
7
|
+
- palkan
|
8
|
+
autorequire:
|
9
|
+
bindir: bin
|
10
|
+
cert_chain: []
|
11
|
+
date: 2017-06-06 00:00:00.000000000 Z
|
12
|
+
dependencies:
|
13
|
+
- !ruby/object:Gem::Dependency
|
14
|
+
name: bundler
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
16
|
+
requirements:
|
17
|
+
- - "~>"
|
18
|
+
- !ruby/object:Gem::Version
|
19
|
+
version: '1.10'
|
20
|
+
type: :development
|
21
|
+
prerelease: false
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
23
|
+
requirements:
|
24
|
+
- - "~>"
|
25
|
+
- !ruby/object:Gem::Version
|
26
|
+
version: '1.10'
|
27
|
+
- !ruby/object:Gem::Dependency
|
28
|
+
name: rake
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
30
|
+
requirements:
|
31
|
+
- - "~>"
|
32
|
+
- !ruby/object:Gem::Version
|
33
|
+
version: '10.0'
|
34
|
+
type: :development
|
35
|
+
prerelease: false
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
37
|
+
requirements:
|
38
|
+
- - "~>"
|
39
|
+
- !ruby/object:Gem::Version
|
40
|
+
version: '10.0'
|
41
|
+
- !ruby/object:Gem::Dependency
|
42
|
+
name: rspec
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
44
|
+
requirements:
|
45
|
+
- - "~>"
|
46
|
+
- !ruby/object:Gem::Version
|
47
|
+
version: '3.5'
|
48
|
+
type: :development
|
49
|
+
prerelease: false
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
51
|
+
requirements:
|
52
|
+
- - "~>"
|
53
|
+
- !ruby/object:Gem::Version
|
54
|
+
version: '3.5'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: minitest
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '5.9'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '5.9'
|
69
|
+
- !ruby/object:Gem::Dependency
|
70
|
+
name: factory_girl
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
72
|
+
requirements:
|
73
|
+
- - "~>"
|
74
|
+
- !ruby/object:Gem::Version
|
75
|
+
version: 4.8.0
|
76
|
+
type: :development
|
77
|
+
prerelease: false
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
79
|
+
requirements:
|
80
|
+
- - "~>"
|
81
|
+
- !ruby/object:Gem::Version
|
82
|
+
version: 4.8.0
|
83
|
+
- !ruby/object:Gem::Dependency
|
84
|
+
name: rubocop
|
85
|
+
requirement: !ruby/object:Gem::Requirement
|
86
|
+
requirements:
|
87
|
+
- - "~>"
|
88
|
+
- !ruby/object:Gem::Version
|
89
|
+
version: '0.49'
|
90
|
+
type: :development
|
91
|
+
prerelease: false
|
92
|
+
version_requirements: !ruby/object:Gem::Requirement
|
93
|
+
requirements:
|
94
|
+
- - "~>"
|
95
|
+
- !ruby/object:Gem::Version
|
96
|
+
version: '0.49'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: activerecord
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: '5.1'
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: '5.1'
|
111
|
+
- !ruby/object:Gem::Dependency
|
112
|
+
name: sqlite3
|
113
|
+
requirement: !ruby/object:Gem::Requirement
|
114
|
+
requirements:
|
115
|
+
- - ">="
|
116
|
+
- !ruby/object:Gem::Version
|
117
|
+
version: '0'
|
118
|
+
type: :development
|
119
|
+
prerelease: false
|
120
|
+
version_requirements: !ruby/object:Gem::Requirement
|
121
|
+
requirements:
|
122
|
+
- - ">="
|
123
|
+
- !ruby/object:Gem::Version
|
124
|
+
version: '0'
|
125
|
+
- !ruby/object:Gem::Dependency
|
126
|
+
name: pry-byebug
|
127
|
+
requirement: !ruby/object:Gem::Requirement
|
128
|
+
requirements:
|
129
|
+
- - ">="
|
130
|
+
- !ruby/object:Gem::Version
|
131
|
+
version: '0'
|
132
|
+
type: :development
|
133
|
+
prerelease: false
|
134
|
+
version_requirements: !ruby/object:Gem::Requirement
|
135
|
+
requirements:
|
136
|
+
- - ">="
|
137
|
+
- !ruby/object:Gem::Version
|
138
|
+
version: '0'
|
139
|
+
description: "\n RSpec and Minitest matchers to prevent N+1 queries problem.\n\n
|
140
|
+
\ Evaluates code under consideration several times with different scale factors\n
|
141
|
+
\ to make sure that the number of DB queries behaves as expected (i.e. O(1) instead
|
142
|
+
of O(N)).\n\n Example:\n\n ```ruby\n context \"N+1\", :n_plus_one do\n
|
143
|
+
\ populate { |n| create_list(:post, n) }\n\n specify do\n expect
|
144
|
+
{ get :index }.to perform_constant_number_of_queries\n end\n end\n ```\n
|
145
|
+
\ "
|
146
|
+
email:
|
147
|
+
- dementiev.vm@gmail.com
|
148
|
+
executables: []
|
149
|
+
extensions: []
|
150
|
+
extra_rdoc_files: []
|
151
|
+
files:
|
152
|
+
- ".gitignore"
|
153
|
+
- ".rspec"
|
154
|
+
- ".rubocop.yml"
|
155
|
+
- ".travis.yml"
|
156
|
+
- Gemfile
|
157
|
+
- LICENSE.txt
|
158
|
+
- README.md
|
159
|
+
- Rakefile
|
160
|
+
- bin/console
|
161
|
+
- bin/setup
|
162
|
+
- lib/n_plus_one_control.rb
|
163
|
+
- lib/n_plus_one_control/executor.rb
|
164
|
+
- lib/n_plus_one_control/minitest.rb
|
165
|
+
- lib/n_plus_one_control/rspec.rb
|
166
|
+
- lib/n_plus_one_control/rspec/context.rb
|
167
|
+
- lib/n_plus_one_control/rspec/dsl.rb
|
168
|
+
- lib/n_plus_one_control/rspec/matcher.rb
|
169
|
+
- lib/n_plus_one_control/version.rb
|
170
|
+
- n_plus_one_control.gemspec
|
171
|
+
- spec/n_plus_one_control/executor_spec.rb
|
172
|
+
- spec/n_plus_one_control/rspec_spec.rb
|
173
|
+
- spec/n_plus_one_control_spec.rb
|
174
|
+
- spec/spec_helper.rb
|
175
|
+
- spec/support/post.rb
|
176
|
+
- spec/support/user.rb
|
177
|
+
- tests/minitest_test.rb
|
178
|
+
- tests/test_helper.rb
|
179
|
+
homepage: http://github.com/palkan/n_plus_one_control
|
180
|
+
licenses:
|
181
|
+
- MIT
|
182
|
+
metadata: {}
|
183
|
+
post_install_message:
|
184
|
+
rdoc_options: []
|
185
|
+
require_paths:
|
186
|
+
- lib
|
187
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
188
|
+
requirements:
|
189
|
+
- - ">="
|
190
|
+
- !ruby/object:Gem::Version
|
191
|
+
version: '0'
|
192
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
193
|
+
requirements:
|
194
|
+
- - ">="
|
195
|
+
- !ruby/object:Gem::Version
|
196
|
+
version: '0'
|
197
|
+
requirements: []
|
198
|
+
rubyforge_project:
|
199
|
+
rubygems_version: 2.6.4
|
200
|
+
signing_key:
|
201
|
+
specification_version: 4
|
202
|
+
summary: RSpec and Minitest matchers to prevent N+1 queries problem
|
203
|
+
test_files: []
|