diff options
Diffstat (limited to 'comm/mailnews/test/resources/MailTestUtils.jsm')
-rw-r--r-- | comm/mailnews/test/resources/MailTestUtils.jsm | 611 |
1 files changed, 611 insertions, 0 deletions
diff --git a/comm/mailnews/test/resources/MailTestUtils.jsm b/comm/mailnews/test/resources/MailTestUtils.jsm new file mode 100644 index 0000000000..588e21521a --- /dev/null +++ b/comm/mailnews/test/resources/MailTestUtils.jsm @@ -0,0 +1,611 @@ +/* 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 = ["mailTestUtils"]; + +var { ctypes } = ChromeUtils.importESModule( + "resource://gre/modules/ctypes.sys.mjs" +); +var { MailServices } = ChromeUtils.import( + "resource:///modules/MailServices.jsm" +); + +// See Bug 903946 +function avoidUncaughtExceptionInExternalProtocolService() { + try { + Services.prefs.setCharPref( + "helpers.private_mime_types_file", + Services.prefs.getCharPref("helpers.global_mime_types_file") + ); + } catch (ex) {} + try { + Services.prefs.setCharPref( + "helpers.private_mailcap_file", + Services.prefs.getCharPref("helpers.global_mailcap_file") + ); + } catch (ex) {} +} +avoidUncaughtExceptionInExternalProtocolService(); + +var mailTestUtils = { + // Loads a file to a string + // If aCharset is specified, treats the file as being of that charset + loadFileToString(aFile, aCharset) { + var data = ""; + var fstream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance( + Ci.nsIFileInputStream + ); + fstream.init(aFile, -1, 0, 0); + + if (aCharset) { + var cstream = Cc[ + "@mozilla.org/intl/converter-input-stream;1" + ].createInstance(Ci.nsIConverterInputStream); + cstream.init(fstream, aCharset, 4096, 0x0000); + let str = {}; + while (cstream.readString(4096, str) != 0) { + data += str.value; + } + + cstream.close(); + } else { + var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + + sstream.init(fstream); + + let str = sstream.read(4096); + while (str.length > 0) { + data += str; + str = sstream.read(4096); + } + + sstream.close(); + } + + fstream.close(); + + return data; + }, + + // Loads a message to a string + // If aCharset is specified, treats the file as being of that charset + loadMessageToString(aFolder, aMsgHdr, aCharset) { + var data = ""; + let reusable = {}; + let bytesLeft = aMsgHdr.messageSize; + let stream = aFolder.getMsgInputStream(aMsgHdr, reusable); + if (aCharset) { + let cstream = Cc[ + "@mozilla.org/intl/converter-input-stream;1" + ].createInstance(Ci.nsIConverterInputStream); + cstream.init(stream, aCharset, 4096, 0x0000); + let str = {}; + let bytesToRead = Math.min(bytesLeft, 4096); + while (cstream.readString(bytesToRead, str) != 0) { + data += str.value; + bytesLeft -= bytesToRead; + if (bytesLeft <= 0) { + break; + } + bytesToRead = Math.min(bytesLeft, 4096); + } + cstream.close(); + } else { + var sstream = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( + Ci.nsIScriptableInputStream + ); + + sstream.init(stream); + + let bytesToRead = Math.min(bytesLeft, 4096); + var str = sstream.read(bytesToRead); + bytesLeft -= str.length; + while (str.length > 0) { + data += str; + if (bytesLeft <= 0) { + break; + } + bytesToRead = Math.min(bytesLeft, 4096); + str = sstream.read(bytesToRead); + bytesLeft -= str.length; + } + sstream.close(); + } + stream.close(); + + return data; + }, + + // Loads a message to a UTF-16 string. + loadMessageToUTF16String(folder, msgHdr, charset) { + let str = this.loadMessageToString(folder, msgHdr, charset); + let arr = new Uint8Array(Array.from(str, x => x.charCodeAt(0))); + return new TextDecoder().decode(arr); + }, + + // Gets the first message header in a folder. + firstMsgHdr(folder) { + let enumerator = folder.msgDatabase.enumerateMessages(); + let first = enumerator[Symbol.iterator]().next(); + return first.done ? null : first.value; + }, + + // Gets message header number N (0 based index) in a folder. + getMsgHdrN(folder, n) { + let i = 0; + for (let next of folder.msgDatabase.enumerateMessages()) { + if (i == n) { + return next; + } + i++; + } + return null; + }, + + /** + * Returns the file system a particular file is on. + * Currently supported on Windows only. + * + * @param {nsIFile} aFile - The file to get the file system for. + * @returns {string} The file system a particular file is on, or 'null' + * if not on Windows. + */ + get_file_system(aFile) { + if (!("@mozilla.org/windows-registry-key;1" in Cc)) { + dump("get_file_system() is supported on Windows only.\n"); + return null; + } + + // Win32 type and other constants. + const BOOL = ctypes.int32_t; + const MAX_PATH = 260; + + let kernel32 = ctypes.open("kernel32.dll"); + + try { + // Returns the path of the volume a file is on. + let GetVolumePathName = kernel32.declare( + "GetVolumePathNameW", + ctypes.winapi_abi, + BOOL, // return type: 1 indicates success, 0 failure + ctypes.char16_t.ptr, // in: lpszFileName + ctypes.char16_t.ptr, // out: lpszVolumePathName + ctypes.uint32_t // in: cchBufferLength + ); + + let filePath = aFile.path; + // The volume path should be at most 1 greater than than the length of the + // path -- add 1 for a trailing backslash if necessary, and 1 for the + // terminating null character. Note that the parentheses around the type are + // necessary for new to apply correctly. + let volumePath = new (ctypes.char16_t.array(filePath.length + 2))(); + + if (!GetVolumePathName(filePath, volumePath, volumePath.length)) { + throw new Error( + "Unable to get volume path for " + + filePath + + ", error " + + ctypes.winLastError + ); + } + + // Returns information about the file system for the given volume path. We just need + // the file system name. + let GetVolumeInformation = kernel32.declare( + "GetVolumeInformationW", + ctypes.winapi_abi, + BOOL, // return type: 1 indicates success, 0 failure + ctypes.char16_t.ptr, // in, optional: lpRootPathName + ctypes.char16_t.ptr, // out: lpVolumeNameBuffer + ctypes.uint32_t, // in: nVolumeNameSize + ctypes.uint32_t.ptr, // out, optional: lpVolumeSerialNumber + ctypes.uint32_t.ptr, // out, optional: lpMaximumComponentLength + ctypes.uint32_t.ptr, // out, optional: lpFileSystemFlags + ctypes.char16_t.ptr, // out: lpFileSystemNameBuffer + ctypes.uint32_t // in: nFileSystemNameSize + ); + + // We're only interested in the name of the file system. + let fsName = new (ctypes.char16_t.array(MAX_PATH + 1))(); + + if ( + !GetVolumeInformation( + volumePath, + null, + 0, + null, + null, + null, + fsName, + fsName.length + ) + ) { + throw new Error( + "Unable to get volume information for " + + volumePath.readString() + + ", error " + + ctypes.winLastError + ); + } + + return fsName.readString(); + } finally { + kernel32.close(); + } + }, + + /** + * Try marking a region of a file as sparse, so that zeros don't consume + * significant amounts of disk space. This is a platform-dependent routine and + * is not supported on all platforms. The current status of this function is: + * - Windows: Supported, but only on NTFS volumes. + * - Mac: Not supported. + * - Linux: As long as you seek to a position before writing, happens automatically + * on most file systems, so this function is a no-op. + * + * @param {nsIFile} aFile - The file to mark as sparse. + * @param {integer} aRegionStart - The start position of the sparse region, + * in bytes. + * @param {integer} aRegionBytes - The number of bytes to mark as sparse. + * @returns {boolean} Whether the OS and file system supports marking files as + * sparse. If this is true, then the file has been marked as sparse. + * If this isfalse, then the underlying system doesn't support marking files as + * sparse. If an exception is thrown, then the system does support marking + * files as sparse, but an error occurred while doing so. + * + */ + mark_file_region_sparse(aFile, aRegionStart, aRegionBytes) { + let fileSystem = this.get_file_system(aFile); + dump( + "[mark_file_region_sparse()] File system = " + + (fileSystem || "(unknown)") + + ", file region = at " + + this.toMiBString(aRegionStart) + + " for " + + this.toMiBString(aRegionBytes) + + "\n" + ); + + if ("@mozilla.org/windows-registry-key;1" in Cc) { + // On Windows, check whether the drive is NTFS. If it is, proceed. + // If it isn't, then bail out now, because in all probability it is + // FAT32, which doesn't support sparse files. + if (fileSystem != "NTFS") { + return false; + } + + // Win32 type and other constants. + const BOOL = ctypes.int32_t; + const HANDLE = ctypes.voidptr_t; + // A BOOLEAN (= BYTE = unsigned char) is distinct from a BOOL. + // http://blogs.msdn.com/b/oldnewthing/archive/2004/12/22/329884.aspx + const BOOLEAN = ctypes.unsigned_char; + const FILE_SET_SPARSE_BUFFER = new ctypes.StructType( + "FILE_SET_SPARSE_BUFFER", + [{ SetSparse: BOOLEAN }] + ); + // LARGE_INTEGER is actually a type union. We'll use the int64 representation + const LARGE_INTEGER = ctypes.int64_t; + const FILE_ZERO_DATA_INFORMATION = new ctypes.StructType( + "FILE_ZERO_DATA_INFORMATION", + [{ FileOffset: LARGE_INTEGER }, { BeyondFinalZero: LARGE_INTEGER }] + ); + + const GENERIC_WRITE = 0x40000000; + const OPEN_ALWAYS = 4; + const FILE_ATTRIBUTE_NORMAL = 0x80; + const INVALID_HANDLE_VALUE = new ctypes.Int64(-1); + const FSCTL_SET_SPARSE = 0x900c4; + const FSCTL_SET_ZERO_DATA = 0x980c8; + const FILE_BEGIN = 0; + + let kernel32 = ctypes.open("kernel32.dll"); + + try { + let CreateFile = kernel32.declare( + "CreateFileW", + ctypes.winapi_abi, + HANDLE, // return type: handle to the file + ctypes.char16_t.ptr, // in: lpFileName + ctypes.uint32_t, // in: dwDesiredAccess + ctypes.uint32_t, // in: dwShareMode + ctypes.voidptr_t, // in, optional: lpSecurityAttributes (note that + // we're cheating here by not declaring a + // SECURITY_ATTRIBUTES structure -- that's because + // we're going to pass in null anyway) + ctypes.uint32_t, // in: dwCreationDisposition + ctypes.uint32_t, // in: dwFlagsAndAttributes + HANDLE // in, optional: hTemplateFile + ); + + let filePath = aFile.path; + let hFile = CreateFile( + filePath, + GENERIC_WRITE, + 0, + null, + OPEN_ALWAYS, + FILE_ATTRIBUTE_NORMAL, + null + ); + let hFileInt = ctypes.cast(hFile, ctypes.intptr_t); + if (ctypes.Int64.compare(hFileInt.value, INVALID_HANDLE_VALUE) == 0) { + throw new Error( + "CreateFile failed for " + + filePath + + ", error " + + ctypes.winLastError + ); + } + + try { + let DeviceIoControl = kernel32.declare( + "DeviceIoControl", + ctypes.winapi_abi, + BOOL, // return type: 1 indicates success, 0 failure + HANDLE, // in: hDevice + ctypes.uint32_t, // in: dwIoControlCode + ctypes.voidptr_t, // in, optional: lpInBuffer + ctypes.uint32_t, // in: nInBufferSize + ctypes.voidptr_t, // out, optional: lpOutBuffer + ctypes.uint32_t, // in: nOutBufferSize + ctypes.uint32_t.ptr, // out, optional: lpBytesReturned + ctypes.voidptr_t // inout, optional: lpOverlapped (again, we're + // cheating here by not having this as an + // OVERLAPPED structure + ); + // bytesReturned needs to be passed in, even though it's meaningless + let bytesReturned = new ctypes.uint32_t(); + let sparseBuffer = new FILE_SET_SPARSE_BUFFER(); + sparseBuffer.SetSparse = 1; + + // Mark the file as sparse + if ( + !DeviceIoControl( + hFile, + FSCTL_SET_SPARSE, + sparseBuffer.address(), + FILE_SET_SPARSE_BUFFER.size, + null, + 0, + bytesReturned.address(), + null + ) + ) { + throw new Error( + "Unable to mark file as sparse, error " + ctypes.winLastError + ); + } + + let zdInfo = new FILE_ZERO_DATA_INFORMATION(); + zdInfo.FileOffset = aRegionStart; + let regionEnd = aRegionStart + aRegionBytes; + zdInfo.BeyondFinalZero = regionEnd; + // Mark the region as a sparse region + if ( + !DeviceIoControl( + hFile, + FSCTL_SET_ZERO_DATA, + zdInfo.address(), + FILE_ZERO_DATA_INFORMATION.size, + null, + 0, + bytesReturned.address(), + null + ) + ) { + throw new Error( + "Unable to mark region as zero, error " + ctypes.winLastError + ); + } + + // Move to past the sparse region and mark it as the end of the file. The + // above DeviceIoControl call is useless unless followed by this. + let SetFilePointerEx = kernel32.declare( + "SetFilePointerEx", + ctypes.winapi_abi, + BOOL, // return type: 1 indicates success, 0 failure + HANDLE, // in: hFile + LARGE_INTEGER, // in: liDistanceToMove + LARGE_INTEGER.ptr, // out, optional: lpNewFilePointer + ctypes.uint32_t // in: dwMoveMethod + ); + if (!SetFilePointerEx(hFile, regionEnd, null, FILE_BEGIN)) { + throw new Error( + "Unable to set file pointer to end, error " + ctypes.winLastError + ); + } + + let SetEndOfFile = kernel32.declare( + "SetEndOfFile", + ctypes.winapi_abi, + BOOL, // return type: 1 indicates success, 0 failure + HANDLE // in: hFile + ); + if (!SetEndOfFile(hFile)) { + throw new Error( + "Unable to set end of file, error " + ctypes.winLastError + ); + } + + return true; + } finally { + let CloseHandle = kernel32.declare( + "CloseHandle", + ctypes.winapi_abi, + BOOL, // return type: 1 indicates success, 0 failure + HANDLE // in: hObject + ); + CloseHandle(hFile); + } + } finally { + kernel32.close(); + } + } else if ("nsILocalFileMac" in Ci) { + // Macs don't support marking files as sparse. + return false; + } else { + // Assuming Unix here. Unix file systems generally automatically sparsify + // files. + return true; + } + }, + + /** + * Converts a size in bytes into its mebibytes string representation. + * NB: 1 MiB = 1024 * 1024 = 1048576 B. + * + * @param {integer} aSize - The size in bytes. + * @returns {string} A string representing the size in mebibytes. + */ + toMiBString(aSize) { + return aSize / 1048576 + " MiB"; + }, + + /** + * A variant of do_timeout that accepts an actual function instead of + * requiring you to pass a string to evaluate. If the function throws an + * exception when invoked, we will use do_throw to ensure that the test fails. + * + * @param {integer} aDelayInMS - The number of milliseconds to wait before firing the timer. + * @param {Function} aFunc - The function to invoke when the timer fires. + * @param {object} [aFuncThis] - Optional 'this' pointer to use. + * @param {*[]} aFuncArgs - Optional list of arguments to pass to the function. + */ + _timer: null, + do_timeout_function(aDelayInMS, aFunc, aFuncThis, aFuncArgs) { + this._timer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer); + let wrappedFunc = function () { + try { + aFunc.apply(aFuncThis, aFuncArgs); + } catch (ex) { + // we want to make sure that if the thing we call throws an exception, + // that this terminates the test. + do_throw(ex); + } + }; + this._timer.initWithCallback( + wrappedFunc, + aDelayInMS, + Ci.nsITimer.TYPE_ONE_SHOT + ); + }, + + /** + * Ensure the given nsIMsgFolder's database is up-to-date, calling the provided + * callback once the folder has been loaded. (This may be instantly or + * after a re-parse.) + * + * @param {nsIMsgFolder} aFolder - The nsIMsgFolder whose database you want + * to ensure is up-to-date. + * @param {Function} aCallback - The callback function to invoke once the + * folder has been loaded. + * @param {object} aCallbackThis - The 'this' to use when calling the callback. + * Pass null if your callback does not rely on 'this'. + * @param {*[]} aCallbackArgs - A list of arguments to pass to the callback + * via apply. If you provide [1,2,3], we will effectively call: + * aCallbackThis.aCallback(1,2,3); + * @param {boolean} [aSomeoneElseWillTriggerTheUpdate=false] If this is true, + * we do not trigger the updateFolder call and it is assumed someone else is + * taking care of that. + */ + updateFolderAndNotify( + aFolder, + aCallback, + aCallbackThis, + aCallbackArgs, + aSomeoneElseWillTriggerTheUpdate = false + ) { + // register for the folder loaded notification ahead of time... even though + // we may not need it... + let folderListener = { + onFolderEvent(aEventFolder, aEvent) { + if (aEvent == "FolderLoaded" && aFolder.URI == aEventFolder.URI) { + MailServices.mailSession.RemoveFolderListener(this); + aCallback.apply(aCallbackThis, aCallbackArgs); + } + }, + }; + + MailServices.mailSession.AddFolderListener( + folderListener, + Ci.nsIFolderListener.event + ); + + if (!aSomeoneElseWillTriggerTheUpdate) { + aFolder.updateFolder(null); + } + }, + + /** + * For when you want to compare elements non-strictly. + */ + non_strict_index_of(aArray, aElem) { + for (let [i, elem] of aArray.entries()) { + if (elem == aElem) { + return i; + } + } + return -1; + }, + + /** + * Click on a particular cell in a tree. `window` is not defined here in this + * file, so we can't provide it as a default argument. Similarly, we pass in + * `EventUtils` as an argument because importing it here does not work + * because `window` is not defined. + * + * @param {object} EventUtils - The EventUtils object. + * @param {Window} win - The window the tree is in. + * @param {Element} tree - The tree element. + * @param {number} row - The tree row to click on. + * @param {number} column - The tree column to click on. + * @param {object} event - The mouse event to synthesize, e.g. `{ clickCount: 2 }`. + */ + treeClick(EventUtils, win, tree, row, column, event) { + let coords = tree.getCoordsForCellItem(row, tree.columns[column], "cell"); + let treeChildren = tree.lastElementChild; + EventUtils.synthesizeMouse( + treeChildren, + coords.x + coords.width / 2, + coords.y + coords.height / 2, + event, + win + ); + }, + + /** + * For waiting until an element exists in a given document. Pass in the + * `MutationObserver` as an argument because importing it here does not work + * because `window` is not defined here. + * + * @param {object} MutationObserver - The MutationObserver object. + * @param {Document} doc - Document that contains the elements. + * @param {string} observedNodeId - Id of the element to observe. + * @param {string} awaitedNodeId - Id of the element that will soon exist. + * @returns {Promise.<undefined>} - A promise fulfilled when the element exists. + */ + awaitElementExistence(MutationObserver, doc, observedNodeId, awaitedNodeId) { + return new Promise(resolve => { + let outerObserver = new MutationObserver((mutationsList, observer) => { + for (let mutation of mutationsList) { + if (mutation.type == "childList" && mutation.addedNodes.length) { + let element = doc.getElementById(awaitedNodeId); + + if (element) { + observer.disconnect(); + resolve(); + return; + } + } + } + }); + + let nodeToObserve = doc.getElementById(observedNodeId); + outerObserver.observe(nodeToObserve, { childList: true }); + }); + }, +}; |