AsyncTestUtils extended framework

The AsyncTestUtils Extended Framework is one mechanism for testing the MailNews component of Thunderbird. See MailNews automated testing for a description of the other testing mechanisms.

Boilerplate

Add the following code to the top of your test file to import everything you need:

load("../../mailnews/resources/logHelper.js");
load("../../mailnews/resources/asyncTestUtils.js");
load("../../mailnews/resources/messageGenerator.js");
load("../../mailnews/resources/messageModifier.js");
load("../../mailnews/resources/messageInjection.js");

If the directory where you are adding the tests does not have a head_*.js file that has the two following lines, add them at the top of your test file (before the lines shown above):

load("../../mailnews/resources/mailDirService.js");
load("../../mailnews/resources/mailTestUtils.js");

At the bottom of the test file, add the following:

var tests =[
// list your tests here
];
function run_test() {
configure_message_injection({mode: "local"});
async_run_tests(tests);
}

Asynchronous testing basics

Why do we need it?

Sometimes in your tests you need to wait for an operation to complete that does not occur synchronously (that is, it is not done when the function call you made to initiate the operation returns control to you). This is likely to happen when I/O is involved or a potentially expensive process wants to break itself up into smaller chunks (like a search operation) so that the UI stays responsive.

In the Mozilla platform, the "top level" of a program is the event loop. It is a never-ending loop that dequeues pending events and runs them. This is how asynchronous callbacks get their chance to run again. When I/O results in newly read data it places an event in the queue. When a timer fires because the requested duration is up, it also places an event in the queue.

In order to give these events a chance to run we either need to make sure we yield control to the top-level event loop or spin our own nested event loop. AsyncTestUtils is currently implemented by the first method (yielding control to the top-level event loop). MozMill is an example of a testing framework that uses a nested event loop. In the future we will probably move the AsyncTestUtils framework to a nested event loop in a backwards-compatible fashion.

How does this affect my tests?

Thanks to JavaScript enhancements available on the Mozilla platform, it is possible for a function to yield control in such a way that the function stops running at the line where you use a yield statement and resumes execution on the next line when resumed. This allows you to write reasonably normal looking functions instead of having to chain together a whole bunch of functions and callbacks. Your test functions need to agree to the following contract:

The Asynchronous Function Contract

  1. A function should yield false or return false when something asynchronous is going on. When the asynchronous operation is complete, something needs to call async_driver(). Returning false tells the asynchronous driver that it should yield control up to the top-level. Calling async_driver() lets it know to start up again.
  2. A function should yield true or return true when the asynchronous driver should continue executing without stopping.
  3. A function that does not return anything (or returns undefined) and is not a generator is treated like a function that returned true.

Most of the things you will want to do already have helper functions that take care of all of this, so all you need to do is pass their return values through. For example, you would do "yield async_move_messages(...);" and be done with it.

Helpers for the Asynchronous Function Contract

It can be annoying to have to write interface boilerplate just to call async_driver. If you have the following types of listeners you can use the following pre-defined helpers:

  • nsIUrlListener: asyncUrlListener (predefined). If you need to wrap an existing url listener or need a callback or fancy promise, create an instance of AsyncUrlListener.
  • copy listener: asyncCopyListener.

Bringing messages into the picture

Synthetic set definitions

Most of the code involved in creating synthetic messages takes an object that defines how to generate the set. The following is a list of frequently used attributes where the default value is listed after the attribute name. There are more attributes you can specify; consult the documentation for more information. (See the bottom of this page for links to the source files.)

