n_plus_one_control 0.3.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
@@ -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,15 +16,64 @@ module NPlusOneControl
20
16
 
21
17
  warming_up warmup
22
18
 
23
- queries = NPlusOneControl::Executor.call(
24
- population: populate || method(:populate),
25
- matching: matching || /^SELECT/i,
19
+ @executor = NPlusOneControl::Executor.new(
20
+ population: populate || population_method,
21
+ matching: matching || NPlusOneControl.default_matching,
26
22
  scale_factors: scale_factors || NPlusOneControl.default_scale_factors
27
- ) { yield }
23
+ )
24
+
25
+ queries = @executor.call { yield }
28
26
 
29
27
  counts = queries.map(&:last).map(&:size)
30
28
 
31
- assert counts.max == counts.min, NPlusOneControl.failure_message(queries)
29
+ assert counts.max == counts.min, NPlusOneControl.failure_message(:constant_queries, queries)
30
+ end
31
+
32
+ def assert_perform_linear_number_of_queries(
33
+ slope: 1,
34
+ populate: nil,
35
+ matching: nil,
36
+ scale_factors: nil,
37
+ warmup: nil
38
+ )
39
+
40
+ raise ArgumentError, "Block is required" unless block_given?
41
+
42
+ warming_up warmup
43
+
44
+ @executor = NPlusOneControl::Executor.new(
45
+ population: populate || population_method,
46
+ matching: matching || NPlusOneControl.default_matching,
47
+ scale_factors: scale_factors || NPlusOneControl.default_scale_factors
48
+ )
49
+
50
+ queries = @executor.call { yield }
51
+
52
+ assert linear?(queries, slope: slope), NPlusOneControl.failure_message(:linear_queries, queries)
53
+ end
54
+
55
+ def current_scale
56
+ @executor&.current_scale
57
+ end
58
+
59
+ private
60
+
61
+ def warming_up(warmup)
62
+ (warmup || methods.include?(:warmup) ? method(:warmup) : nil)&.call
63
+ end
64
+
65
+ def population_method
66
+ methods.include?(:populate) ? method(:populate) : nil
67
+ end
68
+
69
+ def linear?(queries, slope:)
70
+ queries.each_cons(2).all? do |pair|
71
+ scales = pair.map(&:first)
72
+ query_lists = pair.map(&:last)
73
+
74
+ actual_slope = (query_lists[1].size - query_lists[0].size) / (scales[1] - scales[0])
75
+ actual_slope <= slope
76
+ end
32
77
  end
33
78
  end
34
79
  end
@@ -0,0 +1,13 @@
1
+ # frozen_string_literal: true
2
+
3
+ module NPlusOneControl # :nodoc:
4
+ class Railtie < ::Rails::Railtie # :nodoc:
5
+ initializer "n_plus_one_control.backtrace_cleaner" do
6
+ ActiveSupport.on_load(:active_record) do
7
+ NPlusOneControl.backtrace_cleaner = lambda do |locations|
8
+ ::Rails.backtrace_cleaner.clean(locations)
9
+ end
10
+ end
11
+ end
12
+ end
13
+ end
@@ -1,8 +1,11 @@
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
- require "n_plus_one_control/rspec/matcher"
7
+ require "n_plus_one_control/rspec/matchers/perform_constant_number_of_queries"
8
+ require "n_plus_one_control/rspec/matchers/perform_linear_number_of_queries"
6
9
  require "n_plus_one_control/rspec/context"
7
10
 
8
11
  module NPlusOneControl
@@ -11,5 +14,6 @@ module NPlusOneControl
11
14
  end
12
15
 
13
16
  ::RSpec.configure do |config|
14
- config.extend NPlusOneControl::RSpec::DSL, n_plus_one: true
17
+ config.extend NPlusOneControl::RSpec::DSL::ClassMethods, n_plus_one: true
18
+ config.include NPlusOneControl::RSpec::DSL, n_plus_one: true
15
19
  end
@@ -1,24 +1,20 @@
1
1
  # frozen_string_literal: true
2
2
 
3
- ::RSpec.shared_context "n_plus_one_control", n_plus_one: true do
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
 
