toolkit/components/places/tests/queries/test_querySerialization.js

changeset 0
6474c204b198
     1.1 --- /dev/null	Thu Jan 01 00:00:00 1970 +0000
     1.2 +++ b/toolkit/components/places/tests/queries/test_querySerialization.js	Wed Dec 31 06:09:35 2014 +0100
     1.3 @@ -0,0 +1,818 @@
     1.4 +/* -*- Mode: Java; tab-width: 2; indent-tabs-mode: nil; c-basic-offset: 2 -*- */
     1.5 +/* vim:set ts=2 sw=2 sts=2 et: */
     1.6 +/* This Source Code Form is subject to the terms of the Mozilla Public
     1.7 + * License, v. 2.0. If a copy of the MPL was not distributed with this
     1.8 + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     1.9 +
    1.10 +/**
    1.11 + * Tests Places query serialization.  Associated bug is
    1.12 + * https://bugzilla.mozilla.org/show_bug.cgi?id=370197
    1.13 + *
    1.14 + * The simple idea behind this test is to try out different combinations of
    1.15 + * query switches and ensure that queries are the same before serialization
    1.16 + * as they are after de-serialization.
    1.17 + *
    1.18 + * In the code below, "switch" refers to a query option -- "option" in a broad
    1.19 + * sense, not nsINavHistoryQueryOptions specifically (which is why we refer to
    1.20 + * them as switches, not options).  Both nsINavHistoryQuery and
    1.21 + * nsINavHistoryQueryOptions allow you to specify switches that affect query
    1.22 + * strings.  nsINavHistoryQuery instances have attributes hasBeginTime,
    1.23 + * hasEndTime, hasSearchTerms, and so on.  nsINavHistoryQueryOptions instances
    1.24 + * have attributes sortingMode, resultType, excludeItems, etc.
    1.25 + *
    1.26 + * Ideally we would like to test all 2^N subsets of switches, where N is the
    1.27 + * total number of switches; switches might interact in erroneous or other ways
    1.28 + * we do not expect.  However, since N is large (21 at this time), that's
    1.29 + * impractical for a single test in a suite.
    1.30 + *
    1.31 + * Instead we choose all possible subsets of a certain, smaller size.  In fact
    1.32 + * we begin by choosing CHOOSE_HOW_MANY_SWITCHES_LO and ramp up to
    1.33 + * CHOOSE_HOW_MANY_SWITCHES_HI.
    1.34 + *
    1.35 + * There are two more wrinkles.  First, for some switches we'd like to be able to
    1.36 + * test multiple values.  For example, it seems like a good idea to test both an
    1.37 + * empty string and a non-empty string for switch nsINavHistoryQuery.searchTerms.
    1.38 + * When switches have more than one value for a test run, we use the Cartesian
    1.39 + * product of their values to generate all possible combinations of values.
    1.40 + *
    1.41 + * Second, we need to also test serialization of multiple nsINavHistoryQuery
    1.42 + * objects at once.  To do this, we remember the previous NUM_MULTIPLE_QUERIES
    1.43 + * queries we tested individually and then serialize them together.  We do this
    1.44 + * each time we test an individual query.  Thus the set of queries we test
    1.45 + * together loses one query and gains another each time.
    1.46 + *
    1.47 + * To summarize, here's how this test works:
    1.48 + *
    1.49 + * - For n = CHOOSE_HOW_MANY_SWITCHES_LO to CHOOSE_HOW_MANY_SWITCHES_HI:
    1.50 + *   - From the total set of switches choose all possible subsets of size n.
    1.51 + *     For each of those subsets s:
    1.52 + *     - Collect the test runs of each switch in subset s and take their
    1.53 + *       Cartesian product.  For each sequence in the product:
    1.54 + *       - Create nsINavHistoryQuery and nsINavHistoryQueryOptions objects
    1.55 + *         with the chosen switches and test run values.
    1.56 + *       - Serialize the query.
    1.57 + *       - De-serialize and ensure that the de-serialized query objects equal
    1.58 + *         the originals.
    1.59 + *       - For each of the previous NUM_MULTIPLE_QUERIES
    1.60 + *         nsINavHistoryQueryOptions objects o we created:
    1.61 + *         - Serialize the previous NUM_MULTIPLE_QUERIES nsINavHistoryQuery
    1.62 + *           objects together with o.
    1.63 + *         - De-serialize and ensure that the de-serialized query objects
    1.64 + *           equal the originals.
    1.65 + */
    1.66 +
    1.67 +const CHOOSE_HOW_MANY_SWITCHES_LO = 1;
    1.68 +const CHOOSE_HOW_MANY_SWITCHES_HI = 2;
    1.69 +
    1.70 +const NUM_MULTIPLE_QUERIES        = 2;
    1.71 +
    1.72 +// The switches are represented by objects below, in arrays querySwitches and
    1.73 +// queryOptionSwitches.  Use them to set up test runs.
    1.74 +//
    1.75 +// Some switches have special properties (where noted), but all switches must
    1.76 +// have the following properties:
    1.77 +//
    1.78 +//   matches: A function that takes two nsINavHistoryQuery objects (in the case
    1.79 +//            of nsINavHistoryQuery switches) or two nsINavHistoryQueryOptions
    1.80 +//            objects (for nsINavHistoryQueryOptions switches) and returns true
    1.81 +//            if the values of the switch in the two objects are equal.  This is
    1.82 +//            the foundation of how we determine if two queries are equal.
    1.83 +//   runs:    An array of functions.  Each function takes an nsINavHistoryQuery
    1.84 +//            object and an nsINavHistoryQueryOptions object.  The functions
    1.85 +//            should set the attributes of one of the two objects as appropriate
    1.86 +//            to their switches.  This is how switch values are set for each test
    1.87 +//            run.
    1.88 +//
    1.89 +// The following properties are optional:
    1.90 +//
    1.91 +//   desc:    An informational string to print out during runs when the switch
    1.92 +//            is chosen.  Hopefully helpful if the test fails.
    1.93 +
    1.94 +// nsINavHistoryQuery switches
    1.95 +const querySwitches = [
    1.96 +  // hasBeginTime
    1.97 +  {
    1.98 +    // flag and subswitches are used by the flagSwitchMatches function.  Several
    1.99 +    // of the nsINavHistoryQuery switches (like this one) are really guard flags
   1.100 +    // that indicate if other "subswitches" are enabled.
   1.101 +    flag:        "hasBeginTime",
   1.102 +    subswitches: ["beginTime", "beginTimeReference", "absoluteBeginTime"],
   1.103 +    desc:        "nsINavHistoryQuery.hasBeginTime",
   1.104 +    matches:     flagSwitchMatches,
   1.105 +    runs:        [
   1.106 +      function (aQuery, aQueryOptions) {
   1.107 +        aQuery.beginTime = Date.now() * 1000;
   1.108 +        aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH;
   1.109 +      },
   1.110 +      function (aQuery, aQueryOptions) {
   1.111 +        aQuery.beginTime = Date.now() * 1000;
   1.112 +        aQuery.beginTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY;
   1.113 +      }
   1.114 +    ]
   1.115 +  },
   1.116 +  // hasEndTime
   1.117 +  {
   1.118 +    flag:        "hasEndTime",
   1.119 +    subswitches: ["endTime", "endTimeReference", "absoluteEndTime"],
   1.120 +    desc:        "nsINavHistoryQuery.hasEndTime",
   1.121 +    matches:     flagSwitchMatches,
   1.122 +    runs:        [
   1.123 +      function (aQuery, aQueryOptions) {
   1.124 +        aQuery.endTime = Date.now() * 1000;
   1.125 +        aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_EPOCH;
   1.126 +      },
   1.127 +      function (aQuery, aQueryOptions) {
   1.128 +        aQuery.endTime = Date.now() * 1000;
   1.129 +        aQuery.endTimeReference = Ci.nsINavHistoryQuery.TIME_RELATIVE_TODAY;
   1.130 +      }
   1.131 +    ]
   1.132 +  },
   1.133 +  // hasSearchTerms
   1.134 +  {
   1.135 +    flag:        "hasSearchTerms",
   1.136 +    subswitches: ["searchTerms"],
   1.137 +    desc:        "nsINavHistoryQuery.hasSearchTerms",
   1.138 +    matches:     flagSwitchMatches,
   1.139 +    runs:        [
   1.140 +      function (aQuery, aQueryOptions) {
   1.141 +        aQuery.searchTerms = "shrimp and white wine";
   1.142 +      },
   1.143 +      function (aQuery, aQueryOptions) {
   1.144 +        aQuery.searchTerms = "";
   1.145 +      }
   1.146 +    ]
   1.147 +  },
   1.148 +  // hasDomain
   1.149 +  {
   1.150 +    flag:        "hasDomain",
   1.151 +    subswitches: ["domain", "domainIsHost"],
   1.152 +    desc:        "nsINavHistoryQuery.hasDomain",
   1.153 +    matches:     flagSwitchMatches,
   1.154 +    runs:        [
   1.155 +      function (aQuery, aQueryOptions) {
   1.156 +        aQuery.domain = "mozilla.com";
   1.157 +        aQuery.domainIsHost = false;
   1.158 +      },
   1.159 +      function (aQuery, aQueryOptions) {
   1.160 +        aQuery.domain = "www.mozilla.com";
   1.161 +        aQuery.domainIsHost = true;
   1.162 +      },
   1.163 +      function (aQuery, aQueryOptions) {
   1.164 +        aQuery.domain = "";
   1.165 +      }
   1.166 +    ]
   1.167 +  },
   1.168 +  // hasUri
   1.169 +  {
   1.170 +    flag:        "hasUri",
   1.171 +    subswitches: ["uri", "uriIsPrefix"],
   1.172 +    desc:        "nsINavHistoryQuery.hasUri",
   1.173 +    matches:     flagSwitchMatches,
   1.174 +    runs:        [
   1.175 +      function (aQuery, aQueryOptions) {
   1.176 +        aQuery.uri = uri("http://mozilla.com");
   1.177 +        aQuery.uriIsPrefix = false;
   1.178 +      },
   1.179 +      function (aQuery, aQueryOptions) {
   1.180 +        aQuery.uri = uri("http://mozilla.com");
   1.181 +        aQuery.uriIsPrefix = true;
   1.182 +      }
   1.183 +    ]
   1.184 +  },
   1.185 +  // hasAnnotation
   1.186 +  {
   1.187 +    flag:        "hasAnnotation",
   1.188 +    subswitches: ["annotation", "annotationIsNot"],
   1.189 +    desc:        "nsINavHistoryQuery.hasAnnotation",
   1.190 +    matches:     flagSwitchMatches,
   1.191 +    runs:        [
   1.192 +      function (aQuery, aQueryOptions) {
   1.193 +        aQuery.annotation = "bookmarks/toolbarFolder";
   1.194 +        aQuery.annotationIsNot = false;
   1.195 +      },
   1.196 +      function (aQuery, aQueryOptions) {
   1.197 +        aQuery.annotation = "bookmarks/toolbarFolder";
   1.198 +        aQuery.annotationIsNot = true;
   1.199 +      }
   1.200 +    ]
   1.201 +  },
   1.202 +  // minVisits
   1.203 +  {
   1.204 +    // property is used by function simplePropertyMatches.
   1.205 +    property: "minVisits",
   1.206 +    desc:     "nsINavHistoryQuery.minVisits",
   1.207 +    matches:  simplePropertyMatches,
   1.208 +    runs:     [
   1.209 +      function (aQuery, aQueryOptions) {
   1.210 +        aQuery.minVisits = 0x7fffffff; // 2^31 - 1
   1.211 +      }
   1.212 +    ]
   1.213 +  },
   1.214 +  // maxVisits
   1.215 +  {
   1.216 +    property: "maxVisits",
   1.217 +    desc:     "nsINavHistoryQuery.maxVisits",
   1.218 +    matches:  simplePropertyMatches,
   1.219 +    runs:     [
   1.220 +      function (aQuery, aQueryOptions) {
   1.221 +        aQuery.maxVisits = 0x7fffffff; // 2^31 - 1
   1.222 +      }
   1.223 +    ]
   1.224 +  },
   1.225 +  // onlyBookmarked
   1.226 +  {
   1.227 +    property: "onlyBookmarked",
   1.228 +    desc:     "nsINavHistoryQuery.onlyBookmarked",
   1.229 +    matches:  simplePropertyMatches,
   1.230 +    runs:     [
   1.231 +      function (aQuery, aQueryOptions) {
   1.232 +        aQuery.onlyBookmarked = true;
   1.233 +      }
   1.234 +    ]
   1.235 +  },
   1.236 +  // getFolders
   1.237 +  {
   1.238 +    desc:    "nsINavHistoryQuery.getFolders",
   1.239 +    matches: function (aQuery1, aQuery2) {
   1.240 +      var q1Folders = aQuery1.getFolders();
   1.241 +      var q2Folders = aQuery2.getFolders();
   1.242 +      if (q1Folders.length !== q2Folders.length)
   1.243 +        return false;
   1.244 +      for (let i = 0; i < q1Folders.length; i++) {
   1.245 +        if (q2Folders.indexOf(q1Folders[i]) < 0)
   1.246 +          return false;
   1.247 +      }
   1.248 +      for (let i = 0; i < q2Folders.length; i++) {
   1.249 +        if (q1Folders.indexOf(q2Folders[i]) < 0)
   1.250 +          return false;
   1.251 +      }
   1.252 +      return true;
   1.253 +    },
   1.254 +    runs: [
   1.255 +      function (aQuery, aQueryOptions) {
   1.256 +        aQuery.setFolders([], 0);
   1.257 +      },
   1.258 +      function (aQuery, aQueryOptions) {
   1.259 +        aQuery.setFolders([PlacesUtils.placesRootId], 1);
   1.260 +      },
   1.261 +      function (aQuery, aQueryOptions) {
   1.262 +        aQuery.setFolders([PlacesUtils.placesRootId, PlacesUtils.tagsFolderId], 2);
   1.263 +      }
   1.264 +    ]
   1.265 +  },
   1.266 +  // tags
   1.267 +  {
   1.268 +    desc:    "nsINavHistoryQuery.getTags",
   1.269 +    matches: function (aQuery1, aQuery2) {
   1.270 +      if (aQuery1.tagsAreNot !== aQuery2.tagsAreNot)
   1.271 +        return false;
   1.272 +      var q1Tags = aQuery1.tags;
   1.273 +      var q2Tags = aQuery2.tags;
   1.274 +      if (q1Tags.length !== q2Tags.length)
   1.275 +        return false;
   1.276 +      for (let i = 0; i < q1Tags.length; i++) {
   1.277 +        if (q2Tags.indexOf(q1Tags[i]) < 0)
   1.278 +          return false;
   1.279 +      }
   1.280 +      for (let i = 0; i < q2Tags.length; i++) {
   1.281 +        if (q1Tags.indexOf(q2Tags[i]) < 0)
   1.282 +          return false;
   1.283 +      }
   1.284 +      return true;
   1.285 +    },
   1.286 +    runs: [
   1.287 +      function (aQuery, aQueryOptions) {
   1.288 +        aQuery.tags = [];
   1.289 +      },
   1.290 +      function (aQuery, aQueryOptions) {
   1.291 +        aQuery.tags = [""];
   1.292 +      },
   1.293 +      function (aQuery, aQueryOptions) {
   1.294 +        aQuery.tags = [
   1.295 +          "foo",
   1.296 +          "七難",
   1.297 +          "",
   1.298 +          "いっぱいおっぱい",
   1.299 +          "Abracadabra",
   1.300 +          "123",
   1.301 +          "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
   1.302 +          "アスキーでございません",
   1.303 +          "あいうえお",
   1.304 +        ];
   1.305 +      },
   1.306 +      function (aQuery, aQueryOptions) {
   1.307 +        aQuery.tags = [
   1.308 +          "foo",
   1.309 +          "七難",
   1.310 +          "",
   1.311 +          "いっぱいおっぱい",
   1.312 +          "Abracadabra",
   1.313 +          "123",
   1.314 +          "Here's a pretty long tag name with some = signs and 1 2 3s and spaces oh jeez will it work I hope so!",
   1.315 +          "アスキーでございません",
   1.316 +          "あいうえお",
   1.317 +        ];
   1.318 +        aQuery.tagsAreNot =  true;
   1.319 +      }
   1.320 +    ]
   1.321 +  },
   1.322 +  // transitions
   1.323 +  {
   1.324 +    desc: "tests nsINavHistoryQuery.getTransitions",
   1.325 +    matches: function (aQuery1, aQuery2) {
   1.326 +      var q1Trans = aQuery1.getTransitions();
   1.327 +      var q2Trans = aQuery2.getTransitions();
   1.328 +      if (q1Trans.length !== q2Trans.length)
   1.329 +        return false;
   1.330 +      for (let i = 0; i < q1Trans.length; i++) {
   1.331 +        if (q2Trans.indexOf(q1Trans[i]) < 0)
   1.332 +          return false;
   1.333 +      }
   1.334 +      for (let i = 0; i < q2Trans.length; i++) {
   1.335 +        if (q1Trans.indexOf(q2Trans[i]) < 0)
   1.336 +          return false;
   1.337 +      }
   1.338 +      return true;
   1.339 +    },
   1.340 +    runs: [
   1.341 +      function (aQuery, aQueryOptions) {
   1.342 +        aQuery.setTransitions([], 0);
   1.343 +      },
   1.344 +      function (aQuery, aQueryOptions) {
   1.345 +        aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_DOWNLOAD],
   1.346 +                              1);
   1.347 +      },
   1.348 +      function (aQuery, aQueryOptions) {
   1.349 +        aQuery.setTransitions([Ci.nsINavHistoryService.TRANSITION_TYPED,
   1.350 +                               Ci.nsINavHistoryService.TRANSITION_BOOKMARK], 2);
   1.351 +      }
   1.352 +    ]
   1.353 +  },
   1.354 +];
   1.355 +
   1.356 +// nsINavHistoryQueryOptions switches
   1.357 +const queryOptionSwitches = [
   1.358 +  // sortingMode
   1.359 +  {
   1.360 +    desc:    "nsINavHistoryQueryOptions.sortingMode",
   1.361 +    matches: function (aOptions1, aOptions2) {
   1.362 +      if (aOptions1.sortingMode === aOptions2.sortingMode) {
   1.363 +        switch (aOptions1.sortingMode) {
   1.364 +          case aOptions1.SORT_BY_ANNOTATION_ASCENDING:
   1.365 +          case aOptions1.SORT_BY_ANNOTATION_DESCENDING:
   1.366 +            return aOptions1.sortingAnnotation === aOptions2.sortingAnnotation;
   1.367 +            break;
   1.368 +        }
   1.369 +        return true;
   1.370 +      }
   1.371 +      return false;
   1.372 +    },
   1.373 +    runs: [
   1.374 +      function (aQuery, aQueryOptions) {
   1.375 +        aQueryOptions.sortingMode = aQueryOptions.SORT_BY_DATE_ASCENDING;
   1.376 +      },
   1.377 +      function (aQuery, aQueryOptions) {
   1.378 +        aQueryOptions.sortingMode = aQueryOptions.SORT_BY_ANNOTATION_ASCENDING;
   1.379 +        aQueryOptions.sortingAnnotation = "bookmarks/toolbarFolder";
   1.380 +      }
   1.381 +    ]
   1.382 +  },
   1.383 +  // resultType
   1.384 +  {
   1.385 +    // property is used by function simplePropertyMatches.
   1.386 +    property: "resultType",
   1.387 +    desc:     "nsINavHistoryQueryOptions.resultType",
   1.388 +    matches:  simplePropertyMatches,
   1.389 +    runs:     [
   1.390 +      function (aQuery, aQueryOptions) {
   1.391 +        aQueryOptions.resultType = aQueryOptions.RESULTS_AS_URI;
   1.392 +      },
   1.393 +      function (aQuery, aQueryOptions) {
   1.394 +        aQueryOptions.resultType = aQueryOptions.RESULTS_AS_FULL_VISIT;
   1.395 +      }
   1.396 +    ]
   1.397 +  },
   1.398 +  // excludeItems
   1.399 +  {
   1.400 +    property: "excludeItems",
   1.401 +    desc:     "nsINavHistoryQueryOptions.excludeItems",
   1.402 +    matches:  simplePropertyMatches,
   1.403 +    runs:     [
   1.404 +      function (aQuery, aQueryOptions) {
   1.405 +        aQueryOptions.excludeItems = true;
   1.406 +      }
   1.407 +    ]
   1.408 +  },
   1.409 +  // excludeQueries
   1.410 +  {
   1.411 +    property: "excludeQueries",
   1.412 +    desc:     "nsINavHistoryQueryOptions.excludeQueries",
   1.413 +    matches:  simplePropertyMatches,
   1.414 +    runs:     [
   1.415 +      function (aQuery, aQueryOptions) {
   1.416 +        aQueryOptions.excludeQueries = true;
   1.417 +      }
   1.418 +    ]
   1.419 +  },
   1.420 +  // excludeReadOnlyFolders
   1.421 +  {
   1.422 +    property: "excludeReadOnlyFolders",
   1.423 +    desc:     "nsINavHistoryQueryOptions.excludeReadOnlyFolders",
   1.424 +    matches:  simplePropertyMatches,
   1.425 +    runs:     [
   1.426 +      function (aQuery, aQueryOptions) {
   1.427 +        aQueryOptions.excludeReadOnlyFolders = true;
   1.428 +      }
   1.429 +    ]
   1.430 +  },
   1.431 +  // expandQueries
   1.432 +  {
   1.433 +    property: "expandQueries",
   1.434 +    desc:     "nsINavHistoryQueryOptions.expandQueries",
   1.435 +    matches:  simplePropertyMatches,
   1.436 +    runs:     [
   1.437 +      function (aQuery, aQueryOptions) {
   1.438 +        aQueryOptions.expandQueries = true;
   1.439 +      }
   1.440 +    ]
   1.441 +  },
   1.442 +  // includeHidden
   1.443 +  {
   1.444 +    property: "includeHidden",
   1.445 +    desc:     "nsINavHistoryQueryOptions.includeHidden",
   1.446 +    matches:  simplePropertyMatches,
   1.447 +    runs:     [
   1.448 +      function (aQuery, aQueryOptions) {
   1.449 +        aQueryOptions.includeHidden = true;
   1.450 +      }
   1.451 +    ]
   1.452 +  },
   1.453 +  // maxResults
   1.454 +  {
   1.455 +    property: "maxResults",
   1.456 +    desc:     "nsINavHistoryQueryOptions.maxResults",
   1.457 +    matches:  simplePropertyMatches,
   1.458 +    runs:     [
   1.459 +      function (aQuery, aQueryOptions) {
   1.460 +        aQueryOptions.maxResults = 0xffffffff; // 2^32 - 1
   1.461 +      }
   1.462 +    ]
   1.463 +  },
   1.464 +  // queryType
   1.465 +  {
   1.466 +    property: "queryType",
   1.467 +    desc:     "nsINavHistoryQueryOptions.queryType",
   1.468 +    matches:  simplePropertyMatches,
   1.469 +    runs:     [
   1.470 +      function (aQuery, aQueryOptions) {
   1.471 +        aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_HISTORY;
   1.472 +      },
   1.473 +      function (aQuery, aQueryOptions) {
   1.474 +        aQueryOptions.queryType = aQueryOptions.QUERY_TYPE_UNIFIED;
   1.475 +      }
   1.476 +    ]
   1.477 +  },
   1.478 +];
   1.479 +
   1.480 +///////////////////////////////////////////////////////////////////////////////
   1.481 +
   1.482 +/**
   1.483 + * Enumerates all the sequences of the cartesian product of the arrays contained
   1.484 + * in aSequences.  Examples:
   1.485 + *
   1.486 + *   cartProd([[1, 2, 3], ["a", "b"]], callback);
   1.487 + *   // callback is called 3 * 2 = 6 times with the following arrays:
   1.488 + *   // [1, "a"], [1, "b"], [2, "a"], [2, "b"], [3, "a"], [3, "b"]
   1.489 + *
   1.490 + *   cartProd([["a"], [1, 2, 3], ["X", "Y"]], callback);
   1.491 + *   // callback is called 1 * 3 * 2 = 6 times with the following arrays:
   1.492 + *   // ["a", 1, "X"], ["a", 1, "Y"], ["a", 2, "X"], ["a", 2, "Y"],
   1.493 + *   // ["a", 3, "X"], ["a", 3, "Y"]
   1.494 + *
   1.495 + *   cartProd([[1], [2], [3], [4]], callback);
   1.496 + *   // callback is called 1 * 1 * 1 * 1 = 1 time with the following array:
   1.497 + *   // [1, 2, 3, 4]
   1.498 + *
   1.499 + *   cartProd([], callback);
   1.500 + *   // callback is 0 times
   1.501 + *
   1.502 + *   cartProd([[1, 2, 3, 4]], callback);
   1.503 + *   // callback is called 4 times with the following arrays:
   1.504 + *   // [1], [2], [3], [4]
   1.505 + *
   1.506 + * @param  aSequences
   1.507 + *         an array that contains an arbitrary number of arrays
   1.508 + * @param  aCallback
   1.509 + *         a function that is passed each sequence of the product as it's
   1.510 + *         computed
   1.511 + * @return the total number of sequences in the product
   1.512 + */
   1.513 +function cartProd(aSequences, aCallback)
   1.514 +{
   1.515 +  if (aSequences.length === 0)
   1.516 +    return 0;
   1.517 +
   1.518 +  // For each sequence in aSequences, we maintain a pointer (an array index,
   1.519 +  // really) to the element we're currently enumerating in that sequence
   1.520 +  var seqEltPtrs = aSequences.map(function (i) 0);
   1.521 +
   1.522 +  var numProds = 0;
   1.523 +  var done = false;
   1.524 +  while (!done) {
   1.525 +    numProds++;
   1.526 +
   1.527 +    // prod = sequence in product we're currently enumerating
   1.528 +    let prod = [];
   1.529 +    for (let i = 0; i < aSequences.length; i++) {
   1.530 +      prod.push(aSequences[i][seqEltPtrs[i]]);
   1.531 +    }
   1.532 +    aCallback(prod);
   1.533 +
   1.534 +    // The next sequence in the product differs from the current one by just a
   1.535 +    // single element.  Determine which element that is.  We advance the
   1.536 +    // "rightmost" element pointer to the "right" by one.  If we move past the
   1.537 +    // end of that pointer's sequence, reset the pointer to the first element
   1.538 +    // in its sequence and then try the sequence to the "left", and so on.
   1.539 +
   1.540 +    // seqPtr = index of rightmost input sequence whose element pointer is not
   1.541 +    // past the end of the sequence
   1.542 +    let seqPtr = aSequences.length - 1;
   1.543 +    while (!done) {
   1.544 +      // Advance the rightmost element pointer.
   1.545 +      seqEltPtrs[seqPtr]++;
   1.546 +
   1.547 +      // The rightmost element pointer is past the end of its sequence.
   1.548 +      if (seqEltPtrs[seqPtr] >= aSequences[seqPtr].length) {
   1.549 +        seqEltPtrs[seqPtr] = 0;
   1.550 +        seqPtr--;
   1.551 +
   1.552 +        // All element pointers are past the ends of their sequences.
   1.553 +        if (seqPtr < 0)
   1.554 +          done = true;
   1.555 +      }
   1.556 +      else break;
   1.557 +    }
   1.558 +  }
   1.559 +  return numProds;
   1.560 +}
   1.561 +
   1.562 +/**
   1.563 + * Enumerates all the subsets in aSet of size aHowMany.  There are
   1.564 + * C(aSet.length, aHowMany) such subsets.  aCallback will be passed each subset
   1.565 + * as it is generated.  Note that aSet and the subsets enumerated are -- even
   1.566 + * though they're arrays -- not sequences; the ordering of their elements is not
   1.567 + * important.  Example:
   1.568 + *
   1.569 + *   choose([1, 2, 3, 4], 2, callback);
   1.570 + *   // callback is called C(4, 2) = 6 times with the following sets (arrays):
   1.571 + *   // [1, 2], [1, 3], [1, 4], [2, 3], [2, 4], [3, 4]
   1.572 + *
   1.573 + * @param  aSet
   1.574 + *         an array from which to choose elements, aSet.length > 0
   1.575 + * @param  aHowMany
   1.576 + *         the number of elements to choose, > 0 and <= aSet.length
   1.577 + * @return the total number of sets chosen
   1.578 + */
   1.579 +function choose(aSet, aHowMany, aCallback)
   1.580 +{
   1.581 +  // ptrs = indices of the elements in aSet we're currently choosing
   1.582 +  var ptrs = [];
   1.583 +  for (let i = 0; i < aHowMany; i++) {
   1.584 +    ptrs.push(i);
   1.585 +  }
   1.586 +
   1.587 +  var numFound = 0;
   1.588 +  var done = false;
   1.589 +  while (!done) {
   1.590 +    numFound++;
   1.591 +    aCallback(ptrs.map(function (p) aSet[p]));
   1.592 +
   1.593 +    // The next subset to be chosen differs from the current one by just a
   1.594 +    // single element.  Determine which element that is.  Advance the "rightmost"
   1.595 +    // pointer to the "right" by one.  If we move past the end of set, move the
   1.596 +    // next non-adjacent rightmost pointer to the right by one, and reset all
   1.597 +    // succeeding pointers so that they're adjacent to it.  When all pointers
   1.598 +    // are clustered all the way to the right, we're done.
   1.599 +
   1.600 +    // Advance the rightmost pointer.
   1.601 +    ptrs[ptrs.length - 1]++;
   1.602 +
   1.603 +    // The rightmost pointer has gone past the end of set.
   1.604 +    if (ptrs[ptrs.length - 1] >= aSet.length) {
   1.605 +      // Find the next rightmost pointer that is not adjacent to the current one.
   1.606 +      let si = aSet.length - 2; // aSet index
   1.607 +      let pi = ptrs.length - 2; // ptrs index
   1.608 +      while (pi >= 0 && ptrs[pi] === si) {
   1.609 +        pi--;
   1.610 +        si--;
   1.611 +      }
   1.612 +
   1.613 +      // All pointers are adjacent and clustered all the way to the right.
   1.614 +      if (pi < 0)
   1.615 +        done = true;
   1.616 +      else {
   1.617 +        // pi = index of rightmost pointer with a gap between it and its
   1.618 +        // succeeding pointer.  Move it right and reset all succeeding pointers
   1.619 +        // so that they're adjacent to it.
   1.620 +        ptrs[pi]++;
   1.621 +        for (let i = 0; i < ptrs.length - pi - 1; i++) {
   1.622 +          ptrs[i + pi + 1] = ptrs[pi] + i + 1;
   1.623 +        }
   1.624 +      }
   1.625 +    }
   1.626 +  }
   1.627 +  return numFound;
   1.628 +}
   1.629 +
   1.630 +/**
   1.631 + * Convenience function for nsINavHistoryQuery switches that act as flags.  This
   1.632 + * is attached to switch objects.  See querySwitches array above.
   1.633 + *
   1.634 + * @param  aQuery1
   1.635 + *         an nsINavHistoryQuery object
   1.636 + * @param  aQuery2
   1.637 + *         another nsINavHistoryQuery object
   1.638 + * @return true if this switch is the same in both aQuery1 and aQuery2
   1.639 + */
   1.640 +function flagSwitchMatches(aQuery1, aQuery2)
   1.641 +{
   1.642 +  if (aQuery1[this.flag] && aQuery2[this.flag]) {
   1.643 +    for (let p in this.subswitches) {
   1.644 +      if (p in aQuery1 && p in aQuery2) {
   1.645 +        if (aQuery1[p] instanceof Ci.nsIURI) {
   1.646 +          if (!aQuery1[p].equals(aQuery2[p]))
   1.647 +            return false;
   1.648 +        }
   1.649 +        else if (aQuery1[p] !== aQuery2[p])
   1.650 +          return false;
   1.651 +      }
   1.652 +    }
   1.653 +  }
   1.654 +  else if (aQuery1[this.flag] || aQuery2[this.flag])
   1.655 +    return false;
   1.656 +
   1.657 +  return true;
   1.658 +}
   1.659 +
   1.660 +/**
   1.661 + * Tests if aObj1 and aObj2 are equal.  This function is general and may be used
   1.662 + * for either nsINavHistoryQuery or nsINavHistoryQueryOptions objects.  aSwitches
   1.663 + * determines which set of switches is used for comparison.  Pass in either
   1.664 + * querySwitches or queryOptionSwitches.
   1.665 + *
   1.666 + * @param  aSwitches
   1.667 + *         determines which set of switches applies to aObj1 and aObj2, either
   1.668 + *         querySwitches or queryOptionSwitches
   1.669 + * @param  aObj1
   1.670 + *         an nsINavHistoryQuery or nsINavHistoryQueryOptions object
   1.671 + * @param  aObj2
   1.672 + *         another nsINavHistoryQuery or nsINavHistoryQueryOptions object
   1.673 + * @return true if aObj1 and aObj2 are equal
   1.674 + */
   1.675 +function queryObjsEqual(aSwitches, aObj1, aObj2)
   1.676 +{
   1.677 +  for (let i = 0; i < aSwitches.length; i++) {
   1.678 +    if (!aSwitches[i].matches(aObj1, aObj2))
   1.679 +      return false;
   1.680 +  }
   1.681 +  return true;
   1.682 +}
   1.683 +
   1.684 +/**
   1.685 + * This drives the test runs.  See the comment at the top of this file.
   1.686 + *
   1.687 + * @param aHowManyLo
   1.688 + *        the size of the switch subsets to start with
   1.689 + * @param aHowManyHi
   1.690 + *        the size of the switch subsets to end with (inclusive)
   1.691 + */
   1.692 +function runQuerySequences(aHowManyLo, aHowManyHi)
   1.693 +{
   1.694 +  var allSwitches = querySwitches.concat(queryOptionSwitches);
   1.695 +  var prevQueries = [];
   1.696 +  var prevOpts = [];
   1.697 +
   1.698 +  // Choose aHowManyLo switches up to aHowManyHi switches.
   1.699 +  for (let howMany = aHowManyLo; howMany <= aHowManyHi; howMany++) {
   1.700 +    let numIters = 0;
   1.701 +    print("CHOOSING " + howMany + " SWITCHES");
   1.702 +
   1.703 +    // Choose all subsets of size howMany from allSwitches.
   1.704 +    choose(allSwitches, howMany, function (chosenSwitches) {
   1.705 +      print(numIters);
   1.706 +      numIters++;
   1.707 +
   1.708 +      // Collect the runs.
   1.709 +      // runs = [ [runs from switch 1], ..., [runs from switch howMany] ]
   1.710 +      var runs = chosenSwitches.map(function (s) {
   1.711 +        if (s.desc)
   1.712 +          print("  " + s.desc);
   1.713 +        return s.runs;
   1.714 +      });
   1.715 +
   1.716 +      // cartProd(runs) => [
   1.717 +      //   [switch 1 run 1, switch 2 run 1, ..., switch howMany run 1 ],
   1.718 +      //   ...,
   1.719 +      //   [switch 1 run 1, switch 2 run 1, ..., switch howMany run N ],
   1.720 +      //   ..., ...,
   1.721 +      //   [switch 1 run N, switch 2 run N, ..., switch howMany run 1 ],
   1.722 +      //   ...,
   1.723 +      //   [switch 1 run N, switch 2 run N, ..., switch howMany run N ],
   1.724 +      // ]
   1.725 +      cartProd(runs, function (runSet) {
   1.726 +        // Create a new query, apply the switches in runSet, and test it.
   1.727 +        var query = PlacesUtils.history.getNewQuery();
   1.728 +        var opts = PlacesUtils.history.getNewQueryOptions();
   1.729 +        for (let i = 0; i < runSet.length; i++) {
   1.730 +          runSet[i](query, opts);
   1.731 +        }
   1.732 +        serializeDeserialize([query], opts);
   1.733 +
   1.734 +        // Test the previous NUM_MULTIPLE_QUERIES queries together.
   1.735 +        prevQueries.push(query);
   1.736 +        prevOpts.push(opts);
   1.737 +        if (prevQueries.length >= NUM_MULTIPLE_QUERIES) {
   1.738 +          // We can serialize multiple nsINavHistoryQuery objects together but
   1.739 +          // only one nsINavHistoryQueryOptions object with them.  So, test each
   1.740 +          // of the previous NUM_MULTIPLE_QUERIES nsINavHistoryQueryOptions.
   1.741 +          for (let i = 0; i < prevOpts.length; i++) {
   1.742 +            serializeDeserialize(prevQueries, prevOpts[i]);
   1.743 +          }
   1.744 +          prevQueries.shift();
   1.745 +          prevOpts.shift();
   1.746 +        }
   1.747 +      });
   1.748 +    });
   1.749 +  }
   1.750 +  print("\n");
   1.751 +}
   1.752 +
   1.753 +/**
   1.754 + * Serializes the nsINavHistoryQuery objects in aQueryArr and the
   1.755 + * nsINavHistoryQueryOptions object aQueryOptions, de-serializes the
   1.756 + * serialization, and ensures (using do_check_* functions) that the
   1.757 + * de-serialized objects equal the originals.
   1.758 + *
   1.759 + * @param aQueryArr
   1.760 + *        an array containing nsINavHistoryQuery objects
   1.761 + * @param aQueryOptions
   1.762 + *        an nsINavHistoryQueryOptions object
   1.763 + */
   1.764 +function serializeDeserialize(aQueryArr, aQueryOptions)
   1.765 +{
   1.766 +  var queryStr = PlacesUtils.history.queriesToQueryString(aQueryArr,
   1.767 +                                                        aQueryArr.length,
   1.768 +                                                        aQueryOptions);
   1.769 +  print("  " + queryStr);
   1.770 +  var queryArr2 = {};
   1.771 +  var opts2 = {};
   1.772 +  PlacesUtils.history.queryStringToQueries(queryStr, queryArr2, {}, opts2);
   1.773 +  queryArr2 = queryArr2.value;
   1.774 +  opts2 = opts2.value;
   1.775 +
   1.776 +  // The two sets of queries cannot be the same if their lengths differ.
   1.777 +  do_check_eq(aQueryArr.length, queryArr2.length);
   1.778 +
   1.779 +  // Although the query serialization code as it is written now practically
   1.780 +  // ensures that queries appear in the query string in the same order they
   1.781 +  // appear in both the array to be serialized and the array resulting from
   1.782 +  // de-serialization, the interface does not guarantee any ordering.  So, for
   1.783 +  // each query in aQueryArr, find its equivalent in queryArr2 and delete it
   1.784 +  // from queryArr2.  If queryArr2 is empty after looping through aQueryArr,
   1.785 +  // the two sets of queries are equal.
   1.786 +  for (let i = 0; i < aQueryArr.length; i++) {
   1.787 +    let j = 0;
   1.788 +    for (; j < queryArr2.length; j++) {
   1.789 +      if (queryObjsEqual(querySwitches, aQueryArr[i], queryArr2[j]))
   1.790 +        break;
   1.791 +    }
   1.792 +    if (j < queryArr2.length)
   1.793 +      queryArr2.splice(j, 1);
   1.794 +  }
   1.795 +  do_check_eq(queryArr2.length, 0);
   1.796 +
   1.797 +  // Finally check the query options objects.
   1.798 +  do_check_true(queryObjsEqual(queryOptionSwitches, aQueryOptions, opts2));
   1.799 +}
   1.800 +
   1.801 +/**
   1.802 + * Convenience function for switches that have simple values.  This is attached
   1.803 + * to switch objects.  See querySwitches and queryOptionSwitches arrays above.
   1.804 + *
   1.805 + * @param  aObj1
   1.806 + *         an nsINavHistoryQuery or nsINavHistoryQueryOptions object
   1.807 + * @param  aObj2
   1.808 + *         another nsINavHistoryQuery or nsINavHistoryQueryOptions object
   1.809 + * @return true if this switch is the same in both aObj1 and aObj2
   1.810 + */
   1.811 +function simplePropertyMatches(aObj1, aObj2)
   1.812 +{
   1.813 +  return aObj1[this.property] === aObj2[this.property];
   1.814 +}
   1.815 +
   1.816 +///////////////////////////////////////////////////////////////////////////////
   1.817 +
   1.818 +function run_test()
   1.819 +{
   1.820 +  runQuerySequences(CHOOSE_HOW_MANY_SWITCHES_LO, CHOOSE_HOW_MANY_SWITCHES_HI);
   1.821 +}

mercurial