count: 10
The number of messages that should be in the set. Note that the default is subject to change, so if you want 10, say 10, instead of relying on the default.
msgsPerThread: 1
How many messages should be in each thread? The default is that every message is its own thread; no message is a reply to any other message. If you pass a number greater than 1, then you get a direct reply-chain that long. For example, if you pass 3, then 1 <- 2 <- 3 is what the reply chain looks like. If you need more complicated threads you will need to use the MessageScenarioFactory.
age: (strictly incrementing from arbitrary origin)
The default starts at Jan 1, 2000 and adds an hour for every message. If you pass an object, it should be an object with one or more of the following attributes: minutes, hours, days, weeks. These attributes specify how old the message should be. For example, {weeks: 2, days: 3} would be a message sent exactly 17 days ago. If you pass age, you should also pass age_incr.
age_incr: (no increment)
Takes an object of the same style taken by age. This specifies the time interval we should add to the date for each message. Due to a bug, you should pass negative values. (We will probably auto-correct that in the future.) If your set definition was {count: 3, age: {days: 7}, age_incr: {days: -1}}, then you would generate messages from 7, 6, and 5 days ago.
subject: (automatically generated random subject)
Force all the messages to have the same subject you pass in. For example, {count: 1, subject: "my suitcase"} would result in a single message with the subject "my suitcase" with a random sender and random recipient.
to: (automatically generated single recipient)
A list of recipients, where each recipient is a list whose first element is a (display) name and second element is an email address. For example, {to: [["John Doe", "john_doe@example.com"], ["John Smith", "smitty@example.com"]]} would specify two recipients.
cc: (none)
A list of recipients like 'to', but in this case the default is zero recipients.
from: (automatically generated)
A list whose first element is a (display) name and second element is an email address. For example, {count: 4, from: ["John Doe", "john_doe@example.com"]} would result in four messages from John Doe to random recipients.

Synthetic message sets

The code that creates synthetic message sets returns instances of the SyntheticMessageSet class. This class not only holds references to the SyntheticMessage instances, but it also tracks what folders they were injected into as well as what folders you move them to. This allows you to get at the nsIMsgDBHdr instances directly. Keep in mind that the class is not magic and will lose track of the message headers if you manipulate them without referencing the message set.

Accessing synthetic messages and headers

synMessages [attribute]
The JS list of SyntheticMessages held in the set. While you should not modify this list, you can get its length and read from it.
getMsgHdr(aIndex) [function]
Retrieve the nsIMsgDBHdr at the given index.
getMsgURI(aIndex) [function]
Retrieve the URI of the message header at the given index.
msgHdrList [getter]
Return a JS list with all the nsIMsgDbHdrs for the messages that have been injected into folders.
xpcomHdrArray [getter]
Return an nsIMutableArray of the message headers that have been injected into folders.
foldersWithMsgHdrs [getter]
Return a list where each element is a list with two sub-elements. The first is an nsIMsgFolder and the second is a JS list of all the nsIMsgDBHdrs for the messages that were inserted into the folder. This is used by routines that need to process the messages grouped by the folder they belong to, such as initiating message moves.
foldersWithXpcomHdrArrays [getter]
Same as foldersWithMsgHdrs but the second element in each sub-list is an nsIMutableArray instead of a JS list.

Manipulating messages

setRead(aBeRead)
setRead(true) marks all the messages in the set as read, setRead(false) marks them as unread.
setStarred(aBeStarred)
setStarred(true) marks all the messages in the set as starred ("flagged" in IMAP parlance), setStarred(false) makes them not be starred.
addTag(aTagKey)
Adds the given tag to all the messages in the set. The tag key must correspond to an existing tag. The tag key is not the label, but what is actually stored on the IMAP server. For example, all our default tags are actually things like $label1 and $label2, but that is not what we show to the user.
removeTag(aTagKey)
Removes the given tag from all the messages in the set. See the addTag notes.
setJunk(aBeJunk)
Sets the junk score for the messages in the set as junk (true) / not junk (false). This does not involves the bayesian classifier and does not do anything like moving the message to the junk folder. It does, however, send a JunkStatusChanged notification via the nsIMsgFolderNotificationService's itemEvent mechanism.

Set manipulation

union(aOtherSet)
Take the union of this set and the provided other set and return the (new, not modified) result. You should stop using both the message set that you called this on and the other set that you passed in unless you are very careful. Namely, if you use async_move_messages / async_trash_messages on the resulting set, the original sets won't know the messages moved and will get confused if you try and access headers via them again.
slice(...)
Uses the JS Array.slice semantics to slice the set. Same warnings as union apply.

Configuring message injection

Local Injection
let inboxFolder = configure_message_injection({mode: "local"});
Set up message injection to happen locally. This is different from using a POP fake-server. We just cram them using addMessage, although we try and approximate what would happen with POP in terms of still invoking filters and such.
IMAP Injection, do not bring messages offline
let inboxFolder = configure_message_injection({mode: "imap", offline: false});
Use an IMAP fake-server to inject messages. We do not mark the folders as offline and therefore do not attempt to download the messages so that they are immediately available for offline use. If you later want to bring the messages offline, use the make_folder_and_contents_offline function.
IMAP Injection, do bring messages offline
let inboxFolder = configure_message_injection({mode: "imap", offline: true});
Use an IMAP fake-server to inject messages. We mark the folders as offline and download the messages so that they are immediately available for offline use.

