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