sunspot_matchers 1.1.0.0

Sign up to get free protection for your applications and to get access to all the features.
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.gemspec
4
+ gemspec
data/Gemfile.lock ADDED
@@ -0,0 +1,33 @@
1
+ PATH
2
+ remote: .
3
+ specs:
4
+ sunspot_matchers (1.1.0.0)
5
+
6
+ GEM
7
+ remote: http://rubygems.org/
8
+ specs:
9
+ builder (3.0.0)
10
+ diff-lcs (1.1.2)
11
+ escape (0.0.4)
12
+ rsolr (0.12.1)
13
+ builder (>= 2.1.2)
14
+ rspec (2.1.0)
15
+ rspec-core (~> 2.1.0)
16
+ rspec-expectations (~> 2.1.0)
17
+ rspec-mocks (~> 2.1.0)
18
+ rspec-core (2.1.0)
19
+ rspec-expectations (2.1.0)
20
+ diff-lcs (~> 1.1.2)
21
+ rspec-mocks (2.1.0)
22
+ sunspot (1.1.0)
23
+ escape (= 0.0.4)
24
+ rsolr (= 0.12.1)
25
+
26
+ PLATFORMS
27
+ ruby
28
+
29
+ DEPENDENCIES
30
+ bundler (>= 1.0.0)
31
+ rspec (~> 2.1.0)
32
+ sunspot (~> 1.1.0)
33
+ sunspot_matchers!
data/README.markdown ADDED
@@ -0,0 +1,229 @@
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
+ # Installation
11
+
12
+ You will need to replace the Sunspot Session object with the spy provided. You can do this globally by putting the
13
+ following in your spec_helper.
14
+
15
+ config.before do
16
+ Sunspot.session = SunspotMatchers::SunspotSessionSpy.new(Sunspot.session)
17
+ end
18
+
19
+ Keep in mind, this will prevent any test from actually hitting Solr, so if you have integration tests, you'll either
20
+ need to be more careful which tests you replace the session for, or you'll need to restore the original session before
21
+ those tests
22
+
23
+ Sunspot.session = Sunspot.session.original_session
24
+
25
+ You will also need to include the matchers in your specs. Again, this can be done globally in your spec_helper.
26
+
27
+ config.include SunspotMatchers
28
+
29
+ Alternately, you could include them into individual tests if needed.
30
+
31
+ # Matchers
32
+
33
+ ## be_a_search_for
34
+
35
+ If you perform a search against your Post model, you could write this expectation:
36
+
37
+ `Sunspot.session.should be_a_search_for(Post)`
38
+
39
+ Individual searches are stored in an array, so if you perform multiple, you'll have to match against them manually. Without
40
+ an explicit search specified, it will use the last one.
41
+
42
+ `Sunspot.session.searches.first.should be_a_search_for(Post)`
43
+
44
+ ## have_search_params
45
+
46
+ This is where the bulk of the functionality lies. There are seven types of search matches you can perform: `keywords`,
47
+ `with`, `without`, `paginate`, `order_by`, `facet`, and `boost`.
48
+
49
+ In all of the examples below, the expectation fully matches the search terms. This is not expected or required. You can
50
+ have a dozen `with` restrictions on a search and still write an expectation on a single one of them.
51
+
52
+ Negative expectations also work correctly. `should_not` will fail if the search actually includes the expectation.
53
+
54
+ With all matchers, you can specify a `Proc` as the second argument, and perform multi statement expectations inside the
55
+ Proc. Keep in mind, that only the expectation type specified in the first argument will actually be checked. So if
56
+ you specify `keywords` and `with` restrictions in the same Proc, but you said `have_search_params(:keywords, ...`
57
+ the `with` restrictions are simply ignored.
58
+
59
+ ### wildcard matching
60
+
61
+ keywords, with, and without support wildcard expectations using the `any_param` parameter:
62
+
63
+ Sunspot.search(Post) do
64
+ with :blog_id, 4
65
+ end
66
+
67
+ Sunspot.session.should have_search_params(:with, :blog_id, any_param)
68
+
69
+ ### :keywords
70
+
71
+ You can match against a keyword search:
72
+
73
+ Sunspot.search(Post) do
74
+ keywords 'great pizza'
75
+ end
76
+
77
+ Sunspot.session.should have_search_params(:keywords, 'great pizza')
78
+
79
+ ### :with
80
+
81
+ You can match against a with restriction:
82
+
83
+ Sunspot.search(Post) do
84
+ with :author_name, 'Mark Twain'
85
+ end
86
+
87
+ Sunspot.session.should have_search_params(:with, :author_name, 'Mark Twain')
88
+
89
+ Complex conditions can be matched by using a Proc instead of a value. Be aware that order does matter, not for
90
+ the actual results that would come out of Solr, but the matcher will fail of the order of `with` restrictions is
91
+ different.
92
+
93
+ Sunspot.search(Post) do
94
+ any_of do
95
+ with :category_ids, 1
96
+ with :category_ids, 2
97
+ end
98
+ end
99
+
100
+ Sunspot.session.should have_search_params(:with, Proc.new {
101
+ any_of do
102
+ with :category_ids, 1
103
+ with :category_ids, 2
104
+ end
105
+ })
106
+
107
+ ### :without
108
+
109
+ Without is nearly identical to with:
110
+
111
+ Sunspot.search(Post) do
112
+ without :author_name, 'Mark Twain'
113
+ end
114
+
115
+ Sunspot.session.should have_search_params(:without, :author_name, 'Mark Twain')
116
+
117
+ ### :paginate
118
+
119
+ You can also specify only page or per_page, both are not required.
120
+
121
+ Sunspot.search(Post) do
122
+ paginate :page => 3, :per_page => 15
123
+ end
124
+
125
+ Sunspot.session.should have_search_params(:paginate, :page => 3, :per_page => 15)
126
+
127
+ ### :order_by
128
+
129
+ Expectations on multiple orderings are supported using using the Proc format mentioned above.
130
+
131
+ Sunspot.search(Post) do
132
+ order_by :published_at, :desc
133
+ end
134
+
135
+ Sunspot.session.should have_search_params(:order_by, :published_at, :desc)
136
+
137
+ ### :facet
138
+
139
+ Standard faceting expectation:
140
+
141
+ Sunspot.search(Post) do
142
+ facet :category_ids
143
+ end
144
+
145
+ Sunspot.session.should have_search_params(:facet, :category_ids)
146
+
147
+ Faceting where a query is excluded:
148
+
149
+ Sunspot.search(Post) do
150
+ category_filter = with(:category_ids, 2)
151
+ facet(:category_ids, :exclude => category_filter)
152
+ end
153
+
154
+ Sunspot.session.should have_search_params(:facet, Proc.new {
155
+ category_filter = with(:category_ids, 2)
156
+ facet(:category_ids, :exclude => category_filter)
157
+ })
158
+
159
+ Query faceting:
160
+
161
+ Sunspot.search(Post) do
162
+ facet(:average_rating) do
163
+ row(1.0..2.0) do
164
+ with(:average_rating, 1.0..2.0)
165
+ end
166
+ row(2.0..3.0) do
167
+ with(:average_rating, 2.0..3.0)
168
+ end
169
+ end
170
+ end
171
+
172
+ Sunspot.session.should have_search_params(:facet, Proc.new {
173
+ facet(:average_rating) do
174
+ row(1.0..2.0) do
175
+ with(:average_rating, 1.0..2.0)
176
+ end
177
+ row(2.0..3.0) do
178
+ with(:average_rating, 2.0..3.0)
179
+ end
180
+ end
181
+ })
182
+
183
+ ### :boost
184
+
185
+ Field boost matching:
186
+
187
+ Sunspot.search(Post) do
188
+ keywords 'great pizza' do
189
+ boost_fields :body => 2.0
190
+ end
191
+ end
192
+
193
+ Sunspot.session.should have_search_params(:boost, Proc.new {
194
+ keywords 'great pizza' do
195
+ boost_fields :body => 2.0
196
+ end
197
+ })
198
+
199
+ Boost query matching:
200
+
201
+ Sunspot.search(Post) do
202
+ keywords 'great pizza' do
203
+ boost(2.0) do
204
+ with :blog_id, 4
205
+ end
206
+ end
207
+ end
208
+
209
+ Sunspot.session.should have_search_params(:boost, Proc.new {
210
+ keywords 'great pizza' do
211
+ boost(2.0) do
212
+ with :blog_id, 4
213
+ end
214
+ end
215
+ })
216
+
217
+ Boost function matching:
218
+
219
+ Sunspot.search(Post) do
220
+ keywords 'great pizza' do
221
+ boost(function { sum(:average_rating, product(:popularity, 10)) })
222
+ end
223
+ end
224
+
225
+ Sunspot.session.should have_search_params(:boost, Proc.new {
226
+ keywords 'great pizza' do
227
+ boost(function { sum(:average_rating, product(:popularity, 10)) })
228
+ end
229
+ })
data/Rakefile ADDED
@@ -0,0 +1,2 @@
1
+ require 'bundler'
2
+ Bundler::GemHelper.install_tasks
@@ -0,0 +1,237 @@
1
+ module SunspotMatchers
2
+ class BaseMatcher
3
+ def initialize(actual_search, comparison_search, args)
4
+ @args = args
5
+ @actual_search, @comparison_search = actual_search, comparison_search
6
+ end
7
+
8
+ def search_tuple
9
+ search_tuple = @actual.is_a?(Array) ? @actual : @actual.searches.last
10
+ raise 'no search found' unless search_tuple
11
+ search_tuple
12
+ end
13
+
14
+ def actual_search
15
+ search_tuple.last
16
+ end
17
+
18
+ def search_types
19
+ search_tuple.first
20
+ end
21
+
22
+ def wildcard?
23
+ @args && @args.last == any_param
24
+ end
25
+
26
+ def field
27
+ @args && @args.first
28
+ end
29
+
30
+ def query_params_for_search(search)
31
+ search.instance_variable_get(:@query).to_params
32
+ end
33
+
34
+ def actual_params
35
+ @actual_params ||= query_params_for_search(@actual_search)
36
+ end
37
+
38
+ def comparison_params
39
+ @comparison_params ||= query_params_for_search(@comparison_search)
40
+ end
41
+
42
+ def match?
43
+ differences.empty?
44
+ end
45
+
46
+ def missing_param_error_message
47
+ missing_params = differences
48
+ actual_values = missing_params.keys.collect {|key| "#{key} => #{actual_params[key]}"}
49
+ missing_values = missing_params.collect{ |key, value| "#{key} => #{value}"}
50
+ "expected search params: #{actual_values.join(' and ')} to match expected: #{missing_values.join(' and ')}"
51
+ end
52
+
53
+ def unexpected_match_error_message
54
+ actual_values = keys_to_compare.collect {|key| "#{key} => #{actual_params[key]}"}
55
+ comparison_values = keys_to_compare.collect {|key| "#{key} => #{comparison_params[key]}"}
56
+ "expected search params: #{actual_values.join(' and ')} NOT to match expected: #{comparison_values.join(' and ')}"
57
+ end
58
+
59
+ def differences
60
+ keys_to_compare.inject({}) do |hsh, key|
61
+ result = compare_key(key)
62
+ hsh[key] = result unless result.empty?
63
+ hsh
64
+ end
65
+ end
66
+
67
+ def compare_key(key)
68
+ if(actual_params[key].is_a?(Array) || comparison_params[key].is_a?(Array))
69
+ compare_multi_value(actual_params[key], comparison_params[key])
70
+ else
71
+ compare_single_value(actual_params[key], comparison_matcher_for_key(key))
72
+ end
73
+ end
74
+
75
+ def comparison_matcher_for_key(key)
76
+ if wildcard? && wildcard_matcher_for_keys.has_key?(key)
77
+ wildcard_matcher_for_keys[key]
78
+ else
79
+ comparison_params[key]
80
+ end
81
+ end
82
+
83
+ def compare_single_value(actual, comparison)
84
+ if comparison.is_a?(Regexp)
85
+ return [] if comparison =~ actual
86
+ return [comparison]
87
+ end
88
+ return [comparison] unless actual == comparison
89
+ []
90
+ end
91
+
92
+ def compare_multi_value(actual, comparison)
93
+ filter_values(comparison).reject do |value|
94
+ next false unless actual
95
+ value_matcher = Regexp.new(Regexp.escape(value))
96
+ actual.any?{ |actual_value| actual_value =~ value_matcher }
97
+ end
98
+ end
99
+
100
+ def normalize_value(value)
101
+
102
+ end
103
+
104
+ def filter_values(values)
105
+ return values unless wildcard?
106
+ field_matcher = Regexp.new(field.to_s)
107
+ values.select{ |value| field_matcher =~ value }.collect{|value| value.gsub(/:.*/, '')}
108
+ end
109
+
110
+ def wildcard_matcher_for_keys
111
+ {}
112
+ end
113
+ end
114
+
115
+ class HaveSearchParams < BaseMatcher
116
+ def initialize(method, *args)
117
+ @method = method
118
+ @args = args
119
+ end
120
+
121
+ def matches?(actual)
122
+ @actual = actual
123
+ @matcher = build_matcher
124
+ @matcher.match?
125
+ end
126
+
127
+ def failure_message_for_should
128
+ @matcher.missing_param_error_message
129
+ end
130
+
131
+ def failure_message_for_should_not
132
+ @matcher.unexpected_match_error_message
133
+ end
134
+
135
+ def build_matcher
136
+ comparison_search = if(@args.last.is_a?(Proc))
137
+ SunspotMatchers::SunspotSessionSpy.new(nil).build_search(search_types, &@args.last)
138
+ else
139
+ method = @method
140
+ args = @args
141
+ SunspotMatchers::SunspotSessionSpy.new(nil).build_search(search_types) do
142
+ send(method, *args)
143
+ end
144
+ end
145
+
146
+ get_matcher.new(actual_search, comparison_search, @args)
147
+ end
148
+
149
+ def get_matcher
150
+ case @method
151
+ when :with, :without
152
+ WithMatcher
153
+ when :keywords
154
+ KeywordsMatcher
155
+ when :boost
156
+ BoostMatcher
157
+ when :facet
158
+ FacetMatcher
159
+ when :order_by
160
+ SortMatcher
161
+ when :paginate
162
+ PaginationMatcher
163
+ end
164
+ end
165
+ end
166
+
167
+ def have_search_params(method, *args)
168
+ HaveSearchParams.new(method, *args)
169
+ end
170
+
171
+ class WithMatcher < BaseMatcher
172
+ def keys_to_compare
173
+ [:fq]
174
+ end
175
+ end
176
+
177
+ class KeywordsMatcher < BaseMatcher
178
+ def keys_to_compare
179
+ [:q, :qf]
180
+ end
181
+
182
+ def wildcard_matcher_for_keys
183
+ {:q => /./, :qf => /./}
184
+ end
185
+ end
186
+
187
+ class BoostMatcher < BaseMatcher
188
+ def keys_to_compare
189
+ [:qf, :bq, :bf]
190
+ end
191
+ end
192
+
193
+ class FacetMatcher < BaseMatcher
194
+ def keys_to_compare
195
+ comparison_params.keys.select {|key| /facet/ =~ key.to_s}
196
+ end
197
+ end
198
+
199
+ class SortMatcher < BaseMatcher
200
+ def keys_to_compare
201
+ [:sort]
202
+ end
203
+ end
204
+
205
+ class PaginationMatcher < BaseMatcher
206
+ def keys_to_compare
207
+ [:rows, :start]
208
+ end
209
+ end
210
+
211
+ class BeASearchFor < BaseMatcher
212
+ def initialize(expected_class)
213
+ @expected_class = expected_class
214
+ end
215
+
216
+ def matches?(actual)
217
+ @actual = actual
218
+ search_types.include?(@expected_class)
219
+ end
220
+
221
+ def failure_message_for_should
222
+ "expected search class: #{search_types.join(' and ')} to match expected class: #{@expected_class}"
223
+ end
224
+
225
+ def failure_message_for_should_not
226
+ "expected search class: #{search_types.join(' and ')} NOT to match expected class: #{@expected_class}"
227
+ end
228
+ end
229
+
230
+ def be_a_search_for(expected_class)
231
+ BeASearchFor.new(expected_class)
232
+ end
233
+ end
234
+
235
+ def any_param
236
+ "ANY_PARAM"
237
+ end
@@ -0,0 +1,111 @@
1
+ module SunspotMatchers
2
+ class SunspotSearchSpy < Sunspot::Search::StandardSearch
3
+ def execute
4
+ self
5
+ end
6
+ def solr_response
7
+ {}
8
+ end
9
+ def facet_response
10
+ {'facet_queries' => {}}
11
+ end
12
+ end
13
+
14
+ class SunspotSessionSpy < Sunspot::Session
15
+ attr_reader :original_session
16
+ attr_reader :current_search_class
17
+
18
+ attr_accessor :searches
19
+
20
+ def initialize(original_session)
21
+ @searches = []
22
+ @original_session = original_session
23
+ @config = Sunspot::Configuration.build
24
+ end
25
+
26
+ def inspect
27
+ 'Solr Search'
28
+ end
29
+
30
+ def index(*objects)
31
+ end
32
+
33
+ def index!(*objects)
34
+ end
35
+
36
+ def remove(*objects)
37
+ end
38
+
39
+ def remove!(*objects)
40
+ end
41
+
42
+ def remove_by_id(clazz, id)
43
+ end
44
+
45
+ def remove_by_id!(clazz, id)
46
+ end
47
+
48
+ def remove_all(clazz = nil)
49
+ end
50
+
51
+ def remove_all!(clazz = nil)
52
+ end
53
+
54
+ def dirty?
55
+ false
56
+ end
57
+
58
+ def delete_dirty?
59
+ false
60
+ end
61
+
62
+ def commit_if_dirty
63
+ end
64
+
65
+ def commit_if_delete_dirty
66
+ end
67
+
68
+ def commit
69
+ end
70
+
71
+ def search(*types, &block)
72
+ new_search(*types, &block)
73
+ end
74
+
75
+ def new_search(*types, &block)
76
+ search = build_search(*types, &block)
77
+ @searches << [types, search]
78
+ search
79
+ end
80
+
81
+ def build_search(*types, &block)
82
+ types.flatten!
83
+ search = SunspotSearchSpy.new(
84
+ nil,
85
+ setup_for_types(types),
86
+ Sunspot::Query::StandardQuery.new(types),
87
+ @config
88
+ )
89
+ search.build(&block) if block
90
+ search
91
+ end
92
+
93
+ def setup_for_types(types)
94
+ if types.empty?
95
+ raise(ArgumentError, "You must specify at least one type to search")
96
+ end
97
+ if types.length == 1
98
+ Sunspot::Setup.for(types.first)
99
+ else
100
+ CompositeSetup.for(types)
101
+ end
102
+ end
103
+ end
104
+ end
105
+
106
+ # Support Sunspot random field in test -- Sunspot originally generate a random number for the field
107
+ class Sunspot::Query::Sort::RandomSort < Sunspot::Query::Sort::Abstract
108
+ def to_param
109
+ "random #{direction_for_solr}"
110
+ end
111
+ end
@@ -0,0 +1,3 @@
1
+ module SunspotMatchers
2
+ VERSION = "1.1.0.0"
3
+ end
@@ -0,0 +1,2 @@
1
+ require 'sunspot_matchers/matchers'
2
+ require 'sunspot_matchers/sunspot_session_spy'