Creating / injecting messages

All of these functions take synthetic set definitions. Look at the section on Synthetic Message Sets above to understand how to specify these.

Create a single folder with messages in it
let [folder, set1, set2, ...] = make_folder_with_sets([aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();


This creates a folder with an automatically generated name and adds one or more sets of messages to it. If multiple sets are specified, their messages will be interleaved. Example:
let [folder, messageSet] = make_folder_with_sets([{count: 3}]);
This creates a new folder with a set of three messages in it and saves the folder handle into 'folder' and the message set into 'messageSet'. Another example:
let [fooBarFolder, fooSet, barSet] = make_folder_with_sets([{count: 3, subject: "foo"}, {count: 3, subject: "bar"}]);
This creates a folder with two message sets in it. Three of the messages will have the subject "foo" and three will have the subject "bar". Because the message sets are interleaved, if you read the subject lines in the order they were added (or in date order), you would read "foo", "bar", "foo", "bar", "foo", "bar".
Create multiple folders with a set of messages distributed across the folders
let [[folder1, folder2, ...], set1, set2, ...] = make_folders_with_sets(aFolderCount, [aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();

Like make_folder_with_sets but multiple folders are created and the messages are spread across the folders. You would only want to do this when you are testing logic that could be impacted by having multiple folders.
Add messages to an existing folder
let [set1, set2, ...] = make_new_sets_in_folder(aFolder, [aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();

Create one or more new sets of messages and add them to a Folder.
Add messages distributed among multiple folders
let [set1, set2, ...] = make_new_sets_in_folder([aFolder1, aFolder2, ...], [aSynSetDef1, aSynSetDef2, ...]);
yield wait_for_message_injection();

Create one or more new sets of messages and add them to the provided folders. Like make_folders_with_sets, the messages are spread across the folders.

Folders

Create a folder
let folderHandle = make_empty_folder(aOptionalFolderName);
You do not have to specify a folder name - if you don't care, we'll make one up for you. If you need to get fancy about having flags set on the folder, we support that, but please check the method documentation.
Get / create the Junk folder
let junkFolder = get_junk_folder();
Create a Virtual folder (a folder whose contents are the result of a saved search)
let virtualFolder = make_virtual_folder([aFolderToSearch1, aFolderToSearch2, ...],
{subject: "", body: "", from: "", to: "", cc: "", recipient: "", involves: ""},
aAndTermsTogether, aOptionalName);

This is a convenience function to help you create a new virtual folder. The first argument is the list of folders we should search. The second argument defines the search query; each attribute defines a "contains" string constraint. If you include the attribute, the constraint is created. So if you want no constraints, pass an empty object, but if you just want a subject constraint, pass {subject: "foo"}. The third argument decides whether to "AND" together multiple constraints (if there are multiple constraints); true for AND, false for OR.
Mark an IMAP folder as offline and bring the messages offline
yield make_folder_and_contents_offline(folderHandle);

Messages and folders

Move messages to a folder
yield async_move_messages(aSynMessageSet, aDestFolder);
Trash messages (move them to the trash folder)
yield async_trash_messages(aSynMessageSet);
Empty the trash folder
yield async_empty_trash();
Delete messages (without moving them to the trash folder)
yield async_delete_messages(aSynMessageSet);

Implementation details

The following files make up the framework:

  • asyncTestUtils.js: Core async testing logic. Dependent on logHelper.js so it can log what test it is on, etc.
  • logHelper.js: This allows rich logging via LogSploder. If you aren't using LogSploder, then this just makes your tests fail if errors get logged to the error console (like you see if you go to the "Tools | Error Console" menu).
  • messageGenerator.js: Provides the SyntheticMessage abstraction and MessageGenerator class that can generate one or more SyntheticMessages at a time. The MessageScenarioFactory produces specific pre-configured message threading configurations using fresh messages (important in avoiding duplicate messages for code that cares, like gloda).
  • messageModifier.js: Slightly misnamed, this provides the SyntheticMessageSet abstraction that is a set abstraction for SyntheticMessages that also tracks what folders they got injected into and provides code to directly manipulate or aid other code that wants to directly manipulate them.
  • messageInjection.js: All the logic for injecting messages into folders via local / IMAP and then doing further folder-level manipulations.