n_plus_one_control 0.3.1 → 0.4.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 +8 -0
- data/LICENSE.txt +1 -1
- data/README.md +30 -0
- data/lib/n_plus_one_control/executor.rb +44 -22
- data/lib/n_plus_one_control/minitest.rb +19 -7
- data/lib/n_plus_one_control/rspec.rb +4 -1
- data/lib/n_plus_one_control/rspec/context.rb +1 -10
- data/lib/n_plus_one_control/rspec/dsl.rb +21 -12
- data/lib/n_plus_one_control/rspec/matcher.rb +4 -3
- data/lib/n_plus_one_control/version.rb +1 -1
- metadata +12 -24
- data/.gitignore +0 -11
- data/.rspec +0 -2
- data/.rubocop.yml +0 -78
- data/.travis.yml +0 -9
- data/Gemfile +0 -4
- data/Rakefile +0 -13
- data/bin/console +0 -14
- data/bin/setup +0 -8
- data/n_plus_one_control.gemspec +0 -35
- data/spec/n_plus_one_control/executor_spec.rb +0 -65
- data/spec/n_plus_one_control/rspec_spec.rb +0 -113
- data/spec/n_plus_one_control_spec.rb +0 -9
- data/spec/spec_helper.rb +0 -30
- data/spec/support/post.rb +0 -19
- data/spec/support/user.rb +0 -17
- data/tests/minitest_test.rb +0 -63
- data/tests/test_helper.rb +0 -32
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 227fb8257ab67628ffd449aff0358d729dd3ff3eafc291f9fca427aa355e0650
|
4
|
+
data.tar.gz: 2e65e5f09da3ad989fbb1e2418a657c8142ea8d25540a0daf729adeb9f0b0d5b
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e79cf5bd9ec1afd8b10ca184a483edd9e8d8fbbeaca62e147d277102715ead2fa909d720eb0085fc6d2c2441abb5e61aa08f8a081a4b37079b3f552694d3e220
|
7
|
+
data.tar.gz: 59c76b177dc8b6ad4d58cfbad1f3c3e89204eef6e92d2d39989b5829f714939c77d0d38806e935d3624e20469447607f7b403af2b2bd633a02486e82c6cc4f6c
|
data/CHANGELOG.md
ADDED
data/LICENSE.txt
CHANGED
data/README.md
CHANGED
@@ -92,6 +92,26 @@ expect { ... }.to perform_constant_number_of_queries.matching(/INSERT/)
|
|
92
92
|
expect { ... }.to perform_constant_number_of_queries.with_scale_factors(10, 100)
|
93
93
|
```
|
94
94
|
|
95
|
+
#### Using scale factor in spec
|
96
|
+
|
97
|
+
Let's suppose your action accepts parameter, which can make impact on the number of returned records:
|
98
|
+
|
99
|
+
```ruby
|
100
|
+
get :index, params: { per_page: 10 }
|
101
|
+
```
|
102
|
+
|
103
|
+
Then it is enough to just change `per_page` parameter between executions and do not recreate records in DB. For this purpose, you can use `current_scale` method in your example:
|
104
|
+
|
105
|
+
```ruby
|
106
|
+
context "N+1", :n_plus_one do
|
107
|
+
before { create_list :post, 3 }
|
108
|
+
|
109
|
+
specify do
|
110
|
+
expect { get :index, params: { per_page: current_scale } }.to perform_constant_number_of_queries
|
111
|
+
end
|
112
|
+
end
|
113
|
+
```
|
114
|
+
|
95
115
|
### Minitest
|
96
116
|
|
97
117
|
First, add NPlusOneControl to your `test_helper.rb`:
|
@@ -147,6 +167,16 @@ def test_no_n_plus_one_error
|
|
147
167
|
end
|
148
168
|
```
|
149
169
|
|
170
|
+
As in RSpec, you can use `current_scale` factor instead of `populate` block:
|
171
|
+
|
172
|
+
```ruby
|
173
|
+
def test_no_n_plus_one_error
|
174
|
+
assert_perform_constant_number_of_queries do
|
175
|
+
get :index, params: { per_page: current_scale }
|
176
|
+
end
|
177
|
+
end
|
178
|
+
```
|
179
|
+
|
150
180
|
### With caching
|
151
181
|
|
152
182
|
If you use caching you can face the problem when first request performs more DB queries than others. The solution is:
|
@@ -3,7 +3,7 @@
|
|
3
3
|
module NPlusOneControl
|
4
4
|
# Runs code for every scale factor
|
5
5
|
# and returns collected queries.
|
6
|
-
|
6
|
+
class Executor
|
7
7
|
# Subscribes to ActiveSupport notifications and collect matching queries.
|
8
8
|
class Collector
|
9
9
|
def initialize(pattern)
|
@@ -29,37 +29,59 @@ module NPlusOneControl
|
|
29
29
|
class << self
|
30
30
|
attr_accessor :transaction_begin
|
31
31
|
attr_accessor :transaction_rollback
|
32
|
+
end
|
33
|
+
|
34
|
+
attr_reader :current_scale
|
35
|
+
|
36
|
+
self.transaction_begin = -> do
|
37
|
+
ActiveRecord::Base.connection.begin_transaction(joinable: false)
|
38
|
+
end
|
39
|
+
|
40
|
+
self.transaction_rollback = -> do
|
41
|
+
ActiveRecord::Base.connection.rollback_transaction
|
42
|
+
end
|
32
43
|
|
33
|
-
|
34
|
-
|
44
|
+
def initialize(population: nil, scale_factors: nil, matching: nil)
|
45
|
+
@population = population
|
46
|
+
@scale_factors = scale_factors
|
47
|
+
@matching = matching
|
48
|
+
end
|
49
|
+
|
50
|
+
# rubocop:disable Metrics/MethodLength
|
51
|
+
def call
|
52
|
+
raise ArgumentError, "Block is required!" unless block_given?
|
35
53
|
|
36
|
-
|
37
|
-
|
54
|
+
results = []
|
55
|
+
collector = Collector.new(matching)
|
38
56
|
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
57
|
+
(scale_factors || NPlusOneControl.default_scale_factors).each do |scale|
|
58
|
+
@current_scale = scale
|
59
|
+
with_transaction do
|
60
|
+
population&.call(scale)
|
61
|
+
results << [scale, collector.call { yield }]
|
44
62
|
end
|
45
|
-
results
|
46
63
|
end
|
64
|
+
results
|
65
|
+
end
|
66
|
+
# rubocop:enable Metrics/MethodLength
|
47
67
|
|
48
|
-
|
68
|
+
private
|
49
69
|
|
50
|
-
|
51
|
-
|
52
|
-
|
53
|
-
|
54
|
-
|
55
|
-
end
|
70
|
+
def with_transaction
|
71
|
+
transaction_begin.call
|
72
|
+
yield
|
73
|
+
ensure
|
74
|
+
transaction_rollback.call
|
56
75
|
end
|
57
76
|
|
58
|
-
|
59
|
-
|
77
|
+
def transaction_begin
|
78
|
+
self.class.transaction_begin
|
60
79
|
end
|
61
|
-
|
62
|
-
|
80
|
+
|
81
|
+
def transaction_rollback
|
82
|
+
self.class.transaction_rollback
|
63
83
|
end
|
84
|
+
|
85
|
+
attr_reader :population, :scale_factors, :matching
|
64
86
|
end
|
65
87
|
end
|
@@ -5,10 +5,6 @@ require "n_plus_one_control"
|
|
5
5
|
module NPlusOneControl
|
6
6
|
# Minitest assertions
|
7
7
|
module MinitestHelper
|
8
|
-
def warming_up(warmup)
|
9
|
-
(warmup || methods.include?(:warmup) ? method(:warmup) : nil)&.call
|
10
|
-
end
|
11
|
-
|
12
8
|
def assert_perform_constant_number_of_queries(
|
13
9
|
populate: nil,
|
14
10
|
matching: nil,
|
@@ -20,16 +16,32 @@ module NPlusOneControl
|
|
20
16
|
|
21
17
|
warming_up warmup
|
22
18
|
|
23
|
-
|
24
|
-
population:
|
19
|
+
@executor = NPlusOneControl::Executor.new(
|
20
|
+
population: population_proc(populate),
|
25
21
|
matching: matching || /^SELECT/i,
|
26
22
|
scale_factors: scale_factors || NPlusOneControl.default_scale_factors
|
27
|
-
)
|
23
|
+
)
|
24
|
+
|
25
|
+
queries = @executor.call { yield }
|
28
26
|
|
29
27
|
counts = queries.map(&:last).map(&:size)
|
30
28
|
|
31
29
|
assert counts.max == counts.min, NPlusOneControl.failure_message(queries)
|
32
30
|
end
|
31
|
+
|
32
|
+
def current_scale
|
33
|
+
@executor&.current_scale
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def warming_up(warmup)
|
39
|
+
(warmup || methods.include?(:warmup) ? method(:warmup) : nil)&.call
|
40
|
+
end
|
41
|
+
|
42
|
+
def population_proc(populate)
|
43
|
+
(populate || methods.include?(:populate) ? method(:populate) : nil)
|
44
|
+
end
|
33
45
|
end
|
34
46
|
end
|
35
47
|
|
@@ -1,5 +1,7 @@
|
|
1
1
|
# frozen_string_literal: true
|
2
2
|
|
3
|
+
gem "rspec-core", ">= 3.5"
|
4
|
+
|
3
5
|
require "n_plus_one_control"
|
4
6
|
require "n_plus_one_control/rspec/dsl"
|
5
7
|
require "n_plus_one_control/rspec/matcher"
|
@@ -11,5 +13,6 @@ module NPlusOneControl
|
|
11
13
|
end
|
12
14
|
|
13
15
|
::RSpec.configure do |config|
|
14
|
-
config.extend NPlusOneControl::RSpec::DSL, n_plus_one: true
|
16
|
+
config.extend NPlusOneControl::RSpec::DSL::ClassMethods, n_plus_one: true
|
17
|
+
config.include NPlusOneControl::RSpec::DSL, n_plus_one: true
|
15
18
|
end
|
@@ -3,17 +3,8 @@
|
|
3
3
|
RSpec.shared_context "n_plus_one_control" do
|
4
4
|
# Helper to access populate block from within example/matcher
|
5
5
|
let(:n_plus_one_populate) do |ex|
|
6
|
-
if ex.example_group.populate.nil?
|
7
|
-
raise(
|
8
|
-
<<-MSG
|
9
|
-
Populate block is missing!
|
6
|
+
return if ex.example_group.populate.nil?
|
10
7
|
|
11
|
-
Please provide populate callback, e.g.:
|
12
|
-
|
13
|
-
populate { |n| n.times { create_some_stuff } }
|
14
|
-
MSG
|
15
|
-
)
|
16
|
-
end
|
17
8
|
->(n) { ex.instance_exec(n, &ex.example_group.populate) }
|
18
9
|
end
|
19
10
|
|
@@ -2,23 +2,32 @@
|
|
2
2
|
|
3
3
|
module NPlusOneControl
|
4
4
|
module RSpec
|
5
|
-
#
|
5
|
+
# Includes scale method into RSpec Example
|
6
6
|
module DSL
|
7
|
-
#
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
7
|
+
# Extends RSpec ExampleGroup with populate & warmup methods
|
8
|
+
module ClassMethods
|
9
|
+
# Setup warmup block, wich will run before matching
|
10
|
+
# for example, if using cache, then later queries
|
11
|
+
# will perform less DB queries than first
|
12
|
+
def warmup
|
13
|
+
return @warmup unless block_given?
|
12
14
|
|
13
|
-
|
15
|
+
@warmup = Proc.new
|
16
|
+
end
|
17
|
+
|
18
|
+
# Setup populate callback, which is used
|
19
|
+
# to prepare data for each run.
|
20
|
+
def populate
|
21
|
+
return @populate unless block_given?
|
22
|
+
|
23
|
+
@populate = Proc.new
|
24
|
+
end
|
14
25
|
end
|
15
26
|
|
16
|
-
|
17
|
-
# to prepare data for each run.
|
18
|
-
def populate
|
19
|
-
return @populate unless block_given?
|
27
|
+
attr_accessor :executor
|
20
28
|
|
21
|
-
|
29
|
+
def current_scale
|
30
|
+
executor&.current_scale
|
22
31
|
end
|
23
32
|
end
|
24
33
|
end
|
@@ -30,13 +30,14 @@
|
|
30
30
|
# by default we're looking for select queries
|
31
31
|
pattern = @pattern || /^SELECT/i
|
32
32
|
|
33
|
-
@
|
33
|
+
@matcher_execution_context.executor = NPlusOneControl::Executor.new(
|
34
34
|
population: populate,
|
35
35
|
matching: pattern,
|
36
|
-
scale_factors: @factors
|
37
|
-
&actual
|
36
|
+
scale_factors: @factors
|
38
37
|
)
|
39
38
|
|
39
|
+
@queries = @matcher_execution_context.executor.call(&actual)
|
40
|
+
|
40
41
|
counts = @queries.map(&:last).map(&:size)
|
41
42
|
|
42
43
|
counts.max == counts.min
|
metadata
CHANGED
@@ -1,27 +1,27 @@
|
|
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.4.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- palkan
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date:
|
11
|
+
date: 2020-07-20 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: bundler
|
15
15
|
requirement: !ruby/object:Gem::Requirement
|
16
16
|
requirements:
|
17
|
-
- - "
|
17
|
+
- - ">="
|
18
18
|
- !ruby/object:Gem::Version
|
19
19
|
version: '1.10'
|
20
20
|
type: :development
|
21
21
|
prerelease: false
|
22
22
|
version_requirements: !ruby/object:Gem::Requirement
|
23
23
|
requirements:
|
24
|
-
- - "
|
24
|
+
- - ">="
|
25
25
|
- !ruby/object:Gem::Version
|
26
26
|
version: '1.10'
|
27
27
|
- !ruby/object:Gem::Dependency
|
@@ -146,16 +146,9 @@ executables: []
|
|
146
146
|
extensions: []
|
147
147
|
extra_rdoc_files: []
|
148
148
|
files:
|
149
|
-
-
|
150
|
-
- ".rspec"
|
151
|
-
- ".rubocop.yml"
|
152
|
-
- ".travis.yml"
|
153
|
-
- Gemfile
|
149
|
+
- CHANGELOG.md
|
154
150
|
- LICENSE.txt
|
155
151
|
- README.md
|
156
|
-
- Rakefile
|
157
|
-
- bin/console
|
158
|
-
- bin/setup
|
159
152
|
- lib/n_plus_one_control.rb
|
160
153
|
- lib/n_plus_one_control/executor.rb
|
161
154
|
- lib/n_plus_one_control/minitest.rb
|
@@ -164,19 +157,15 @@ files:
|
|
164
157
|
- lib/n_plus_one_control/rspec/dsl.rb
|
165
158
|
- lib/n_plus_one_control/rspec/matcher.rb
|
166
159
|
- lib/n_plus_one_control/version.rb
|
167
|
-
- n_plus_one_control.gemspec
|
168
|
-
- spec/n_plus_one_control/executor_spec.rb
|
169
|
-
- spec/n_plus_one_control/rspec_spec.rb
|
170
|
-
- spec/n_plus_one_control_spec.rb
|
171
|
-
- spec/spec_helper.rb
|
172
|
-
- spec/support/post.rb
|
173
|
-
- spec/support/user.rb
|
174
|
-
- tests/minitest_test.rb
|
175
|
-
- tests/test_helper.rb
|
176
160
|
homepage: http://github.com/palkan/n_plus_one_control
|
177
161
|
licenses:
|
178
162
|
- MIT
|
179
|
-
metadata:
|
163
|
+
metadata:
|
164
|
+
bug_tracker_uri: http://github.com/palkan/n_plus_one_control/issues
|
165
|
+
changelog_uri: https://github.com/palkan/n_plus_one_control/blob/master/CHANGELOG.md
|
166
|
+
documentation_uri: http://github.com/palkan/n_plus_one_control
|
167
|
+
homepage_uri: http://github.com/palkan/n_plus_one_control
|
168
|
+
source_code_uri: http://github.com/palkan/n_plus_one_control
|
180
169
|
post_install_message:
|
181
170
|
rdoc_options: []
|
182
171
|
require_paths:
|
@@ -192,8 +181,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
|
|
192
181
|
- !ruby/object:Gem::Version
|
193
182
|
version: '0'
|
194
183
|
requirements: []
|
195
|
-
|
196
|
-
rubygems_version: 2.7.6
|
184
|
+
rubygems_version: 3.0.3
|
197
185
|
signing_key:
|
198
186
|
specification_version: 4
|
199
187
|
summary: RSpec and Minitest matchers to prevent N+1 queries problem
|
data/.gitignore
DELETED
data/.rspec
DELETED
data/.rubocop.yml
DELETED
@@ -1,78 +0,0 @@
|
|
1
|
-
AllCops:
|
2
|
-
Include:
|
3
|
-
- 'lib/**/*.rb'
|
4
|
-
- 'lib/**/*.rake'
|
5
|
-
- 'spec/**/*.rb'
|
6
|
-
Exclude:
|
7
|
-
- 'bin/**/*'
|
8
|
-
- 'spec/dummy/**/*'
|
9
|
-
- 'tmp/**/*'
|
10
|
-
- 'vendor/**/*'
|
11
|
-
- 'Rakefile'
|
12
|
-
- 'Gemfile'
|
13
|
-
- '*.gemspec'
|
14
|
-
DisplayCopNames: true
|
15
|
-
StyleGuideCopsOnly: false
|
16
|
-
TargetRubyVersion: 2.4
|
17
|
-
|
18
|
-
Rails:
|
19
|
-
Enabled: false
|
20
|
-
|
21
|
-
Naming/UncommunicativeMethodParamName:
|
22
|
-
Enabled: false
|
23
|
-
|
24
|
-
Naming/AccessorMethodName:
|
25
|
-
Enabled: false
|
26
|
-
|
27
|
-
Style/TrivialAccessors:
|
28
|
-
Enabled: false
|
29
|
-
|
30
|
-
Style/Documentation:
|
31
|
-
Exclude:
|
32
|
-
- 'spec/**/*.rb'
|
33
|
-
- 'tests/**/*.rb'
|
34
|
-
|
35
|
-
Style/StringLiterals:
|
36
|
-
Enabled: false
|
37
|
-
|
38
|
-
Style/RegexpLiteral:
|
39
|
-
Enabled: false
|
40
|
-
|
41
|
-
Style/Lambda:
|
42
|
-
Enabled: false
|
43
|
-
|
44
|
-
Layout/SpaceInsideStringInterpolation:
|
45
|
-
EnforcedStyle: no_space
|
46
|
-
|
47
|
-
Style/ClassAndModuleChildren:
|
48
|
-
Enabled: false
|
49
|
-
|
50
|
-
Style/BlockDelimiters:
|
51
|
-
Exclude:
|
52
|
-
- 'spec/**/*.rb'
|
53
|
-
|
54
|
-
Lint/AmbiguousRegexpLiteral:
|
55
|
-
Enabled: false
|
56
|
-
|
57
|
-
|
58
|
-
Metrics/MethodLength:
|
59
|
-
Exclude:
|
60
|
-
- 'spec/**/*.rb'
|
61
|
-
|
62
|
-
Metrics/AbcSize:
|
63
|
-
Max: 20
|
64
|
-
|
65
|
-
Metrics/LineLength:
|
66
|
-
Max: 100
|
67
|
-
Exclude:
|
68
|
-
- 'spec/**/*.rb'
|
69
|
-
|
70
|
-
Metrics/BlockLength:
|
71
|
-
Exclude:
|
72
|
-
- 'spec/**/*.rb'
|
73
|
-
|
74
|
-
Rails/Date:
|
75
|
-
Enabled: false
|
76
|
-
|
77
|
-
Rails/TimeZone:
|
78
|
-
Enabled: false
|
data/.travis.yml
DELETED
data/Gemfile
DELETED
data/Rakefile
DELETED
@@ -1,13 +0,0 @@
|
|
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
DELETED
@@ -1,14 +0,0 @@
|
|
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
DELETED
data/n_plus_one_control.gemspec
DELETED
@@ -1,35 +0,0 @@
|
|
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.required_ruby_version = '>= 2.0.0'
|
14
|
-
spec.description = %{
|
15
|
-
RSpec and Minitest matchers to prevent N+1 queries problem.
|
16
|
-
|
17
|
-
Evaluates code under consideration several times with different scale factors
|
18
|
-
to make sure that the number of DB queries behaves as expected (i.e. O(1) instead of O(N)).
|
19
|
-
}
|
20
|
-
spec.homepage = "http://github.com/palkan/n_plus_one_control"
|
21
|
-
spec.license = "MIT"
|
22
|
-
|
23
|
-
spec.files = `git ls-files`.split($/)
|
24
|
-
spec.require_paths = ["lib"]
|
25
|
-
|
26
|
-
spec.add_development_dependency "bundler", "~> 1.10"
|
27
|
-
spec.add_development_dependency "rake", "~> 10.0"
|
28
|
-
spec.add_development_dependency "rspec", "~> 3.5"
|
29
|
-
spec.add_development_dependency "minitest", "~> 5.9"
|
30
|
-
spec.add_development_dependency "factory_girl", "~> 4.8.0"
|
31
|
-
spec.add_development_dependency "rubocop", "~> 0.61.0"
|
32
|
-
spec.add_development_dependency "activerecord", "~> 5.1"
|
33
|
-
spec.add_development_dependency "sqlite3", "~> 1.3.6"
|
34
|
-
spec.add_development_dependency "pry-byebug"
|
35
|
-
end
|
@@ -1,65 +0,0 @@
|
|
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
|
@@ -1,113 +0,0 @@
|
|
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
|
-
|
85
|
-
context 'with warming up', :n_plus_one do
|
86
|
-
let(:cache) { double "cache" }
|
87
|
-
|
88
|
-
before do
|
89
|
-
allow(cache).to receive(:setup).and_return(:result)
|
90
|
-
allow(NPlusOneControl::Executor).to receive(:call) { raise StandardError }
|
91
|
-
end
|
92
|
-
|
93
|
-
populate { |n| create_list(:post, n) }
|
94
|
-
|
95
|
-
warmup { cache.setup }
|
96
|
-
|
97
|
-
it "runs warmup before calling Executor" do
|
98
|
-
expect(cache).to receive(:setup)
|
99
|
-
expect do
|
100
|
-
expect { Post.find_each(&:id) }.to perform_constant_number_of_queries
|
101
|
-
end.to raise_error StandardError
|
102
|
-
end
|
103
|
-
end
|
104
|
-
|
105
|
-
context 'with_warming_up', :n_plus_one do
|
106
|
-
populate { |n| create_list(:post, n) }
|
107
|
-
|
108
|
-
it "runs actual one more time" do
|
109
|
-
expect(Post).to receive(:all).exactly(3).times
|
110
|
-
expect { Post.all }.to perform_constant_number_of_queries.with_warming_up
|
111
|
-
end
|
112
|
-
end
|
113
|
-
end
|
data/spec/spec_helper.rb
DELETED
@@ -1,30 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
$LOAD_PATH.unshift File.expand_path('../lib', __dir__)
|
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
|
data/spec/support/post.rb
DELETED
@@ -1,19 +0,0 @@
|
|
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
|
data/spec/support/user.rb
DELETED
@@ -1,17 +0,0 @@
|
|
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
|
data/tests/minitest_test.rb
DELETED
@@ -1,63 +0,0 @@
|
|
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
|
data/tests/test_helper.rb
DELETED
@@ -1,32 +0,0 @@
|
|
1
|
-
# frozen_string_literal: true
|
2
|
-
|
3
|
-
require "minitest/autorun"
|
4
|
-
require "minitest/pride"
|
5
|
-
|
6
|
-
$LOAD_PATH << File.expand_path('../lib', __dir__)
|
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
|