|
1 /* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */ |
|
2 /* vim:set ts=2 sw=2 sts=2 et: */ |
|
3 /* This Source Code Form is subject to the terms of the Mozilla Public |
|
4 * License, v. 2.0. If a copy of the MPL was not distributed with this |
|
5 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ |
|
6 |
|
7 /** |
|
8 * Tests Places query serialization. Associated bug is |
|
9 * https://bugzilla.mozilla.org/show_bug.cgi?id=370197 |
|
10 * |
|
11 * The simple idea behind this test is to try out different combinations of |
|
12 * query switches and ensure that queries are the same before serialization |
|
13 * as they are after de-serialization. |
|
14 * |
|
15 * In the code below, "switch" refers to a query option -- "option" in a broad |
|
16 * sense, not nsINavHistoryQueryOptions specifically (which is why we refer to |
|
17 * them as switches, not options). Both nsINavHistoryQuery and |
|
18 * nsINavHistoryQueryOptions allow you to specify switches that affect query |
|
19 * strings. nsINavHistoryQuery instances have attributes hasBeginTime, |
|
20 * hasEndTime, hasSearchTerms, and so on. nsINavHistoryQueryOptions instances |
|
21 * have attributes sortingMode, resultType, excludeItems, etc. |
|
22 * |
|
23 * Ideally we would like to test all 2^N subsets of switches, where N is the |
|
24 * total number of switches; switches might interact in erroneous or other ways |
|
25 * we do not expect. However, since N is large (21 at this time), that's |
|
26 * impractical for a single test in a suite. |
|
27 * |
|
28 * Instead we choose all possible subsets of a certain, smaller size. In fact |
|
29 * we begin by choosing CHOOSE_HOW_MANY_SWITCHES_LO and ramp up to |
|
30 * CHOOSE_HOW_MANY_SWITCHES_HI. |
|
31 * |
|
32 * There are two more wrinkles. First, for some switches we'd like to be able to |
|
33 * test multiple values. For example, it seems like a good idea to test both an |
|
34 * empty string and a non-empty string for switch nsINavHistoryQuery.searchTerms. |
|
35 * When switches have more than one value for a test run, we use the Cartesian |
|
36 * product of their values to generate all possible combinations of values. |
|
37 * |
|
38 * Second, we need to also test serialization of multiple nsINavHistoryQuery |
|
39 * objects at once. To do this, we remember the previous NUM_MULTIPLE_QUERIES |
|
40 * queries we tested individually and then serialize them together. We do this |
|
41 * each time we test an individual query. Thus the set of queries we test |
|
42 * together loses one query and gains another each time. |
|
43 * |
|
44 * To summarize, here's how this test works: |
|
45 * |
|
46 * - For n = CHOOSE_HOW_MANY_SWITCHES_LO to CHOOSE_HOW_MANY_SWITCHES_HI: |
|
47 * - From the total set of switches choose all possible subsets of size n. |
|
48 * For each of those subsets s: |
|
49 * - Collect the test runs of each switch in subset s and take their |
|
50 * Cartesian product. For each sequence in the product: |
|
51 * - Create nsINavHistoryQuery and nsINavHistoryQueryOptions objects |
|
52 * with the chosen switches and test run values. |
|
53 * - Serialize the query. |
|
54 * - De-serialize and ensure that the de-serialized query objects equal |
|
55 * the originals. |
|
56 * - For each of the previous NUM_MULTIPLE_QUERIES |
|
57 * nsINavHistoryQueryOptions objects o we created: |
|
58 * - Serialize the previous NUM_MULTIPLE_QUERIES nsINavHistoryQuery |
|
59 * objects together with o. |
|
60 * - De-serialize and ensure that the de-serialized query objects |
|
61 * equal the originals. |
|
62 */ |
|
63 |
|
64 const CHOOSE_HOW_MANY_SWITCHES_LO = 1; |
|
65 const CHOOSE_HOW_MANY_SWITCHES_HI = 2; |
|
66 |
|
67 const NUM_MULTIPLE_QUERIES = 2; |
|
68 |
|
69 // The switches are represented by objects below, in arrays querySwitches and |
|
70 // queryOptionSwitches. Use them to set up test runs. |
|
71 // |
|
72 // Some switches have special properties (where noted), but all switches must |
|
73 // have the following properties: |
|
74 // |
|
75 // matches: A function that takes two nsINavHistoryQuery objects (in the case |
|
76 // of nsINavHistoryQuery switches) or two nsINavHistoryQueryOptions |
|
77 // objects (for nsINavHistoryQueryOptions switches) and returns true |
|
78 // if the values of the switch in the two objects are equal. This is |
|
79 // the foundation of how we determine if two queries are equal. |
|
80 // runs: An array of functions. Each function takes an nsINavHistoryQuery |
|
81 // object and an nsINavHistoryQueryOptions object. The functions |
|
82 // should set the attributes of one of the two objects as appropriate |
|
83 // to their switches. This is how switch values are set for each test |
|
84 // run. |
|
85 // |
|
86 // The following properties are optional: |
|
87 // |
|
88 // desc: An informational string to print out during runs when the switch |
|
89 // is chosen. Hopefully helpful if the test fails. |
|
90 |
|
91 // nsINavHistoryQuery switches |
|
92 const querySwitches = [ |
|
93 // hasBeginTime |
|
94 { |
|
95 // flag and subswitches are used by the flagSwitchMatches function. Several |
|
96 // of the nsINavHistoryQuery switches (like this one) are really guard flags |
|
97 // that indicate if other "subswitches" are enabled. |
|
98 flag: "hasBeginTime", |
|
99 subswitches: ["beginTime", "beginTimeReference", "absoluteBeginTime"], |
|
100 desc: "nsINavHistoryQuery.hasBeginTime", |
|
101 matches: flagSwitchMatches, |
|
102 runs: [ |
|
103 function (aQuery, aQueryOptions) { |
|
104 aQuery.beginTime = Date.now() * 1000; |
|
105 aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH; |
|
106 }, |
|
107 function (aQuery, aQueryOptions) { |
|
108 aQuery.beginTime = Date.now() * 1000; |
|
109 aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY; |
|
110 } |
|
111 ] |
|
112 }, |
|
113 // hasEndTime |
|
114 { |
|
115 flag: "hasEndTime", |
|
116 subswitches: ["endTime", "endTimeReference", "absoluteEndTime"], |
|
117 desc: "nsINavHistoryQuery.hasEndTime", |
|
118 matches: flagSwitchMatches, |
|
119 runs: [ |
|
120 function (aQuery, aQueryOptions) { |
|
121 aQuery.endTime = Date.now() * 1000; |
|
122 aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH; |
|
123 }, |
|
124 function (aQuery, aQueryOptions) { |
|
125 aQuery.endTime = Date.now() * 1000; |
|
126 aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY; |
|
127 } |
|
128 ] |
|
129 }, |
|
130 // hasSearchTerms |
|
131 { |
|
132 flag: "hasSearchTerms", |
|
133 subswitches: ["searchTerms"], |
|
134 desc: "nsINavHistoryQuery.hasSearchTerms", |
|
135 matches: flagSwitchMatches, |
|
136 runs: [ |
|
137 function (aQuery, aQueryOptions) { |
|
138 aQuery.searchTerms = "shrimp and white wine"; |
|
139 }, |
|
140 function (aQuery, aQueryOptions) { |
|
141 aQuery.searchTerms = ""; |
|
142 } |
|
143 ] |
|
144 }, |
|
145 // hasDomain |
|
146 { |
|
147 flag: "hasDomain", |
|
148 subswitches: ["domain", "domainIsHost"], |
|
149 desc: "nsINavHistoryQuery.hasDomain", |
|
150 matches: flagSwitchMatches, |
|
151 runs: [ |
|
152 function (aQuery, aQueryOptions) { |
|
153 aQuery.domain = "mozilla.com"; |
|
154 aQuery.domainIsHost = false; |
|
155 }, |
|
156 function (aQuery, aQueryOptions) { |
|
157 aQuery.domain = "www.mozilla.com"; |
|
158 aQuery.domainIsHost = true; |
|
159 }, |
|
160 function (aQuery, aQueryOptions) { |
|
161 aQuery.domain = ""; |
|
162 } |
|
163 ] |
|
164 }, |
|
165 // hasUri |
|
166 { |
|
167 flag: "hasUri", |
|
168 subswitches: ["uri", "uriIsPrefix"], |
|
169 desc: "nsINavHistoryQuery.hasUri", |
|
170 matches: flagSwitchMatches, |
|
171 runs: [ |
|
172 function (aQuery, aQueryOptions) { |
|
173 aQuery.uri = uri("http://mozilla.com"); |
|
174 aQuery.uriIsPrefix = false; |
|
175 }, |
|
176 function (aQuery, aQueryOptions) { |
|
177 aQuery.uri = uri("http://mozilla.com"); |
|
178 aQuery.uriIsPrefix = true; |
|
179 } |
|
180 ] |
|
181 }, |
|
182 // hasAnnotation |
|
183 { |
|
184 flag: "hasAnnotation", |
|
185 subswitches: ["annotation", "annotationIsNot"], |
|
186 desc: "nsINavHistoryQuery.hasAnnotation", |
|
187 matches: flagSwitchMatches, |
|
188 runs: [ |
|
189 function (aQuery, aQueryOptions) { |
|
190 aQuery.annotation = "bookmarks/toolbarFolder"; |
|
191 aQuery.annotationIsNot = false; |
|
192 }, |
|
193 function (aQuery, aQueryOptions) { |
|
194 aQuery.annotation = "bookmarks/toolbarFolder"; |
|
195 aQuery.annotationIsNot = true; |
|
196 } |
|
197 ] |
|
198 }, |
|
199 // minVisits |
|
200 { |
|
201 // property is used by function simplePropertyMatches. |
|
202 property: "minVisits", |
|
203 desc: "nsINavHistoryQuery.minVisits", |
|
204 matches: simplePropertyMatches, |
|
205 runs: [ |
|
206 function (aQuery, aQueryOptions) { |
|
207 aQuery.minVisits = 0x7fffffff; // 2^31 - 1 |
|
208 } |
|
209 ] |
|
210 }, |
|
211 // maxVisits |
|
212 { |
|
213 property: "maxVisits", |
|
214 desc: "nsINavHistoryQuery.maxVisits", |
|
215 matches: simplePropertyMatches, |
|
216 runs: [ |
|
217 function (aQuery, aQueryOptions) { |
|
218 aQuery.maxVisits = 0x7fffffff; // 2^31 - 1 |
|
219 } |
|
220 ] |
|
221 }, |
|
222 // onlyBookmarked |
|
223 { |
|
224 property: "onlyBookmarked", |
|
225 desc: "nsINavHistoryQuery.onlyBookmarked", |
|
226 matches: simplePropertyMatches, |
|
227 runs: [ |
|
228 function (aQuery, aQueryOptions) { |
|
229 aQuery.onlyBookmarked = true; |
|
230 } |
|
231 ] |
|
232 }, |
|
233 // getFolders |
|
234 { |
|
235 desc: "nsINavHistoryQuery.getFolders", |
|
236 matches: function (aQuery1, aQuery2) { |
|
237 var q1Folders = aQuery1.getFolders(); |
|
238 var q2Folders = aQuery2.getFolders(); |
|
239 if (q1Folders.length !== q2Folders.length) |
|
240 return false; |
|
241 for (let i = 0; i < q1Folders.length; i++) { |
|
242 if (q2Folders.indexOf(q1Folders[i]) < 0) |
|
243 return false; |
|
244 } |
|
245 for (let i = 0; i < q2Folders.length; i++) { |
|
246 if (q1Folders.indexOf(q2Folders[i]) < 0) |
|
247 return false; |
|
248 } |
|
249 return true; |
|
250 }, |
|
251 runs: [ |
|
252 function (aQuery, aQueryOptions) { |
|
253 aQuery.setFolders([], 0); |
|
254 }, |
|
255 function (aQuery, aQueryOptions) { |
|
256 aQuery.setFolders([PlacesUtils.placesRootId], 1); |
|
257 }, |
|
258 function (aQuery, aQueryOptions) { |
|
259 aQuery.setFolders([PlacesUtils.placesRootId, PlacesUtils.tagsFolderId], 2); |
|
260 } |
|
261 ] |
|
262 }, |
|
263 // tags |
|
264 { |
|
265 desc: "nsINavHistoryQuery.getTags", |
|
266 matches: function (aQuery1, aQuery2) { |
|
267 if (aQuery1.tagsAreNot !== aQuery2.tagsAreNot) |
|
268 return false; |
|
269 var q1Tags = aQuery1.tags; |
|
270 var q2Tags = aQuery2.tags; |
|
271 if (q1Tags.length !== q2Tags.length) |
|
272 return false; |
|
273 for (let i = 0; i < q1Tags.length; i++) { |
|
274 if (q2Tags.indexOf(q1Tags[i]) < 0) |
|
275 return false; |
|
276 } |
|
277 for (let i = 0; i < q2Tags.length; i++) { |
|
278 if (q1Tags.indexOf(q2Tags[i]) < 0) |
|
279 return false; |
|
280 } |
|
281 return true; |
|
282 }, |
|
283 runs: [ |
|
284 function (aQuery, aQueryOptions) { |
|
285 aQuery.tags = []; |
|
286 }, |
|
287 function (aQuery, aQueryOptions) { |
|
288 aQuery.tags = [""]; |
|
289 }, |
|
290 function (aQuery, aQueryOptions) { |
|
291 aQuery.tags = [ |
|
292 "foo", |
|
293 "七難", |
|
294 "", |
|
295 "いっぱいおっぱい", |
|
296 "Abracadabra", |
|
297 "123", |
|
298 "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!", |
|
299 "アスキーでございません", |
|
300 "あいうえお", |
|
301 ]; |
|
302 }, |
|
303 function (aQuery, aQueryOptions) { |
|
304 aQuery.tags = [ |
|
305 "foo", |
|
306 "七難", |
|
307 "", |
|
308 "いっぱいおっぱい", |
|
309 "Abracadabra", |
|
310 "123", |
|
311 "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!", |
|
312 "アスキーでございません", |
|
313 "あいうえお", |
|
314 ]; |
|
315 aQuery.tagsAreNot = true; |
|
316 } |
|
317 ] |
|
318 }, |
|
319 // transitions |
|
320 { |
|
321 desc: "tests nsINavHistoryQuery.getTransitions", |
|
322 matches: function (aQuery1, aQuery2) { |
|
323 var q1Trans = aQuery1.getTransitions(); |
|
324 var q2Trans = aQuery2.getTransitions(); |
|
325 if (q1Trans.length !== q2Trans.length) |
|
326 return false; |
|
327 for (let i = 0; i < q1Trans.length; i++) { |
|
328 if (q2Trans.indexOf(q1Trans[i]) < 0) |
|
329 return false; |
|
330 } |
|
331 for (let i = 0; i < q2Trans.length; i++) { |
|
332 if (q1Trans.indexOf(q2Trans[i]) < 0) |
|
333 return false; |
|
334 } |
|
335 return true; |
|
336 }, |
|
337 runs: [ |
|
338 function (aQuery, aQueryOptions) { |
|
339 aQuery.setTransitions([], 0); |
|
340 }, |
|
341 function (aQuery, aQueryOptions) { |
|
342 aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD], |
|
343 1); |
|
344 }, |
|
345 function (aQuery, aQueryOptions) { |
|
346 aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_TYPED, |
|
347 Ci.nsINavHistoryService.TRANSITION_BOOKMARK], 2); |
|
348 } |
|
349 ] |
|
350 }, |
|
351 ]; |
|
352 |
|
353 // nsINavHistoryQueryOptions switches |
|
354 const queryOptionSwitches = [ |
|
355 // sortingMode |
|
356 { |
|
357 desc: "nsINavHistoryQueryOptions.sortingMode", |
|
358 matches: function (aOptions1, aOptions2) { |
|
359 if (aOptions1.sortingMode === aOptions2.sortingMode) { |
|
360 switch (aOptions1.sortingMode) { |
|
361 case aOptions1.SORT_BY_ANNOTATION_ASCENDING: |
|
362 case aOptions1.SORT_BY_ANNOTATION_DESCENDING: |
|
363 return aOptions1.sortingAnnotation === aOptions2.sortingAnnotation; |
|
364 break; |
|
365 } |
|
366 return true; |
|
367 } |
|
368 return false; |
|
369 }, |
|
370 runs: [ |
|
371 function (aQuery, aQueryOptions) { |
|
372 aQueryOptions.sortingMode = aQueryOptions.SORT_BY_DATE_ASCENDING; |
|
373 }, |
|
374 function (aQuery, aQueryOptions) { |
|
375 aQueryOptions.sortingMode = aQueryOptions.SORT_BY_ANNOTATION_ASCENDING; |
|
376 aQueryOptions.sortingAnnotation = "bookmarks/toolbarFolder"; |
|
377 } |
|
378 ] |
|
379 }, |
|
380 // resultType |
|
381 { |
|
382 // property is used by function simplePropertyMatches. |
|
383 property: "resultType", |
|
384 desc: "nsINavHistoryQueryOptions.resultType", |
|
385 matches: simplePropertyMatches, |
|
386 runs: [ |
|
387 function (aQuery, aQueryOptions) { |
|
388 aQueryOptions.resultType = aQueryOptions.RESULTS_AS_URI; |
|
389 }, |
|
390 function (aQuery, aQueryOptions) { |
|
391 aQueryOptions.resultType = aQueryOptions.RESULTS_AS_FULL_VISIT; |
|
392 } |
|
393 ] |
|
394 }, |
|
395 // excludeItems |
|
396 { |
|
397 property: "excludeItems", |
|
398 desc: "nsINavHistoryQueryOptions.excludeItems", |
|
399 matches: simplePropertyMatches, |
|
400 runs: [ |
|
401 function (aQuery, aQueryOptions) { |
|
402 aQueryOptions.excludeItems = true; |
|
403 } |
|
404 ] |
|
405 }, |
|
406 // excludeQueries |
|
407 { |
|
408 property: "excludeQueries", |
|
409 desc: "nsINavHistoryQueryOptions.excludeQueries", |
|
410 matches: simplePropertyMatches, |
|
411 runs: [ |
|
412 function (aQuery, aQueryOptions) { |
|
413 aQueryOptions.excludeQueries = true; |
|
414 } |
|
415 ] |
|
416 }, |
|
417 // excludeReadOnlyFolders |
|
418 { |
|
419 property: "excludeReadOnlyFolders", |
|
420 desc: "nsINavHistoryQueryOptions.excludeReadOnlyFolders", |
|
421 matches: simplePropertyMatches, |
|
422 runs: [ |
|
423 function (aQuery, aQueryOptions) { |
|
424 aQueryOptions.excludeReadOnlyFolders = true; |
|
425 } |
|
426 ] |
|
427 }, |
|
428 // expandQueries |
|
429 { |
|
430 property: "expandQueries", |
|
431 desc: "nsINavHistoryQueryOptions.expandQueries", |
|
432 matches: simplePropertyMatches, |
|
433 runs: [ |
|
434 function (aQuery, aQueryOptions) { |
|
435 aQueryOptions.expandQueries = true; |
|
436 } |
|
437 ] |
|
438 }, |
|
439 // includeHidden |
|
440 { |
|
441 property: "includeHidden", |
|
442 desc: "nsINavHistoryQueryOptions.includeHidden", |
|
443 matches: simplePropertyMatches, |
|
444 runs: [ |
|
445 function (aQuery, aQueryOptions) { |
|
446 aQueryOptions.includeHidden = true; |
|
447 } |
|
448 ] |
|
449 }, |
|
450 // maxResults |
|
451 { |
|
452 property: "maxResults", |
|
453 desc: "nsINavHistoryQueryOptions.maxResults", |
|
454 matches: simplePropertyMatches, |
|
455 runs: [ |
|
456 function (aQuery, aQueryOptions) { |
|
457 aQueryOptions.maxResults = 0xffffffff; // 2^32 - 1 |
|
458 } |
|
459 ] |
|
460 }, |
|
461 // queryType |
|
462 { |
|
463 property: "queryType", |
|
464 desc: "nsINavHistoryQueryOptions.queryType", |
|
465 matches: simplePropertyMatches, |
|
466 runs: [ |
|
467 function (aQuery, aQueryOptions) { |
|
468 aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_HISTORY; |
|
469 }, |
|
470 function (aQuery, aQueryOptions) { |
|
471 aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_UNIFIED; |
|
472 } |
|
473 ] |
|
474 }, |
|
475 ]; |
|
476 |
|
477 /////////////////////////////////////////////////////////////////////////////// |
|
478 |
|
479 /** |
|
480 * Enumerates all the sequences of the cartesian product of the arrays contained |
|
481 * in aSequences. Examples: |
|
482 * |
|
483 * cartProd([[1, 2, 3], ["a", "b"]], callback); |
|
484 * // callback is called 3 * 2 = 6 times with the following arrays: |
|
485 * // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"] |
|
486 * |
|
487 * cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback); |
|
488 * // callback is called 1 * 3 * 2 = 6 times with the following arrays: |
|
489 * // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"], |
|
490 * // ["a", 3, "X"], ["a", 3, "Y"] |
|
491 * |
|
492 * cartProd([[1], [2], [3], [4]], callback); |
|
493 * // callback is called 1 * 1 * 1 * 1 = 1 time with the following array: |
|
494 * // [1, 2, 3, 4] |
|
495 * |
|
496 * cartProd([], callback); |
|
497 * // callback is 0 times |
|
498 * |
|
499 * cartProd([[1, 2, 3, 4]], callback); |
|
500 * // callback is called 4 times with the following arrays: |
|
501 * // [1], [2], [3], [4] |
|
502 * |
|
503 * @param aSequences |
|
504 * an array that contains an arbitrary number of arrays |
|
505 * @param aCallback |
|
506 * a function that is passed each sequence of the product as it's |
|
507 * computed |
|
508 * @return the total number of sequences in the product |
|
509 */ |
|
510 function cartProd(aSequences, aCallback) |
|
511 { |
|
512 if (aSequences.length === 0) |
|
513 return 0; |
|
514 |
|
515 // For each sequence in aSequences, we maintain a pointer (an array index, |
|
516 // really) to the element we're currently enumerating in that sequence |
|
517 var seqEltPtrs = aSequences.map(function (i) 0); |
|
518 |
|
519 var numProds = 0; |
|
520 var done = false; |
|
521 while (!done) { |
|
522 numProds++; |
|
523 |
|
524 // prod = sequence in product we're currently enumerating |
|
525 let prod = []; |
|
526 for (let i = 0; i < aSequences.length; i++) { |
|
527 prod.push(aSequences[i][seqEltPtrs[i]]); |
|
528 } |
|
529 aCallback(prod); |
|
530 |
|
531 // The next sequence in the product differs from the current one by just a |
|
532 // single element. Determine which element that is. We advance the |
|
533 // "rightmost" element pointer to the "right" by one. If we move past the |
|
534 // end of that pointer's sequence, reset the pointer to the first element |
|
535 // in its sequence and then try the sequence to the "left", and so on. |
|
536 |
|
537 // seqPtr = index of rightmost input sequence whose element pointer is not |
|
538 // past the end of the sequence |
|
539 let seqPtr = aSequences.length - 1; |
|
540 while (!done) { |
|
541 // Advance the rightmost element pointer. |
|
542 seqEltPtrs[seqPtr]++; |
|
543 |
|
544 // The rightmost element pointer is past the end of its sequence. |
|
545 if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) { |
|
546 seqEltPtrs[seqPtr] = 0; |
|
547 seqPtr--; |
|
548 |
|
549 // All element pointers are past the ends of their sequences. |
|
550 if (seqPtr < 0) |
|
551 done = true; |
|
552 } |
|
553 else break; |
|
554 } |
|
555 } |
|
556 return numProds; |
|
557 } |
|
558 |
|
559 /** |
|
560 * Enumerates all the subsets in aSet of size aHowMany. There are |
|
561 * C(aSet.length, aHowMany) such subsets. aCallback will be passed each subset |
|
562 * as it is generated. Note that aSet and the subsets enumerated are -- even |
|
563 * though they're arrays -- not sequences; the ordering of their elements is not |
|
564 * important. Example: |
|
565 * |
|
566 * choose([1, 2, 3, 4], 2, callback); |
|
567 * // callback is called C(4, 2) = 6 times with the following sets (arrays): |
|
568 * // [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4] |
|
569 * |
|
570 * @param aSet |
|
571 * an array from which to choose elements, aSet.length > 0 |
|
572 * @param aHowMany |
|
573 * the number of elements to choose, > 0 and <= aSet.length |
|
574 * @return the total number of sets chosen |
|
575 */ |
|
576 function choose(aSet, aHowMany, aCallback) |
|
577 { |
|
578 // ptrs = indices of the elements in aSet we're currently choosing |
|
579 var ptrs = []; |
|
580 for (let i = 0; i < aHowMany; i++) { |
|
581 ptrs.push(i); |
|
582 } |
|
583 |
|
584 var numFound = 0; |
|
585 var done = false; |
|
586 while (!done) { |
|
587 numFound++; |
|
588 aCallback(ptrs.map(function (p) aSet[p])); |
|
589 |
|
590 // The next subset to be chosen differs from the current one by just a |
|
591 // single element. Determine which element that is. Advance the "rightmost" |
|
592 // pointer to the "right" by one. If we move past the end of set, move the |
|
593 // next non-adjacent rightmost pointer to the right by one, and reset all |
|
594 // succeeding pointers so that they're adjacent to it. When all pointers |
|
595 // are clustered all the way to the right, we're done. |
|
596 |
|
597 // Advance the rightmost pointer. |
|
598 ptrs[ptrs.length - 1]++; |
|
599 |
|
600 // The rightmost pointer has gone past the end of set. |
|
601 if (ptrs[ptrs.length - 1] >= aSet.length) { |
|
602 // Find the next rightmost pointer that is not adjacent to the current one. |
|
603 let si = aSet.length - 2; // aSet index |
|
604 let pi = ptrs.length - 2; // ptrs index |
|
605 while (pi >= 0 && ptrs[pi] === si) { |
|
606 pi--; |
|
607 si--; |
|
608 } |
|
609 |
|
610 // All pointers are adjacent and clustered all the way to the right. |
|
611 if (pi < 0) |
|
612 done = true; |
|
613 else { |
|
614 // pi = index of rightmost pointer with a gap between it and its |
|
615 // succeeding pointer. Move it right and reset all succeeding pointers |
|
616 // so that they're adjacent to it. |
|
617 ptrs[pi]++; |
|
618 for (let i = 0; i < ptrs.length - pi - 1; i++) { |
|
619 ptrs[i + pi + 1] = ptrs[pi] + i + 1; |
|
620 } |
|
621 } |
|
622 } |
|
623 } |
|
624 return numFound; |
|
625 } |
|
626 |
|
627 /** |
|
628 * Convenience function for nsINavHistoryQuery switches that act as flags. This |
|
629 * is attached to switch objects. See querySwitches array above. |
|
630 * |
|
631 * @param aQuery1 |
|
632 * an nsINavHistoryQuery object |
|
633 * @param aQuery2 |
|
634 * another nsINavHistoryQuery object |
|
635 * @return true if this switch is the same in both aQuery1 and aQuery2 |
|
636 */ |
|
637 function flagSwitchMatches(aQuery1, aQuery2) |
|
638 { |
|
639 if (aQuery1[this.flag] && aQuery2[this.flag]) { |
|
640 for (let p in this.subswitches) { |
|
641 if (p in aQuery1 && p in aQuery2) { |
|
642 if (aQuery1[p] instanceof Ci.nsIURI) { |
|
643 if (!aQuery1[p].equals(aQuery2[p])) |
|
644 return false; |
|
645 } |
|
646 else if (aQuery1[p] !== aQuery2[p]) |
|
647 return false; |
|
648 } |
|
649 } |
|
650 } |
|
651 else if (aQuery1[this.flag] || aQuery2[this.flag]) |
|
652 return false; |
|
653 |
|
654 return true; |
|
655 } |
|
656 |
|
657 /** |
|
658 * Tests if aObj1 and aObj2 are equal. This function is general and may be used |
|
659 * for either nsINavHistoryQuery or nsINavHistoryQueryOptions objects. aSwitches |
|
660 * determines which set of switches is used for comparison. Pass in either |
|
661 * querySwitches or queryOptionSwitches. |
|
662 * |
|
663 * @param aSwitches |
|
664 * determines which set of switches applies to aObj1 and aObj2, either |
|
665 * querySwitches or queryOptionSwitches |
|
666 * @param aObj1 |
|
667 * an nsINavHistoryQuery or nsINavHistoryQueryOptions object |
|
668 * @param aObj2 |
|
669 * another nsINavHistoryQuery or nsINavHistoryQueryOptions object |
|
670 * @return true if aObj1 and aObj2 are equal |
|
671 */ |
|
672 function queryObjsEqual(aSwitches, aObj1, aObj2) |
|
673 { |
|
674 for (let i = 0; i < aSwitches.length; i++) { |
|
675 if (!aSwitches[i].matches(aObj1, aObj2)) |
|
676 return false; |
|
677 } |
|
678 return true; |
|
679 } |
|
680 |
|
681 /** |
|
682 * This drives the test runs. See the comment at the top of this file. |
|
683 * |
|
684 * @param aHowManyLo |
|
685 * the size of the switch subsets to start with |
|
686 * @param aHowManyHi |
|
687 * the size of the switch subsets to end with (inclusive) |
|
688 */ |
|
689 function runQuerySequences(aHowManyLo, aHowManyHi) |
|
690 { |
|
691 var allSwitches = querySwitches.concat(queryOptionSwitches); |
|
692 var prevQueries = []; |
|
693 var prevOpts = []; |
|
694 |
|
695 // Choose aHowManyLo switches up to aHowManyHi switches. |
|
696 for (let howMany = aHowManyLo; howMany <= aHowManyHi; howMany++) { |
|
697 let numIters = 0; |
|
698 print("CHOOSING " + howMany + " SWITCHES"); |
|
699 |
|
700 // Choose all subsets of size howMany from allSwitches. |
|
701 choose(allSwitches, howMany, function (chosenSwitches) { |
|
702 print(numIters); |
|
703 numIters++; |
|
704 |
|
705 // Collect the runs. |
|
706 // runs = [ [runs from switch 1], ..., [runs from switch howMany] ] |
|
707 var runs = chosenSwitches.map(function (s) { |
|
708 if (s.desc) |
|
709 print(" " + s.desc); |
|
710 return s.runs; |
|
711 }); |
|
712 |
|
713 // cartProd(runs) => [ |
|
714 // [switch 1 run 1, switch 2 run 1, ..., switch howMany run 1 ], |
|
715 // ..., |
|
716 // [switch 1 run 1, switch 2 run 1, ..., switch howMany run N ], |
|
717 // ..., ..., |
|
718 // [switch 1 run N, switch 2 run N, ..., switch howMany run 1 ], |
|
719 // ..., |
|
720 // [switch 1 run N, switch 2 run N, ..., switch howMany run N ], |
|
721 // ] |
|
722 cartProd(runs, function (runSet) { |
|
723 // Create a new query, apply the switches in runSet, and test it. |
|
724 var query = PlacesUtils.history.getNewQuery(); |
|
725 var opts = PlacesUtils.history.getNewQueryOptions(); |
|
726 for (let i = 0; i < runSet.length; i++) { |
|
727 runSet[i](query, opts); |
|
728 } |
|
729 serializeDeserialize([query], opts); |
|
730 |
|
731 // Test the previous NUM_MULTIPLE_QUERIES queries together. |
|
732 prevQueries.push(query); |
|
733 prevOpts.push(opts); |
|
734 if (prevQueries.length >= NUM_MULTIPLE_QUERIES) { |
|
735 // We can serialize multiple nsINavHistoryQuery objects together but |
|
736 // only one nsINavHistoryQueryOptions object with them. So, test each |
|
737 // of the previous NUM_MULTIPLE_QUERIES nsINavHistoryQueryOptions. |
|
738 for (let i = 0; i < prevOpts.length; i++) { |
|
739 serializeDeserialize(prevQueries, prevOpts[i]); |
|
740 } |
|
741 prevQueries.shift(); |
|
742 prevOpts.shift(); |
|
743 } |
|
744 }); |
|
745 }); |
|
746 } |
|
747 print("\n"); |
|
748 } |
|
749 |
|
750 /** |
|
751 * Serializes the nsINavHistoryQuery objects in aQueryArr and the |
|
752 * nsINavHistoryQueryOptions object aQueryOptions, de-serializes the |
|
753 * serialization, and ensures (using do_check_* functions) that the |
|
754 * de-serialized objects equal the originals. |
|
755 * |
|
756 * @param aQueryArr |
|
757 * an array containing nsINavHistoryQuery objects |
|
758 * @param aQueryOptions |
|
759 * an nsINavHistoryQueryOptions object |
|
760 */ |
|
761 function serializeDeserialize(aQueryArr, aQueryOptions) |
|
762 { |
|
763 var queryStr = PlacesUtils.history.queriesToQueryString(aQueryArr, |
|
764 aQueryArr.length, |
|
765 aQueryOptions); |
|
766 print(" " + queryStr); |
|
767 var queryArr2 = {}; |
|
768 var opts2 = {}; |
|
769 PlacesUtils.history.queryStringToQueries(queryStr, queryArr2, {}, opts2); |
|
770 queryArr2 = queryArr2.value; |
|
771 opts2 = opts2.value; |
|
772 |
|
773 // The two sets of queries cannot be the same if their lengths differ. |
|
774 do_check_eq(aQueryArr.length, queryArr2.length); |
|
775 |
|
776 // Although the query serialization code as it is written now practically |
|
777 // ensures that queries appear in the query string in the same order they |
|
778 // appear in both the array to be serialized and the array resulting from |
|
779 // de-serialization, the interface does not guarantee any ordering. So, for |
|
780 // each query in aQueryArr, find its equivalent in queryArr2 and delete it |
|
781 // from queryArr2. If queryArr2 is empty after looping through aQueryArr, |
|
782 // the two sets of queries are equal. |
|
783 for (let i = 0; i < aQueryArr.length; i++) { |
|
784 let j = 0; |
|
785 for (; j < queryArr2.length; j++) { |
|
786 if (queryObjsEqual(querySwitches, aQueryArr[i], queryArr2[j])) |
|
787 break; |
|
788 } |
|
789 if (j < queryArr2.length) |
|
790 queryArr2.splice(j, 1); |
|
791 } |
|
792 do_check_eq(queryArr2.length, 0); |
|
793 |
|
794 // Finally check the query options objects. |
|
795 do_check_true(queryObjsEqual(queryOptionSwitches, aQueryOptions, opts2)); |
|
796 } |
|
797 |
|
798 /** |
|
799 * Convenience function for switches that have simple values. This is attached |
|
800 * to switch objects. See querySwitches and queryOptionSwitches arrays above. |
|
801 * |
|
802 * @param aObj1 |
|
803 * an nsINavHistoryQuery or nsINavHistoryQueryOptions object |
|
804 * @param aObj2 |
|
805 * another nsINavHistoryQuery or nsINavHistoryQueryOptions object |
|
806 * @return true if this switch is the same in both aObj1 and aObj2 |
|
807 */ |
|
808 function simplePropertyMatches(aObj1, aObj2) |
|
809 { |
|
810 return aObj1[this.property] === aObj2[this.property]; |
|
811 } |
|
812 |
|
813 /////////////////////////////////////////////////////////////////////////////// |
|
814 |
|
815 function run_test() |
|
816 { |
|
817 runQuerySequences(CHOOSE_HOW_MANY_SWITCHES_LO, CHOOSE_HOW_MANY_SWITCHES_HI); |
|
818 } |