Source code

Revision control

Copy as Markdown

Other Tools

Test Info: Warnings

/* -*- 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
*
* 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.
*
* 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.
*/
const CHOOSE_HOW_MANY_SWITCHES_LO = 1;
const CHOOSE_HOW_MANY_SWITCHES_HI = 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) {
aQuery.beginTime = Date.now() * 1000;
aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH;
},
function (aQuery) {
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) {
aQuery.endTime = Date.now() * 1000;
aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH;
},
function (aQuery) {
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) {
aQuery.searchTerms = "shrimp and white wine";
},
function (aQuery) {
aQuery.searchTerms = "";
},
],
},
// hasDomain
{
flag: "hasDomain",
subswitches: ["domain", "domainIsHost"],
desc: "nsINavHistoryQuery.hasDomain",
matches: flagSwitchMatches,
runs: [
function (aQuery) {
aQuery.domain = "mozilla.com";
aQuery.domainIsHost = false;
},
function (aQuery) {
aQuery.domain = "www.mozilla.com";
aQuery.domainIsHost = true;
},
function (aQuery) {
aQuery.domain = "";
},
],
},
// hasUri
{
flag: "hasUri",
subswitches: ["uri"],
desc: "nsINavHistoryQuery.hasUri",
matches: flagSwitchMatches,
runs: [
function (aQuery) {
aQuery.uri = uri("http://mozilla.com");
},
],
},
// minVisits
{
// property is used by function simplePropertyMatches.
property: "minVisits",
desc: "nsINavHistoryQuery.minVisits",
matches: simplePropertyMatches,
runs: [
function (aQuery) {
aQuery.minVisits = 0x7fffffff; // 2^31 - 1
},
],
},
// maxVisits
{
property: "maxVisits",
desc: "nsINavHistoryQuery.maxVisits",
matches: simplePropertyMatches,
runs: [
function (aQuery) {
aQuery.maxVisits = 0x7fffffff; // 2^31 - 1
},
],
},
// getFolders
{
desc: "nsINavHistoryQuery.getParents",
matches(aQuery1, aQuery2) {
var q1Parents = aQuery1.getParents();
var q2Parents = aQuery2.getParents();
if (q1Parents.length !== q2Parents.length) {
return false;
}
for (let i = 0; i < q1Parents.length; i++) {
if (!q2Parents.includes(q1Parents[i])) {
return false;
}
}
for (let i = 0; i < q2Parents.length; i++) {
if (!q1Parents.includes(q2Parents[i])) {
return false;
}
}
return true;
},
runs: [
function (aQuery) {
aQuery.setParents([]);
},
function (aQuery) {
aQuery.setParents([PlacesUtils.bookmarks.rootGuid]);
},
function (aQuery) {
aQuery.setParents([
PlacesUtils.bookmarks.rootGuid,
PlacesUtils.bookmarks.tagsGuid,
]);
},
],
},
// tags
{
desc: "nsINavHistoryQuery.getTags",
matches(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.includes(q1Tags[i])) {
return false;
}
}
for (let i = 0; i < q2Tags.length; i++) {
if (!q1Tags.includes(q2Tags[i])) {
return false;
}
}
return true;
},
runs: [
function (aQuery) {
aQuery.tags = [];
},
function (aQuery) {
aQuery.tags = [""];
},
function (aQuery) {
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) {
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(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.includes(q1Trans[i])) {
return false;
}
}
for (let i = 0; i < q2Trans.length; i++) {
if (!q1Trans.includes(q2Trans[i])) {
return false;
}
}
return true;
},
runs: [
function (aQuery) {
aQuery.setTransitions([]);
},
function (aQuery) {
aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD]);
},
function (aQuery) {
aQuery.setTransitions([
Ci.nsINavHistoryService.TRANSITION_TYPED,
Ci.nsINavHistoryService.TRANSITION_BOOKMARK,
]);
},
],
},
];
// nsINavHistoryQueryOptions switches
const queryOptionSwitches = [
// sortingMode
{
desc: "nsINavHistoryQueryOptions.sortingMode",
matches(aOptions1, aOptions2) {
if (aOptions1.sortingMode === aOptions2.sortingMode) {
return true;
}
return false;
},
runs: [
function (aQuery, aQueryOptions) {
aQueryOptions.sortingMode = aQueryOptions.SORT_BY_DATE_ASCENDING;
},
],
},
// resultType
{
// property is used by function simplePropertyMatches.
property: "resultType",
desc: "nsINavHistoryQueryOptions.resultType",
matches: simplePropertyMatches,
runs: [
function (aQuery, aQueryOptions) {
aQueryOptions.resultType = aQueryOptions.RESULTS_AS_URI;
},
],
},
// 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_BOOKMARKS;
},
],
},
];
/**
* 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(() => 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);
// 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);
});
});
}
print("\n");
}
/**
* Serializes the nsINavHistoryQuery objects in aQuery and the
* nsINavHistoryQueryOptions object aQueryOptions, de-serializes the
* serialization, and ensures (using do_check_* functions) that the
* de-serialized objects equal the originals.
*
* @param aQuery
* an nsINavHistoryQuery object
* @param aQueryOptions
* an nsINavHistoryQueryOptions object
*/
function serializeDeserialize(aQuery, aQueryOptions) {
let queryStr = PlacesUtils.history.queryToQueryString(aQuery, aQueryOptions);
print(" " + queryStr);
let query2 = {},
opts2 = {};
PlacesUtils.history.queryStringToQuery(queryStr, query2, opts2);
query2 = query2.value;
opts2 = opts2.value;
Assert.ok(queryObjsEqual(querySwitches, aQuery, query2));
// Finally check the query options objects.
Assert.ok(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);
}