1 /** @file soaktest_queries.cc
2  * @brief Soaktest generating lots of random queries.
3  */
4 /* Copyright (C) 2010 Richard Boulton
5  *
6  * This program is free software; you can redistribute it and/or
7  * modify it under the terms of the GNU General Public License as
8  * published by the Free Software Foundation; either version 2 of the
9  * License, or (at your option) any later version.
10  *
11  * This program is distributed in the hope that it will be useful,
12  * but WITHOUT ANY WARRANTY; without even the implied warranty of
13  * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
14  * GNU General Public License for more details.
15  *
16  * You should have received a copy of the GNU General Public License
17  * along with this program; if not, write to the Free Software
18  * Foundation, Inc., 51 Franklin St, Fifth Floor, Boston, MA  02110-1301
19  * USA
20  */
21 
22 #include <config.h>
23 
24 #include "soaktest/soaktest.h"
25 #include "soaktest/soaktest_queries.h"
26 
27 #include <xapian.h>
28 
29 #include "backendmanager.h"
30 #include "str.h"
31 #include "testrunner.h"
32 #include "testsuite.h"
33 #include "testutils.h"
34 #include "utils.h"
35 
36 #include <list>
37 
38 using namespace std;
39 
40 /** Make a database in which docs have the fields:
41  *
42  *  - N: values are numbers between 1 and maxtermsperfield.  Each document
43  *  contains all Nx for x from 1 to randint(maxtermsperfield).
44  */
45 static void
builddb_queries1(Xapian::WritableDatabase & db,const string & arg)46 builddb_queries1(Xapian::WritableDatabase &db, const string &arg)
47 {
48     unsigned int doccount = 1000;
49     unsigned int maxtermsperfield = atoi(arg.c_str());
50     for (unsigned int i = 0; i < doccount; ++i) {
51 	Xapian::Document doc;
52 	for (unsigned int j = randint(maxtermsperfield) + 1; j != 0; --j) {
53 	    doc.add_term("N" + str(j));
54 	}
55 	db.add_document(doc);
56     }
57     db.commit();
58 }
59 
60 /// The environment used by the steps when building a query.
61 struct QueryBuilderEnv {
62     /// Workspace for the query builder steps.
63     list<Xapian::Query> pieces;
64 
65     unsigned int maxtermsperfield;
66     unsigned int maxchildren;
67 
QueryBuilderEnvQueryBuilderEnv68     QueryBuilderEnv(unsigned int maxtermsperfield_,
69 		    unsigned int maxchildren_)
70 	    : maxtermsperfield(maxtermsperfield_),
71 	      maxchildren(maxchildren_)
72     {}
73 
74     /// Pop a query from the front of the list of pieces, and return it.
popQueryBuilderEnv75     Xapian::Query pop() {
76 	if (pieces.empty()) return Xapian::Query();
77 	Xapian::Query result = pieces.front();
78 	pieces.pop_front();
79 	return result;
80     }
81 
82     /// Get an iterator pointing to the "num"th element in pieces.
pickQueryBuilderEnv83     list<Xapian::Query>::iterator pick(unsigned int num) {
84 	list<Xapian::Query>::iterator i = pieces.begin();
85 	for (unsigned int c = 0; c != num && i != pieces.end(); ++c, ++i) {}
86 	return i;
87     }
88 };
89 
90 typedef void (*QueryStep)(QueryBuilderEnv &);
91 
92 /// Push a leaf query on field N onto the list of query pieces.
push_leaf_N(QueryBuilderEnv & env)93 static void push_leaf_N(QueryBuilderEnv & env)
94 {
95     env.pieces.push_back(Xapian::Query(
96 	"N" + str(randint(env.maxtermsperfield) + 1)));
97 }
98 
99 /** Combine some queries with OR.
100  *
101  *  The queries are removed from the list and the combined query is added to
102  *  the end.
103  */
combine_OR(QueryBuilderEnv & env)104 static void combine_OR(QueryBuilderEnv & env)
105 {
106     list<Xapian::Query>::iterator i = env.pick(randint(env.maxchildren));
107     Xapian::Query combined(Xapian::Query::OP_OR, env.pieces.begin(), i);
108     env.pieces.erase(env.pieces.begin(), i);
109     env.pieces.push_back(combined);
110 }
111 
112 /** Combine some queries with AND.
113  *
114  *  The queries are removed from the list and the combined query is added to
115  *  the end.
116  */
combine_AND(QueryBuilderEnv & env)117 static void combine_AND(QueryBuilderEnv & env)
118 {
119     list<Xapian::Query>::iterator i = env.pick(randint(env.maxchildren));
120     Xapian::Query combined(Xapian::Query::OP_AND, env.pieces.begin(), i);
121     env.pieces.erase(env.pieces.begin(), i);
122     env.pieces.push_back(combined);
123 }
124 
125 /** Combine some queries with XOR.
126  *
127  *  The queries are removed from the list and the combined query is added to
128  *  the end.
129  */
combine_XOR(QueryBuilderEnv & env)130 static void combine_XOR(QueryBuilderEnv & env)
131 {
132     list<Xapian::Query>::iterator i = env.pick(randint(env.maxchildren));
133     Xapian::Query combined(Xapian::Query::OP_XOR, env.pieces.begin(), i);
134     env.pieces.erase(env.pieces.begin(), i);
135     env.pieces.push_back(combined);
136 }
137 
138 /** Combine some queries with AND_NOT.
139  *
140  *  The queries are removed from the list and the combined query is added to
141  *  the end.
142  */
combine_NOT(QueryBuilderEnv & env)143 static void combine_NOT(QueryBuilderEnv & env)
144 {
145     if (env.pieces.size() < 2) return;
146     list<Xapian::Query>::iterator i = env.pick(2);
147     Xapian::Query combined(Xapian::Query::OP_AND_NOT, env.pieces.begin(), i);
148     env.pieces.erase(env.pieces.begin(), i);
149     env.pieces.push_back(combined);
150 }
151 
152 /// Random query builder.
153 class QueryBuilder {
154     /// The possible steps.
155     vector<QueryStep> options;
156 
157     /// The environment for the build steps.
158     QueryBuilderEnv env;
159 
160     /// Maximum number of steps to take when building a query.
161     unsigned int maxsteps;
162 
163   public:
QueryBuilder(unsigned int maxtermsperfield_,unsigned int maxchildren_,unsigned int maxsteps_)164     QueryBuilder(unsigned int maxtermsperfield_,
165 		 unsigned int maxchildren_,
166 		 unsigned int maxsteps_)
167 	    : env(maxtermsperfield_, maxchildren_),
168 	      maxsteps(maxsteps_)
169     {
170 	// Build up the set of options.
171 	// Some options are added multiple times to make them more likely.
172 	options.push_back(push_leaf_N);
173 	options.push_back(push_leaf_N);
174 	options.push_back(push_leaf_N);
175 	options.push_back(push_leaf_N);
176 	options.push_back(combine_OR);
177 	options.push_back(combine_AND);
178 	options.push_back(combine_XOR);
179 	options.push_back(combine_NOT);
180     }
181 
182     /** Build a random query.
183      *
184      *  This performs a random number of steps, each of which modifies the
185      *  QueryBuilderEnv by picking a random one of the options.
186      *
187      *  After the steps have been performed, the first item on the list in
188      *  QueryBuilderEnv is popped and returned.
189      */
make_query()190     Xapian::Query make_query() {
191 	unsigned int steps = randint(maxsteps) + 1;
192 	while (steps-- != 0) {
193 	    QueryStep & step = options[randint(options.size())];
194 	    step(env);
195 	}
196 	return env.pop();
197     }
198 };
199 
200 // Generate a load of random queries, and run them checking that the first
201 // returned result is the same when asking for 1 result as it is when asking
202 // for all results.  This is a basic test that early-termination query
203 // optimisations aren't causing different results to be returned.
204 DEFINE_TESTCASE(queries1, writable && !remote && !inmemory) {
205     unsigned int seed = initrand();
206     unsigned int maxtermsperfield = 100;
207     unsigned int repetitions = 10000;
208     QueryBuilder builder(maxtermsperfield, 10, 10);
209 
210     Xapian::Database db;
211     string arg(str(maxtermsperfield));
212     db = backendmanager->get_database("queries1_" + str(seed) + "_" + arg,
213 				      builddb_queries1, arg);
214 
215     // Reset the random seed, to make results repeatable whether database was
216     // created or not.
217     initrand();
218 
219     Xapian::Enquire enquire(db);
220 
221     unsigned int count = 0;
222     while (++count != repetitions) {
223 	Xapian::Query query(builder.make_query());
224 	tout.str(string());
225 	tout << "query " << count << ": " << query << "\n";
226 
227 	enquire.set_query(query);
228 	Xapian::MSet mset1 = enquire.get_mset(0, 1);
229 	Xapian::MSet mset10 = enquire.get_mset(0, 10);
230 	Xapian::MSet msetall = enquire.get_mset(0, db.get_doccount());
231 	tout << mset1 << "\n";
232 	tout << mset10 << "\n";
233 	tout << msetall << "\n";
234 	if (mset1.empty()) {
235 	    TEST(mset10.empty());
236 	    TEST(msetall.empty());
237 	    continue;
238 	}
239 	TEST(mset_range_is_same(mset1, 0, msetall, 0, mset1.size()));
240 	TEST(mset_range_is_same(mset10, 0, msetall, 0, mset10.size()));
241     }
242 
243     return true;
244 }
245