20
11
  let(:n_plus_one_warmup) do |ex|
21
12
  return if ex.example_group.warmup.nil?
13
+
22
14
  -> { ex.instance_exec(&ex.example_group.warmup) }
23
15
  end
24
16
  end
17
+
18
+ RSpec.configure do |config|
19
+ config.include_context "n_plus_one_control", n_plus_one: true
20
+ end
@@ -2,23 +2,32 @@
2
2
 
3
3
  module NPlusOneControl
4
4
  module RSpec
5
- # Extends RSpec ExampleGroup with populate & warmup methods
5
+ # Includes scale method into RSpec Example
6
6
  module DSL
7
- # Setup warmup block, wich will run before matching
8
- # for example, if using cache, then later queries
9
- # will perform less DB queries than first
10
- def warmup
11
- return @warmup unless block_given?
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
- @warmup = Proc.new
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
- # Setup populate callback, which is used
17
- # to prepare data for each run.
18
- def populate
19
- return @populate unless block_given?
27
+ attr_accessor :executor
20
28
 
21
- @populate = Proc.new
29
+ def current_scale
30
+ executor&.current_scale
22
31
  end
23
32
  end
24
33
  end
@@ -27,16 +27,16 @@
27
27
 
28
28
  warmup.call if warmup.present?
29
29
 
30
- # by default we're looking for select queries
31
- pattern = @pattern || /^SELECT/i
30
+ pattern = @pattern || NPlusOneControl.default_matching
32
31
 
33
- @queries = NPlusOneControl::Executor.call(
32
+ @matcher_execution_context.executor = NPlusOneControl::Executor.new(
34
33
  population: populate,
35
34
  matching: pattern,
36
- scale_factors: @factors,
37
- &actual
35
+ scale_factors: @factors
38
36
  )
39
37
 
38
+ @queries = @matcher_execution_context.executor.call(&actual)
39
+
40
40
  counts = @queries.map(&:last).map(&:size)
41
41
 
42
42
  counts.max == counts.min
@@ -46,6 +46,6 @@
46
46
  raise "This matcher doesn't support negation"
47
47
  end
48
48
 
49
- failure_message { |_actual| NPlusOneControl.failure_message(@queries) }
49
+ failure_message { |_actual| NPlusOneControl.failure_message(:constant_queries, @queries) }
50
50
  end
51
51
  # rubocop:enable Metrics/BlockLength
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ # rubocop:disable Metrics/BlockLength
4
+ ::RSpec::Matchers.define :perform_linear_number_of_queries do |slope: 1|
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
+ chain :with_warming_up do
16
+ @warmup = true
17
+ end
18
+
19
+ match do |actual, *_args|
20
+ raise ArgumentError, "Block is required" unless actual.is_a? Proc
21
+
22
+ raise "Missing tag :n_plus_one" unless
23
+ @matcher_execution_context.respond_to?(:n_plus_one_populate)
24
+
25
+ populate = @matcher_execution_context.n_plus_one_populate
26
+ warmup = @warmup ? actual : @matcher_execution_context.n_plus_one_warmup
27
+
28
+ warmup.call if warmup.present?
29
+
30
+ @matcher_execution_context.executor = NPlusOneControl::Executor.new(
31
+ population: populate,
32
+ matching: nil,
33
+ scale_factors: @factors
34
+ )
35
+
36
+ @queries = @matcher_execution_context.executor.call(&actual)
37
+
38
+ @queries.each_cons(2).all? do |pair|
39
+ scales = pair.map(&:first)
40
+ query_lists = pair.map(&:last)
41
+
42
+ actual_slope = (query_lists[1].size - query_lists[0].size) / (scales[1] - scales[0])
43
+ actual_slope <= slope
44
+ end
45
+ end
46
+
47
+ match_when_negated do |_actual|
48
+ raise "This matcher doesn't support negation"
49
+ end
50
+
51
+ failure_message { |_actual| NPlusOneControl.failure_message(:linear_queries, @queries) }
52
+ end
53
+ # rubocop:enable Metrics/BlockLength
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module NPlusOneControl
4
- VERSION = "0.3.0"
4
+ VERSION = "0.6.0"
5
5
  end
metadata CHANGED
@@ -1,43 +1,43 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: n_plus_one_control
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.3.0
4
+ version: 0.6.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: 2018-06-19 00:00:00.000000000 Z
11
+ date: 2020-11-27 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
28
28
  name: rake
29
29
  requirement: !ruby/object:Gem::Requirement
30
30
  requirements:
31
- - - "~>"
31
+ - - ">="
32
32
  - !ruby/object:Gem::Version
33
- version: '10.0'
33
+ version: '13.0'
34
34
  type: :development
35
35
  prerelease: false
36
36
  version_requirements: !ruby/object:Gem::Requirement
37
37
  requirements:
38
- - - "~>"
38
+ - - ">="
39
39
  - !ruby/object:Gem::Version
40
- version: '10.0'
40
+ version: '13.0'
41
41
  - !ruby/object:Gem::Dependency
42
42
  name: rspec
43
43
  requirement: !ruby/object:Gem::Requirement
@@ -80,107 +80,39 @@ dependencies:
80
80
  - - "~>"
81
81
  - !ruby/object:Gem::Version
82
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
83
  description: "\n RSpec and Minitest matchers to prevent N+1 queries problem.\n\n
140
84
  \ Evaluates code under consideration several times with different scale factors\n
141
85
  \ 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
- \ "
86
+ of O(N)).\n "
146
87
  email:
147
88
  - dementiev.vm@gmail.com
148
89
  executables: []
149
90
  extensions: []
150
91
  extra_rdoc_files: []
151
92
  files:
152
- - ".gitignore"
153
- - ".rspec"
154
- - ".rubocop.yml"
155
- - ".travis.yml"
156
- - Gemfile
93
+ - CHANGELOG.md
157
94
  - LICENSE.txt
158
95
  - README.md
159
- - Rakefile
160
- - bin/console
161
- - bin/setup
162
96
  - lib/n_plus_one_control.rb
163
97
  - lib/n_plus_one_control/executor.rb
164
98
  - lib/n_plus_one_control/minitest.rb
99
+ - lib/n_plus_one_control/railtie.rb
165
100
  - lib/n_plus_one_control/rspec.rb
166
101
  - lib/n_plus_one_control/rspec/context.rb
167
102
  - lib/n_plus_one_control/rspec/dsl.rb
168
- - lib/n_plus_one_control/rspec/matcher.rb
103
+ - lib/n_plus_one_control/rspec/matchers/perform_constant_number_of_queries.rb
104
+ - lib/n_plus_one_control/rspec/matchers/perform_linear_number_of_queries.rb
169
105
  - 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
106
  homepage: http://github.com/palkan/n_plus_one_control
180
107
  licenses:
181
108
  - MIT
182
- metadata: {}
183
- post_install_message:
109
+ metadata:
110
+ bug_tracker_uri: http://github.com/palkan/n_plus_one_control/issues
111
+ changelog_uri: https://github.com/palkan/n_plus_one_control/blob/master/CHANGELOG.md
112
+ documentation_uri: http://github.com/palkan/n_plus_one_control
113
+ homepage_uri: http://github.com/palkan/n_plus_one_control
114
+ source_code_uri: http://github.com/palkan/n_plus_one_control
115
+ post_install_message:
184
116
  rdoc_options: []
185
117
  require_paths:
186
118
  - lib
@@ -188,16 +120,15 @@ required_ruby_version: !ruby/object:Gem::Requirement
188
120
  requirements:
189
121
  - - ">="
190
122
  - !ruby/object:Gem::Version
191
- version: 2.0.0
123
+ version: 2.5.0
192
124
  required_rubygems_version: !ruby/object:Gem::Requirement
193
125
  requirements:
194
126
  - - ">="
195
127
  - !ruby/object:Gem::Version
196
128
  version: '0'
197
129
  requirements: []
198
- rubyforge_project:
199
- rubygems_version: 2.7.6
200
- signing_key:
130
+ rubygems_version: 3.0.3
131
+ signing_key:
201
132
  specification_version: 4
202
133
  summary: RSpec and Minitest matchers to prevent N+1 queries problem
203
134
  test_files: []