]> git.evergreen-ils.org Git - working/Evergreen.git/blob - Open-ILS/src/perlmods/t/21-QueryParser.t
LP1615805 No inputs after submit in patron search (AngularJS)
[working/Evergreen.git] / Open-ILS / src / perlmods / t / 21-QueryParser.t
1 #!perl
2
3 use strict;
4 use warnings; # FATAL => qw(all);
5 use Test::More;
6
7 BEGIN {
8         use_ok( 'OpenILS::Application::Storage::QueryParser' );
9 #    use_ok( 'OpenILS::Application::Storage::Driver::Pg::QueryParser' );
10 }
11
12 my %args = ( debug => 0 );
13 my $QParser = QueryParser->new(%args);
14 is(ref $QParser, 'QueryParser', 'Created QueryParser');
15 is($QParser->operator('and'), '&&', 'Expected and operator');
16
17 $Data::Dumper::Indent = 1;
18
19 $QParser->add_search_class_alias( keyword => 'kw' );
20 is ($QParser->search_class_count, 1, "Added one search class");
21 init_qp();
22
23 is ($QParser->search_class_count, 5, "Correct number of search classes");
24 is (scalar(@{$QParser->search_fields()->{'author'}}), 3, "Correct number of search fields for 'author' class");
25 $QParser->remove_search_field('author', 'personal');
26 is (scalar(@{$QParser->search_fields()->{'author'}}), 2, "Removed search field");
27 $QParser->remove_search_class('title');
28 is ($QParser->search_class_count, 4, "Removed search class");
29 is (scalar(@{$QParser->search_class_aliases->{'author'}}), 3, "Correct number of aliases for 'author' class");
30 $QParser->remove_search_class_alias( author => 'au' );
31 is (scalar(@{$QParser->search_class_aliases->{'author'}}), 2, "Removed alias for 'author' class");
32 is (scalar(@{$QParser->search_field_aliases->{'subject'}->{'name'}}), 2, "Correct number of search field aliases for 'subject' class");
33 $QParser->remove_search_field_alias( subject => name => 'nomen' );
34 is (scalar(@{$QParser->search_field_aliases->{'subject'}->{'name'}}), 1, "Removed search field alias");
35
36 is ($QParser->facet_class_count, 2, "Correct number of facet classes");
37 is (scalar(@{$QParser->facet_fields()->{'author'}}), 2, "Correct number of facet fields for 'author' class");
38 $QParser->remove_facet_field('author', 'personal');
39 is (scalar(@{$QParser->facet_fields()->{'author'}}), 1, "Removed facet field");
40 $QParser->remove_facet_class('author');
41 is ($QParser->facet_class_count, 1, "Removed facet class");
42
43 is ($QParser->filter_count, 30, "Correct number of filters");
44 is (scalar(@{$QParser->filter_normalizers('skip_check')}), 0, 'No filter normalizers by default');
45 $QParser->add_filter_normalizer('skip_check', \&test_filter_norm);
46 is (scalar(@{$QParser->filter_normalizers('skip_check')}), 1, 'Added filter normalizer');
47 is ($QParser->modifier_count, 8, "Correct number of modifiers");
48
49 is_deeply ($QParser->custom_data('string'), { }, "No custom data set for 'string'");
50
51 is($QParser->core_limit(25000), 25000, 'Core limit setting works');
52 is($QParser->core_limit(), 25000, 'Core limit stays set');
53
54 is($QParser->superpage(1), 1, 'Superpage setting works');
55 is($QParser->superpage(), 1, 'Superpage stays set');
56
57 # see QueryParser.pm, this won't work:
58 # is($QParser->superpage(0), 0, 'Superpage can be unset');
59
60 is($QParser->superpage_size(1000), 1000, 'Superpage size setting works');
61 is($QParser->superpage_size(), 1000, 'Superpage size stays set');
62
63 init_qp();
64 eval {
65     local $SIG{ALRM} = sub { die "timed out!\n" };
66     alarm 1;
67     $QParser->parse('-"unclosed phrase');
68 };
69 if ($@) {
70     fail('parsing modified unclosed phrase query timed out');
71 } else {
72     pass('successfully parsed modified unclosed phrase query');
73 }
74
75 init_qp();
76 $QParser->parse('concerto -on_reserve(1)');
77 my $course_filter = $QParser->parse_tree()->find_filter('on_reserve');
78 is($course_filter->negate, 1, 'Query parser can handle negated filters');
79
80 # It's unfortunate not to be able to use the following tests immediately, but
81 # they reflect assumptions that need to be updated in light of new qp_fix code.
82 # Also,, canonicalization may not preserve insignificant whitespace nor the
83 # exact, original number of non-semantic parentheses.
84
85 =cut
86
87 init_qp();
88
89 my %queries = (
90     '(keyword1 keyword2) || keyword3' => undef,
91     'keyword1 || keyword2' => undef,
92     'author:keyword1 keyword2' => undef,
93     '(keyword1) || (keyword2)' => undef,
94     'keyword1 || keyword2 || keyword3' => undef,
95     '(keyword1 || keyword2) && keyword3' => undef,
96     'keyword1 keyword2 || keyword3 keyword4' => sub {
97         my $query = shift;
98         # Unfortunately, the canonical representation of a query in master
99         # as of 2012/09/07 is not unambiguous
100         is($QParser->parse_tree()->to_abstract_query()->{children}->{'&'}, undef, "Outer-most operator in query {$query} is not AND");
101         is(ref $QParser->parse_tree()->to_abstract_query()->{children}->{'|'}, 'ARRAY', "Outer-most operator in query {$query} is OR");
102     },
103     'keyword1 keyword2 && keyword3 keyword4' => undef,
104     'keyword1 author:keyword2' => undef,
105     'au:keyword1 kw:keyword2' => undef,
106     'keyword1 pref_ou(lib)' => sub {
107         my $query = shift;
108         is($QParser->parse_tree->to_abstract_query()->{filters}->[0]->{name}, 'pref_ou', 'Generated filter for query');
109     },
110     'keyword1 #available' => sub {
111         my $query = shift;
112         is($QParser->parse_tree->to_abstract_query()->{modifiers}->[0], 'available', 'Set modifier for query');
113     },
114     '(keyword1 keyword2) || keyword3 #available' => sub {
115         my $query = shift;
116         is($QParser->parse_tree->to_abstract_query()->{modifiers}->[0], 'available', 'Set modifier for query');
117     },
118     'keyword1 testfilter(whatever)' => undef,
119     'keyword1 sort:something' => undef,
120     '"phrase1 phrase2" keyword1' => undef, # NOTE: phrases do not have a stable canonical representation, 2012-09-09
121     'keyword1 -keyword2' => undef,
122     'keyword1 +keyword2' => undef,
123 );
124
125 my $query;
126 my $testfunc;
127 while (($query, $testfunc) = each (%queries)) {
128     init_qp();
129     $QParser->parse($query);
130     # TODO: Test initial parse
131     &$testfunc($query) if ($testfunc);
132     my $canonical = clean(QueryParser::Canonicalize::abstract_query2str_impl($QParser->parse_tree()->to_abstract_query()));
133     $canonical = reparse($canonical);
134     init_qp();
135     $QParser->parse($canonical);
136     is(clean(QueryParser::Canonicalize::abstract_query2str_impl($QParser->parse_tree()->to_abstract_query())), $canonical, "Building query from canonical query is idempotent for query {$query}");
137 }
138
139 my %equivalences = (
140     'keyword1 keyword2' => 'keyword1 && keyword2',
141     'keyword1 keyword2 || keyword3 keyword4' => 'keyword1 && keyword2 || keyword3 && keyword4',
142     'keyword1 keyword2 || keyword3 keyword4' => '(keyword1 keyword2) || (keyword3 keyword4)',
143     'keyword1 keyword2 && keyword3 keyword4' => '(keyword1 && keyword2) && (keyword3 && keyword4)',
144     'keyword1 || && keyword2' => 'keyword1 || keyword2',
145     'keyword1' => 'keyword:keyword1',
146 );
147
148 my $equivalent;
149 while (($query, $equivalent) = each (%equivalences)) {
150     init_qp();
151     $QParser->parse($query);
152     my $canonical1 = reparse(clean(QueryParser::Canonicalize::abstract_query2str_impl($QParser->parse_tree()->to_abstract_query())));
153     init_qp();
154     $QParser->parse($equivalent);
155     my $canonical2 = reparse(clean(QueryParser::Canonicalize::abstract_query2str_impl($QParser->parse_tree()->to_abstract_query())));
156     is($canonical1, $canonical2, "Queries {$query} and {$equivalent} are equivalent");
157 }
158
159 my %differences = (
160     '(keyword1 keyword2) || keyword3' => 'keyword1 && (keyword2 || keyword3)',
161     'keyword1 || (keyword2 && keyword3)' => '(keyword1 || keyword2) && keyword3',
162     '(keyword1 || keyword2) && keyword3' => 'keyword1 || (keyword2 && keyword3)',
163     'keyword1 keyword2 || keyword3 keyword4' => '(keyword1 keyword2 || keyword3) keyword4', # this should fail on master, 2012-09-07
164 );
165
166
167 my $different;
168 while (($query, $different) = each (%differences)) {
169     init_qp();
170     $QParser->parse($query);
171     my $canonical1 = reparse(clean(QueryParser::Canonicalize::abstract_query2str_impl($QParser->parse_tree()->to_abstract_query())));
172     init_qp();
173     $QParser->parse($different);
174     my $canonical2 = reparse(clean(QueryParser::Canonicalize::abstract_query2str_impl($QParser->parse_tree()->to_abstract_query())));
175     isnt($canonical1, $canonical2, "Queries {$query} and {$different} are not equivalent");
176 }
177
178 =cut
179
180 done_testing;
181
182 sub test_filter_norm {
183     return;
184 }
185
186 sub test_filter_callback {
187     my ($QParser, $struct, $filter, $params, $negate) = @_;
188     is($filter, 'testfilter', 'Filter callback on correct filter');
189     return;
190 }
191
192 sub clean {
193     my $string = shift;
194     $string =~ s/\s+/ /g;
195     $string =~ s/ \)/\)/g;
196     $string =~ s/\( /\(/g;
197     $string =~ s/ $//g;
198     $string =~ s/^ //g;
199     
200     ($string, undef) = parse_parens($string);
201
202     $string =~ s/(^| )\(([^) ]+)\)/$2/g;
203     $string =~ s/^\(([^)]*)\)$/$1/g;
204
205     return $string;
206 }
207
208 sub parse_parens {
209     my $string = shift;
210     my $subres;
211     my $result = '';
212     while (my $nextchar = substr($string, 0, 1)) {
213         $string = substr($string, 1);
214         if ($nextchar eq '(') {
215             ($subres, $string) = parse_parens($string);
216             if ($result || ! (substr($string, 0, 1) eq ')')) {
217                 $result .= "($subres)";
218             } else {
219                 $result = $subres;
220             }
221         } elsif ($nextchar eq ')') {
222             return ($result, $string);
223         } else {
224             $result .= $nextchar;
225         }
226     }
227     return $result;
228 }
229
230 sub reparse {
231     my $canonical = shift;
232     my $repeats = $canonical =~ tr/&/&/;
233     $repeats = ($repeats / 2) + 1;
234     my $result;
235     while (--$repeats) {
236         init_qp();
237         $QParser->parse($canonical);
238         $canonical = clean(QueryParser::Canonicalize::abstract_query2str_impl($QParser->parse_tree()->to_abstract_query()));
239     }
240     return $canonical;
241 }
242
243 sub init_qp {
244     $QueryParser::parser_config{QueryParser}->{allow_nested_modifiers} = 1;
245     $QParser = QueryParser->new(%args);
246     $QParser->add_search_class_alias( title => 'ti' );
247     $QParser->add_search_class_alias( author => 'au' );
248     $QParser->add_search_class_alias( author => 'name' );
249     $QParser->add_search_class_alias( author => 'dc.contributor' );
250     $QParser->add_search_class_alias( subject => 'su' );
251     $QParser->add_search_class_alias( subject => 'bib.subject(?:Title|Place|Occupation)' );
252     $QParser->add_search_class_alias( series => 'se' );
253     $QParser->add_search_class_alias( keyword => 'dc.identifier' );
254
255     $QParser->add_query_normalizer( author => corporate => 'search_normalize' );
256     $QParser->add_query_normalizer( keyword => keyword => 'search_normalize' );
257     
258     $QParser->add_search_field_alias( subject => name => 'bib.subjectName' );
259     $QParser->add_search_field_alias( subject => name => 'nomen' );
260
261     $QParser->add_search_field( 'author' => 'personal' );
262     $QParser->add_search_field( 'author' => 'corporate' );
263     $QParser->add_search_field( 'author' => 'meeting' );
264
265     $QParser->default_search_class( 'keyword' );
266
267     # will be retained simply for back-compat
268     $QParser->add_search_filter( 'format' );
269
270     # grumble grumble, special cases against date1 and date2
271     $QParser->add_search_filter( 'before' );
272     $QParser->add_search_filter( 'after' );
273     $QParser->add_search_filter( 'between' );
274     $QParser->add_search_filter( 'during' );
275
276     # used by layers above this
277     $QParser->add_search_filter( 'statuses' );
278     $QParser->add_search_filter( 'locations' );
279     $QParser->add_search_filter( 'location_groups' );
280     $QParser->add_search_filter( 'site' );
281     $QParser->add_search_filter( 'pref_ou' );
282     $QParser->add_search_filter( 'lasso' );
283     $QParser->add_search_filter( 'my_lasso' );
284     $QParser->add_search_filter( 'depth' );
285     $QParser->add_search_filter( 'language' );
286     $QParser->add_search_filter( 'offset' );
287     $QParser->add_search_filter( 'limit' );
288     $QParser->add_search_filter( 'check_limit' );
289     $QParser->add_search_filter( 'skip_check' );
290     $QParser->add_search_filter( 'superpage' );
291     $QParser->add_search_filter( 'estimation_strategy' );
292     $QParser->add_search_filter( 'copy_tag' );
293     $QParser->add_search_filter( 'on_reserve' );
294     $QParser->add_search_modifier( 'available' );
295     $QParser->add_search_modifier( 'staff' );
296
297     # Start from container data (bre, acn, acp): container(bre,bookbag,123,deadb33fdeadb33fdeadb33fdeadb33f)
298     $QParser->add_search_filter( 'container' );
299
300     # Start from a list of record ids, either bre or metarecords, depending on the #metabib modifier
301     $QParser->add_search_filter( 'record_list' );
302
303     # used internally, but generally not user-settable
304     $QParser->add_search_filter( 'preferred_language' );
305     $QParser->add_search_filter( 'preferred_language_weight' );
306     $QParser->add_search_filter( 'preferred_language_multiplier' );
307     $QParser->add_search_filter( 'core_limit' );
308
309     # XXX Valid values to be supplied by SVF
310     $QParser->add_search_filter( 'sort' );
311
312     # modifies core query, not configurable
313     $QParser->add_search_modifier( 'descending' );
314     $QParser->add_search_modifier( 'ascending' );
315     $QParser->add_search_modifier( 'nullsfirst' );
316     $QParser->add_search_modifier( 'nullslast' );
317     $QParser->add_search_modifier( 'metarecord' );
318     $QParser->add_search_modifier( 'metabib' );
319
320     $QParser->add_facet_field( 'author' => 'personal' );
321     $QParser->add_facet_field( 'author' => 'corporate' );
322     $QParser->add_facet_field( 'subject' => 'topic' );
323     $QParser->add_facet_field( 'subject' => 'geographic' );
324
325     $QParser->add_search_filter( 'testfilter', \&test_filter_callback );
326 }