sunspot_matchers_testunit 1.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.
data/.gitignore ADDED
@@ -0,0 +1,3 @@
1
+ pkg/*
2
+ *.gem
3
+ .bundle
data/Gemfile ADDED
@@ -0,0 +1,4 @@
1
+ source :gemcutter
2
+
3
+ # Specify your gem's dependencies in sunspot_matchers_testunit.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,27 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sunspot_matchers_testunit (1.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ builder (3.0.0)
10
+ escape (0.0.4)
11
+ pr_geohash (1.0.0)
12
+ rake (0.8.7)
13
+ rsolr (0.12.1)
14
+ builder (>= 2.1.2)
15
+ sunspot (1.2.1)
16
+ escape (= 0.0.4)
17
+ pr_geohash (~> 1.0)
18
+ rsolr (= 0.12.1)
19
+
20
+ PLATFORMS
21
+ ruby
22
+
23
+ DEPENDENCIES
24
+ bundler (>= 1.0.0)
25
+ rake
26
+ sunspot (~> 1.2.1)
27
+ sunspot_matchers_testunit!
data/MIT-LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ The MIT License
2
+
3
+ Copyright (c) 2011 Joseph Palermo, Pivotal Labs, Inc.
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.markdown ADDED
@@ -0,0 +1,238 @@
1
+ # Sunspot Matchers
2
+
3
+ [Sunspot](http://outoftime.github.com/sunspot/) is a great Ruby library for constructing searches against Solr. However,
4
+ because of the way the Sunspot DSL is constructed, it can be difficult to do simple assertions about your searches
5
+ without doing full integration tests.
6
+
7
+ The goal of these matchers are to make it easier to unit test search logic without having to construct the individual
8
+ fixture scenarios inside of Solr and then actually perform a search against Solr.
9
+
10
+ This is a direct port of the excellent [Sunspot Matchers](http://github.com/pivotal/sunspot_matchers) library by Joseph
11
+ Palermo, rewritten for use with Test::Unit.
12
+
13
+ # Installation
14
+
15
+ You will need to replace the Sunspot Session object with the spy provided. You can do this globally by putting the
16
+ following in a setup block or your test_helper.
17
+
18
+ def setup
19
+ Sunspot.session = SunspotMatchers::SunspotSessionSpy.new(Sunspot.session)
20
+ end
21
+
22
+ Keep in mind, this will prevent any test from actually hitting Solr, so if you have integration tests, you'll either
23
+ need to be more careful which tests you replace the session for, or you'll need to restore the original session before
24
+ those tests
25
+
26
+ Sunspot.session = Sunspot.session.original_session
27
+
28
+ You will also need to include the matchers in your tests. Again, this can be done globally in your test_helper.
29
+
30
+ require 'sunspot_matchers_testunit'
31
+ include SunspotMatchersTestunit
32
+
33
+ Alternately, you could include them into individual tests if needed.
34
+
35
+ # Matchers
36
+
37
+ ## assert_is_search_for
38
+
39
+ If you perform a search against your Post model, you could write this assertion:
40
+
41
+ `assert_is_search_for Sunspot.session, Post`
42
+
43
+ Individual searches are stored in an array, so if you perform multiple, you'll have to match against them manually. Without
44
+ an explicit search specified, it will use the last one.
45
+
46
+ `assert_is_search_for Sunspot.session.searches.first, Post`
47
+
48
+ ## assert_has_search_params
49
+
50
+ This is where the bulk of the functionality lies. There are seven types of search matches you can perform: `keywords`,
51
+ `with`, `without`, `paginate`, `order_by`, `facet`, and `boost`.
52
+
53
+ In all of the examples below, the arguments fully match the search terms. This is not expected or required. You can
54
+ have a dozen `with` restrictions on a search and still write an expectation on a single one of them.
55
+
56
+ Negative expectations also work correctly. `assert_has_no_search_params` will fail if the search actually includes the
57
+ provided arguments.
58
+
59
+ With all matchers, you can specify a `Proc` as the second argument, and perform multi statement expectations inside the
60
+ Proc. Keep in mind, that only the search type specified in the first argument will actually be checked. So if you specify
61
+ `keywords` and `with` restrictions in the same Proc, but you said `assert_has_search_params Sunspot.session, :keywords, ...`
62
+ the `with` restrictions are simply ignored.
63
+
64
+ ### wildcard matching
65
+
66
+ keywords, with, without, and order_by support wildcard expectations using the `any_param` parameter:
67
+
68
+ Sunspot.search(Post) do
69
+ with :blog_id, 4
70
+ order_by :blog_id, :desc
71
+ end
72
+
73
+ assert_has_search_params Sunspot.session, [ :with, :blog_id, any_param ]
74
+ assert_has_search_params Sunspot.session, [ :order_by, :blog_id, any_param ]
75
+ assert_has_search_params Sunspot.session, [ :order_by, any_param ]
76
+ assert_has_no_search_params Sunspot.session, [ :order_by, :category_ids, any_param ]
77
+
78
+ ### :keywords
79
+
80
+ You can match against a keyword search:
81
+
82
+ Sunspot.search(Post) do
83
+ keywords 'great pizza'
84
+ end
85
+
86
+ assert_has_search_params Sunspot.session, [ :keywords, 'great pizza' ]
87
+
88
+ ### :with
89
+
90
+ You can match against a with restriction:
91
+
92
+ Sunspot.search(Post) do
93
+ with :author_name, 'Mark Twain'
94
+ end
95
+
96
+ assert_has_search_params Sunspot.session, [ :with, :author_name, 'Mark Twain' ]
97
+
98
+ Complex conditions can be matched by using a Proc instead of a value. Be aware that order does matter, not for
99
+ the actual results that would come out of Solr, but the matcher will fail of the order of `with` restrictions is
100
+ different.
101
+
102
+ Sunspot.search(Post) do
103
+ any_of do
104
+ with :category_ids, 1
105
+ with :category_ids, 2
106
+ end
107
+ end
108
+
109
+ assert_has_search_params Sunspot.session, [ :with, Proc.new {
110
+ any_of do
111
+ with :category_ids, 1
112
+ with :category_ids, 2
113
+ end
114
+ } ]
115
+
116
+ ### :without
117
+
118
+ Without is nearly identical to with:
119
+
120
+ Sunspot.search(Post) do
121
+ without :author_name, 'Mark Twain'
122
+ end
123
+
124
+ assert_has_search_params Sunspot.session, [ :without, :author_name, 'Mark Twain' ]
125
+
126
+ ### :paginate
127
+
128
+ You can also specify only page or per_page, both are not required.
129
+
130
+ Sunspot.search(Post) do
131
+ paginate :page => 3, :per_page => 15
132
+ end
133
+
134
+ assert_has_search_params Sunspot.session, [ :paginate, :page => 3, :per_page => 15 ]
135
+
136
+ ### :order_by
137
+
138
+ Expectations on multiple orderings are supported using using the Proc format mentioned above.
139
+
140
+ Sunspot.search(Post) do
141
+ order_by :published_at, :desc
142
+ end
143
+
144
+ assert_has_search_params Sunspot.session, [ :order_by, :published_at, :desc ]
145
+
146
+ ### :facet
147
+
148
+ Standard faceting expectation:
149
+
150
+ Sunspot.search(Post) do
151
+ facet :category_ids
152
+ end
153
+
154
+ assert_has_search_params Sunspot.session, [ :facet, :category_ids ]
155
+
156
+ Faceting where a query is excluded:
157
+
158
+ Sunspot.search(Post) do
159
+ category_filter = with(:category_ids, 2)
160
+ facet(:category_ids, :exclude => category_filter)
161
+ end
162
+
163
+ assert_has_search_params Sunspot.session, [ :facet, Proc.new {
164
+ category_filter = with(:category_ids, 2)
165
+ facet(:category_ids, :exclude => category_filter)
166
+ } ]
167
+
168
+ Query faceting:
169
+
170
+ Sunspot.search(Post) do
171
+ facet(:average_rating) do
172
+ row(1.0..2.0) do
173
+ with(:average_rating, 1.0..2.0)
174
+ end
175
+ row(2.0..3.0) do
176
+ with(:average_rating, 2.0..3.0)
177
+ end
178
+ end
179
+ end
180
+
181
+ assert_has_search_params Sunspot.session, [ :facet, Proc.new {
182
+ facet(:average_rating) do
183
+ row(1.0..2.0) do
184
+ with(:average_rating, 1.0..2.0)
185
+ end
186
+ row(2.0..3.0) do
187
+ with(:average_rating, 2.0..3.0)
188
+ end
189
+ end
190
+ } ]
191
+
192
+ ### :boost
193
+
194
+ Field boost matching:
195
+
196
+ Sunspot.search(Post) do
197
+ keywords 'great pizza' do
198
+ boost_fields :body => 2.0
199
+ end
200
+ end
201
+
202
+ assert_has_search_params Sunspot.session, [ :boost, Proc.new {
203
+ keywords 'great pizza' do
204
+ boost_fields :body => 2.0
205
+ end
206
+ } ]
207
+
208
+ Boost query matching:
209
+
210
+ Sunspot.search(Post) do
211
+ keywords 'great pizza' do
212
+ boost(2.0) do
213
+ with :blog_id, 4
214
+ end
215
+ end
216
+ end
217
+
218
+ assert_has_search_params Sunspot.session, [ :boost, Proc.new {
219
+ keywords 'great pizza' do
220
+ boost(2.0) do
221
+ with :blog_id, 4
222
+ end
223
+ end
224
+ } ]
225
+
226
+ Boost function matching:
227
+
228
+ Sunspot.search(Post) do
229
+ keywords 'great pizza' do
230
+ boost(function { sum(:average_rating, product(:popularity, 10)) })
231
+ end
232
+ end
233
+
234
+ assert_has_search_params Sunspot.session, [ :boost, Proc.new {
235
+ keywords 'great pizza' do
236
+ boost(function { sum(:average_rating, product(:popularity, 10)) })
237
+ end
238
+ } ]
data/Rakefile ADDED
@@ -0,0 +1,11 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
3
+
4
+ require 'rake/testtask'
5
+ Rake::TestTask.new(:test) do |test|
6
+ test.libs << 'lib' << 'test'
7
+ test.pattern = 'test/*_test.rb'
8
+ test.verbose = true
9
+ end
10
+
11
+ task :default => :test
@@ -0,0 +1,2 @@
1
+ require 'sunspot_matchers_testunit/matchers'
2
+ require 'sunspot_matchers_testunit/sunspot_session_spy'
@@ -0,0 +1,317 @@
1
+ require 'test/unit/assertions'
2
+
3
+ module SunspotMatchersTestunit
4
+ class BaseMatcher
5
+ attr_accessor :args
6
+
7
+ def initialize(session, args)
8
+ @session = session
9
+ @args = args
10
+ build_comparison_search
11
+ end
12
+
13
+ def build_comparison_search
14
+ @comparison_search = if(@args.last.is_a?(Proc))
15
+ SunspotMatchersTestunit::SunspotSessionSpy.new(@session).build_search(search_types, &args.last)
16
+ else
17
+ SunspotMatchersTestunit::SunspotSessionSpy.new(@session).build_search(search_types) do
18
+ send(search_method, *args)
19
+ end
20
+ end
21
+ end
22
+
23
+ def search_tuple
24
+ search_tuple = @session.is_a?(Array) ? @session : @session.searches.last
25
+ raise 'no search found' unless search_tuple
26
+ search_tuple
27
+ end
28
+
29
+ def actual_search
30
+ search_tuple.last
31
+ end
32
+
33
+ def search_types
34
+ search_tuple.first
35
+ end
36
+
37
+ def wildcard?
38
+ @args && @args.last == any_param
39
+ end
40
+
41
+ def field
42
+ @args && @args.first
43
+ end
44
+
45
+ def query_params_for_search(search)
46
+ search.instance_variable_get(:@query).to_params
47
+ end
48
+
49
+ def actual_params
50
+ @actual_params ||= query_params_for_search(actual_search)
51
+ end
52
+
53
+ def comparison_params
54
+ @comparison_params ||= query_params_for_search(@comparison_search)
55
+ end
56
+
57
+ def match?
58
+ differences.empty?
59
+ end
60
+
61
+ def missing_param_error_message
62
+ missing_params = differences
63
+ actual_values = missing_params.keys.collect {|key| "#{key} => #{actual_params[key]}"}
64
+ missing_values = missing_params.collect{ |key, value| "#{key} => #{value}"}
65
+ "expected search params: #{actual_values.join(' and ')} to match expected: #{missing_values.join(' and ')}"
66
+ end
67
+
68
+ def unexpected_match_error_message
69
+ actual_values = keys_to_compare.collect {|key| "#{key} => #{actual_params[key]}"}
70
+ comparison_values = keys_to_compare.collect {|key| "#{key} => #{comparison_params[key]}"}
71
+ "expected search params: #{actual_values.join(' and ')} NOT to match expected: #{comparison_values.join(' and ')}"
72
+ end
73
+
74
+ def differences
75
+ keys_to_compare.inject({}) do |hsh, key|
76
+ result = compare_key(key)
77
+ hsh[key] = result unless result.empty?
78
+ hsh
79
+ end
80
+ end
81
+
82
+ def compare_key(key)
83
+ if(actual_params[key].is_a?(Array) || comparison_params[key].is_a?(Array))
84
+ compare_multi_value(actual_params[key], comparison_params[key])
85
+ else
86
+ compare_single_value(actual_params[key], comparison_matcher_for_key(key))
87
+ end
88
+ end
89
+
90
+ def comparison_matcher_for_key(key)
91
+ if wildcard? && wildcard_matcher_for_keys.has_key?(key)
92
+ wildcard_matcher_for_keys[key]
93
+ else
94
+ comparison_params[key]
95
+ end
96
+ end
97
+
98
+ def compare_single_value(actual, comparison)
99
+ if comparison.is_a?(Regexp)
100
+ return [] if comparison =~ actual
101
+ return [comparison.source]
102
+ end
103
+ return [comparison] unless actual == comparison
104
+ []
105
+ end
106
+
107
+ def compare_multi_value(actual, comparison)
108
+ filter_values(comparison).reject do |value|
109
+ next false unless actual
110
+ value_matcher = Regexp.new(Regexp.escape(value))
111
+ actual.any?{ |actual_value| actual_value =~ value_matcher }
112
+ end
113
+ end
114
+
115
+ def filter_values(values)
116
+ return values unless wildcard?
117
+ field_matcher = Regexp.new(field.to_s)
118
+ values.select{ |value| field_matcher =~ value }.collect{|value| value.gsub(/:.*/, '')}
119
+ end
120
+
121
+ def wildcard_matcher_for_keys
122
+ {}
123
+ end
124
+ end
125
+
126
+ class HaveSearchParams
127
+ include Test::Unit::Assertions
128
+
129
+ def initialize(session, method, *args)
130
+ @session = session
131
+ @method = method
132
+ @args = args
133
+ end
134
+
135
+ def get_matcher
136
+ matcher_class = case @method
137
+ when :with
138
+ WithMatcher
139
+ when :without
140
+ WithoutMatcher
141
+ when :keywords
142
+ KeywordsMatcher
143
+ when :boost
144
+ BoostMatcher
145
+ when :facet
146
+ FacetMatcher
147
+ when :order_by
148
+ OrderByMatcher
149
+ when :paginate
150
+ PaginationMatcher
151
+ end
152
+ matcher_class.new(@session, @args)
153
+ end
154
+ end
155
+
156
+ def assert_has_search_params(session, method_and_args)
157
+ method, *args = method_and_args
158
+ matcher = HaveSearchParams.new(session, method, *args).get_matcher
159
+ assert matcher.match?, matcher.missing_param_error_message
160
+ end
161
+
162
+ def assert_has_no_search_params(session, method_and_args)
163
+ method, *args = method_and_args
164
+ matcher = HaveSearchParams.new(session, method, *args).get_matcher
165
+ assert !matcher.match?, matcher.unexpected_match_error_message
166
+ end
167
+
168
+ class WithMatcher < BaseMatcher
169
+ def search_method
170
+ :with
171
+ end
172
+
173
+ def keys_to_compare
174
+ [:fq]
175
+ end
176
+ end
177
+
178
+ class WithoutMatcher < BaseMatcher
179
+ def search_method
180
+ :without
181
+ end
182
+
183
+ def keys_to_compare
184
+ [:fq]
185
+ end
186
+ end
187
+
188
+ class KeywordsMatcher < BaseMatcher
189
+ def search_method
190
+ :keywords
191
+ end
192
+
193
+ def keys_to_compare
194
+ [:q, :qf]
195
+ end
196
+
197
+ def wildcard_matcher_for_keys
198
+ {:q => /./, :qf => /./}
199
+ end
200
+ end
201
+
202
+ class BoostMatcher < BaseMatcher
203
+ def search_method
204
+ :boost
205
+ end
206
+
207
+
208
+ def keys_to_compare
209
+ [:qf, :bq, :bf]
210
+ end
211
+ end
212
+
213
+ class FacetMatcher < BaseMatcher
214
+ def search_method
215
+ :facet
216
+ end
217
+
218
+ def keys_to_compare
219
+ comparison_params.keys.select {|key| /facet/ =~ key.to_s}
220
+ end
221
+ end
222
+
223
+ class OrderByMatcher < BaseMatcher
224
+ def search_method
225
+ :order_by
226
+ end
227
+
228
+ def keys_to_compare
229
+ [:sort]
230
+ end
231
+
232
+ def wildcard_matcher_for_keys
233
+ return {:sort => /./} if field_wildcard?
234
+ param = comparison_params[:sort]
235
+ regex = Regexp.new(param.gsub(any_param, '.*'))
236
+ {:sort => regex}
237
+ end
238
+
239
+ def field_wildcard?
240
+ @args.first == any_param
241
+ end
242
+
243
+ def direction_wildcard?
244
+ @args.length == 2 && @args.last == any_param
245
+ end
246
+
247
+ def args
248
+ return @args unless direction_wildcard?
249
+ @args[0...-1] + [:asc]
250
+ end
251
+
252
+ def build_comparison_search
253
+ if field_wildcard?
254
+ @comparison_params = {:sort => any_param}
255
+ elsif direction_wildcard?
256
+ super
257
+ @comparison_params = comparison_params
258
+ @comparison_params[:sort].gsub!("asc", any_param)
259
+ else
260
+ super
261
+ end
262
+ end
263
+ end
264
+
265
+ class PaginationMatcher < BaseMatcher
266
+ def search_method
267
+ :paginate
268
+ end
269
+
270
+ def keys_to_compare
271
+ [:rows, :start]
272
+ end
273
+ end
274
+
275
+ class BeASearchFor
276
+ def initialize(session, expected_class)
277
+ @session = session
278
+ @expected_class = expected_class
279
+ end
280
+
281
+ def match?
282
+ search_types.include?(@expected_class)
283
+ end
284
+
285
+ def search_tuple
286
+ search_tuple = @session.is_a?(Array) ? @session : @session.searches.last
287
+ raise 'no search found' unless search_tuple
288
+ search_tuple
289
+ end
290
+
291
+ def search_types
292
+ search_tuple.first
293
+ end
294
+
295
+ def failure_message_for_should
296
+ "expected search class: #{search_types.join(' and ')} to match expected class: #{@expected_class}"
297
+ end
298
+
299
+ def failure_message_for_should_not
300
+ "expected search class: #{search_types.join(' and ')} NOT to match expected class: #{@expected_class}"
301
+ end
302
+ end
303
+
304
+ def assert_is_search_for(session, expected_class)
305
+ matcher = BeASearchFor.new(session, expected_class)
306
+ assert matcher.match?, matcher.failure_message_for_should
307
+ end
308
+
309
+ def assert_is_not_search_for(session, expected_class)
310
+ matcher = BeASearchFor.new(session, expected_class)
311
+ assert !matcher.match?, matcher.failure_message_for_should_not
312
+ end
313
+ end
314
+
315
+ def any_param
316
+ "ANY_PARAM"
317
+ end