summaryrefslogtreecommitdiffstats
path: root/comm/mailnews/db/gloda/test/unit/resources/GlodaTestHelperFunctions.jsm
blob: f7a5199ba38824f2bdf319e9e2dfbb5ad25c8db9 (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
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}`);
}