diff options
Diffstat (limited to '')
-rw-r--r-- | comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelperFunctions.jsm | 293 |
1 files changed, 293 insertions, 0 deletions
diff --git a/comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelperFunctions.jsm b/comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelperFunctions.jsm new file mode 100644 index 0000000000..f7a5199ba3 --- /dev/null +++ b/comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelperFunctions.jsm @@ -0,0 +1,293 @@ +/* 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/. */ + +const EXPORTED_SYMBOLS = [ + "configureGlodaIndexing", + "waitForGlodaDBFlush", + "waitForIndexingHang", + "resumeFromSimulatedHang", + "permuteMessages", + "makeABCardForAddressPair", +]; + +/* + * This file provides gloda testing infrastructure functions which are not coupled + * with the IndexMessageState from GlodaTestHelper.jsm + */ + +var { GlodaDatastore } = ChromeUtils.import( + "resource:///modules/gloda/GlodaDatastore.jsm" +); +var { GlodaIndexer } = ChromeUtils.import( + "resource:///modules/gloda/GlodaIndexer.jsm" +); +var { GlodaMsgIndexer } = ChromeUtils.import( + "resource:///modules/gloda/IndexMsg.jsm" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); +var { MsgHdrToMimeMessage } = ChromeUtils.import( + "resource:///modules/gloda/MimeMessage.jsm" +); +var { SyntheticMessageSet } = ChromeUtils.import( + "resource://testing-common/mailnews/MessageGenerator.jsm" +); + +var log = console.createInstance({ + prefix: "gloda.helperFunctions", + maxLogLevel: "Warn", + maxLogLevelPref: "gloda.loglevel", +}); + +/** + * Resume execution when the db has run all the async statements whose execution + * was queued prior to this call. We trigger a commit to accomplish this, + * although this could also be accomplished without a commit. (Though we would + * have to reach into GlodaDatastore.jsm and get at the raw connection or extend + * datastore to provide a way to accomplish this.) + */ +async function waitForGlodaDBFlush() { + // We already have a mechanism to do this by forcing a commit. Arguably, + // it would be better to use a mechanism that does not induce an fsync. + var savedDepth = GlodaDatastore._transactionDepth; + if (!savedDepth) { + GlodaDatastore._beginTransaction(); + } + + let promiseResolve; + let promise = new Promise(resolve => { + promiseResolve = resolve; + }); + GlodaDatastore.runPostCommit(promiseResolve); + // We don't actually need to run things to zero. We can just wait for the + // outer transaction to close itself. + GlodaDatastore._commitTransaction(); + if (savedDepth) { + GlodaDatastore._beginTransaction(); + } + await promise; +} + +/** + * An injected fault exception. + */ +function InjectedFault(aWhy) { + this.message = aWhy; +} +InjectedFault.prototype = { + toString() { + return "[InjectedFault: " + this.message + "]"; + }, +}; + +function _inject_failure_on_MsgHdrToMimeMessage() { + throw new InjectedFault("MsgHdrToMimeMessage"); +} + +let hangResolve; +let hangPromise = new Promise(resolve => { + hangResolve = resolve; +}); + +function _simulate_hang_on_MsgHdrToMimeMessage(...aArgs) { + hangResolve([MsgHdrToMimeMessage, null, aArgs]); +} + +/** + * If you have configured gloda to hang while indexing, this is the thing + * you wait on to make sure the indexer actually gets to the point where it + * hangs. + */ +async function waitForIndexingHang() { + await hangPromise; +} + +/** + * Configure gloda indexing. For most settings, the settings get clobbered by + * the next time this method is called. Omitted settings reset to the defaults. + * However, anything labeled as a 'sticky' setting stays that way until + * explicitly changed. + * + * @param {boolean} [aArgs.event=true] Should event-driven indexing be enabled + * (true) or disabled (false)? Right now, this actually suppresses + * indexing... the semantics will be ironed out as-needed. + * @param [aArgs.hangWhile] Must be either omitted (for don't force a hang) or + * "streaming" indicating that we should do a no-op instead of performing + * the message streaming. This will manifest as a hang until + * |resumeFromSimulatedHang| is invoked or the test explicitly causes the + * indexer to abort (in which case you do not need to call the resume + * function.) You must omit injectFaultIn if you use hangWhile. + * @param [aArgs.injectFaultIn=null] Must be omitted (for don't inject a + * failure) or "streaming" indicating that we should inject a failure when + * the message indexer attempts to stream a message. The fault will be an + * appropriate exception. You must omit hangWhile if you use injectFaultIn. + */ +function configureGlodaIndexing(aArgs) { + let shouldSuppress = "event" in aArgs ? !aArgs.event : false; + if (shouldSuppress != GlodaIndexer.suppressIndexing) { + log.debug(`Setting suppress indexing to ${shouldSuppress}.`); + GlodaIndexer.suppressIndexing = shouldSuppress; + } + + if ("hangWhile" in aArgs) { + log.debug(`Enabling hang injection in ${aArgs.hangWhile}.`); + switch (aArgs.hangWhile) { + case "streaming": + GlodaMsgIndexer._MsgHdrToMimeMessageFunc = + _simulate_hang_on_MsgHdrToMimeMessage; + break; + default: + throw new Error( + aArgs.hangWhile + " is not a legal choice for hangWhile" + ); + } + } else if ("injectFaultIn" in aArgs) { + log.debug(`Enabling fault injection in ${aArgs.hangWhile}.`); + switch (aArgs.injectFaultIn) { + case "streaming": + GlodaMsgIndexer._MsgHdrToMimeMessageFunc = + _inject_failure_on_MsgHdrToMimeMessage; + break; + default: + throw new Error( + aArgs.injectFaultIn + " is not a legal choice for injectFaultIn" + ); + } + } else { + if (GlodaMsgIndexer._MsgHdrToMimeMessageFunc != MsgHdrToMimeMessage) { + log.debug("Clearing hang/fault injection."); + } + GlodaMsgIndexer._MsgHdrToMimeMessageFunc = MsgHdrToMimeMessage; + } +} + +/** + * Call this to resume from the hang induced by configuring the indexer with + * a "hangWhile" argument to |configureGlodaIndexing|. + * + * @param [aJustResumeExecution=false] Should we just poke the callback driver + * for the indexer rather than continuing the call. You would likely want + * to do this if you committed a lot of violence while in the simulated + * hang and proper resumption would throw exceptions all over the place. + * (For example; if you hang before streaming and destroy the message + * header while suspended, resuming the attempt to stream will throw.) + */ +async function resumeFromSimulatedHang(aJustResumeExecution) { + if (aJustResumeExecution) { + log.debug("Resuming from simulated hang with direct wrapper callback."); + GlodaIndexer._wrapCallbackDriver(); + } else { + let [func, dis, args] = await hangPromise; + log.debug(`Resuming from simulated hang with call to: ${func.name}.`); + func.apply(dis, args); + } + // Reset the promise for the hang. + hangPromise = new Promise(resolve => { + hangResolve = resolve; + }); +} + +/** + * Prepares permutations for messages with aScenarioMaker. Be sure to wait for the indexer + * for every permutation and verify the result. + * + * This process is executed once for each possible permutation of observation + * of the synthetic messages. (Well, we cap it; brute-force test your logic + * on your own time; you should really only be feeding us minimal scenarios.) + * + * @param aScenarioMaker A function that, when called, will generate a series + * of SyntheticMessage instances. Each call to this method should generate + * a new set of conceptually equivalent, but not identical, messages. This + * allows us to process without having to reset our state back to nothing each + * time. (This is more to try and make sure we run the system with a 'dirty' + * state than a bid for efficiency.) + * @param {MessageInjection} messageInjection An instance to use for permuting + * the messages and creating folders. + * + * @returns {[async () => SyntheticMessageSet]} Await it sequentially with a for...of loop. + * Wait for each element for the Indexer and assert afterwards. + */ +async function permuteMessages(aScenarioMaker, messageInjection) { + let folder = await messageInjection.makeEmptyFolder(); + + // To calculate the permutations, we need to actually see what gets produced. + let scenarioMessages = aScenarioMaker(); + let numPermutations = Math.min(factorial(scenarioMessages.length), 32); + + let permutations = []; + for (let iPermutation = 0; iPermutation < numPermutations; iPermutation++) { + permutations.push(async () => { + log.debug(`Run permutation: ${iPermutation + 1} / ${numPermutations}`); + // If this is not the first time through, we need to create a new set. + if (iPermutation) { + scenarioMessages = aScenarioMaker(); + } + scenarioMessages = permute(scenarioMessages, iPermutation); + let scenarioSet = new SyntheticMessageSet(scenarioMessages); + await messageInjection.addSetsToFolders([folder], [scenarioSet]); + return scenarioSet; + }); + } + return permutations; +} + +/** + * A simple factorial function used to calculate the number of permutations + * possible for a given set of messages. + */ +function factorial(i, rv) { + if (i <= 1) { + return rv || 1; + } + return factorial(i - 1, (rv || 1) * i); // tail-call capable +} + +/** + * Permute an array given a 'permutation id' that is an integer that fully + * characterizes the permutation through the decisions that need to be made + * at each step. + * + * @param aArray Source array that is destructively processed. + * @param aPermutationId The permutation id. A permutation id of 0 results in + * the original array's sequence being maintained. + */ +function permute(aArray, aPermutationId) { + let out = []; + for (let i = aArray.length; i > 0; i--) { + let offset = aPermutationId % i; + out.push(aArray[offset]); + aArray.splice(offset, 1); + aPermutationId = Math.floor(aPermutationId / i); + } + return out; +} + +/** + * Add a name-and-address pair as generated by `makeNameAndAddress` to the + * personal address book. + */ +function makeABCardForAddressPair(nameAndAddress) { + // XXX bug 314448 demands that we trigger creation of the ABs... If we don't + // do this, then the call to addCard will fail if someone else hasn't tickled + // this. + MailServices.ab.directories; + + // kPABData is copied from abSetup.js + let kPABData = { + URI: "jsaddrbook://abook.sqlite", + }; + let addressBook = MailServices.ab.getDirectory(kPABData.URI); + + let card = Cc["@mozilla.org/addressbook/cardproperty;1"].createInstance( + Ci.nsIAbCard + ); + card.displayName = nameAndAddress[0]; + card.primaryEmail = nameAndAddress[1]; + + // Just save the new node straight away. + addressBook.addCard(card); + + log.debug(`Adding address book card for: ${nameAndAddress}`); +} |