summaryrefslogtreecommitdiffstats
path: root/toolkit/components/places/tests/queries/test_querySerialization.js
diff options
context:
space:
mode:
Diffstat (limited to 'toolkit/components/places/tests/queries/test_querySerialization.js')
-rw-r--r--toolkit/components/places/tests/queries/test_querySerialization.js797
1 files changed, 797 insertions, 0 deletions
diff --git a/toolkit/components/places/tests/queries/test_querySerialization.js b/toolkit/components/places/tests/queries/test_querySerialization.js
new file mode 100644
index 000000000..24cf8aa9b
--- /dev/null
+++ b/toolkit/components/places/tests/queries/test_querySerialization.js
@@ -0,0 +1,797 @@
+/* -*- indent-tabs-mode: nil; js-indent-level: 2 -*- */
+/* vim:set ts=2 sw=2 sts=2 et: */
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+/**
+ * Tests Places query serialization. Associated bug is
+ * https://bugzilla.mozilla.org/show_bug.cgi?id=370197
+ *
+ * The simple idea behind this test is to try out different combinations of
+ * query switches and ensure that queries are the same before serialization
+ * as they are after de-serialization.
+ *
+ * In the code below, "switch" refers to a query option -- "option" in a broad
+ * sense, not nsINavHistoryQueryOptions specifically (which is why we refer to
+ * them as switches, not options). Both nsINavHistoryQuery and
+ * nsINavHistoryQueryOptions allow you to specify switches that affect query
+ * strings. nsINavHistoryQuery instances have attributes hasBeginTime,
+ * hasEndTime, hasSearchTerms, and so on. nsINavHistoryQueryOptions instances
+ * have attributes sortingMode, resultType, excludeItems, etc.
+ *
+ * Ideally we would like to test all 2^N subsets of switches, where N is the
+ * total number of switches; switches might interact in erroneous or other ways
+ * we do not expect. However, since N is large (21 at this time), that's
+ * impractical for a single test in a suite.
+ *
+ * Instead we choose all possible subsets of a certain, smaller size. In fact
+ * we begin by choosing CHOOSE_HOW_MANY_SWITCHES_LO and ramp up to
+ * CHOOSE_HOW_MANY_SWITCHES_HI.
+ *
+ * There are two more wrinkles. First, for some switches we'd like to be able to
+ * test multiple values. For example, it seems like a good idea to test both an
+ * empty string and a non-empty string for switch nsINavHistoryQuery.searchTerms.
+ * When switches have more than one value for a test run, we use the Cartesian
+ * product of their values to generate all possible combinations of values.
+ *
+ * Second, we need to also test serialization of multiple nsINavHistoryQuery
+ * objects at once. To do this, we remember the previous NUM_MULTIPLE_QUERIES
+ * queries we tested individually and then serialize them together. We do this
+ * each time we test an individual query. Thus the set of queries we test
+ * together loses one query and gains another each time.
+ *
+ * To summarize, here's how this test works:
+ *
+ * - For n = CHOOSE_HOW_MANY_SWITCHES_LO to CHOOSE_HOW_MANY_SWITCHES_HI:
+ * - From the total set of switches choose all possible subsets of size n.
+ * For each of those subsets s:
+ * - Collect the test runs of each switch in subset s and take their
+ * Cartesian product. For each sequence in the product:
+ * - Create nsINavHistoryQuery and nsINavHistoryQueryOptions objects
+ * with the chosen switches and test run values.
+ * - Serialize the query.
+ * - De-serialize and ensure that the de-serialized query objects equal
+ * the originals.
+ * - For each of the previous NUM_MULTIPLE_QUERIES
+ * nsINavHistoryQueryOptions objects o we created:
+ * - Serialize the previous NUM_MULTIPLE_QUERIES nsINavHistoryQuery
+ * objects together with o.
+ * - De-serialize and ensure that the de-serialized query objects
+ * equal the originals.
+ */
+
+const CHOOSE_HOW_MANY_SWITCHES_LO = 1;
+const CHOOSE_HOW_MANY_SWITCHES_HI = 2;
+
+const NUM_MULTIPLE_QUERIES = 2;
+
+// The switches are represented by objects below, in arrays querySwitches and
+// queryOptionSwitches. Use them to set up test runs.
+//
+// Some switches have special properties (where noted), but all switches must
+// have the following properties:
+//
+// matches: A function that takes two nsINavHistoryQuery objects (in the case
+// of nsINavHistoryQuery switches) or two nsINavHistoryQueryOptions
+// objects (for nsINavHistoryQueryOptions switches) and returns true
+// if the values of the switch in the two objects are equal. This is
+// the foundation of how we determine if two queries are equal.
+// runs: An array of functions. Each function takes an nsINavHistoryQuery
+// object and an nsINavHistoryQueryOptions object. The functions
+// should set the attributes of one of the two objects as appropriate
+// to their switches. This is how switch values are set for each test
+// run.
+//
+// The following properties are optional:
+//
+// desc: An informational string to print out during runs when the switch
+// is chosen. Hopefully helpful if the test fails.
+
+// nsINavHistoryQuery switches
+const querySwitches = [
+ // hasBeginTime
+ {
+ // flag and subswitches are used by the flagSwitchMatches function. Several
+ // of the nsINavHistoryQuery switches (like this one) are really guard flags
+ // that indicate if other "subswitches" are enabled.
+ flag: "hasBeginTime",
+ subswitches: ["beginTime", "beginTimeReference", "absoluteBeginTime"],
+ desc: "nsINavHistoryQuery.hasBeginTime",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.beginTime = Date.now() * 1000;
+ aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.beginTime = Date.now() * 1000;
+ aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY;
+ }
+ ]
+ },
+ // hasEndTime
+ {
+ flag: "hasEndTime",
+ subswitches: ["endTime", "endTimeReference", "absoluteEndTime"],
+ desc: "nsINavHistoryQuery.hasEndTime",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.endTime = Date.now() * 1000;
+ aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.endTime = Date.now() * 1000;
+ aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY;
+ }
+ ]
+ },
+ // hasSearchTerms
+ {
+ flag: "hasSearchTerms",
+ subswitches: ["searchTerms"],
+ desc: "nsINavHistoryQuery.hasSearchTerms",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.searchTerms = "shrimp and white wine";
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.searchTerms = "";
+ }
+ ]
+ },
+ // hasDomain
+ {
+ flag: "hasDomain",
+ subswitches: ["domain", "domainIsHost"],
+ desc: "nsINavHistoryQuery.hasDomain",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.domain = "mozilla.com";
+ aQuery.domainIsHost = false;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.domain = "www.mozilla.com";
+ aQuery.domainIsHost = true;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.domain = "";
+ }
+ ]
+ },
+ // hasUri
+ {
+ flag: "hasUri",
+ subswitches: ["uri"],
+ desc: "nsINavHistoryQuery.hasUri",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.uri = uri("http://mozilla.com");
+ },
+ ]
+ },
+ // hasAnnotation
+ {
+ flag: "hasAnnotation",
+ subswitches: ["annotation", "annotationIsNot"],
+ desc: "nsINavHistoryQuery.hasAnnotation",
+ matches: flagSwitchMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.annotation = "bookmarks/toolbarFolder";
+ aQuery.annotationIsNot = false;
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.annotation = "bookmarks/toolbarFolder";
+ aQuery.annotationIsNot = true;
+ }
+ ]
+ },
+ // minVisits
+ {
+ // property is used by function simplePropertyMatches.
+ property: "minVisits",
+ desc: "nsINavHistoryQuery.minVisits",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.minVisits = 0x7fffffff; // 2^31 - 1
+ }
+ ]
+ },
+ // maxVisits
+ {
+ property: "maxVisits",
+ desc: "nsINavHistoryQuery.maxVisits",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.maxVisits = 0x7fffffff; // 2^31 - 1
+ }
+ ]
+ },
+ // onlyBookmarked
+ {
+ property: "onlyBookmarked",
+ desc: "nsINavHistoryQuery.onlyBookmarked",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.onlyBookmarked = true;
+ }
+ ]
+ },
+ // getFolders
+ {
+ desc: "nsINavHistoryQuery.getFolders",
+ matches: function (aQuery1, aQuery2) {
+ var q1Folders = aQuery1.getFolders();
+ var q2Folders = aQuery2.getFolders();
+ if (q1Folders.length !== q2Folders.length)
+ return false;
+ for (let i = 0; i < q1Folders.length; i++) {
+ if (q2Folders.indexOf(q1Folders[i]) < 0)
+ return false;
+ }
+ for (let i = 0; i < q2Folders.length; i++) {
+ if (q1Folders.indexOf(q2Folders[i]) < 0)
+ return false;
+ }
+ return true;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.setFolders([], 0);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setFolders([PlacesUtils.placesRootId], 1);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setFolders([PlacesUtils.placesRootId, PlacesUtils.tagsFolderId], 2);
+ }
+ ]
+ },
+ // tags
+ {
+ desc: "nsINavHistoryQuery.getTags",
+ matches: function (aQuery1, aQuery2) {
+ if (aQuery1.tagsAreNot !== aQuery2.tagsAreNot)
+ return false;
+ var q1Tags = aQuery1.tags;
+ var q2Tags = aQuery2.tags;
+ if (q1Tags.length !== q2Tags.length)
+ return false;
+ for (let i = 0; i < q1Tags.length; i++) {
+ if (q2Tags.indexOf(q1Tags[i]) < 0)
+ return false;
+ }
+ for (let i = 0; i < q2Tags.length; i++) {
+ if (q1Tags.indexOf(q2Tags[i]) < 0)
+ return false;
+ }
+ return true;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [];
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [""];
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [
+ "foo",
+ "七難",
+ "",
+ "いっぱいおっぱい",
+ "Abracadabra",
+ "123",
+ "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
+ "アスキーでございません",
+ "あいうえお",
+ ];
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.tags = [
+ "foo",
+ "七難",
+ "",
+ "いっぱいおっぱい",
+ "Abracadabra",
+ "123",
+ "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
+ "アスキーでございません",
+ "あいうえお",
+ ];
+ aQuery.tagsAreNot = true;
+ }
+ ]
+ },
+ // transitions
+ {
+ desc: "tests nsINavHistoryQuery.getTransitions",
+ matches: function (aQuery1, aQuery2) {
+ var q1Trans = aQuery1.getTransitions();
+ var q2Trans = aQuery2.getTransitions();
+ if (q1Trans.length !== q2Trans.length)
+ return false;
+ for (let i = 0; i < q1Trans.length; i++) {
+ if (q2Trans.indexOf(q1Trans[i]) < 0)
+ return false;
+ }
+ for (let i = 0; i < q2Trans.length; i++) {
+ if (q1Trans.indexOf(q2Trans[i]) < 0)
+ return false;
+ }
+ return true;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQuery.setTransitions([], 0);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD],
+ 1);
+ },
+ function (aQuery, aQueryOptions) {
+ aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_TYPED,
+ Ci.nsINavHistoryService.TRANSITION_BOOKMARK], 2);
+ }
+ ]
+ },
+];
+
+// nsINavHistoryQueryOptions switches
+const queryOptionSwitches = [
+ // sortingMode
+ {
+ desc: "nsINavHistoryQueryOptions.sortingMode",
+ matches: function (aOptions1, aOptions2) {
+ if (aOptions1.sortingMode === aOptions2.sortingMode) {
+ switch (aOptions1.sortingMode) {
+ case aOptions1.SORT_BY_ANNOTATION_ASCENDING:
+ case aOptions1.SORT_BY_ANNOTATION_DESCENDING:
+ return aOptions1.sortingAnnotation === aOptions2.sortingAnnotation;
+ }
+ return true;
+ }
+ return false;
+ },
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.sortingMode = aQueryOptions.SORT_BY_DATE_ASCENDING;
+ },
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.sortingMode = aQueryOptions.SORT_BY_ANNOTATION_ASCENDING;
+ aQueryOptions.sortingAnnotation = "bookmarks/toolbarFolder";
+ }
+ ]
+ },
+ // resultType
+ {
+ // property is used by function simplePropertyMatches.
+ property: "resultType",
+ desc: "nsINavHistoryQueryOptions.resultType",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.resultType = aQueryOptions.RESULTS_AS_URI;
+ },
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.resultType = aQueryOptions.RESULTS_AS_FULL_VISIT;
+ }
+ ]
+ },
+ // excludeItems
+ {
+ property: "excludeItems",
+ desc: "nsINavHistoryQueryOptions.excludeItems",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.excludeItems = true;
+ }
+ ]
+ },
+ // excludeQueries
+ {
+ property: "excludeQueries",
+ desc: "nsINavHistoryQueryOptions.excludeQueries",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.excludeQueries = true;
+ }
+ ]
+ },
+ // expandQueries
+ {
+ property: "expandQueries",
+ desc: "nsINavHistoryQueryOptions.expandQueries",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.expandQueries = true;
+ }
+ ]
+ },
+ // includeHidden
+ {
+ property: "includeHidden",
+ desc: "nsINavHistoryQueryOptions.includeHidden",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.includeHidden = true;
+ }
+ ]
+ },
+ // maxResults
+ {
+ property: "maxResults",
+ desc: "nsINavHistoryQueryOptions.maxResults",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.maxResults = 0xffffffff; // 2^32 - 1
+ }
+ ]
+ },
+ // queryType
+ {
+ property: "queryType",
+ desc: "nsINavHistoryQueryOptions.queryType",
+ matches: simplePropertyMatches,
+ runs: [
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_HISTORY;
+ },
+ function (aQuery, aQueryOptions) {
+ aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_UNIFIED;
+ }
+ ]
+ },
+];
+
+/**
+ * Enumerates all the sequences of the cartesian product of the arrays contained
+ * in aSequences. Examples:
+ *
+ * cartProd([[1, 2, 3], ["a", "b"]], callback);
+ * // callback is called 3 * 2 = 6 times with the following arrays:
+ * // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]
+ *
+ * cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback);
+ * // callback is called 1 * 3 * 2 = 6 times with the following arrays:
+ * // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"],
+ * // ["a", 3, "X"], ["a", 3, "Y"]
+ *
+ * cartProd([[1], [2], [3], [4]], callback);
+ * // callback is called 1 * 1 * 1 * 1 = 1 time with the following array:
+ * // [1, 2, 3, 4]
+ *
+ * cartProd([], callback);
+ * // callback is 0 times
+ *
+ * cartProd([[1, 2, 3, 4]], callback);
+ * // callback is called 4 times with the following arrays:
+ * // [1], [2], [3], [4]
+ *
+ * @param aSequences
+ * an array that contains an arbitrary number of arrays
+ * @param aCallback
+ * a function that is passed each sequence of the product as it's
+ * computed
+ * @return the total number of sequences in the product
+ */
+function cartProd(aSequences, aCallback)
+{
+ if (aSequences.length === 0)
+ return 0;
+
+ // For each sequence in aSequences, we maintain a pointer (an array index,
+ // really) to the element we're currently enumerating in that sequence
+ var seqEltPtrs = aSequences.map(i => 0);
+
+ var numProds = 0;
+ var done = false;
+ while (!done) {
+ numProds++;
+
+ // prod = sequence in product we're currently enumerating
+ let prod = [];
+ for (let i = 0; i < aSequences.length; i++) {
+ prod.push(aSequences[i][seqEltPtrs[i]]);
+ }
+ aCallback(prod);
+
+ // The next sequence in the product differs from the current one by just a
+ // single element. Determine which element that is. We advance the
+ // "rightmost" element pointer to the "right" by one. If we move past the
+ // end of that pointer's sequence, reset the pointer to the first element
+ // in its sequence and then try the sequence to the "left", and so on.
+
+ // seqPtr = index of rightmost input sequence whose element pointer is not
+ // past the end of the sequence
+ let seqPtr = aSequences.length - 1;
+ while (!done) {
+ // Advance the rightmost element pointer.
+ seqEltPtrs[seqPtr]++;
+
+ // The rightmost element pointer is past the end of its sequence.
+ if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) {
+ seqEltPtrs[seqPtr] = 0;
+ seqPtr--;
+
+ // All element pointers are past the ends of their sequences.
+ if (seqPtr < 0)
+ done = true;
+ }
+ else break;
+ }
+ }
+ return numProds;
+}
+
+/**
+ * Enumerates all the subsets in aSet of size aHowMany. There are
+ * C(aSet.length, aHowMany) such subsets. aCallback will be passed each subset
+ * as it is generated. Note that aSet and the subsets enumerated are -- even
+ * though they're arrays -- not sequences; the ordering of their elements is not
+ * important. Example:
+ *
+ * choose([1, 2, 3, 4], 2, callback);
+ * // callback is called C(4, 2) = 6 times with the following sets (arrays):
+ * // [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]
+ *
+ * @param aSet
+ * an array from which to choose elements, aSet.length > 0
+ * @param aHowMany
+ * the number of elements to choose, > 0 and <= aSet.length
+ * @return the total number of sets chosen
+ */
+function choose(aSet, aHowMany, aCallback)
+{
+ // ptrs = indices of the elements in aSet we're currently choosing
+ var ptrs = [];
+ for (let i = 0; i < aHowMany; i++) {
+ ptrs.push(i);
+ }
+
+ var numFound = 0;
+ var done = false;
+ while (!done) {
+ numFound++;
+ aCallback(ptrs.map(p => aSet[p]));
+
+ // The next subset to be chosen differs from the current one by just a
+ // single element. Determine which element that is. Advance the "rightmost"
+ // pointer to the "right" by one. If we move past the end of set, move the
+ // next non-adjacent rightmost pointer to the right by one, and reset all
+ // succeeding pointers so that they're adjacent to it. When all pointers
+ // are clustered all the way to the right, we're done.
+
+ // Advance the rightmost pointer.
+ ptrs[ptrs.length - 1]++;
+
+ // The rightmost pointer has gone past the end of set.
+ if (ptrs[ptrs.length - 1] >= aSet.length) {
+ // Find the next rightmost pointer that is not adjacent to the current one.
+ let si = aSet.length - 2; // aSet index
+ let pi = ptrs.length - 2; // ptrs index
+ while (pi >= 0 && ptrs[pi] === si) {
+ pi--;
+ si--;
+ }
+
+ // All pointers are adjacent and clustered all the way to the right.
+ if (pi < 0)
+ done = true;
+ else {
+ // pi = index of rightmost pointer with a gap between it and its
+ // succeeding pointer. Move it right and reset all succeeding pointers
+ // so that they're adjacent to it.
+ ptrs[pi]++;
+ for (let i = 0; i < ptrs.length - pi - 1; i++) {
+ ptrs[i + pi + 1] = ptrs[pi] + i + 1;
+ }
+ }
+ }
+ }
+ return numFound;
+}
+
+/**
+ * Convenience function for nsINavHistoryQuery switches that act as flags. This
+ * is attached to switch objects. See querySwitches array above.
+ *
+ * @param aQuery1
+ * an nsINavHistoryQuery object
+ * @param aQuery2
+ * another nsINavHistoryQuery object
+ * @return true if this switch is the same in both aQuery1 and aQuery2
+ */
+function flagSwitchMatches(aQuery1, aQuery2)
+{
+ if (aQuery1[this.flag] && aQuery2[this.flag]) {
+ for (let p in this.subswitches) {
+ if (p in aQuery1 && p in aQuery2) {
+ if (aQuery1[p] instanceof Ci.nsIURI) {
+ if (!aQuery1[p].equals(aQuery2[p]))
+ return false;
+ }
+ else if (aQuery1[p] !== aQuery2[p])
+ return false;
+ }
+ }
+ }
+ else if (aQuery1[this.flag] || aQuery2[this.flag])
+ return false;
+
+ return true;
+}
+
+/**
+ * Tests if aObj1 and aObj2 are equal. This function is general and may be used
+ * for either nsINavHistoryQuery or nsINavHistoryQueryOptions objects. aSwitches
+ * determines which set of switches is used for comparison. Pass in either
+ * querySwitches or queryOptionSwitches.
+ *
+ * @param aSwitches
+ * determines which set of switches applies to aObj1 and aObj2, either
+ * querySwitches or queryOptionSwitches
+ * @param aObj1
+ * an nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @param aObj2
+ * another nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @return true if aObj1 and aObj2 are equal
+ */
+function queryObjsEqual(aSwitches, aObj1, aObj2)
+{
+ for (let i = 0; i < aSwitches.length; i++) {
+ if (!aSwitches[i].matches(aObj1, aObj2))
+ return false;
+ }
+ return true;
+}
+
+/**
+ * This drives the test runs. See the comment at the top of this file.
+ *
+ * @param aHowManyLo
+ * the size of the switch subsets to start with
+ * @param aHowManyHi
+ * the size of the switch subsets to end with (inclusive)
+ */
+function runQuerySequences(aHowManyLo, aHowManyHi)
+{
+ var allSwitches = querySwitches.concat(queryOptionSwitches);
+ var prevQueries = [];
+ var prevOpts = [];
+
+ // Choose aHowManyLo switches up to aHowManyHi switches.
+ for (let howMany = aHowManyLo; howMany <= aHowManyHi; howMany++) {
+ let numIters = 0;
+ print("CHOOSING " + howMany + " SWITCHES");
+
+ // Choose all subsets of size howMany from allSwitches.
+ choose(allSwitches, howMany, function (chosenSwitches) {
+ print(numIters);
+ numIters++;
+
+ // Collect the runs.
+ // runs = [ [runs from switch 1], ..., [runs from switch howMany] ]
+ var runs = chosenSwitches.map(function (s) {
+ if (s.desc)
+ print(" " + s.desc);
+ return s.runs;
+ });
+
+ // cartProd(runs) => [
+ // [switch 1 run 1, switch 2 run 1, ..., switch howMany run 1 ],
+ // ...,
+ // [switch 1 run 1, switch 2 run 1, ..., switch howMany run N ],
+ // ..., ...,
+ // [switch 1 run N, switch 2 run N, ..., switch howMany run 1 ],
+ // ...,
+ // [switch 1 run N, switch 2 run N, ..., switch howMany run N ],
+ // ]
+ cartProd(runs, function (runSet) {
+ // Create a new query, apply the switches in runSet, and test it.
+ var query = PlacesUtils.history.getNewQuery();
+ var opts = PlacesUtils.history.getNewQueryOptions();
+ for (let i = 0; i < runSet.length; i++) {
+ runSet[i](query, opts);
+ }
+ serializeDeserialize([query], opts);
+
+ // Test the previous NUM_MULTIPLE_QUERIES queries together.
+ prevQueries.push(query);
+ prevOpts.push(opts);
+ if (prevQueries.length >= NUM_MULTIPLE_QUERIES) {
+ // We can serialize multiple nsINavHistoryQuery objects together but
+ // only one nsINavHistoryQueryOptions object with them. So, test each
+ // of the previous NUM_MULTIPLE_QUERIES nsINavHistoryQueryOptions.
+ for (let i = 0; i < prevOpts.length; i++) {
+ serializeDeserialize(prevQueries, prevOpts[i]);
+ }
+ prevQueries.shift();
+ prevOpts.shift();
+ }
+ });
+ });
+ }
+ print("\n");
+}
+
+/**
+ * Serializes the nsINavHistoryQuery objects in aQueryArr and the
+ * nsINavHistoryQueryOptions object aQueryOptions, de-serializes the
+ * serialization, and ensures (using do_check_* functions) that the
+ * de-serialized objects equal the originals.
+ *
+ * @param aQueryArr
+ * an array containing nsINavHistoryQuery objects
+ * @param aQueryOptions
+ * an nsINavHistoryQueryOptions object
+ */
+function serializeDeserialize(aQueryArr, aQueryOptions)
+{
+ var queryStr = PlacesUtils.history.queriesToQueryString(aQueryArr,
+ aQueryArr.length,
+ aQueryOptions);
+ print(" " + queryStr);
+ var queryArr2 = {};
+ var opts2 = {};
+ PlacesUtils.history.queryStringToQueries(queryStr, queryArr2, {}, opts2);
+ queryArr2 = queryArr2.value;
+ opts2 = opts2.value;
+
+ // The two sets of queries cannot be the same if their lengths differ.
+ do_check_eq(aQueryArr.length, queryArr2.length);
+
+ // Although the query serialization code as it is written now practically
+ // ensures that queries appear in the query string in the same order they
+ // appear in both the array to be serialized and the array resulting from
+ // de-serialization, the interface does not guarantee any ordering. So, for
+ // each query in aQueryArr, find its equivalent in queryArr2 and delete it
+ // from queryArr2. If queryArr2 is empty after looping through aQueryArr,
+ // the two sets of queries are equal.
+ for (let i = 0; i < aQueryArr.length; i++) {
+ let j = 0;
+ for (; j < queryArr2.length; j++) {
+ if (queryObjsEqual(querySwitches, aQueryArr[i], queryArr2[j]))
+ break;
+ }
+ if (j < queryArr2.length)
+ queryArr2.splice(j, 1);
+ }
+ do_check_eq(queryArr2.length, 0);
+
+ // Finally check the query options objects.
+ do_check_true(queryObjsEqual(queryOptionSwitches, aQueryOptions, opts2));
+}
+
+/**
+ * Convenience function for switches that have simple values. This is attached
+ * to switch objects. See querySwitches and queryOptionSwitches arrays above.
+ *
+ * @param aObj1
+ * an nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @param aObj2
+ * another nsINavHistoryQuery or nsINavHistoryQueryOptions object
+ * @return true if this switch is the same in both aObj1 and aObj2
+ */
+function simplePropertyMatches(aObj1, aObj2)
+{
+ return aObj1[this.property] === aObj2[this.property];
+}
+
+function run_test()
+{
+ runQuerySequences(CHOOSE_HOW_MANY_SWITCHES_LO, CHOOSE_HOW_MANY_SWITCHES_HI);
+}