summaryrefslogtreecommitdiffstats
path: root/toolkit/mozapps/update/tests/data
diff options
context:
space:
mode:
authorDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
committerDaniel Baumann <daniel.baumann@progress-linux.org>2024-04-07 17:32:43 +0000
commit6bf0a5cb5034a7e684dcc3500e841785237ce2dd (patch)
treea68f146d7fa01f0134297619fbe7e33db084e0aa /toolkit/mozapps/update/tests/data
parentInitial commit. (diff)
downloadthunderbird-upstream.tar.xz
thunderbird-upstream.zip
Adding upstream version 1:115.7.0.upstream/1%115.7.0upstream
Signed-off-by: Daniel Baumann <daniel.baumann@progress-linux.org>
Diffstat (limited to 'toolkit/mozapps/update/tests/data')
-rw-r--r--toolkit/mozapps/update/tests/data/app_update.sjs251
-rw-r--r--toolkit/mozapps/update/tests/data/complete.marbin0 -> 86612 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/complete.pngbin0 -> 878 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/complete_log_success_mac332
-rw-r--r--toolkit/mozapps/update/tests/data/complete_log_success_win320
-rw-r--r--toolkit/mozapps/update/tests/data/complete_mac.marbin0 -> 87129 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/complete_precomplete18
-rw-r--r--toolkit/mozapps/update/tests/data/complete_precomplete_mac21
-rw-r--r--toolkit/mozapps/update/tests/data/complete_removed-files41
-rw-r--r--toolkit/mozapps/update/tests/data/complete_removed-files_mac41
-rw-r--r--toolkit/mozapps/update/tests/data/complete_update_manifest59
-rw-r--r--toolkit/mozapps/update/tests/data/old_version.marbin0 -> 709 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/partial.marbin0 -> 9872 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/partial.pngbin0 -> 776 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/partial_log_failure_mac192
-rw-r--r--toolkit/mozapps/update/tests/data/partial_log_failure_win192
-rw-r--r--toolkit/mozapps/update/tests/data/partial_log_success_mac279
-rw-r--r--toolkit/mozapps/update/tests/data/partial_log_success_win279
-rw-r--r--toolkit/mozapps/update/tests/data/partial_mac.marbin0 -> 10361 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/partial_precomplete19
-rw-r--r--toolkit/mozapps/update/tests/data/partial_precomplete_mac22
-rw-r--r--toolkit/mozapps/update/tests/data/partial_removed-files41
-rw-r--r--toolkit/mozapps/update/tests/data/partial_removed-files_mac41
-rw-r--r--toolkit/mozapps/update/tests/data/partial_update_manifest63
-rw-r--r--toolkit/mozapps/update/tests/data/replace_log_success6
-rw-r--r--toolkit/mozapps/update/tests/data/shared.js928
-rw-r--r--toolkit/mozapps/update/tests/data/sharedUpdateXML.js417
-rw-r--r--toolkit/mozapps/update/tests/data/simple.marbin0 -> 1419 bytes
-rw-r--r--toolkit/mozapps/update/tests/data/syncManagerTestChild.js55
-rw-r--r--toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js29
-rw-r--r--toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js4803
31 files changed, 8449 insertions, 0 deletions
diff --git a/toolkit/mozapps/update/tests/data/app_update.sjs b/toolkit/mozapps/update/tests/data/app_update.sjs
new file mode 100644
index 0000000000..2081118547
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/app_update.sjs
@@ -0,0 +1,251 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+"use strict";
+
+/**
+ * Server side http server script for application update tests.
+ */
+
+// Definitions from test and other files used by the tests
+/* global getState */
+
+function getTestDataFile(aFilename) {
+ let file = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ let pathParts = REL_PATH_DATA.split("/");
+ for (let i = 0; i < pathParts.length; ++i) {
+ file.append(pathParts[i]);
+ }
+ if (aFilename) {
+ file.append(aFilename);
+ }
+ return file;
+}
+
+function loadHelperScript(aScriptFile) {
+ let scriptSpec = Services.io.newFileURI(aScriptFile).spec;
+ Services.scriptloader.loadSubScript(scriptSpec, this);
+}
+
+var scriptFile = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+scriptFile.initWithPath(getState("__LOCATION__"));
+scriptFile = scriptFile.parent;
+/* import-globals-from ../browser/testConstants.js */
+scriptFile.append("testConstants.js");
+loadHelperScript(scriptFile);
+
+/* import-globals-from sharedUpdateXML.js */
+scriptFile = getTestDataFile("sharedUpdateXML.js");
+loadHelperScript(scriptFile);
+
+const SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + FILE_SIMPLE_MAR;
+const BAD_SERVICE_URL = URL_HOST + "/" + REL_PATH_DATA + "not_here.mar";
+
+// A value of 10 caused the tests to intermittently fail on Mac OS X so be
+// careful when changing this value.
+const SLOW_RESPONSE_INTERVAL = 100;
+const MAX_SLOW_RESPONSE_RETRIES = 200;
+var gSlowDownloadTimer;
+var gSlowCheckTimer;
+
+function handleRequest(aRequest, aResponse) {
+ let params = {};
+ if (aRequest.queryString) {
+ params = parseQueryString(aRequest.queryString);
+ }
+
+ let statusCode = params.statusCode ? parseInt(params.statusCode) : 200;
+ let statusReason = params.statusReason ? params.statusReason : "OK";
+ aResponse.setStatusLine(aRequest.httpVersion, statusCode, statusReason);
+ aResponse.setHeader("Cache-Control", "no-cache", false);
+
+ // When a mar download is started by the update service it can finish
+ // downloading before the ui has loaded. By specifying a serviceURL for the
+ // update patch that points to this file and has a slowDownloadMar param the
+ // mar will be downloaded asynchronously which will allow the ui to load
+ // before the download completes.
+ if (params.slowDownloadMar) {
+ aResponse.processAsync();
+ aResponse.setHeader("Content-Type", "binary/octet-stream");
+ aResponse.setHeader("Content-Length", SIZE_SIMPLE_MAR);
+
+ // BITS will first make a HEAD request followed by a GET request.
+ if (aRequest.method == "HEAD") {
+ aResponse.finish();
+ return;
+ }
+
+ let retries = 0;
+ gSlowDownloadTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ gSlowDownloadTimer.initWithCallback(
+ function (aTimer) {
+ let continueFile = getTestDataFile(CONTINUE_DOWNLOAD);
+ retries++;
+ if (continueFile.exists() || retries == MAX_SLOW_RESPONSE_RETRIES) {
+ try {
+ // If the continue file is in use try again the next time the timer
+ // fires unless the retries has reached the value defined by
+ // MAX_SLOW_RESPONSE_RETRIES in which case let the test remove the
+ // continue file.
+ if (retries < MAX_SLOW_RESPONSE_RETRIES) {
+ continueFile.remove(false);
+ }
+ gSlowDownloadTimer.cancel();
+ aResponse.write(readFileBytes(getTestDataFile(FILE_SIMPLE_MAR)));
+ aResponse.finish();
+ } catch (e) {}
+ }
+ },
+ SLOW_RESPONSE_INTERVAL,
+ Ci.nsITimer.TYPE_REPEATING_SLACK
+ );
+ return;
+ }
+
+ if (params.uiURL) {
+ aResponse.write(
+ '<html><head><meta http-equiv="content-type" content=' +
+ '"text/html; charset=utf-8"></head><body>' +
+ params.uiURL +
+ "<br><br>this is a test mar that will not " +
+ "affect your build.</body></html>"
+ );
+ return;
+ }
+
+ if (params.xmlMalformed) {
+ respond(aResponse, params, "xml error");
+ return;
+ }
+
+ if (params.noUpdates) {
+ respond(aResponse, params, getRemoteUpdatesXMLString(""));
+ return;
+ }
+
+ if (params.unsupported) {
+ let detailsURL = params.detailsURL ? params.detailsURL : URL_HOST;
+ let unsupportedXML = getRemoteUpdatesXMLString(
+ ' <update type="major" ' +
+ 'unsupported="true" ' +
+ 'detailsURL="' +
+ detailsURL +
+ '"></update>\n'
+ );
+ respond(aResponse, params, unsupportedXML);
+ return;
+ }
+
+ let size;
+ let patches = "";
+ let url = "";
+ if (params.useSlowDownloadMar) {
+ url = URL_HTTP_UPDATE_SJS + "?slowDownloadMar=1";
+ } else {
+ url = params.badURL ? BAD_SERVICE_URL : SERVICE_URL;
+ }
+ if (!params.partialPatchOnly) {
+ size = SIZE_SIMPLE_MAR + (params.invalidCompleteSize ? "1" : "");
+ let patchProps = { type: "complete", url, size };
+ patches += getRemotePatchString(patchProps);
+ }
+
+ if (!params.completePatchOnly) {
+ size = SIZE_SIMPLE_MAR + (params.invalidPartialSize ? "1" : "");
+ let patchProps = { type: "partial", url, size };
+ patches += getRemotePatchString(patchProps);
+ }
+
+ let updateProps = {};
+ if (params.type) {
+ updateProps.type = params.type;
+ }
+
+ if (params.name) {
+ updateProps.name = params.name;
+ }
+
+ if (params.appVersion) {
+ updateProps.appVersion = params.appVersion;
+ }
+
+ if (params.displayVersion) {
+ updateProps.displayVersion = params.displayVersion;
+ }
+
+ if (params.buildID) {
+ updateProps.buildID = params.buildID;
+ }
+
+ if (params.promptWaitTime) {
+ updateProps.promptWaitTime = params.promptWaitTime;
+ }
+
+ if (params.disableBITS) {
+ updateProps.disableBITS = params.disableBITS;
+ }
+
+ let updates = getRemoteUpdateString(updateProps, patches);
+ let xml = getRemoteUpdatesXMLString(updates);
+ respond(aResponse, params, xml);
+}
+
+function respond(aResponse, aParams, aResponseString) {
+ if (aParams.slowUpdateCheck) {
+ let retries = 0;
+ aResponse.processAsync();
+ gSlowCheckTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ gSlowCheckTimer.initWithCallback(
+ function (aTimer) {
+ retries++;
+ let continueFile = getTestDataFile(CONTINUE_CHECK);
+ if (continueFile.exists() || retries == MAX_SLOW_RESPONSE_RETRIES) {
+ try {
+ // If the continue file is in use try again the next time the timer
+ // fires unless the retries has reached the value defined by
+ // MAX_SLOW_RESPONSE_RETRIES in which case let the test remove the
+ // continue file.
+ if (retries < MAX_SLOW_RESPONSE_RETRIES) {
+ continueFile.remove(false);
+ }
+ gSlowCheckTimer.cancel();
+ aResponse.write(aResponseString);
+ aResponse.finish();
+ } catch (e) {}
+ }
+ },
+ SLOW_RESPONSE_INTERVAL,
+ Ci.nsITimer.TYPE_REPEATING_SLACK
+ );
+ } else {
+ aResponse.write(aResponseString);
+ }
+}
+
+/**
+ * Helper function to create a JS object representing the url parameters from
+ * the request's queryString.
+ *
+ * @param aQueryString
+ * The request's query string.
+ * @return A JS object representing the url parameters from the request's
+ * queryString.
+ */
+function parseQueryString(aQueryString) {
+ let paramArray = aQueryString.split("&");
+ let regex = /^([^=]+)=(.*)$/;
+ let params = {};
+ for (let i = 0, sz = paramArray.length; i < sz; i++) {
+ let match = regex.exec(paramArray[i]);
+ if (!match) {
+ throw Components.Exception(
+ "Bad parameter in queryString! '" + paramArray[i] + "'",
+ Cr.NS_ERROR_ILLEGAL_VALUE
+ );
+ }
+ params[decodeURIComponent(match[1])] = decodeURIComponent(match[2]);
+ }
+
+ return params;
+}
diff --git a/toolkit/mozapps/update/tests/data/complete.mar b/toolkit/mozapps/update/tests/data/complete.mar
new file mode 100644
index 0000000000..375fd7bd08
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/complete.png b/toolkit/mozapps/update/tests/data/complete.png
new file mode 100644
index 0000000000..2990a539ff
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete.png
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/complete_log_success_mac b/toolkit/mozapps/update/tests/data/complete_log_success_mac
new file mode 100644
index 0000000000..4f992a1374
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_log_success_mac
@@ -0,0 +1,332 @@
+UPDATE TYPE complete
+PREPARE REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0
+PREPARE REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png
+PREPARE REMOVEFILE Contents/Resources/removed-files
+PREPARE REMOVEFILE Contents/Resources/precomplete
+PREPARE REMOVEFILE Contents/Resources/2/20/20text0
+PREPARE REMOVEFILE Contents/Resources/2/20/20png0.png
+PREPARE REMOVEFILE Contents/Resources/0/0exe0.exe
+PREPARE REMOVEFILE Contents/Resources/0/00/00text0
+PREPARE REMOVEFILE Contents/MacOS/exe0.exe
+PREPARE REMOVEDIR Contents/Resources/searchplugins/
+PREPARE REMOVEDIR Contents/Resources/defaults/pref/
+PREPARE REMOVEDIR Contents/Resources/defaults/
+PREPARE REMOVEDIR Contents/Resources/2/20/
+PREPARE REMOVEDIR Contents/Resources/2/
+PREPARE REMOVEDIR Contents/Resources/0/00/
+PREPARE REMOVEDIR Contents/Resources/0/
+PREPARE REMOVEDIR Contents/Resources/
+PREPARE REMOVEDIR Contents/MacOS/
+PREPARE REMOVEDIR Contents/
+PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0
+PREPARE ADD Contents/Resources/searchplugins/searchpluginspng1.png
+PREPARE ADD Contents/Resources/searchplugins/searchpluginspng0.png
+PREPARE ADD Contents/Resources/removed-files
+PREPARE ADD Contents/Resources/precomplete
+PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+PREPARE ADD Contents/Resources/1/10/10text0
+PREPARE ADD Contents/Resources/0/0exe0.exe
+PREPARE ADD Contents/Resources/0/00/00text1
+PREPARE ADD Contents/Resources/0/00/00text0
+PREPARE ADD Contents/Resources/0/00/00png0.png
+PREPARE ADD Contents/MacOS/exe0.exe
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/98/
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/970/
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/971/
+PREPARE REMOVEDIR Contents/Resources/9/97/
+PREPARE REMOVEFILE Contents/Resources/9/96/96text0
+PREPARE REMOVEFILE Contents/Resources/9/96/96text1
+PREPARE REMOVEDIR Contents/Resources/9/96/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/93/
+PREPARE REMOVEDIR Contents/Resources/9/92/
+PREPARE REMOVEDIR Contents/Resources/9/91/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/88/
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/870/
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/871/
+PREPARE REMOVEDIR Contents/Resources/8/87/
+PREPARE REMOVEFILE Contents/Resources/8/86/86text0
+PREPARE REMOVEFILE Contents/Resources/8/86/86text1
+PREPARE REMOVEDIR Contents/Resources/8/86/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/83/
+PREPARE REMOVEDIR Contents/Resources/8/82/
+PREPARE REMOVEDIR Contents/Resources/8/81/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/70/
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/71/
+PREPARE REMOVEFILE Contents/Resources/7/7text0
+PREPARE REMOVEFILE Contents/Resources/7/7text1
+PREPARE REMOVEDIR Contents/Resources/7/
+PREPARE REMOVEDIR Contents/Resources/6/
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5test.exe
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEDIR Contents/Resources/5/
+PREPARE REMOVEFILE Contents/Resources/4/4text1
+PREPARE REMOVEFILE Contents/Resources/4/4text0
+PREPARE REMOVEDIR Contents/Resources/4/
+PREPARE REMOVEFILE Contents/Resources/3/3text1
+PREPARE REMOVEFILE Contents/Resources/3/3text0
+EXECUTE REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0
+EXECUTE REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png
+EXECUTE REMOVEFILE Contents/Resources/removed-files
+EXECUTE REMOVEFILE Contents/Resources/precomplete
+EXECUTE REMOVEFILE Contents/Resources/2/20/20text0
+EXECUTE REMOVEFILE Contents/Resources/2/20/20png0.png
+EXECUTE REMOVEFILE Contents/Resources/0/0exe0.exe
+EXECUTE REMOVEFILE Contents/Resources/0/00/00text0
+EXECUTE REMOVEFILE Contents/MacOS/exe0.exe
+EXECUTE REMOVEDIR Contents/Resources/searchplugins/
+EXECUTE REMOVEDIR Contents/Resources/defaults/pref/
+EXECUTE REMOVEDIR Contents/Resources/defaults/
+EXECUTE REMOVEDIR Contents/Resources/2/20/
+EXECUTE REMOVEDIR Contents/Resources/2/
+EXECUTE REMOVEDIR Contents/Resources/0/00/
+EXECUTE REMOVEDIR Contents/Resources/0/
+EXECUTE REMOVEDIR Contents/Resources/
+EXECUTE REMOVEDIR Contents/MacOS/
+EXECUTE REMOVEDIR Contents/
+EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0
+EXECUTE ADD Contents/Resources/searchplugins/searchpluginspng1.png
+EXECUTE ADD Contents/Resources/searchplugins/searchpluginspng0.png
+EXECUTE ADD Contents/Resources/removed-files
+EXECUTE ADD Contents/Resources/precomplete
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+EXECUTE ADD Contents/Resources/1/10/10text0
+EXECUTE ADD Contents/Resources/0/0exe0.exe
+EXECUTE ADD Contents/Resources/0/00/00text1
+EXECUTE ADD Contents/Resources/0/00/00text0
+EXECUTE ADD Contents/Resources/0/00/00png0.png
+EXECUTE ADD Contents/MacOS/exe0.exe
+EXECUTE REMOVEDIR Contents/Resources/9/99/
+EXECUTE REMOVEDIR Contents/Resources/9/99/
+EXECUTE REMOVEDIR Contents/Resources/9/98/
+EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext0
+EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext1
+EXECUTE REMOVEDIR Contents/Resources/9/97/970/
+EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext0
+EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext1
+EXECUTE REMOVEDIR Contents/Resources/9/97/971/
+EXECUTE REMOVEDIR Contents/Resources/9/97/
+EXECUTE REMOVEFILE Contents/Resources/9/96/96text0
+EXECUTE REMOVEFILE Contents/Resources/9/96/96text1
+EXECUTE REMOVEDIR Contents/Resources/9/96/
+EXECUTE REMOVEDIR Contents/Resources/9/95/
+EXECUTE REMOVEDIR Contents/Resources/9/95/
+EXECUTE REMOVEDIR Contents/Resources/9/94/
+EXECUTE REMOVEDIR Contents/Resources/9/94/
+EXECUTE REMOVEDIR Contents/Resources/9/93/
+EXECUTE REMOVEDIR Contents/Resources/9/92/
+EXECUTE REMOVEDIR Contents/Resources/9/91/
+EXECUTE REMOVEDIR Contents/Resources/9/90/
+EXECUTE REMOVEDIR Contents/Resources/9/90/
+EXECUTE REMOVEDIR Contents/Resources/8/89/
+EXECUTE REMOVEDIR Contents/Resources/8/89/
+EXECUTE REMOVEDIR Contents/Resources/8/88/
+EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext0
+EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext1
+EXECUTE REMOVEDIR Contents/Resources/8/87/870/
+EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext0
+EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext1
+EXECUTE REMOVEDIR Contents/Resources/8/87/871/
+EXECUTE REMOVEDIR Contents/Resources/8/87/
+EXECUTE REMOVEFILE Contents/Resources/8/86/86text0
+EXECUTE REMOVEFILE Contents/Resources/8/86/86text1
+EXECUTE REMOVEDIR Contents/Resources/8/86/
+EXECUTE REMOVEDIR Contents/Resources/8/85/
+EXECUTE REMOVEDIR Contents/Resources/8/85/
+EXECUTE REMOVEDIR Contents/Resources/8/84/
+EXECUTE REMOVEDIR Contents/Resources/8/84/
+EXECUTE REMOVEDIR Contents/Resources/8/83/
+EXECUTE REMOVEDIR Contents/Resources/8/82/
+EXECUTE REMOVEDIR Contents/Resources/8/81/
+EXECUTE REMOVEDIR Contents/Resources/8/80/
+EXECUTE REMOVEDIR Contents/Resources/8/80/
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtest.exe
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext0
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext1
+EXECUTE REMOVEDIR Contents/Resources/7/70/
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtest.exe
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext0
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext1
+EXECUTE REMOVEDIR Contents/Resources/7/71/
+EXECUTE REMOVEFILE Contents/Resources/7/7text0
+EXECUTE REMOVEFILE Contents/Resources/7/7text1
+EXECUTE REMOVEDIR Contents/Resources/7/
+EXECUTE REMOVEDIR Contents/Resources/6/
+EXECUTE REMOVEFILE Contents/Resources/5/5text1
+EXECUTE REMOVEFILE Contents/Resources/5/5text0
+EXECUTE REMOVEFILE Contents/Resources/5/5test.exe
+EXECUTE REMOVEFILE Contents/Resources/5/5text0
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEFILE Contents/Resources/5/5text1
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEDIR Contents/Resources/5/
+EXECUTE REMOVEFILE Contents/Resources/4/4text1
+EXECUTE REMOVEFILE Contents/Resources/4/4text0
+EXECUTE REMOVEDIR Contents/Resources/4/
+EXECUTE REMOVEFILE Contents/Resources/3/3text1
+EXECUTE REMOVEFILE Contents/Resources/3/3text0
+FINISH REMOVEFILE Contents/Resources/searchplugins/searchpluginstext0
+FINISH REMOVEFILE Contents/Resources/searchplugins/searchpluginspng0.png
+FINISH REMOVEFILE Contents/Resources/removed-files
+FINISH REMOVEFILE Contents/Resources/precomplete
+FINISH REMOVEFILE Contents/Resources/2/20/20text0
+FINISH REMOVEFILE Contents/Resources/2/20/20png0.png
+FINISH REMOVEFILE Contents/Resources/0/0exe0.exe
+FINISH REMOVEFILE Contents/Resources/0/00/00text0
+FINISH REMOVEFILE Contents/MacOS/exe0.exe
+FINISH REMOVEDIR Contents/Resources/searchplugins/
+removing directory: Contents/Resources/searchplugins/, rv: 0
+FINISH REMOVEDIR Contents/Resources/defaults/pref/
+removing directory: Contents/Resources/defaults/pref/, rv: 0
+FINISH REMOVEDIR Contents/Resources/defaults/
+removing directory: Contents/Resources/defaults/, rv: 0
+FINISH REMOVEDIR Contents/Resources/2/20/
+FINISH REMOVEDIR Contents/Resources/2/
+FINISH REMOVEDIR Contents/Resources/0/00/
+removing directory: Contents/Resources/0/00/, rv: 0
+FINISH REMOVEDIR Contents/Resources/0/
+removing directory: Contents/Resources/0/, rv: 0
+FINISH REMOVEDIR Contents/Resources/
+removing directory: Contents/Resources/, rv: 0
+FINISH REMOVEDIR Contents/MacOS/
+removing directory: Contents/MacOS/, rv: 0
+FINISH REMOVEDIR Contents/
+removing directory: Contents/, rv: 0
+FINISH ADD Contents/Resources/searchplugins/searchpluginstext0
+FINISH ADD Contents/Resources/searchplugins/searchpluginspng1.png
+FINISH ADD Contents/Resources/searchplugins/searchpluginspng0.png
+FINISH ADD Contents/Resources/removed-files
+FINISH ADD Contents/Resources/precomplete
+FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+FINISH ADD Contents/Resources/1/10/10text0
+FINISH ADD Contents/Resources/0/0exe0.exe
+FINISH ADD Contents/Resources/0/00/00text1
+FINISH ADD Contents/Resources/0/00/00text0
+FINISH ADD Contents/Resources/0/00/00png0.png
+FINISH ADD Contents/MacOS/exe0.exe
+FINISH REMOVEDIR Contents/Resources/9/99/
+FINISH REMOVEDIR Contents/Resources/9/99/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/98/
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1
+FINISH REMOVEDIR Contents/Resources/9/97/970/
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1
+FINISH REMOVEDIR Contents/Resources/9/97/971/
+FINISH REMOVEDIR Contents/Resources/9/97/
+FINISH REMOVEFILE Contents/Resources/9/96/96text0
+FINISH REMOVEFILE Contents/Resources/9/96/96text1
+FINISH REMOVEDIR Contents/Resources/9/96/
+FINISH REMOVEDIR Contents/Resources/9/95/
+FINISH REMOVEDIR Contents/Resources/9/95/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/94/
+FINISH REMOVEDIR Contents/Resources/9/94/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/93/
+FINISH REMOVEDIR Contents/Resources/9/92/
+removing directory: Contents/Resources/9/92/, rv: 0
+FINISH REMOVEDIR Contents/Resources/9/91/
+removing directory: Contents/Resources/9/91/, rv: 0
+FINISH REMOVEDIR Contents/Resources/9/90/
+FINISH REMOVEDIR Contents/Resources/9/90/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/89/
+FINISH REMOVEDIR Contents/Resources/8/89/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/88/
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1
+FINISH REMOVEDIR Contents/Resources/8/87/870/
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1
+FINISH REMOVEDIR Contents/Resources/8/87/871/
+FINISH REMOVEDIR Contents/Resources/8/87/
+FINISH REMOVEFILE Contents/Resources/8/86/86text0
+FINISH REMOVEFILE Contents/Resources/8/86/86text1
+FINISH REMOVEDIR Contents/Resources/8/86/
+FINISH REMOVEDIR Contents/Resources/8/85/
+FINISH REMOVEDIR Contents/Resources/8/85/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/84/
+FINISH REMOVEDIR Contents/Resources/8/84/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/83/
+FINISH REMOVEDIR Contents/Resources/8/82/
+removing directory: Contents/Resources/8/82/, rv: 0
+FINISH REMOVEDIR Contents/Resources/8/81/
+removing directory: Contents/Resources/8/81/, rv: 0
+FINISH REMOVEDIR Contents/Resources/8/80/
+FINISH REMOVEDIR Contents/Resources/8/80/
+directory no longer exists; skipping
+FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext0
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext1
+FINISH REMOVEDIR Contents/Resources/7/70/
+FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext0
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext1
+FINISH REMOVEDIR Contents/Resources/7/71/
+FINISH REMOVEFILE Contents/Resources/7/7text0
+FINISH REMOVEFILE Contents/Resources/7/7text1
+FINISH REMOVEDIR Contents/Resources/7/
+FINISH REMOVEDIR Contents/Resources/6/
+FINISH REMOVEFILE Contents/Resources/5/5text1
+FINISH REMOVEFILE Contents/Resources/5/5text0
+FINISH REMOVEFILE Contents/Resources/5/5test.exe
+FINISH REMOVEDIR Contents/Resources/5/
+FINISH REMOVEFILE Contents/Resources/4/4text1
+FINISH REMOVEFILE Contents/Resources/4/4text0
+FINISH REMOVEDIR Contents/Resources/4/
+FINISH REMOVEFILE Contents/Resources/3/3text1
+FINISH REMOVEFILE Contents/Resources/3/3text0
+succeeded
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/complete_log_success_win b/toolkit/mozapps/update/tests/data/complete_log_success_win
new file mode 100644
index 0000000000..c5a03dc9d6
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_log_success_win
@@ -0,0 +1,320 @@
+UPDATE TYPE complete
+PREPARE REMOVEFILE searchplugins/searchpluginstext0
+PREPARE REMOVEFILE searchplugins/searchpluginspng0.png
+PREPARE REMOVEFILE removed-files
+PREPARE REMOVEFILE precomplete
+PREPARE REMOVEFILE exe0.exe
+PREPARE REMOVEFILE 2/20/20text0
+PREPARE REMOVEFILE 2/20/20png0.png
+PREPARE REMOVEFILE 0/0exe0.exe
+PREPARE REMOVEFILE 0/00/00text0
+PREPARE REMOVEDIR searchplugins/
+PREPARE REMOVEDIR defaults/pref/
+PREPARE REMOVEDIR defaults/
+PREPARE REMOVEDIR 2/20/
+PREPARE REMOVEDIR 2/
+PREPARE REMOVEDIR 0/00/
+PREPARE REMOVEDIR 0/
+PREPARE ADD searchplugins/searchpluginstext0
+PREPARE ADD searchplugins/searchpluginspng1.png
+PREPARE ADD searchplugins/searchpluginspng0.png
+PREPARE ADD removed-files
+PREPARE ADD precomplete
+PREPARE ADD exe0.exe
+PREPARE ADD distribution/extensions/extensions1/extensions1text0
+PREPARE ADD distribution/extensions/extensions1/extensions1png1.png
+PREPARE ADD distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD distribution/extensions/extensions0/extensions0text0
+PREPARE ADD distribution/extensions/extensions0/extensions0png1.png
+PREPARE ADD distribution/extensions/extensions0/extensions0png0.png
+PREPARE ADD 1/10/10text0
+PREPARE ADD 0/0exe0.exe
+PREPARE ADD 0/00/00text1
+PREPARE ADD 0/00/00text0
+PREPARE ADD 0/00/00png0.png
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/98/
+PREPARE REMOVEFILE 9/97/970/97xtext0
+PREPARE REMOVEFILE 9/97/970/97xtext1
+PREPARE REMOVEDIR 9/97/970/
+PREPARE REMOVEFILE 9/97/971/97xtext0
+PREPARE REMOVEFILE 9/97/971/97xtext1
+PREPARE REMOVEDIR 9/97/971/
+PREPARE REMOVEDIR 9/97/
+PREPARE REMOVEFILE 9/96/96text0
+PREPARE REMOVEFILE 9/96/96text1
+PREPARE REMOVEDIR 9/96/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/93/
+PREPARE REMOVEDIR 9/92/
+PREPARE REMOVEDIR 9/91/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/88/
+PREPARE REMOVEFILE 8/87/870/87xtext0
+PREPARE REMOVEFILE 8/87/870/87xtext1
+PREPARE REMOVEDIR 8/87/870/
+PREPARE REMOVEFILE 8/87/871/87xtext0
+PREPARE REMOVEFILE 8/87/871/87xtext1
+PREPARE REMOVEDIR 8/87/871/
+PREPARE REMOVEDIR 8/87/
+PREPARE REMOVEFILE 8/86/86text0
+PREPARE REMOVEFILE 8/86/86text1
+PREPARE REMOVEDIR 8/86/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/83/
+PREPARE REMOVEDIR 8/82/
+PREPARE REMOVEDIR 8/81/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEFILE 7/70/7xtest.exe
+PREPARE REMOVEFILE 7/70/7xtext0
+PREPARE REMOVEFILE 7/70/7xtext1
+PREPARE REMOVEDIR 7/70/
+PREPARE REMOVEFILE 7/71/7xtest.exe
+PREPARE REMOVEFILE 7/71/7xtext0
+PREPARE REMOVEFILE 7/71/7xtext1
+PREPARE REMOVEDIR 7/71/
+PREPARE REMOVEFILE 7/7text0
+PREPARE REMOVEFILE 7/7text1
+PREPARE REMOVEDIR 7/
+PREPARE REMOVEDIR 6/
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5test.exe
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEDIR 5/
+PREPARE REMOVEFILE 4/4text1
+PREPARE REMOVEFILE 4/4text0
+PREPARE REMOVEDIR 4/
+PREPARE REMOVEFILE 3/3text1
+PREPARE REMOVEFILE 3/3text0
+EXECUTE REMOVEFILE searchplugins/searchpluginstext0
+EXECUTE REMOVEFILE searchplugins/searchpluginspng0.png
+EXECUTE REMOVEFILE removed-files
+EXECUTE REMOVEFILE precomplete
+EXECUTE REMOVEFILE exe0.exe
+EXECUTE REMOVEFILE 2/20/20text0
+EXECUTE REMOVEFILE 2/20/20png0.png
+EXECUTE REMOVEFILE 0/0exe0.exe
+EXECUTE REMOVEFILE 0/00/00text0
+EXECUTE REMOVEDIR searchplugins/
+EXECUTE REMOVEDIR defaults/pref/
+EXECUTE REMOVEDIR defaults/
+EXECUTE REMOVEDIR 2/20/
+EXECUTE REMOVEDIR 2/
+EXECUTE REMOVEDIR 0/00/
+EXECUTE REMOVEDIR 0/
+EXECUTE ADD searchplugins/searchpluginstext0
+EXECUTE ADD searchplugins/searchpluginspng1.png
+EXECUTE ADD searchplugins/searchpluginspng0.png
+EXECUTE ADD removed-files
+EXECUTE ADD precomplete
+EXECUTE ADD exe0.exe
+EXECUTE ADD distribution/extensions/extensions1/extensions1text0
+EXECUTE ADD distribution/extensions/extensions1/extensions1png1.png
+EXECUTE ADD distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD distribution/extensions/extensions0/extensions0text0
+EXECUTE ADD distribution/extensions/extensions0/extensions0png1.png
+EXECUTE ADD distribution/extensions/extensions0/extensions0png0.png
+EXECUTE ADD 1/10/10text0
+EXECUTE ADD 0/0exe0.exe
+EXECUTE ADD 0/00/00text1
+EXECUTE ADD 0/00/00text0
+EXECUTE ADD 0/00/00png0.png
+EXECUTE REMOVEDIR 9/99/
+EXECUTE REMOVEDIR 9/99/
+EXECUTE REMOVEDIR 9/98/
+EXECUTE REMOVEFILE 9/97/970/97xtext0
+EXECUTE REMOVEFILE 9/97/970/97xtext1
+EXECUTE REMOVEDIR 9/97/970/
+EXECUTE REMOVEFILE 9/97/971/97xtext0
+EXECUTE REMOVEFILE 9/97/971/97xtext1
+EXECUTE REMOVEDIR 9/97/971/
+EXECUTE REMOVEDIR 9/97/
+EXECUTE REMOVEFILE 9/96/96text0
+EXECUTE REMOVEFILE 9/96/96text1
+EXECUTE REMOVEDIR 9/96/
+EXECUTE REMOVEDIR 9/95/
+EXECUTE REMOVEDIR 9/95/
+EXECUTE REMOVEDIR 9/94/
+EXECUTE REMOVEDIR 9/94/
+EXECUTE REMOVEDIR 9/93/
+EXECUTE REMOVEDIR 9/92/
+EXECUTE REMOVEDIR 9/91/
+EXECUTE REMOVEDIR 9/90/
+EXECUTE REMOVEDIR 9/90/
+EXECUTE REMOVEDIR 8/89/
+EXECUTE REMOVEDIR 8/89/
+EXECUTE REMOVEDIR 8/88/
+EXECUTE REMOVEFILE 8/87/870/87xtext0
+EXECUTE REMOVEFILE 8/87/870/87xtext1
+EXECUTE REMOVEDIR 8/87/870/
+EXECUTE REMOVEFILE 8/87/871/87xtext0
+EXECUTE REMOVEFILE 8/87/871/87xtext1
+EXECUTE REMOVEDIR 8/87/871/
+EXECUTE REMOVEDIR 8/87/
+EXECUTE REMOVEFILE 8/86/86text0
+EXECUTE REMOVEFILE 8/86/86text1
+EXECUTE REMOVEDIR 8/86/
+EXECUTE REMOVEDIR 8/85/
+EXECUTE REMOVEDIR 8/85/
+EXECUTE REMOVEDIR 8/84/
+EXECUTE REMOVEDIR 8/84/
+EXECUTE REMOVEDIR 8/83/
+EXECUTE REMOVEDIR 8/82/
+EXECUTE REMOVEDIR 8/81/
+EXECUTE REMOVEDIR 8/80/
+EXECUTE REMOVEDIR 8/80/
+EXECUTE REMOVEFILE 7/70/7xtest.exe
+EXECUTE REMOVEFILE 7/70/7xtext0
+EXECUTE REMOVEFILE 7/70/7xtext1
+EXECUTE REMOVEDIR 7/70/
+EXECUTE REMOVEFILE 7/71/7xtest.exe
+EXECUTE REMOVEFILE 7/71/7xtext0
+EXECUTE REMOVEFILE 7/71/7xtext1
+EXECUTE REMOVEDIR 7/71/
+EXECUTE REMOVEFILE 7/7text0
+EXECUTE REMOVEFILE 7/7text1
+EXECUTE REMOVEDIR 7/
+EXECUTE REMOVEDIR 6/
+EXECUTE REMOVEFILE 5/5text1
+EXECUTE REMOVEFILE 5/5text0
+EXECUTE REMOVEFILE 5/5test.exe
+EXECUTE REMOVEFILE 5/5text0
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEFILE 5/5text1
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEDIR 5/
+EXECUTE REMOVEFILE 4/4text1
+EXECUTE REMOVEFILE 4/4text0
+EXECUTE REMOVEDIR 4/
+EXECUTE REMOVEFILE 3/3text1
+EXECUTE REMOVEFILE 3/3text0
+FINISH REMOVEFILE searchplugins/searchpluginstext0
+FINISH REMOVEFILE searchplugins/searchpluginspng0.png
+FINISH REMOVEFILE removed-files
+FINISH REMOVEFILE precomplete
+FINISH REMOVEFILE exe0.exe
+FINISH REMOVEFILE 2/20/20text0
+FINISH REMOVEFILE 2/20/20png0.png
+FINISH REMOVEFILE 0/0exe0.exe
+FINISH REMOVEFILE 0/00/00text0
+FINISH REMOVEDIR searchplugins/
+removing directory: searchplugins/, rv: 0
+FINISH REMOVEDIR defaults/pref/
+removing directory: defaults/pref/, rv: 0
+FINISH REMOVEDIR defaults/
+removing directory: defaults/, rv: 0
+FINISH REMOVEDIR 2/20/
+FINISH REMOVEDIR 2/
+FINISH REMOVEDIR 0/00/
+removing directory: 0/00/, rv: 0
+FINISH REMOVEDIR 0/
+removing directory: 0/, rv: 0
+FINISH ADD searchplugins/searchpluginstext0
+FINISH ADD searchplugins/searchpluginspng1.png
+FINISH ADD searchplugins/searchpluginspng0.png
+FINISH ADD removed-files
+FINISH ADD precomplete
+FINISH ADD exe0.exe
+FINISH ADD distribution/extensions/extensions1/extensions1text0
+FINISH ADD distribution/extensions/extensions1/extensions1png1.png
+FINISH ADD distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD distribution/extensions/extensions0/extensions0text0
+FINISH ADD distribution/extensions/extensions0/extensions0png1.png
+FINISH ADD distribution/extensions/extensions0/extensions0png0.png
+FINISH ADD 1/10/10text0
+FINISH ADD 0/0exe0.exe
+FINISH ADD 0/00/00text1
+FINISH ADD 0/00/00text0
+FINISH ADD 0/00/00png0.png
+FINISH REMOVEDIR 9/99/
+FINISH REMOVEDIR 9/99/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/98/
+FINISH REMOVEFILE 9/97/970/97xtext0
+FINISH REMOVEFILE 9/97/970/97xtext1
+FINISH REMOVEDIR 9/97/970/
+FINISH REMOVEFILE 9/97/971/97xtext0
+FINISH REMOVEFILE 9/97/971/97xtext1
+FINISH REMOVEDIR 9/97/971/
+FINISH REMOVEDIR 9/97/
+FINISH REMOVEFILE 9/96/96text0
+FINISH REMOVEFILE 9/96/96text1
+FINISH REMOVEDIR 9/96/
+FINISH REMOVEDIR 9/95/
+FINISH REMOVEDIR 9/95/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/94/
+FINISH REMOVEDIR 9/94/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/93/
+FINISH REMOVEDIR 9/92/
+removing directory: 9/92/, rv: 0
+FINISH REMOVEDIR 9/91/
+removing directory: 9/91/, rv: 0
+FINISH REMOVEDIR 9/90/
+FINISH REMOVEDIR 9/90/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/89/
+FINISH REMOVEDIR 8/89/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/88/
+FINISH REMOVEFILE 8/87/870/87xtext0
+FINISH REMOVEFILE 8/87/870/87xtext1
+FINISH REMOVEDIR 8/87/870/
+FINISH REMOVEFILE 8/87/871/87xtext0
+FINISH REMOVEFILE 8/87/871/87xtext1
+FINISH REMOVEDIR 8/87/871/
+FINISH REMOVEDIR 8/87/
+FINISH REMOVEFILE 8/86/86text0
+FINISH REMOVEFILE 8/86/86text1
+FINISH REMOVEDIR 8/86/
+FINISH REMOVEDIR 8/85/
+FINISH REMOVEDIR 8/85/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/84/
+FINISH REMOVEDIR 8/84/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/83/
+FINISH REMOVEDIR 8/82/
+removing directory: 8/82/, rv: 0
+FINISH REMOVEDIR 8/81/
+removing directory: 8/81/, rv: 0
+FINISH REMOVEDIR 8/80/
+FINISH REMOVEDIR 8/80/
+directory no longer exists; skipping
+FINISH REMOVEFILE 7/70/7xtest.exe
+FINISH REMOVEFILE 7/70/7xtext0
+FINISH REMOVEFILE 7/70/7xtext1
+FINISH REMOVEDIR 7/70/
+FINISH REMOVEFILE 7/71/7xtest.exe
+FINISH REMOVEFILE 7/71/7xtext0
+FINISH REMOVEFILE 7/71/7xtext1
+FINISH REMOVEDIR 7/71/
+FINISH REMOVEFILE 7/7text0
+FINISH REMOVEFILE 7/7text1
+FINISH REMOVEDIR 7/
+FINISH REMOVEDIR 6/
+FINISH REMOVEFILE 5/5text1
+FINISH REMOVEFILE 5/5text0
+FINISH REMOVEFILE 5/5test.exe
+FINISH REMOVEDIR 5/
+FINISH REMOVEFILE 4/4text1
+FINISH REMOVEFILE 4/4text0
+FINISH REMOVEDIR 4/
+FINISH REMOVEFILE 3/3text1
+FINISH REMOVEFILE 3/3text0
+succeeded
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/complete_mac.mar b/toolkit/mozapps/update/tests/data/complete_mac.mar
new file mode 100644
index 0000000000..c54088610a
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_mac.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/complete_precomplete b/toolkit/mozapps/update/tests/data/complete_precomplete
new file mode 100644
index 0000000000..ae7a0013ff
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_precomplete
@@ -0,0 +1,18 @@
+remove "searchplugins/searchpluginstext0"
+remove "searchplugins/searchpluginspng1.png"
+remove "searchplugins/searchpluginspng0.png"
+remove "removed-files"
+remove "precomplete"
+remove "exe0.exe"
+remove "1/10/10text0"
+remove "0/0exe0.exe"
+remove "0/00/00text1"
+remove "0/00/00text0"
+remove "0/00/00png0.png"
+rmdir "searchplugins/"
+rmdir "defaults/pref/"
+rmdir "defaults/"
+rmdir "1/10/"
+rmdir "1/"
+rmdir "0/00/"
+rmdir "0/"
diff --git a/toolkit/mozapps/update/tests/data/complete_precomplete_mac b/toolkit/mozapps/update/tests/data/complete_precomplete_mac
new file mode 100644
index 0000000000..8d81a36d66
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_precomplete_mac
@@ -0,0 +1,21 @@
+remove "Contents/Resources/searchplugins/searchpluginstext0"
+remove "Contents/Resources/searchplugins/searchpluginspng1.png"
+remove "Contents/Resources/searchplugins/searchpluginspng0.png"
+remove "Contents/Resources/removed-files"
+remove "Contents/Resources/precomplete"
+remove "Contents/Resources/1/10/10text0"
+remove "Contents/Resources/0/0exe0.exe"
+remove "Contents/Resources/0/00/00text1"
+remove "Contents/Resources/0/00/00text0"
+remove "Contents/Resources/0/00/00png0.png"
+remove "Contents/MacOS/exe0.exe"
+rmdir "Contents/Resources/searchplugins/"
+rmdir "Contents/Resources/defaults/pref/"
+rmdir "Contents/Resources/defaults/"
+rmdir "Contents/Resources/1/10/"
+rmdir "Contents/Resources/1/"
+rmdir "Contents/Resources/0/00/"
+rmdir "Contents/Resources/0/"
+rmdir "Contents/Resources/"
+rmdir "Contents/MacOS/"
+rmdir "Contents/"
diff --git a/toolkit/mozapps/update/tests/data/complete_removed-files b/toolkit/mozapps/update/tests/data/complete_removed-files
new file mode 100644
index 0000000000..e45c43c1f8
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_removed-files
@@ -0,0 +1,41 @@
+text0
+text1
+3/3text0
+3/3text1
+4/exe0.exe
+4/4text0
+4/4text1
+4/
+5/5text0
+5/5text1
+5/*
+6/
+7/*
+8/80/
+8/81/
+8/82/
+8/83/
+8/84/
+8/85/*
+8/86/*
+8/87/*
+8/88/*
+8/89/*
+8/80/
+8/84/*
+8/85/*
+8/89/
+9/90/
+9/91/
+9/92/
+9/93/
+9/94/
+9/95/*
+9/96/*
+9/97/*
+9/98/*
+9/99/*
+9/90/
+9/94/*
+9/95/*
+9/99/
diff --git a/toolkit/mozapps/update/tests/data/complete_removed-files_mac b/toolkit/mozapps/update/tests/data/complete_removed-files_mac
new file mode 100644
index 0000000000..955dc5b340
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_removed-files_mac
@@ -0,0 +1,41 @@
+Contents/Resources/text0
+Contents/Resources/text1
+Contents/Resources/3/3text0
+Contents/Resources/3/3text1
+Contents/Resources/4/exe0.exe
+Contents/Resources/4/4text0
+Contents/Resources/4/4text1
+Contents/Resources/4/
+Contents/Resources/5/5text0
+Contents/Resources/5/5text1
+Contents/Resources/5/*
+Contents/Resources/6/
+Contents/Resources/7/*
+Contents/Resources/8/80/
+Contents/Resources/8/81/
+Contents/Resources/8/82/
+Contents/Resources/8/83/
+Contents/Resources/8/84/
+Contents/Resources/8/85/*
+Contents/Resources/8/86/*
+Contents/Resources/8/87/*
+Contents/Resources/8/88/*
+Contents/Resources/8/89/*
+Contents/Resources/8/80/
+Contents/Resources/8/84/*
+Contents/Resources/8/85/*
+Contents/Resources/8/89/
+Contents/Resources/9/90/
+Contents/Resources/9/91/
+Contents/Resources/9/92/
+Contents/Resources/9/93/
+Contents/Resources/9/94/
+Contents/Resources/9/95/*
+Contents/Resources/9/96/*
+Contents/Resources/9/97/*
+Contents/Resources/9/98/*
+Contents/Resources/9/99/*
+Contents/Resources/9/90/
+Contents/Resources/9/94/*
+Contents/Resources/9/95/*
+Contents/Resources/9/99/
diff --git a/toolkit/mozapps/update/tests/data/complete_update_manifest b/toolkit/mozapps/update/tests/data/complete_update_manifest
new file mode 100644
index 0000000000..383a324f63
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/complete_update_manifest
@@ -0,0 +1,59 @@
+type "complete"
+add "precomplete"
+add "searchplugins/searchpluginstext0"
+add "searchplugins/searchpluginspng1.png"
+add "searchplugins/searchpluginspng0.png"
+add "removed-files"
+add-if "extensions/extensions1" "extensions/extensions1/extensions1text0"
+add-if "extensions/extensions1" "extensions/extensions1/extensions1png1.png"
+add-if "extensions/extensions1" "extensions/extensions1/extensions1png0.png"
+add-if "extensions/extensions0" "extensions/extensions0/extensions0text0"
+add-if "extensions/extensions0" "extensions/extensions0/extensions0png1.png"
+add-if "extensions/extensions0" "extensions/extensions0/extensions0png0.png"
+add "exe0.exe"
+add "1/10/10text0"
+add "0/0exe0.exe"
+add "0/00/00text1"
+add "0/00/00text0"
+add "0/00/00png0.png"
+remove "text1"
+remove "text0"
+rmrfdir "9/99/"
+rmdir "9/99/"
+rmrfdir "9/98/"
+rmrfdir "9/97/"
+rmrfdir "9/96/"
+rmrfdir "9/95/"
+rmrfdir "9/95/"
+rmrfdir "9/94/"
+rmdir "9/94/"
+rmdir "9/93/"
+rmdir "9/92/"
+rmdir "9/91/"
+rmdir "9/90/"
+rmdir "9/90/"
+rmrfdir "8/89/"
+rmdir "8/89/"
+rmrfdir "8/88/"
+rmrfdir "8/87/"
+rmrfdir "8/86/"
+rmrfdir "8/85/"
+rmrfdir "8/85/"
+rmrfdir "8/84/"
+rmdir "8/84/"
+rmdir "8/83/"
+rmdir "8/82/"
+rmdir "8/81/"
+rmdir "8/80/"
+rmdir "8/80/"
+rmrfdir "7/"
+rmdir "6/"
+remove "5/5text1"
+remove "5/5text0"
+rmrfdir "5/"
+remove "4/exe0.exe"
+remove "4/4text1"
+remove "4/4text0"
+rmdir "4/"
+remove "3/3text1"
+remove "3/3text0"
diff --git a/toolkit/mozapps/update/tests/data/old_version.mar b/toolkit/mozapps/update/tests/data/old_version.mar
new file mode 100644
index 0000000000..b48f1d5fa4
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/old_version.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/partial.mar b/toolkit/mozapps/update/tests/data/partial.mar
new file mode 100644
index 0000000000..b6b04bbdbf
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/partial.png b/toolkit/mozapps/update/tests/data/partial.png
new file mode 100644
index 0000000000..9246f586c7
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial.png
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/partial_log_failure_mac b/toolkit/mozapps/update/tests/data/partial_log_failure_mac
new file mode 100644
index 0000000000..3b2933ebd2
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_log_failure_mac
@@ -0,0 +1,192 @@
+UPDATE TYPE partial
+PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0
+PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+PREPARE ADD Contents/Resources/precomplete
+PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+PREPARE PATCH Contents/Resources/0/0exe0.exe
+PREPARE ADD Contents/Resources/0/00/00text0
+PREPARE PATCH Contents/Resources/0/00/00png0.png
+PREPARE PATCH Contents/MacOS/exe0.exe
+PREPARE ADD Contents/Resources/2/20/20text0
+PREPARE ADD Contents/Resources/2/20/20png0.png
+PREPARE ADD Contents/Resources/0/00/00text2
+PREPARE REMOVEFILE Contents/Resources/1/10/10text0
+PREPARE REMOVEFILE Contents/Resources/0/00/00text1
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/98/
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/970/
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/971/
+PREPARE REMOVEDIR Contents/Resources/9/97/
+PREPARE REMOVEFILE Contents/Resources/9/96/96text0
+PREPARE REMOVEFILE Contents/Resources/9/96/96text1
+PREPARE REMOVEDIR Contents/Resources/9/96/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/93/
+PREPARE REMOVEDIR Contents/Resources/9/92/
+PREPARE REMOVEDIR Contents/Resources/9/91/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/88/
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/870/
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/871/
+PREPARE REMOVEDIR Contents/Resources/8/87/
+PREPARE REMOVEFILE Contents/Resources/8/86/86text0
+PREPARE REMOVEFILE Contents/Resources/8/86/86text1
+PREPARE REMOVEDIR Contents/Resources/8/86/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/83/
+PREPARE REMOVEDIR Contents/Resources/8/82/
+PREPARE REMOVEDIR Contents/Resources/8/81/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/70/
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/71/
+PREPARE REMOVEFILE Contents/Resources/7/7text0
+PREPARE REMOVEFILE Contents/Resources/7/7text1
+PREPARE REMOVEDIR Contents/Resources/7/
+PREPARE REMOVEDIR Contents/Resources/6/
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5test.exe
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEDIR Contents/Resources/5/
+PREPARE REMOVEFILE Contents/Resources/4/4text1
+PREPARE REMOVEFILE Contents/Resources/4/4text0
+PREPARE REMOVEDIR Contents/Resources/4/
+PREPARE REMOVEFILE Contents/Resources/3/3text1
+PREPARE REMOVEFILE Contents/Resources/3/3text0
+PREPARE REMOVEDIR Contents/Resources/1/10/
+PREPARE REMOVEDIR Contents/Resources/1/
+EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0
+EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+EXECUTE ADD Contents/Resources/precomplete
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+EXECUTE PATCH Contents/Resources/0/0exe0.exe
+LoadSourceFile: destination file size 776 does not match expected size 79872
+LoadSourceFile failed
+### execution failed
+FINISH ADD Contents/Resources/searchplugins/searchpluginstext0
+FINISH PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+FINISH PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+FINISH ADD Contents/Resources/precomplete
+FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+backup_restore: backup file doesn't exist: Contents/Resources/distribution/extensions/extensions1/extensions1text0.moz-backup
+FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+FINISH PATCH Contents/Resources/0/0exe0.exe
+backup_restore: backup file doesn't exist: Contents/Resources/0/0exe0.exe.moz-backup
+FINISH ADD Contents/Resources/0/00/00text0
+backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text0.moz-backup
+FINISH PATCH Contents/Resources/0/00/00png0.png
+backup_restore: backup file doesn't exist: Contents/Resources/0/00/00png0.png.moz-backup
+FINISH PATCH Contents/MacOS/exe0.exe
+backup_restore: backup file doesn't exist: Contents/MacOS/exe0.exe.moz-backup
+FINISH ADD Contents/Resources/2/20/20text0
+backup_restore: backup file doesn't exist: Contents/Resources/2/20/20text0.moz-backup
+FINISH ADD Contents/Resources/2/20/20png0.png
+backup_restore: backup file doesn't exist: Contents/Resources/2/20/20png0.png.moz-backup
+FINISH ADD Contents/Resources/0/00/00text2
+backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text2.moz-backup
+FINISH REMOVEFILE Contents/Resources/1/10/10text0
+backup_restore: backup file doesn't exist: Contents/Resources/1/10/10text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/0/00/00text1
+backup_restore: backup file doesn't exist: Contents/Resources/0/00/00text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/9/97/970/97xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/9/97/970/97xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/9/97/971/97xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/9/97/971/97xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/96/96text0
+backup_restore: backup file doesn't exist: Contents/Resources/9/96/96text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/9/96/96text1
+backup_restore: backup file doesn't exist: Contents/Resources/9/96/96text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/8/87/870/87xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/8/87/870/87xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/8/87/871/87xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/8/87/871/87xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/86/86text0
+backup_restore: backup file doesn't exist: Contents/Resources/8/86/86text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/8/86/86text1
+backup_restore: backup file doesn't exist: Contents/Resources/8/86/86text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe
+backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtest.exe.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/7/70/7xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe
+backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtest.exe.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext0
+backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtext0.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext1
+backup_restore: backup file doesn't exist: Contents/Resources/7/71/7xtext1.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/7text0
+backup_restore: backup file doesn't exist: Contents/Resources/7/7text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/7/7text1
+backup_restore: backup file doesn't exist: Contents/Resources/7/7text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/5/5text1
+backup_restore: backup file doesn't exist: Contents/Resources/5/5text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/5/5text0
+backup_restore: backup file doesn't exist: Contents/Resources/5/5text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/5/5test.exe
+backup_restore: backup file doesn't exist: Contents/Resources/5/5test.exe.moz-backup
+FINISH REMOVEFILE Contents/Resources/5/5text0
+backup_restore: backup file doesn't exist: Contents/Resources/5/5text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/5/5text1
+backup_restore: backup file doesn't exist: Contents/Resources/5/5text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/4/4text1
+backup_restore: backup file doesn't exist: Contents/Resources/4/4text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/4/4text0
+backup_restore: backup file doesn't exist: Contents/Resources/4/4text0.moz-backup
+FINISH REMOVEFILE Contents/Resources/3/3text1
+backup_restore: backup file doesn't exist: Contents/Resources/3/3text1.moz-backup
+FINISH REMOVEFILE Contents/Resources/3/3text0
+backup_restore: backup file doesn't exist: Contents/Resources/3/3text0.moz-backup
+failed: 2
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/partial_log_failure_win b/toolkit/mozapps/update/tests/data/partial_log_failure_win
new file mode 100644
index 0000000000..e3d683dc19
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_log_failure_win
@@ -0,0 +1,192 @@
+UPDATE TYPE partial
+PREPARE ADD searchplugins/searchpluginstext0
+PREPARE PATCH searchplugins/searchpluginspng1.png
+PREPARE PATCH searchplugins/searchpluginspng0.png
+PREPARE ADD precomplete
+PREPARE PATCH exe0.exe
+PREPARE ADD distribution/extensions/extensions1/extensions1text0
+PREPARE PATCH distribution/extensions/extensions1/extensions1png1.png
+PREPARE PATCH distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD distribution/extensions/extensions0/extensions0text0
+PREPARE PATCH distribution/extensions/extensions0/extensions0png1.png
+PREPARE PATCH distribution/extensions/extensions0/extensions0png0.png
+PREPARE PATCH 0/0exe0.exe
+PREPARE ADD 0/00/00text0
+PREPARE PATCH 0/00/00png0.png
+PREPARE ADD 2/20/20text0
+PREPARE ADD 2/20/20png0.png
+PREPARE ADD 0/00/00text2
+PREPARE REMOVEFILE 1/10/10text0
+PREPARE REMOVEFILE 0/00/00text1
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/98/
+PREPARE REMOVEFILE 9/97/970/97xtext0
+PREPARE REMOVEFILE 9/97/970/97xtext1
+PREPARE REMOVEDIR 9/97/970/
+PREPARE REMOVEFILE 9/97/971/97xtext0
+PREPARE REMOVEFILE 9/97/971/97xtext1
+PREPARE REMOVEDIR 9/97/971/
+PREPARE REMOVEDIR 9/97/
+PREPARE REMOVEFILE 9/96/96text0
+PREPARE REMOVEFILE 9/96/96text1
+PREPARE REMOVEDIR 9/96/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/93/
+PREPARE REMOVEDIR 9/92/
+PREPARE REMOVEDIR 9/91/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/88/
+PREPARE REMOVEFILE 8/87/870/87xtext0
+PREPARE REMOVEFILE 8/87/870/87xtext1
+PREPARE REMOVEDIR 8/87/870/
+PREPARE REMOVEFILE 8/87/871/87xtext0
+PREPARE REMOVEFILE 8/87/871/87xtext1
+PREPARE REMOVEDIR 8/87/871/
+PREPARE REMOVEDIR 8/87/
+PREPARE REMOVEFILE 8/86/86text0
+PREPARE REMOVEFILE 8/86/86text1
+PREPARE REMOVEDIR 8/86/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/83/
+PREPARE REMOVEDIR 8/82/
+PREPARE REMOVEDIR 8/81/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEFILE 7/70/7xtest.exe
+PREPARE REMOVEFILE 7/70/7xtext0
+PREPARE REMOVEFILE 7/70/7xtext1
+PREPARE REMOVEDIR 7/70/
+PREPARE REMOVEFILE 7/71/7xtest.exe
+PREPARE REMOVEFILE 7/71/7xtext0
+PREPARE REMOVEFILE 7/71/7xtext1
+PREPARE REMOVEDIR 7/71/
+PREPARE REMOVEFILE 7/7text0
+PREPARE REMOVEFILE 7/7text1
+PREPARE REMOVEDIR 7/
+PREPARE REMOVEDIR 6/
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5test.exe
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEDIR 5/
+PREPARE REMOVEFILE 4/4text1
+PREPARE REMOVEFILE 4/4text0
+PREPARE REMOVEDIR 4/
+PREPARE REMOVEFILE 3/3text1
+PREPARE REMOVEFILE 3/3text0
+PREPARE REMOVEDIR 1/10/
+PREPARE REMOVEDIR 1/
+EXECUTE ADD searchplugins/searchpluginstext0
+EXECUTE PATCH searchplugins/searchpluginspng1.png
+EXECUTE PATCH searchplugins/searchpluginspng0.png
+EXECUTE ADD precomplete
+EXECUTE PATCH exe0.exe
+EXECUTE ADD distribution/extensions/extensions1/extensions1text0
+EXECUTE PATCH distribution/extensions/extensions1/extensions1png1.png
+EXECUTE PATCH distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD distribution/extensions/extensions0/extensions0text0
+EXECUTE PATCH distribution/extensions/extensions0/extensions0png1.png
+EXECUTE PATCH distribution/extensions/extensions0/extensions0png0.png
+EXECUTE PATCH 0/0exe0.exe
+LoadSourceFile: destination file size 776 does not match expected size 79872
+LoadSourceFile failed
+### execution failed
+FINISH ADD searchplugins/searchpluginstext0
+FINISH PATCH searchplugins/searchpluginspng1.png
+FINISH PATCH searchplugins/searchpluginspng0.png
+FINISH ADD precomplete
+FINISH PATCH exe0.exe
+FINISH ADD distribution/extensions/extensions1/extensions1text0
+backup_restore: backup file doesn't exist: distribution/extensions/extensions1/extensions1text0.moz-backup
+FINISH PATCH distribution/extensions/extensions1/extensions1png1.png
+FINISH PATCH distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD distribution/extensions/extensions0/extensions0text0
+FINISH PATCH distribution/extensions/extensions0/extensions0png1.png
+FINISH PATCH distribution/extensions/extensions0/extensions0png0.png
+FINISH PATCH 0/0exe0.exe
+backup_restore: backup file doesn't exist: 0/0exe0.exe.moz-backup
+FINISH ADD 0/00/00text0
+backup_restore: backup file doesn't exist: 0/00/00text0.moz-backup
+FINISH PATCH 0/00/00png0.png
+backup_restore: backup file doesn't exist: 0/00/00png0.png.moz-backup
+FINISH ADD 2/20/20text0
+backup_restore: backup file doesn't exist: 2/20/20text0.moz-backup
+FINISH ADD 2/20/20png0.png
+backup_restore: backup file doesn't exist: 2/20/20png0.png.moz-backup
+FINISH ADD 0/00/00text2
+backup_restore: backup file doesn't exist: 0/00/00text2.moz-backup
+FINISH REMOVEFILE 1/10/10text0
+backup_restore: backup file doesn't exist: 1/10/10text0.moz-backup
+FINISH REMOVEFILE 0/00/00text1
+backup_restore: backup file doesn't exist: 0/00/00text1.moz-backup
+FINISH REMOVEFILE 9/97/970/97xtext0
+backup_restore: backup file doesn't exist: 9/97/970/97xtext0.moz-backup
+FINISH REMOVEFILE 9/97/970/97xtext1
+backup_restore: backup file doesn't exist: 9/97/970/97xtext1.moz-backup
+FINISH REMOVEFILE 9/97/971/97xtext0
+backup_restore: backup file doesn't exist: 9/97/971/97xtext0.moz-backup
+FINISH REMOVEFILE 9/97/971/97xtext1
+backup_restore: backup file doesn't exist: 9/97/971/97xtext1.moz-backup
+FINISH REMOVEFILE 9/96/96text0
+backup_restore: backup file doesn't exist: 9/96/96text0.moz-backup
+FINISH REMOVEFILE 9/96/96text1
+backup_restore: backup file doesn't exist: 9/96/96text1.moz-backup
+FINISH REMOVEFILE 8/87/870/87xtext0
+backup_restore: backup file doesn't exist: 8/87/870/87xtext0.moz-backup
+FINISH REMOVEFILE 8/87/870/87xtext1
+backup_restore: backup file doesn't exist: 8/87/870/87xtext1.moz-backup
+FINISH REMOVEFILE 8/87/871/87xtext0
+backup_restore: backup file doesn't exist: 8/87/871/87xtext0.moz-backup
+FINISH REMOVEFILE 8/87/871/87xtext1
+backup_restore: backup file doesn't exist: 8/87/871/87xtext1.moz-backup
+FINISH REMOVEFILE 8/86/86text0
+backup_restore: backup file doesn't exist: 8/86/86text0.moz-backup
+FINISH REMOVEFILE 8/86/86text1
+backup_restore: backup file doesn't exist: 8/86/86text1.moz-backup
+FINISH REMOVEFILE 7/70/7xtest.exe
+backup_restore: backup file doesn't exist: 7/70/7xtest.exe.moz-backup
+FINISH REMOVEFILE 7/70/7xtext0
+backup_restore: backup file doesn't exist: 7/70/7xtext0.moz-backup
+FINISH REMOVEFILE 7/70/7xtext1
+backup_restore: backup file doesn't exist: 7/70/7xtext1.moz-backup
+FINISH REMOVEFILE 7/71/7xtest.exe
+backup_restore: backup file doesn't exist: 7/71/7xtest.exe.moz-backup
+FINISH REMOVEFILE 7/71/7xtext0
+backup_restore: backup file doesn't exist: 7/71/7xtext0.moz-backup
+FINISH REMOVEFILE 7/71/7xtext1
+backup_restore: backup file doesn't exist: 7/71/7xtext1.moz-backup
+FINISH REMOVEFILE 7/7text0
+backup_restore: backup file doesn't exist: 7/7text0.moz-backup
+FINISH REMOVEFILE 7/7text1
+backup_restore: backup file doesn't exist: 7/7text1.moz-backup
+FINISH REMOVEFILE 5/5text1
+backup_restore: backup file doesn't exist: 5/5text1.moz-backup
+FINISH REMOVEFILE 5/5text0
+backup_restore: backup file doesn't exist: 5/5text0.moz-backup
+FINISH REMOVEFILE 5/5test.exe
+backup_restore: backup file doesn't exist: 5/5test.exe.moz-backup
+FINISH REMOVEFILE 5/5text0
+backup_restore: backup file doesn't exist: 5/5text0.moz-backup
+FINISH REMOVEFILE 5/5text1
+backup_restore: backup file doesn't exist: 5/5text1.moz-backup
+FINISH REMOVEFILE 4/4text1
+backup_restore: backup file doesn't exist: 4/4text1.moz-backup
+FINISH REMOVEFILE 4/4text0
+backup_restore: backup file doesn't exist: 4/4text0.moz-backup
+FINISH REMOVEFILE 3/3text1
+backup_restore: backup file doesn't exist: 3/3text1.moz-backup
+FINISH REMOVEFILE 3/3text0
+backup_restore: backup file doesn't exist: 3/3text0.moz-backup
+failed: 2
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/partial_log_success_mac b/toolkit/mozapps/update/tests/data/partial_log_success_mac
new file mode 100644
index 0000000000..fb5272ad2c
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_log_success_mac
@@ -0,0 +1,279 @@
+UPDATE TYPE partial
+PREPARE ADD Contents/Resources/searchplugins/searchpluginstext0
+PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+PREPARE PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+PREPARE ADD Contents/Resources/precomplete
+PREPARE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+PREPARE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+PREPARE PATCH Contents/Resources/0/0exe0.exe
+PREPARE ADD Contents/Resources/0/00/00text0
+PREPARE PATCH Contents/Resources/0/00/00png0.png
+PREPARE PATCH Contents/MacOS/exe0.exe
+PREPARE ADD Contents/Resources/2/20/20text0
+PREPARE ADD Contents/Resources/2/20/20png0.png
+PREPARE ADD Contents/Resources/0/00/00text2
+PREPARE REMOVEFILE Contents/Resources/1/10/10text0
+PREPARE REMOVEFILE Contents/Resources/0/00/00text1
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/99/
+PREPARE REMOVEDIR Contents/Resources/9/98/
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/970/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/970/
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext0
+PREPARE REMOVEFILE Contents/Resources/9/97/971/97xtext1
+PREPARE REMOVEDIR Contents/Resources/9/97/971/
+PREPARE REMOVEDIR Contents/Resources/9/97/
+PREPARE REMOVEFILE Contents/Resources/9/96/96text0
+PREPARE REMOVEFILE Contents/Resources/9/96/96text1
+PREPARE REMOVEDIR Contents/Resources/9/96/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/95/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/94/
+PREPARE REMOVEDIR Contents/Resources/9/93/
+PREPARE REMOVEDIR Contents/Resources/9/92/
+PREPARE REMOVEDIR Contents/Resources/9/91/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/9/90/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/89/
+PREPARE REMOVEDIR Contents/Resources/8/88/
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/870/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/870/
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext0
+PREPARE REMOVEFILE Contents/Resources/8/87/871/87xtext1
+PREPARE REMOVEDIR Contents/Resources/8/87/871/
+PREPARE REMOVEDIR Contents/Resources/8/87/
+PREPARE REMOVEFILE Contents/Resources/8/86/86text0
+PREPARE REMOVEFILE Contents/Resources/8/86/86text1
+PREPARE REMOVEDIR Contents/Resources/8/86/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/85/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/84/
+PREPARE REMOVEDIR Contents/Resources/8/83/
+PREPARE REMOVEDIR Contents/Resources/8/82/
+PREPARE REMOVEDIR Contents/Resources/8/81/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEDIR Contents/Resources/8/80/
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/70/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/70/
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtest.exe
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext0
+PREPARE REMOVEFILE Contents/Resources/7/71/7xtext1
+PREPARE REMOVEDIR Contents/Resources/7/71/
+PREPARE REMOVEFILE Contents/Resources/7/7text0
+PREPARE REMOVEFILE Contents/Resources/7/7text1
+PREPARE REMOVEDIR Contents/Resources/7/
+PREPARE REMOVEDIR Contents/Resources/6/
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5test.exe
+PREPARE REMOVEFILE Contents/Resources/5/5text0
+PREPARE REMOVEFILE Contents/Resources/5/5text1
+PREPARE REMOVEDIR Contents/Resources/5/
+PREPARE REMOVEFILE Contents/Resources/4/4text1
+PREPARE REMOVEFILE Contents/Resources/4/4text0
+PREPARE REMOVEDIR Contents/Resources/4/
+PREPARE REMOVEFILE Contents/Resources/3/3text1
+PREPARE REMOVEFILE Contents/Resources/3/3text0
+PREPARE REMOVEDIR Contents/Resources/1/10/
+PREPARE REMOVEDIR Contents/Resources/1/
+EXECUTE ADD Contents/Resources/searchplugins/searchpluginstext0
+EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+EXECUTE PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+EXECUTE ADD Contents/Resources/precomplete
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+EXECUTE PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+EXECUTE PATCH Contents/Resources/0/0exe0.exe
+EXECUTE ADD Contents/Resources/0/00/00text0
+EXECUTE PATCH Contents/Resources/0/00/00png0.png
+EXECUTE PATCH Contents/MacOS/exe0.exe
+EXECUTE ADD Contents/Resources/2/20/20text0
+EXECUTE ADD Contents/Resources/2/20/20png0.png
+EXECUTE ADD Contents/Resources/0/00/00text2
+EXECUTE REMOVEFILE Contents/Resources/1/10/10text0
+EXECUTE REMOVEFILE Contents/Resources/0/00/00text1
+EXECUTE REMOVEDIR Contents/Resources/9/99/
+EXECUTE REMOVEDIR Contents/Resources/9/99/
+EXECUTE REMOVEDIR Contents/Resources/9/98/
+EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext0
+EXECUTE REMOVEFILE Contents/Resources/9/97/970/97xtext1
+EXECUTE REMOVEDIR Contents/Resources/9/97/970/
+EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext0
+EXECUTE REMOVEFILE Contents/Resources/9/97/971/97xtext1
+EXECUTE REMOVEDIR Contents/Resources/9/97/971/
+EXECUTE REMOVEDIR Contents/Resources/9/97/
+EXECUTE REMOVEFILE Contents/Resources/9/96/96text0
+EXECUTE REMOVEFILE Contents/Resources/9/96/96text1
+EXECUTE REMOVEDIR Contents/Resources/9/96/
+EXECUTE REMOVEDIR Contents/Resources/9/95/
+EXECUTE REMOVEDIR Contents/Resources/9/95/
+EXECUTE REMOVEDIR Contents/Resources/9/94/
+EXECUTE REMOVEDIR Contents/Resources/9/94/
+EXECUTE REMOVEDIR Contents/Resources/9/93/
+EXECUTE REMOVEDIR Contents/Resources/9/92/
+EXECUTE REMOVEDIR Contents/Resources/9/91/
+EXECUTE REMOVEDIR Contents/Resources/9/90/
+EXECUTE REMOVEDIR Contents/Resources/9/90/
+EXECUTE REMOVEDIR Contents/Resources/8/89/
+EXECUTE REMOVEDIR Contents/Resources/8/89/
+EXECUTE REMOVEDIR Contents/Resources/8/88/
+EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext0
+EXECUTE REMOVEFILE Contents/Resources/8/87/870/87xtext1
+EXECUTE REMOVEDIR Contents/Resources/8/87/870/
+EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext0
+EXECUTE REMOVEFILE Contents/Resources/8/87/871/87xtext1
+EXECUTE REMOVEDIR Contents/Resources/8/87/871/
+EXECUTE REMOVEDIR Contents/Resources/8/87/
+EXECUTE REMOVEFILE Contents/Resources/8/86/86text0
+EXECUTE REMOVEFILE Contents/Resources/8/86/86text1
+EXECUTE REMOVEDIR Contents/Resources/8/86/
+EXECUTE REMOVEDIR Contents/Resources/8/85/
+EXECUTE REMOVEDIR Contents/Resources/8/85/
+EXECUTE REMOVEDIR Contents/Resources/8/84/
+EXECUTE REMOVEDIR Contents/Resources/8/84/
+EXECUTE REMOVEDIR Contents/Resources/8/83/
+EXECUTE REMOVEDIR Contents/Resources/8/82/
+EXECUTE REMOVEDIR Contents/Resources/8/81/
+EXECUTE REMOVEDIR Contents/Resources/8/80/
+EXECUTE REMOVEDIR Contents/Resources/8/80/
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtest.exe
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext0
+EXECUTE REMOVEFILE Contents/Resources/7/70/7xtext1
+EXECUTE REMOVEDIR Contents/Resources/7/70/
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtest.exe
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext0
+EXECUTE REMOVEFILE Contents/Resources/7/71/7xtext1
+EXECUTE REMOVEDIR Contents/Resources/7/71/
+EXECUTE REMOVEFILE Contents/Resources/7/7text0
+EXECUTE REMOVEFILE Contents/Resources/7/7text1
+EXECUTE REMOVEDIR Contents/Resources/7/
+EXECUTE REMOVEDIR Contents/Resources/6/
+EXECUTE REMOVEFILE Contents/Resources/5/5text1
+EXECUTE REMOVEFILE Contents/Resources/5/5text0
+EXECUTE REMOVEFILE Contents/Resources/5/5test.exe
+EXECUTE REMOVEFILE Contents/Resources/5/5text0
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEFILE Contents/Resources/5/5text1
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEDIR Contents/Resources/5/
+EXECUTE REMOVEFILE Contents/Resources/4/4text1
+EXECUTE REMOVEFILE Contents/Resources/4/4text0
+EXECUTE REMOVEDIR Contents/Resources/4/
+EXECUTE REMOVEFILE Contents/Resources/3/3text1
+EXECUTE REMOVEFILE Contents/Resources/3/3text0
+EXECUTE REMOVEDIR Contents/Resources/1/10/
+EXECUTE REMOVEDIR Contents/Resources/1/
+FINISH ADD Contents/Resources/searchplugins/searchpluginstext0
+FINISH PATCH Contents/Resources/searchplugins/searchpluginspng1.png
+FINISH PATCH Contents/Resources/searchplugins/searchpluginspng0.png
+FINISH ADD Contents/Resources/precomplete
+FINISH ADD Contents/Resources/distribution/extensions/extensions1/extensions1text0
+FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png1.png
+FINISH PATCH Contents/Resources/distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD Contents/Resources/distribution/extensions/extensions0/extensions0text0
+FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png1.png
+FINISH PATCH Contents/Resources/distribution/extensions/extensions0/extensions0png0.png
+FINISH PATCH Contents/Resources/0/0exe0.exe
+FINISH ADD Contents/Resources/0/00/00text0
+FINISH PATCH Contents/Resources/0/00/00png0.png
+FINISH PATCH Contents/MacOS/exe0.exe
+FINISH ADD Contents/Resources/2/20/20text0
+FINISH ADD Contents/Resources/2/20/20png0.png
+FINISH ADD Contents/Resources/0/00/00text2
+FINISH REMOVEFILE Contents/Resources/1/10/10text0
+FINISH REMOVEFILE Contents/Resources/0/00/00text1
+FINISH REMOVEDIR Contents/Resources/9/99/
+FINISH REMOVEDIR Contents/Resources/9/99/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/98/
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext0
+FINISH REMOVEFILE Contents/Resources/9/97/970/97xtext1
+FINISH REMOVEDIR Contents/Resources/9/97/970/
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext0
+FINISH REMOVEFILE Contents/Resources/9/97/971/97xtext1
+FINISH REMOVEDIR Contents/Resources/9/97/971/
+FINISH REMOVEDIR Contents/Resources/9/97/
+FINISH REMOVEFILE Contents/Resources/9/96/96text0
+FINISH REMOVEFILE Contents/Resources/9/96/96text1
+FINISH REMOVEDIR Contents/Resources/9/96/
+FINISH REMOVEDIR Contents/Resources/9/95/
+FINISH REMOVEDIR Contents/Resources/9/95/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/94/
+FINISH REMOVEDIR Contents/Resources/9/94/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/9/93/
+FINISH REMOVEDIR Contents/Resources/9/92/
+removing directory: Contents/Resources/9/92/, rv: 0
+FINISH REMOVEDIR Contents/Resources/9/91/
+removing directory: Contents/Resources/9/91/, rv: 0
+FINISH REMOVEDIR Contents/Resources/9/90/
+FINISH REMOVEDIR Contents/Resources/9/90/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/89/
+FINISH REMOVEDIR Contents/Resources/8/89/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/88/
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext0
+FINISH REMOVEFILE Contents/Resources/8/87/870/87xtext1
+FINISH REMOVEDIR Contents/Resources/8/87/870/
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext0
+FINISH REMOVEFILE Contents/Resources/8/87/871/87xtext1
+FINISH REMOVEDIR Contents/Resources/8/87/871/
+FINISH REMOVEDIR Contents/Resources/8/87/
+FINISH REMOVEFILE Contents/Resources/8/86/86text0
+FINISH REMOVEFILE Contents/Resources/8/86/86text1
+FINISH REMOVEDIR Contents/Resources/8/86/
+FINISH REMOVEDIR Contents/Resources/8/85/
+FINISH REMOVEDIR Contents/Resources/8/85/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/84/
+FINISH REMOVEDIR Contents/Resources/8/84/
+directory no longer exists; skipping
+FINISH REMOVEDIR Contents/Resources/8/83/
+FINISH REMOVEDIR Contents/Resources/8/82/
+removing directory: Contents/Resources/8/82/, rv: 0
+FINISH REMOVEDIR Contents/Resources/8/81/
+removing directory: Contents/Resources/8/81/, rv: 0
+FINISH REMOVEDIR Contents/Resources/8/80/
+FINISH REMOVEDIR Contents/Resources/8/80/
+directory no longer exists; skipping
+FINISH REMOVEFILE Contents/Resources/7/70/7xtest.exe
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext0
+FINISH REMOVEFILE Contents/Resources/7/70/7xtext1
+FINISH REMOVEDIR Contents/Resources/7/70/
+FINISH REMOVEFILE Contents/Resources/7/71/7xtest.exe
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext0
+FINISH REMOVEFILE Contents/Resources/7/71/7xtext1
+FINISH REMOVEDIR Contents/Resources/7/71/
+FINISH REMOVEFILE Contents/Resources/7/7text0
+FINISH REMOVEFILE Contents/Resources/7/7text1
+FINISH REMOVEDIR Contents/Resources/7/
+FINISH REMOVEDIR Contents/Resources/6/
+FINISH REMOVEFILE Contents/Resources/5/5text1
+FINISH REMOVEFILE Contents/Resources/5/5text0
+FINISH REMOVEFILE Contents/Resources/5/5test.exe
+FINISH REMOVEDIR Contents/Resources/5/
+FINISH REMOVEFILE Contents/Resources/4/4text1
+FINISH REMOVEFILE Contents/Resources/4/4text0
+FINISH REMOVEDIR Contents/Resources/4/
+FINISH REMOVEFILE Contents/Resources/3/3text1
+FINISH REMOVEFILE Contents/Resources/3/3text0
+FINISH REMOVEDIR Contents/Resources/1/10/
+FINISH REMOVEDIR Contents/Resources/1/
+succeeded
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/partial_log_success_win b/toolkit/mozapps/update/tests/data/partial_log_success_win
new file mode 100644
index 0000000000..1f5c4b3b49
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_log_success_win
@@ -0,0 +1,279 @@
+UPDATE TYPE partial
+PREPARE ADD searchplugins/searchpluginstext0
+PREPARE PATCH searchplugins/searchpluginspng1.png
+PREPARE PATCH searchplugins/searchpluginspng0.png
+PREPARE ADD precomplete
+PREPARE PATCH exe0.exe
+PREPARE ADD distribution/extensions/extensions1/extensions1text0
+PREPARE PATCH distribution/extensions/extensions1/extensions1png1.png
+PREPARE PATCH distribution/extensions/extensions1/extensions1png0.png
+PREPARE ADD distribution/extensions/extensions0/extensions0text0
+PREPARE PATCH distribution/extensions/extensions0/extensions0png1.png
+PREPARE PATCH distribution/extensions/extensions0/extensions0png0.png
+PREPARE PATCH 0/0exe0.exe
+PREPARE ADD 0/00/00text0
+PREPARE PATCH 0/00/00png0.png
+PREPARE ADD 2/20/20text0
+PREPARE ADD 2/20/20png0.png
+PREPARE ADD 0/00/00text2
+PREPARE REMOVEFILE 1/10/10text0
+PREPARE REMOVEFILE 0/00/00text1
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/99/
+PREPARE REMOVEDIR 9/98/
+PREPARE REMOVEFILE 9/97/970/97xtext0
+PREPARE REMOVEFILE 9/97/970/97xtext1
+PREPARE REMOVEDIR 9/97/970/
+PREPARE REMOVEFILE 9/97/971/97xtext0
+PREPARE REMOVEFILE 9/97/971/97xtext1
+PREPARE REMOVEDIR 9/97/971/
+PREPARE REMOVEDIR 9/97/
+PREPARE REMOVEFILE 9/96/96text0
+PREPARE REMOVEFILE 9/96/96text1
+PREPARE REMOVEDIR 9/96/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/95/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/94/
+PREPARE REMOVEDIR 9/93/
+PREPARE REMOVEDIR 9/92/
+PREPARE REMOVEDIR 9/91/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 9/90/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/89/
+PREPARE REMOVEDIR 8/88/
+PREPARE REMOVEFILE 8/87/870/87xtext0
+PREPARE REMOVEFILE 8/87/870/87xtext1
+PREPARE REMOVEDIR 8/87/870/
+PREPARE REMOVEFILE 8/87/871/87xtext0
+PREPARE REMOVEFILE 8/87/871/87xtext1
+PREPARE REMOVEDIR 8/87/871/
+PREPARE REMOVEDIR 8/87/
+PREPARE REMOVEFILE 8/86/86text0
+PREPARE REMOVEFILE 8/86/86text1
+PREPARE REMOVEDIR 8/86/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/85/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/84/
+PREPARE REMOVEDIR 8/83/
+PREPARE REMOVEDIR 8/82/
+PREPARE REMOVEDIR 8/81/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEDIR 8/80/
+PREPARE REMOVEFILE 7/70/7xtest.exe
+PREPARE REMOVEFILE 7/70/7xtext0
+PREPARE REMOVEFILE 7/70/7xtext1
+PREPARE REMOVEDIR 7/70/
+PREPARE REMOVEFILE 7/71/7xtest.exe
+PREPARE REMOVEFILE 7/71/7xtext0
+PREPARE REMOVEFILE 7/71/7xtext1
+PREPARE REMOVEDIR 7/71/
+PREPARE REMOVEFILE 7/7text0
+PREPARE REMOVEFILE 7/7text1
+PREPARE REMOVEDIR 7/
+PREPARE REMOVEDIR 6/
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5test.exe
+PREPARE REMOVEFILE 5/5text0
+PREPARE REMOVEFILE 5/5text1
+PREPARE REMOVEDIR 5/
+PREPARE REMOVEFILE 4/4text1
+PREPARE REMOVEFILE 4/4text0
+PREPARE REMOVEDIR 4/
+PREPARE REMOVEFILE 3/3text1
+PREPARE REMOVEFILE 3/3text0
+PREPARE REMOVEDIR 1/10/
+PREPARE REMOVEDIR 1/
+EXECUTE ADD searchplugins/searchpluginstext0
+EXECUTE PATCH searchplugins/searchpluginspng1.png
+EXECUTE PATCH searchplugins/searchpluginspng0.png
+EXECUTE ADD precomplete
+EXECUTE PATCH exe0.exe
+EXECUTE ADD distribution/extensions/extensions1/extensions1text0
+EXECUTE PATCH distribution/extensions/extensions1/extensions1png1.png
+EXECUTE PATCH distribution/extensions/extensions1/extensions1png0.png
+EXECUTE ADD distribution/extensions/extensions0/extensions0text0
+EXECUTE PATCH distribution/extensions/extensions0/extensions0png1.png
+EXECUTE PATCH distribution/extensions/extensions0/extensions0png0.png
+EXECUTE PATCH 0/0exe0.exe
+EXECUTE ADD 0/00/00text0
+EXECUTE PATCH 0/00/00png0.png
+EXECUTE ADD 2/20/20text0
+EXECUTE ADD 2/20/20png0.png
+EXECUTE ADD 0/00/00text2
+EXECUTE REMOVEFILE 1/10/10text0
+EXECUTE REMOVEFILE 0/00/00text1
+EXECUTE REMOVEDIR 9/99/
+EXECUTE REMOVEDIR 9/99/
+EXECUTE REMOVEDIR 9/98/
+EXECUTE REMOVEFILE 9/97/970/97xtext0
+EXECUTE REMOVEFILE 9/97/970/97xtext1
+EXECUTE REMOVEDIR 9/97/970/
+EXECUTE REMOVEFILE 9/97/971/97xtext0
+EXECUTE REMOVEFILE 9/97/971/97xtext1
+EXECUTE REMOVEDIR 9/97/971/
+EXECUTE REMOVEDIR 9/97/
+EXECUTE REMOVEFILE 9/96/96text0
+EXECUTE REMOVEFILE 9/96/96text1
+EXECUTE REMOVEDIR 9/96/
+EXECUTE REMOVEDIR 9/95/
+EXECUTE REMOVEDIR 9/95/
+EXECUTE REMOVEDIR 9/94/
+EXECUTE REMOVEDIR 9/94/
+EXECUTE REMOVEDIR 9/93/
+EXECUTE REMOVEDIR 9/92/
+EXECUTE REMOVEDIR 9/91/
+EXECUTE REMOVEDIR 9/90/
+EXECUTE REMOVEDIR 9/90/
+EXECUTE REMOVEDIR 8/89/
+EXECUTE REMOVEDIR 8/89/
+EXECUTE REMOVEDIR 8/88/
+EXECUTE REMOVEFILE 8/87/870/87xtext0
+EXECUTE REMOVEFILE 8/87/870/87xtext1
+EXECUTE REMOVEDIR 8/87/870/
+EXECUTE REMOVEFILE 8/87/871/87xtext0
+EXECUTE REMOVEFILE 8/87/871/87xtext1
+EXECUTE REMOVEDIR 8/87/871/
+EXECUTE REMOVEDIR 8/87/
+EXECUTE REMOVEFILE 8/86/86text0
+EXECUTE REMOVEFILE 8/86/86text1
+EXECUTE REMOVEDIR 8/86/
+EXECUTE REMOVEDIR 8/85/
+EXECUTE REMOVEDIR 8/85/
+EXECUTE REMOVEDIR 8/84/
+EXECUTE REMOVEDIR 8/84/
+EXECUTE REMOVEDIR 8/83/
+EXECUTE REMOVEDIR 8/82/
+EXECUTE REMOVEDIR 8/81/
+EXECUTE REMOVEDIR 8/80/
+EXECUTE REMOVEDIR 8/80/
+EXECUTE REMOVEFILE 7/70/7xtest.exe
+EXECUTE REMOVEFILE 7/70/7xtext0
+EXECUTE REMOVEFILE 7/70/7xtext1
+EXECUTE REMOVEDIR 7/70/
+EXECUTE REMOVEFILE 7/71/7xtest.exe
+EXECUTE REMOVEFILE 7/71/7xtext0
+EXECUTE REMOVEFILE 7/71/7xtext1
+EXECUTE REMOVEDIR 7/71/
+EXECUTE REMOVEFILE 7/7text0
+EXECUTE REMOVEFILE 7/7text1
+EXECUTE REMOVEDIR 7/
+EXECUTE REMOVEDIR 6/
+EXECUTE REMOVEFILE 5/5text1
+EXECUTE REMOVEFILE 5/5text0
+EXECUTE REMOVEFILE 5/5test.exe
+EXECUTE REMOVEFILE 5/5text0
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEFILE 5/5text1
+file cannot be removed because it does not exist; skipping
+EXECUTE REMOVEDIR 5/
+EXECUTE REMOVEFILE 4/4text1
+EXECUTE REMOVEFILE 4/4text0
+EXECUTE REMOVEDIR 4/
+EXECUTE REMOVEFILE 3/3text1
+EXECUTE REMOVEFILE 3/3text0
+EXECUTE REMOVEDIR 1/10/
+EXECUTE REMOVEDIR 1/
+FINISH ADD searchplugins/searchpluginstext0
+FINISH PATCH searchplugins/searchpluginspng1.png
+FINISH PATCH searchplugins/searchpluginspng0.png
+FINISH ADD precomplete
+FINISH PATCH exe0.exe
+FINISH ADD distribution/extensions/extensions1/extensions1text0
+FINISH PATCH distribution/extensions/extensions1/extensions1png1.png
+FINISH PATCH distribution/extensions/extensions1/extensions1png0.png
+FINISH ADD distribution/extensions/extensions0/extensions0text0
+FINISH PATCH distribution/extensions/extensions0/extensions0png1.png
+FINISH PATCH distribution/extensions/extensions0/extensions0png0.png
+FINISH PATCH 0/0exe0.exe
+FINISH ADD 0/00/00text0
+FINISH PATCH 0/00/00png0.png
+FINISH ADD 2/20/20text0
+FINISH ADD 2/20/20png0.png
+FINISH ADD 0/00/00text2
+FINISH REMOVEFILE 1/10/10text0
+FINISH REMOVEFILE 0/00/00text1
+FINISH REMOVEDIR 9/99/
+FINISH REMOVEDIR 9/99/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/98/
+FINISH REMOVEFILE 9/97/970/97xtext0
+FINISH REMOVEFILE 9/97/970/97xtext1
+FINISH REMOVEDIR 9/97/970/
+FINISH REMOVEFILE 9/97/971/97xtext0
+FINISH REMOVEFILE 9/97/971/97xtext1
+FINISH REMOVEDIR 9/97/971/
+FINISH REMOVEDIR 9/97/
+FINISH REMOVEFILE 9/96/96text0
+FINISH REMOVEFILE 9/96/96text1
+FINISH REMOVEDIR 9/96/
+FINISH REMOVEDIR 9/95/
+FINISH REMOVEDIR 9/95/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/94/
+FINISH REMOVEDIR 9/94/
+directory no longer exists; skipping
+FINISH REMOVEDIR 9/93/
+FINISH REMOVEDIR 9/92/
+removing directory: 9/92/, rv: 0
+FINISH REMOVEDIR 9/91/
+removing directory: 9/91/, rv: 0
+FINISH REMOVEDIR 9/90/
+FINISH REMOVEDIR 9/90/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/89/
+FINISH REMOVEDIR 8/89/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/88/
+FINISH REMOVEFILE 8/87/870/87xtext0
+FINISH REMOVEFILE 8/87/870/87xtext1
+FINISH REMOVEDIR 8/87/870/
+FINISH REMOVEFILE 8/87/871/87xtext0
+FINISH REMOVEFILE 8/87/871/87xtext1
+FINISH REMOVEDIR 8/87/871/
+FINISH REMOVEDIR 8/87/
+FINISH REMOVEFILE 8/86/86text0
+FINISH REMOVEFILE 8/86/86text1
+FINISH REMOVEDIR 8/86/
+FINISH REMOVEDIR 8/85/
+FINISH REMOVEDIR 8/85/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/84/
+FINISH REMOVEDIR 8/84/
+directory no longer exists; skipping
+FINISH REMOVEDIR 8/83/
+FINISH REMOVEDIR 8/82/
+removing directory: 8/82/, rv: 0
+FINISH REMOVEDIR 8/81/
+removing directory: 8/81/, rv: 0
+FINISH REMOVEDIR 8/80/
+FINISH REMOVEDIR 8/80/
+directory no longer exists; skipping
+FINISH REMOVEFILE 7/70/7xtest.exe
+FINISH REMOVEFILE 7/70/7xtext0
+FINISH REMOVEFILE 7/70/7xtext1
+FINISH REMOVEDIR 7/70/
+FINISH REMOVEFILE 7/71/7xtest.exe
+FINISH REMOVEFILE 7/71/7xtext0
+FINISH REMOVEFILE 7/71/7xtext1
+FINISH REMOVEDIR 7/71/
+FINISH REMOVEFILE 7/7text0
+FINISH REMOVEFILE 7/7text1
+FINISH REMOVEDIR 7/
+FINISH REMOVEDIR 6/
+FINISH REMOVEFILE 5/5text1
+FINISH REMOVEFILE 5/5text0
+FINISH REMOVEFILE 5/5test.exe
+FINISH REMOVEDIR 5/
+FINISH REMOVEFILE 4/4text1
+FINISH REMOVEFILE 4/4text0
+FINISH REMOVEDIR 4/
+FINISH REMOVEFILE 3/3text1
+FINISH REMOVEFILE 3/3text0
+FINISH REMOVEDIR 1/10/
+FINISH REMOVEDIR 1/
+succeeded
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/partial_mac.mar b/toolkit/mozapps/update/tests/data/partial_mac.mar
new file mode 100644
index 0000000000..bcc04b9939
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_mac.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/partial_precomplete b/toolkit/mozapps/update/tests/data/partial_precomplete
new file mode 100644
index 0000000000..3ec201463a
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_precomplete
@@ -0,0 +1,19 @@
+remove "searchplugins/searchpluginstext0"
+remove "searchplugins/searchpluginspng1.png"
+remove "searchplugins/searchpluginspng0.png"
+remove "removed-files"
+remove "precomplete"
+remove "exe0.exe"
+remove "2/20/20text0"
+remove "2/20/20png0.png"
+remove "0/0exe0.exe"
+remove "0/00/00text2"
+remove "0/00/00text0"
+remove "0/00/00png0.png"
+rmdir "searchplugins/"
+rmdir "defaults/pref/"
+rmdir "defaults/"
+rmdir "2/20/"
+rmdir "2/"
+rmdir "0/00/"
+rmdir "0/"
diff --git a/toolkit/mozapps/update/tests/data/partial_precomplete_mac b/toolkit/mozapps/update/tests/data/partial_precomplete_mac
new file mode 100644
index 0000000000..c65b6e4e38
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_precomplete_mac
@@ -0,0 +1,22 @@
+remove "Contents/Resources/searchplugins/searchpluginstext0"
+remove "Contents/Resources/searchplugins/searchpluginspng1.png"
+remove "Contents/Resources/searchplugins/searchpluginspng0.png"
+remove "Contents/Resources/removed-files"
+remove "Contents/Resources/precomplete"
+remove "Contents/Resources/2/20/20text0"
+remove "Contents/Resources/2/20/20png0.png"
+remove "Contents/Resources/0/0exe0.exe"
+remove "Contents/Resources/0/00/00text2"
+remove "Contents/Resources/0/00/00text0"
+remove "Contents/Resources/0/00/00png0.png"
+remove "Contents/MacOS/exe0.exe"
+rmdir "Contents/Resources/searchplugins/"
+rmdir "Contents/Resources/defaults/pref/"
+rmdir "Contents/Resources/defaults/"
+rmdir "Contents/Resources/2/20/"
+rmdir "Contents/Resources/2/"
+rmdir "Contents/Resources/0/00/"
+rmdir "Contents/Resources/0/"
+rmdir "Contents/Resources/"
+rmdir "Contents/MacOS/"
+rmdir "Contents/"
diff --git a/toolkit/mozapps/update/tests/data/partial_removed-files b/toolkit/mozapps/update/tests/data/partial_removed-files
new file mode 100644
index 0000000000..881311b82c
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_removed-files
@@ -0,0 +1,41 @@
+a/b/text0
+a/b/text1
+a/b/3/3text0
+a/b/3/3text1
+a/b/4/4exe0.exe
+a/b/4/4text0
+a/b/4/4text1
+a/b/4/
+a/b/5/5text0
+a/b/5/5text1
+a/b/5/*
+a/b/6/
+a/b/7/*
+a/b/8/80/
+a/b/8/81/
+a/b/8/82/
+a/b/8/83/
+a/b/8/84/
+a/b/8/85/*
+a/b/8/86/*
+a/b/8/87/*
+a/b/8/88/*
+a/b/8/89/*
+a/b/8/80/
+a/b/8/84/*
+a/b/8/85/*
+a/b/8/89/
+a/b/9/90/
+a/b/9/91/
+a/b/9/92/
+a/b/9/93/
+a/b/9/94/
+a/b/9/95/*
+a/b/9/96/*
+a/b/9/97/*
+a/b/9/98/*
+a/b/9/99/*
+a/b/9/90/
+a/b/9/94/*
+a/b/9/95/*
+a/b/9/99/
diff --git a/toolkit/mozapps/update/tests/data/partial_removed-files_mac b/toolkit/mozapps/update/tests/data/partial_removed-files_mac
new file mode 100644
index 0000000000..955dc5b340
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_removed-files_mac
@@ -0,0 +1,41 @@
+Contents/Resources/text0
+Contents/Resources/text1
+Contents/Resources/3/3text0
+Contents/Resources/3/3text1
+Contents/Resources/4/exe0.exe
+Contents/Resources/4/4text0
+Contents/Resources/4/4text1
+Contents/Resources/4/
+Contents/Resources/5/5text0
+Contents/Resources/5/5text1
+Contents/Resources/5/*
+Contents/Resources/6/
+Contents/Resources/7/*
+Contents/Resources/8/80/
+Contents/Resources/8/81/
+Contents/Resources/8/82/
+Contents/Resources/8/83/
+Contents/Resources/8/84/
+Contents/Resources/8/85/*
+Contents/Resources/8/86/*
+Contents/Resources/8/87/*
+Contents/Resources/8/88/*
+Contents/Resources/8/89/*
+Contents/Resources/8/80/
+Contents/Resources/8/84/*
+Contents/Resources/8/85/*
+Contents/Resources/8/89/
+Contents/Resources/9/90/
+Contents/Resources/9/91/
+Contents/Resources/9/92/
+Contents/Resources/9/93/
+Contents/Resources/9/94/
+Contents/Resources/9/95/*
+Contents/Resources/9/96/*
+Contents/Resources/9/97/*
+Contents/Resources/9/98/*
+Contents/Resources/9/99/*
+Contents/Resources/9/90/
+Contents/Resources/9/94/*
+Contents/Resources/9/95/*
+Contents/Resources/9/99/
diff --git a/toolkit/mozapps/update/tests/data/partial_update_manifest b/toolkit/mozapps/update/tests/data/partial_update_manifest
new file mode 100644
index 0000000000..8d4e60ed25
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/partial_update_manifest
@@ -0,0 +1,63 @@
+type "partial"
+add "precomplete"
+add "a/b/searchplugins/searchpluginstext0"
+patch-if "a/b/searchplugins/searchpluginspng1.png" "a/b/searchplugins/searchpluginspng1.png.patch" "a/b/searchplugins/searchpluginspng1.png"
+patch-if "a/b/searchplugins/searchpluginspng0.png" "a/b/searchplugins/searchpluginspng0.png.patch" "a/b/searchplugins/searchpluginspng0.png"
+add-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1text0"
+patch-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1png1.png.patch" "a/b/extensions/extensions1/extensions1png1.png"
+patch-if "a/b/extensions/extensions1" "a/b/extensions/extensions1/extensions1png0.png.patch" "a/b/extensions/extensions1/extensions1png0.png"
+add-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0text0"
+patch-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0png1.png.patch" "a/b/extensions/extensions0/extensions0png1.png"
+patch-if "a/b/extensions/extensions0" "a/b/extensions/extensions0/extensions0png0.png.patch" "a/b/extensions/extensions0/extensions0png0.png"
+patch "a/b/exe0.exe.patch" "a/b/exe0.exe"
+patch "a/b/0/0exe0.exe.patch" "a/b/0/0exe0.exe"
+add "a/b/0/00/00text0"
+patch "a/b/0/00/00png0.png.patch" "a/b/0/00/00png0.png"
+add "a/b/2/20/20text0"
+add "a/b/2/20/20png0.png"
+add "a/b/0/00/00text2"
+remove "a/b/1/10/10text0"
+remove "a/b/0/00/00text1"
+remove "a/b/text1"
+remove "a/b/text0"
+rmrfdir "a/b/9/99/"
+rmdir "a/b/9/99/"
+rmrfdir "a/b/9/98/"
+rmrfdir "a/b/9/97/"
+rmrfdir "a/b/9/96/"
+rmrfdir "a/b/9/95/"
+rmrfdir "a/b/9/95/"
+rmrfdir "a/b/9/94/"
+rmdir "a/b/9/94/"
+rmdir "a/b/9/93/"
+rmdir "a/b/9/92/"
+rmdir "a/b/9/91/"
+rmdir "a/b/9/90/"
+rmdir "a/b/9/90/"
+rmrfdir "a/b/8/89/"
+rmdir "a/b/8/89/"
+rmrfdir "a/b/8/88/"
+rmrfdir "a/b/8/87/"
+rmrfdir "a/b/8/86/"
+rmrfdir "a/b/8/85/"
+rmrfdir "a/b/8/85/"
+rmrfdir "a/b/8/84/"
+rmdir "a/b/8/84/"
+rmdir "a/b/8/83/"
+rmdir "a/b/8/82/"
+rmdir "a/b/8/81/"
+rmdir "a/b/8/80/"
+rmdir "a/b/8/80/"
+rmrfdir "a/b/7/"
+rmdir "a/b/6/"
+remove "a/b/5/5text1"
+remove "a/b/5/5text0"
+rmrfdir "a/b/5/"
+remove "a/b/4/4text1"
+remove "a/b/4/4text0"
+remove "a/b/4/4exe0.exe"
+rmdir "a/b/4/"
+remove "a/b/3/3text1"
+remove "a/b/3/3text0"
+rmdir "a/b/1/10/"
+rmdir "a/b/1/"
diff --git a/toolkit/mozapps/update/tests/data/replace_log_success b/toolkit/mozapps/update/tests/data/replace_log_success
new file mode 100644
index 0000000000..323f1db41e
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/replace_log_success
@@ -0,0 +1,6 @@
+Performing a replace request
+rename_file: proceeding to rename the directory
+rename_file: proceeding to rename the directory
+Now, remove the tmpDir
+succeeded
+calling QuitProgressUI
diff --git a/toolkit/mozapps/update/tests/data/shared.js b/toolkit/mozapps/update/tests/data/shared.js
new file mode 100644
index 0000000000..bca14c8b89
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/shared.js
@@ -0,0 +1,928 @@
+/* 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/. */
+
+/* Shared code for xpcshell, mochitests-chrome, and mochitest-browser-chrome. */
+
+// Definitions needed to run eslint on this file.
+/* global AppConstants, DATA_URI_SPEC, LOG_FUNCTION */
+/* global Services, URL_HOST, TestUtils */
+
+const { FileUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/FileUtils.sys.mjs"
+);
+const { XPCOMUtils } = ChromeUtils.importESModule(
+ "resource://gre/modules/XPCOMUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ UpdateUtils: "resource://gre/modules/UpdateUtils.sys.mjs",
+ ctypes: "resource://gre/modules/ctypes.sys.mjs",
+});
+
+const PREF_APP_UPDATE_AUTO = "app.update.auto";
+const PREF_APP_UPDATE_BACKGROUNDERRORS = "app.update.backgroundErrors";
+const PREF_APP_UPDATE_BACKGROUNDMAXERRORS = "app.update.backgroundMaxErrors";
+const PREF_APP_UPDATE_BADGEWAITTIME = "app.update.badgeWaitTime";
+const PREF_APP_UPDATE_BITS_ENABLED = "app.update.BITS.enabled";
+const PREF_APP_UPDATE_CANCELATIONS = "app.update.cancelations";
+const PREF_APP_UPDATE_CHANNEL = "app.update.channel";
+const PREF_APP_UPDATE_DOWNLOAD_MAXATTEMPTS = "app.update.download.maxAttempts";
+const PREF_APP_UPDATE_DOWNLOAD_ATTEMPTS = "app.update.download.attempts";
+const PREF_APP_UPDATE_DISABLEDFORTESTING = "app.update.disabledForTesting";
+const PREF_APP_UPDATE_INTERVAL = "app.update.interval";
+const PREF_APP_UPDATE_LASTUPDATETIME =
+ "app.update.lastUpdateTime.background-update-timer";
+const PREF_APP_UPDATE_LOG = "app.update.log";
+const PREF_APP_UPDATE_NOTIFYDURINGDOWNLOAD = "app.update.notifyDuringDownload";
+const PREF_APP_UPDATE_PROMPTWAITTIME = "app.update.promptWaitTime";
+const PREF_APP_UPDATE_RETRYTIMEOUT = "app.update.socket.retryTimeout";
+const PREF_APP_UPDATE_SERVICE_ENABLED = "app.update.service.enabled";
+const PREF_APP_UPDATE_SOCKET_MAXERRORS = "app.update.socket.maxErrors";
+const PREF_APP_UPDATE_STAGING_ENABLED = "app.update.staging.enabled";
+const PREF_APP_UPDATE_UNSUPPORTED_URL = "app.update.unsupported.url";
+const PREF_APP_UPDATE_URL_DETAILS = "app.update.url.details";
+const PREF_APP_UPDATE_URL_MANUAL = "app.update.url.manual";
+const PREF_APP_UPDATE_LANGPACK_ENABLED = "app.update.langpack.enabled";
+
+const PREFBRANCH_APP_PARTNER = "app.partner.";
+const PREF_DISTRIBUTION_ID = "distribution.id";
+const PREF_DISTRIBUTION_VERSION = "distribution.version";
+
+const CONFIG_APP_UPDATE_AUTO = "app.update.auto";
+
+const NS_APP_PROFILE_DIR_STARTUP = "ProfDS";
+const NS_APP_USER_PROFILE_50_DIR = "ProfD";
+const NS_GRE_BIN_DIR = "GreBinD";
+const NS_GRE_DIR = "GreD";
+const NS_XPCOM_CURRENT_PROCESS_DIR = "XCurProcD";
+const XRE_EXECUTABLE_FILE = "XREExeF";
+const XRE_OLD_UPDATE_ROOT_DIR = "OldUpdRootD";
+const XRE_UPDATE_ROOT_DIR = "UpdRootD";
+
+const DIR_PATCH = "0";
+const DIR_TOBEDELETED = "tobedeleted";
+const DIR_UPDATES = "updates";
+const DIR_UPDATED =
+ AppConstants.platform == "macosx" ? "Updated.app" : "updated";
+const DIR_DOWNLOADING = "downloading";
+
+const FILE_ACTIVE_UPDATE_XML = "active-update.xml";
+const FILE_ACTIVE_UPDATE_XML_TMP = "active-update.xml.tmp";
+const FILE_APPLICATION_INI = "application.ini";
+const FILE_BACKUP_UPDATE_CONFIG_JSON = "backup-update-config.json";
+const FILE_BACKUP_UPDATE_LOG = "backup-update.log";
+const FILE_BT_RESULT = "bt.result";
+const FILE_LAST_UPDATE_LOG = "last-update.log";
+const FILE_PRECOMPLETE = "precomplete";
+const FILE_PRECOMPLETE_BAK = "precomplete.bak";
+const FILE_UPDATE_CONFIG_JSON = "update-config.json";
+const FILE_UPDATE_LOG = "update.log";
+const FILE_UPDATE_MAR = "update.mar";
+const FILE_UPDATE_SETTINGS_INI = "update-settings.ini";
+const FILE_UPDATE_SETTINGS_INI_BAK = "update-settings.ini.bak";
+const FILE_UPDATE_STATUS = "update.status";
+const FILE_UPDATE_TEST = "update.test";
+const FILE_UPDATE_VERSION = "update.version";
+const FILE_UPDATER_INI = "updater.ini";
+const FILE_UPDATES_XML = "updates.xml";
+const FILE_UPDATES_XML_TMP = "updates.xml.tmp";
+
+const UPDATE_SETTINGS_CONTENTS =
+ "[Settings]\nACCEPTED_MAR_CHANNEL_IDS=xpcshell-test\n";
+const PRECOMPLETE_CONTENTS = 'rmdir "nonexistent_dir/"\n';
+
+const PR_RDWR = 0x04;
+const PR_CREATE_FILE = 0x08;
+const PR_TRUNCATE = 0x20;
+
+var gChannel;
+var gDebugTest = false;
+
+/* import-globals-from sharedUpdateXML.js */
+Services.scriptloader.loadSubScript(DATA_URI_SPEC + "sharedUpdateXML.js", this);
+
+const PERMS_FILE = FileUtils.PERMS_FILE;
+const PERMS_DIRECTORY = FileUtils.PERMS_DIRECTORY;
+
+const MODE_WRONLY = FileUtils.MODE_WRONLY;
+const MODE_CREATE = FileUtils.MODE_CREATE;
+const MODE_APPEND = FileUtils.MODE_APPEND;
+const MODE_TRUNCATE = FileUtils.MODE_TRUNCATE;
+
+const URI_UPDATES_PROPERTIES =
+ "chrome://mozapps/locale/update/updates.properties";
+const gUpdateBundle = Services.strings.createBundle(URI_UPDATES_PROPERTIES);
+
+XPCOMUtils.defineLazyGetter(this, "gAUS", function test_gAUS() {
+ return Cc["@mozilla.org/updates/update-service;1"]
+ .getService(Ci.nsIApplicationUpdateService)
+ .QueryInterface(Ci.nsITimerCallback)
+ .QueryInterface(Ci.nsIObserver);
+});
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gUpdateManager",
+ "@mozilla.org/updates/update-manager;1",
+ "nsIUpdateManager"
+);
+
+XPCOMUtils.defineLazyServiceGetter(
+ this,
+ "gUpdateChecker",
+ "@mozilla.org/updates/update-checker;1",
+ "nsIUpdateChecker"
+);
+
+XPCOMUtils.defineLazyGetter(this, "gDefaultPrefBranch", function test_gDPB() {
+ return Services.prefs.getDefaultBranch(null);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gPrefRoot", function test_gPR() {
+ return Services.prefs.getBranch(null);
+});
+
+/**
+ * Waits for the specified topic and (optionally) status.
+ *
+ * @param topic
+ * String representing the topic to wait for.
+ * @param status (optional)
+ * A string representing the status on said topic to wait for.
+ * @return A promise which will resolve the first time an event occurs on the
+ * specified topic, and (optionally) with the specified status.
+ */
+function waitForEvent(topic, status = null) {
+ return new Promise(resolve =>
+ Services.obs.addObserver(
+ {
+ observe(subject, innerTopic, innerStatus) {
+ if (!status || status == innerStatus) {
+ Services.obs.removeObserver(this, topic);
+ resolve(innerStatus);
+ }
+ },
+ },
+ topic
+ )
+ );
+}
+
+/* Triggers post-update processing */
+function testPostUpdateProcessing() {
+ gAUS.observe(null, "test-post-update-processing", "");
+}
+
+/* Initializes the update service stub */
+function initUpdateServiceStub() {
+ Cc["@mozilla.org/updates/update-service-stub;1"].createInstance(
+ Ci.nsISupports
+ );
+}
+
+/**
+ * Reloads the update xml files.
+ *
+ * @param skipFiles (optional)
+ * If true, the update xml files will not be read and the metadata will
+ * be reset. If false (the default), the update xml files will be read
+ * to populate the update metadata.
+ */
+function reloadUpdateManagerData(skipFiles = false) {
+ let observeData = skipFiles ? "skip-files" : "";
+ gUpdateManager
+ .QueryInterface(Ci.nsIObserver)
+ .observe(null, "um-reload-update-data", observeData);
+}
+
+const observer = {
+ observe(aSubject, aTopic, aData) {
+ if (aTopic == "nsPref:changed" && aData == PREF_APP_UPDATE_CHANNEL) {
+ let channel = gDefaultPrefBranch.getCharPref(PREF_APP_UPDATE_CHANNEL);
+ if (channel != gChannel) {
+ debugDump("Changing channel from " + channel + " to " + gChannel);
+ gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, gChannel);
+ }
+ }
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIObserver"]),
+};
+
+/**
+ * Sets the app.update.channel preference.
+ *
+ * @param aChannel
+ * The update channel.
+ */
+function setUpdateChannel(aChannel) {
+ gChannel = aChannel;
+ debugDump(
+ "setting default pref " + PREF_APP_UPDATE_CHANNEL + " to " + gChannel
+ );
+ gDefaultPrefBranch.setCharPref(PREF_APP_UPDATE_CHANNEL, gChannel);
+ gPrefRoot.addObserver(PREF_APP_UPDATE_CHANNEL, observer);
+}
+
+/**
+ * Sets the effective update url.
+ *
+ * @param aURL
+ * The update url. If not specified 'URL_HOST + "/update.xml"' will be
+ * used.
+ */
+function setUpdateURL(aURL) {
+ let url = aURL ? aURL : URL_HOST + "/update.xml";
+ debugDump("setting update URL to " + url);
+
+ // The Update URL is stored in appinfo. We can replace this process's appinfo
+ // directly, but that will affect only this process. Luckily, the update URL
+ // is only ever read from the update process. This means that replacing
+ // Services.appinfo is sufficient and we don't need to worry about registering
+ // a replacement factory or anything like that.
+ let origAppInfo = Services.appinfo;
+ registerCleanupFunction(() => {
+ Services.appinfo = origAppInfo;
+ });
+
+ // Override the appinfo object with an object that exposes all of the same
+ // properties overriding just the updateURL.
+ let mockAppInfo = Object.create(origAppInfo, {
+ updateURL: {
+ configurable: true,
+ enumerable: true,
+ writable: false,
+ value: url,
+ },
+ });
+
+ Services.appinfo = mockAppInfo;
+}
+
+/**
+ * Writes the updates specified to either the active-update.xml or the
+ * updates.xml.
+ *
+ * @param aContent
+ * The updates represented as a string to write to the XML file.
+ * @param isActiveUpdate
+ * If true this will write to the active-update.xml otherwise it will
+ * write to the updates.xml file.
+ */
+function writeUpdatesToXMLFile(aContent, aIsActiveUpdate) {
+ let file = getUpdateDirFile(
+ aIsActiveUpdate ? FILE_ACTIVE_UPDATE_XML : FILE_UPDATES_XML
+ );
+ writeFile(file, aContent);
+}
+
+/**
+ * Writes the current update operation/state to a file in the patch
+ * directory, indicating to the patching system that operations need
+ * to be performed.
+ *
+ * @param aStatus
+ * The status value to write.
+ */
+function writeStatusFile(aStatus) {
+ let file = getUpdateDirFile(FILE_UPDATE_STATUS);
+ writeFile(file, aStatus + "\n");
+}
+
+/**
+ * Writes the current update version to a file in the patch directory,
+ * indicating to the patching system the version of the update.
+ *
+ * @param aVersion
+ * The version value to write.
+ */
+function writeVersionFile(aVersion) {
+ let file = getUpdateDirFile(FILE_UPDATE_VERSION);
+ writeFile(file, aVersion + "\n");
+}
+
+/**
+ * Writes text to a file. This will replace existing text if the file exists
+ * and create the file if it doesn't exist.
+ *
+ * @param aFile
+ * The file to write to. Will be created if it doesn't exist.
+ * @param aText
+ * The text to write to the file. If there is existing text it will be
+ * replaced.
+ */
+function writeFile(aFile, aText) {
+ let fos = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ if (!aFile.exists()) {
+ aFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ }
+ fos.init(aFile, MODE_WRONLY | MODE_CREATE | MODE_TRUNCATE, PERMS_FILE, 0);
+ fos.write(aText, aText.length);
+ fos.close();
+}
+
+/**
+ * Reads the current update operation/state in the status file in the patch
+ * directory including the error code if it is present.
+ *
+ * @return The status value.
+ */
+function readStatusFile() {
+ let file = getUpdateDirFile(FILE_UPDATE_STATUS);
+ if (!file.exists()) {
+ debugDump("update status file does not exists! Path: " + file.path);
+ return STATE_NONE;
+ }
+ return readFile(file).split("\n")[0];
+}
+
+/**
+ * Reads the current update operation/state in the status file in the patch
+ * directory without the error code if it is present.
+ *
+ * @return The state value.
+ */
+function readStatusState() {
+ return readStatusFile().split(": ")[0];
+}
+
+/**
+ * Reads the current update operation/state in the status file in the patch
+ * directory with the error code.
+ *
+ * @return The state value.
+ */
+function readStatusFailedCode() {
+ return readStatusFile().split(": ")[1];
+}
+
+/**
+ * Returns whether or not applying the current update resulted in an error
+ * verifying binary transparency information.
+ *
+ * @return true if there was an error result and false otherwise
+ */
+function updateHasBinaryTransparencyErrorResult() {
+ let file = getUpdateDirFile(FILE_BT_RESULT);
+ return file.exists();
+}
+
+/**
+ * Reads text from a file and returns the string.
+ *
+ * @param aFile
+ * The file to read from.
+ * @return The string of text read from the file.
+ */
+function readFile(aFile) {
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ if (!aFile.exists()) {
+ return null;
+ }
+ // Specifying -1 for ioFlags will open the file with the default of PR_RDONLY.
+ // Specifying -1 for perm will open the file with the default of 0.
+ fis.init(aFile, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+ let sis = Cc["@mozilla.org/scriptableinputstream;1"].createInstance(
+ Ci.nsIScriptableInputStream
+ );
+ sis.init(fis);
+ let text = sis.read(sis.available());
+ sis.close();
+ return text;
+}
+
+/* Returns human readable status text from the updates.properties bundle */
+function getStatusText(aErrCode) {
+ return getString("check_error-" + aErrCode);
+}
+
+/* Returns a string from the updates.properties bundle */
+function getString(aName) {
+ try {
+ return gUpdateBundle.GetStringFromName(aName);
+ } catch (e) {}
+ return null;
+}
+
+/**
+ * Gets the file extension for an nsIFile.
+ *
+ * @param aFile
+ * The file to get the file extension for.
+ * @return The file extension.
+ */
+function getFileExtension(aFile) {
+ return Services.io.newFileURI(aFile).QueryInterface(Ci.nsIURL).fileExtension;
+}
+
+/**
+ * Gets the specified update file or directory.
+ *
+ * @param aLogLeafName
+ * The leafName of the file or directory to get.
+ * @param aWhichDir
+ * Since we started having a separate patch directory and downloading
+ * directory, there are now files with the same name that can be in
+ * either directory. This argument is optional and defaults to the
+ * patch directory for historical reasons. But if it is specified as
+ * DIR_DOWNLOADING, this function will provide the version of the file
+ * in the downloading directory. For files that aren't in the patch
+ * directory or the downloading directory, this value is ignored.
+ * @return nsIFile for the file or directory.
+ */
+function getUpdateDirFile(aLeafName, aWhichDir = null) {
+ let file = Services.dirsvc.get(XRE_UPDATE_ROOT_DIR, Ci.nsIFile);
+ switch (aLeafName) {
+ case undefined:
+ return file;
+ case DIR_UPDATES:
+ case FILE_ACTIVE_UPDATE_XML:
+ case FILE_ACTIVE_UPDATE_XML_TMP:
+ case FILE_UPDATE_CONFIG_JSON:
+ case FILE_BACKUP_UPDATE_CONFIG_JSON:
+ case FILE_UPDATE_TEST:
+ case FILE_UPDATES_XML:
+ case FILE_UPDATES_XML_TMP:
+ file.append(aLeafName);
+ return file;
+ case DIR_PATCH:
+ case DIR_DOWNLOADING:
+ case FILE_BACKUP_UPDATE_LOG:
+ case FILE_LAST_UPDATE_LOG:
+ file.append(DIR_UPDATES);
+ file.append(aLeafName);
+ return file;
+ case FILE_BT_RESULT:
+ case FILE_UPDATE_LOG:
+ case FILE_UPDATE_MAR:
+ case FILE_UPDATE_STATUS:
+ case FILE_UPDATE_VERSION:
+ case FILE_UPDATER_INI:
+ file.append(DIR_UPDATES);
+ if (aWhichDir == DIR_DOWNLOADING) {
+ file.append(DIR_DOWNLOADING);
+ } else {
+ file.append(DIR_PATCH);
+ }
+ file.append(aLeafName);
+ return file;
+ }
+
+ throw new Error(
+ "The leafName specified is not handled by this function, " +
+ "leafName: " +
+ aLeafName
+ );
+}
+
+/**
+ * Helper function for getting the nsIFile for a file in the directory where the
+ * update will be staged.
+ *
+ * The files for the update are located two directories below the stage
+ * directory since Mac OS X sets the last modified time for the root directory
+ * to the current time and if the update changes any files in the root directory
+ * then it wouldn't be possible to test (bug 600098).
+ *
+ * @param aRelPath (optional)
+ * The relative path to the file or directory to get from the root of
+ * the stage directory. If not specified the stage directory will be
+ * returned.
+ * @return The nsIFile for the file in the directory where the update will be
+ * staged.
+ */
+function getStageDirFile(aRelPath) {
+ let file;
+ if (AppConstants.platform == "macosx") {
+ file = getUpdateDirFile(DIR_PATCH);
+ } else {
+ file = getGREBinDir();
+ }
+ file.append(DIR_UPDATED);
+ if (aRelPath) {
+ let pathParts = aRelPath.split("/");
+ for (let i = 0; i < pathParts.length; i++) {
+ if (pathParts[i]) {
+ file.append(pathParts[i]);
+ }
+ }
+ }
+ return file;
+}
+
+/**
+ * Removes the update files that typically need to be removed by tests without
+ * removing the directories since removing the directories has caused issues
+ * when running tests with --verify and recursively removes the stage directory.
+ *
+ * @param aRemoveLogFiles
+ * When true the update log files will also be removed. This allows
+ * for the inspection of the log files while troubleshooting tests.
+ */
+function removeUpdateFiles(aRemoveLogFiles) {
+ let files = [
+ [FILE_ACTIVE_UPDATE_XML],
+ [FILE_UPDATES_XML],
+ [FILE_BT_RESULT],
+ [FILE_UPDATE_STATUS],
+ [FILE_UPDATE_VERSION],
+ [FILE_UPDATE_MAR],
+ [FILE_UPDATE_MAR, DIR_DOWNLOADING],
+ [FILE_UPDATER_INI],
+ ];
+
+ if (aRemoveLogFiles) {
+ files = files.concat([
+ [FILE_BACKUP_UPDATE_LOG],
+ [FILE_LAST_UPDATE_LOG],
+ [FILE_UPDATE_LOG],
+ ]);
+ }
+
+ for (let i = 0; i < files.length; i++) {
+ let file = getUpdateDirFile.apply(null, files[i]);
+ try {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ } catch (e) {
+ logTestInfo(
+ "Unable to remove file. Path: " + file.path + ", Exception: " + e
+ );
+ }
+ }
+
+ let stageDir = getStageDirFile();
+ if (stageDir.exists()) {
+ try {
+ removeDirRecursive(stageDir);
+ } catch (e) {
+ logTestInfo(
+ "Unable to remove directory. Path: " +
+ stageDir.path +
+ ", Exception: " +
+ e
+ );
+ }
+ }
+}
+
+/**
+ * Deletes a directory and its children. First it tries nsIFile::Remove(true).
+ * If that fails it will fall back to recursing, setting the appropriate
+ * permissions, and deleting the current entry.
+ *
+ * @param aDir
+ * nsIFile for the directory to be deleted.
+ */
+function removeDirRecursive(aDir) {
+ if (!aDir.exists()) {
+ return;
+ }
+
+ if (!aDir.isDirectory()) {
+ throw new Error("Only a directory can be passed to this funtion!");
+ }
+
+ try {
+ debugDump("attempting to remove directory. Path: " + aDir.path);
+ aDir.remove(true);
+ return;
+ } catch (e) {
+ logTestInfo("non-fatal error removing directory. Exception: " + e);
+ }
+
+ let dirEntries = aDir.directoryEntries;
+ while (dirEntries.hasMoreElements()) {
+ let entry = dirEntries.nextFile;
+
+ if (entry.isDirectory()) {
+ removeDirRecursive(entry);
+ } else {
+ entry.permissions = PERMS_FILE;
+ try {
+ debugDump("attempting to remove file. Path: " + entry.path);
+ entry.remove(false);
+ } catch (e) {
+ logTestInfo("error removing file. Exception: " + e);
+ throw e;
+ }
+ }
+ }
+
+ aDir.permissions = PERMS_DIRECTORY;
+ try {
+ debugDump("attempting to remove directory. Path: " + aDir.path);
+ aDir.remove(true);
+ } catch (e) {
+ logTestInfo("error removing directory. Exception: " + e);
+ throw e;
+ }
+}
+
+/**
+ * Returns the directory for the currently running process. This is used to
+ * clean up after the tests and to locate the active-update.xml and updates.xml
+ * files.
+ *
+ * @return nsIFile for the current process directory.
+ */
+function getCurrentProcessDir() {
+ return Services.dirsvc.get(NS_XPCOM_CURRENT_PROCESS_DIR, Ci.nsIFile);
+}
+
+/**
+ * Returns the Gecko Runtime Engine directory where files other than executable
+ * binaries are located. On Mac OS X this will be <bundle>/Contents/Resources/
+ * and the installation directory on all other platforms.
+ *
+ * @return nsIFile for the Gecko Runtime Engine directory.
+ */
+function getGREDir() {
+ return Services.dirsvc.get(NS_GRE_DIR, Ci.nsIFile);
+}
+
+/**
+ * Returns the Gecko Runtime Engine Binary directory where the executable
+ * binaries are located such as the updater binary (Windows and Linux) or
+ * updater package (Mac OS X). On Mac OS X this will be
+ * <bundle>/Contents/MacOS/ and the installation directory on all other
+ * platforms.
+ *
+ * @return nsIFile for the Gecko Runtime Engine Binary directory.
+ */
+function getGREBinDir() {
+ return Services.dirsvc.get(NS_GRE_BIN_DIR, Ci.nsIFile);
+}
+
+/**
+ * Gets the unique mutex name for the installation.
+ *
+ * @return Global mutex path.
+ * @throws If the function is called on a platform other than Windows.
+ */
+function getPerInstallationMutexName() {
+ if (AppConstants.platform != "win") {
+ throw new Error("Windows only function called by a different platform!");
+ }
+
+ let hasher = Cc["@mozilla.org/security/hash;1"].createInstance(
+ Ci.nsICryptoHash
+ );
+ hasher.init(hasher.SHA1);
+
+ let exeFile = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile);
+ let converter = Cc[
+ "@mozilla.org/intl/scriptableunicodeconverter"
+ ].createInstance(Ci.nsIScriptableUnicodeConverter);
+ converter.charset = "UTF-8";
+ let data = converter.convertToByteArray(exeFile.path.toLowerCase());
+
+ hasher.update(data, data.length);
+ return "Global\\MozillaUpdateMutex-" + hasher.finish(true);
+}
+
+/**
+ * Closes a Win32 handle.
+ *
+ * @param aHandle
+ * The handle to close.
+ * @throws If the function is called on a platform other than Windows.
+ */
+function closeHandle(aHandle) {
+ if (AppConstants.platform != "win") {
+ throw new Error("Windows only function called by a different platform!");
+ }
+
+ let lib = ctypes.open("kernel32.dll");
+ let CloseHandle = lib.declare(
+ "CloseHandle",
+ ctypes.winapi_abi,
+ ctypes.int32_t /* success */,
+ ctypes.void_t.ptr
+ ); /* handle */
+ CloseHandle(aHandle);
+ lib.close();
+}
+
+/**
+ * Creates a mutex.
+ *
+ * @param aName
+ * The name for the mutex.
+ * @return The Win32 handle to the mutex.
+ * @throws If the function is called on a platform other than Windows.
+ */
+function createMutex(aName) {
+ if (AppConstants.platform != "win") {
+ throw new Error("Windows only function called by a different platform!");
+ }
+
+ const INITIAL_OWN = 1;
+ const ERROR_ALREADY_EXISTS = 0xb7;
+ let lib = ctypes.open("kernel32.dll");
+ let CreateMutexW = lib.declare(
+ "CreateMutexW",
+ ctypes.winapi_abi,
+ ctypes.void_t.ptr /* return handle */,
+ ctypes.void_t.ptr /* security attributes */,
+ ctypes.int32_t /* initial owner */,
+ ctypes.char16_t.ptr
+ ); /* name */
+
+ let handle = CreateMutexW(null, INITIAL_OWN, aName);
+ lib.close();
+ let alreadyExists = ctypes.winLastError == ERROR_ALREADY_EXISTS;
+ if (handle && !handle.isNull() && alreadyExists) {
+ closeHandle(handle);
+ handle = null;
+ }
+
+ if (handle && handle.isNull()) {
+ handle = null;
+ }
+
+ return handle;
+}
+
+/**
+ * Synchronously writes the value of the app.update.auto setting to the update
+ * configuration file on Windows or to a user preference on other platforms.
+ * When the value passed to this function is null or undefined it will remove
+ * the configuration file on Windows or the user preference on other platforms.
+ *
+ * @param aEnabled
+ * Possible values are true, false, null, and undefined. When true or
+ * false this value will be written for app.update.auto in the update
+ * configuration file on Windows or to the user preference on other
+ * platforms. When null or undefined the update configuration file will
+ * be removed on Windows or the user preference will be removed on other
+ * platforms.
+ */
+function setAppUpdateAutoSync(aEnabled) {
+ if (AppConstants.platform == "win") {
+ let file = getUpdateDirFile(FILE_UPDATE_CONFIG_JSON);
+ if (aEnabled === undefined || aEnabled === null) {
+ if (file.exists()) {
+ file.remove(false);
+ }
+ } else {
+ writeFile(
+ file,
+ '{"' + CONFIG_APP_UPDATE_AUTO + '":' + aEnabled.toString() + "}"
+ );
+ }
+ } else if (aEnabled === undefined || aEnabled === null) {
+ if (Services.prefs.prefHasUserValue(PREF_APP_UPDATE_AUTO)) {
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_AUTO);
+ }
+ } else {
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_AUTO, aEnabled);
+ }
+}
+
+/**
+ * Logs TEST-INFO messages.
+ *
+ * @param aText
+ * The text to log.
+ * @param aCaller (optional)
+ * An optional Components.stack.caller. If not specified
+ * Components.stack.caller will be used.
+ */
+function logTestInfo(aText, aCaller) {
+ let caller = aCaller ? aCaller : Components.stack.caller;
+ let now = new Date();
+ let hh = now.getHours();
+ let mm = now.getMinutes();
+ let ss = now.getSeconds();
+ let ms = now.getMilliseconds();
+ let time =
+ (hh < 10 ? "0" + hh : hh) +
+ ":" +
+ (mm < 10 ? "0" + mm : mm) +
+ ":" +
+ (ss < 10 ? "0" + ss : ss) +
+ ":";
+ if (ms < 10) {
+ time += "00";
+ } else if (ms < 100) {
+ time += "0";
+ }
+ time += ms;
+ let msg =
+ time +
+ " | TEST-INFO | " +
+ caller.filename +
+ " | [" +
+ caller.name +
+ " : " +
+ caller.lineNumber +
+ "] " +
+ aText;
+ LOG_FUNCTION(msg);
+}
+
+/**
+ * Logs TEST-INFO messages when gDebugTest evaluates to true.
+ *
+ * @param aText
+ * The text to log.
+ * @param aCaller (optional)
+ * An optional Components.stack.caller. If not specified
+ * Components.stack.caller will be used.
+ */
+function debugDump(aText, aCaller) {
+ if (gDebugTest) {
+ let caller = aCaller ? aCaller : Components.stack.caller;
+ logTestInfo(aText, caller);
+ }
+}
+
+/**
+ * Creates the continue file used to signal that update staging or the mock http
+ * server should continue. The delay this creates allows the tests to verify the
+ * user interfaces before they auto advance to other phases of an update. The
+ * continue file for staging will be deleted by the test updater and the
+ * continue file for the update check and update download requests will be
+ * deleted by the test http server handler implemented in app_update.sjs. The
+ * test returns a promise so the test can wait on the deletion of the continue
+ * file when necessary. If the continue file still exists at the end of a test
+ * it will be removed to prevent it from affecting tests that run after the test
+ * that created it.
+ *
+ * @param leafName
+ * The leafName of the file to create. This should be one of the
+ * folowing constants that are defined in testConstants.js:
+ * CONTINUE_CHECK
+ * CONTINUE_DOWNLOAD
+ * CONTINUE_STAGING
+ * @return Promise
+ * Resolves when the file is deleted or if the file is not deleted when
+ * the check for the file's existence times out. If the file isn't
+ * deleted before the check for the file's existence times out it will
+ * be deleted when the test ends so it doesn't affect tests that run
+ * after the test that created the continue file.
+ * @throws If the file already exists.
+ */
+async function continueFileHandler(leafName) {
+ // The total time to wait with 300 retries and the default interval of 100 is
+ // approximately 30 seconds.
+ let interval = 100;
+ let retries = 300;
+ let continueFile;
+ if (leafName == CONTINUE_STAGING) {
+ // The total time to wait with 600 retries and an interval of 200 is
+ // approximately 120 seconds.
+ interval = 200;
+ retries = 600;
+ continueFile = getGREBinDir();
+ if (AppConstants.platform == "macosx") {
+ continueFile = continueFile.parent.parent;
+ }
+ continueFile.append(leafName);
+ } else {
+ continueFile = Services.dirsvc.get("CurWorkD", Ci.nsIFile);
+ let continuePath = REL_PATH_DATA + leafName;
+ let continuePathParts = continuePath.split("/");
+ for (let i = 0; i < continuePathParts.length; ++i) {
+ continueFile.append(continuePathParts[i]);
+ }
+ }
+ if (continueFile.exists()) {
+ logTestInfo(
+ "The continue file should not exist, path: " + continueFile.path
+ );
+ continueFile.remove(false);
+ }
+ debugDump("Creating continue file, path: " + continueFile.path);
+ continueFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ // If for whatever reason the continue file hasn't been removed when a test
+ // has finished remove it during cleanup so it doesn't affect tests that run
+ // after the test that created it.
+ registerCleanupFunction(() => {
+ if (continueFile.exists()) {
+ logTestInfo(
+ "Removing continue file during test cleanup, path: " + continueFile.path
+ );
+ continueFile.remove(false);
+ }
+ });
+ return TestUtils.waitForCondition(
+ () => !continueFile.exists(),
+ "Waiting for file to be deleted, path: " + continueFile.path,
+ interval,
+ retries
+ ).catch(e => {
+ logTestInfo(
+ "Continue file was not removed after checking " +
+ retries +
+ " times, path: " +
+ continueFile.path
+ );
+ });
+}
diff --git a/toolkit/mozapps/update/tests/data/sharedUpdateXML.js b/toolkit/mozapps/update/tests/data/sharedUpdateXML.js
new file mode 100644
index 0000000000..acff4aec3f
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/sharedUpdateXML.js
@@ -0,0 +1,417 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+/**
+ * Shared code for xpcshell, mochitests-chrome, mochitest-browser-chrome, and
+ * SJS server-side scripts for the test http server.
+ */
+
+/**
+ * Helper functions for creating xml strings used by application update tests.
+ */
+
+/* import-globals-from ../browser/testConstants.js */
+
+/* global Services, UpdateUtils, gURLData */
+
+const FILE_SIMPLE_MAR = "simple.mar";
+const SIZE_SIMPLE_MAR = "1419";
+
+const STATE_NONE = "null";
+const STATE_DOWNLOADING = "downloading";
+const STATE_PENDING = "pending";
+const STATE_PENDING_SVC = "pending-service";
+const STATE_PENDING_ELEVATE = "pending-elevate";
+const STATE_APPLYING = "applying";
+const STATE_APPLIED = "applied";
+const STATE_APPLIED_SVC = "applied-service";
+const STATE_SUCCEEDED = "succeeded";
+const STATE_DOWNLOAD_FAILED = "download-failed";
+const STATE_FAILED = "failed";
+
+const LOADSOURCE_ERROR_WRONG_SIZE = 2;
+const CRC_ERROR = 4;
+const READ_ERROR = 6;
+const WRITE_ERROR = 7;
+const MAR_CHANNEL_MISMATCH_ERROR = 22;
+const VERSION_DOWNGRADE_ERROR = 23;
+const UPDATE_SETTINGS_FILE_CHANNEL = 38;
+const SERVICE_COULD_NOT_COPY_UPDATER = 49;
+const SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR = 52;
+const SERVICE_INVALID_APPLYTO_DIR_ERROR = 54;
+const SERVICE_INVALID_INSTALL_DIR_PATH_ERROR = 55;
+const SERVICE_INVALID_WORKING_DIR_PATH_ERROR = 56;
+const INVALID_APPLYTO_DIR_STAGED_ERROR = 72;
+const INVALID_APPLYTO_DIR_ERROR = 74;
+const INVALID_INSTALL_DIR_PATH_ERROR = 75;
+const INVALID_WORKING_DIR_PATH_ERROR = 76;
+const INVALID_CALLBACK_PATH_ERROR = 77;
+const INVALID_CALLBACK_DIR_ERROR = 78;
+
+// Error codes 80 through 99 are reserved for nsUpdateService.js and are not
+// defined in common/updatererrors.h
+const ERR_OLDER_VERSION_OR_SAME_BUILD = 90;
+const ERR_UPDATE_STATE_NONE = 91;
+const ERR_CHANNEL_CHANGE = 92;
+
+const WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION = 106;
+
+const STATE_FAILED_DELIMETER = ": ";
+
+const STATE_FAILED_LOADSOURCE_ERROR_WRONG_SIZE =
+ STATE_FAILED + STATE_FAILED_DELIMETER + LOADSOURCE_ERROR_WRONG_SIZE;
+const STATE_FAILED_CRC_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + CRC_ERROR;
+const STATE_FAILED_READ_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + READ_ERROR;
+const STATE_FAILED_WRITE_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + WRITE_ERROR;
+const STATE_FAILED_MAR_CHANNEL_MISMATCH_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + MAR_CHANNEL_MISMATCH_ERROR;
+const STATE_FAILED_VERSION_DOWNGRADE_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + VERSION_DOWNGRADE_ERROR;
+const STATE_FAILED_UPDATE_SETTINGS_FILE_CHANNEL =
+ STATE_FAILED + STATE_FAILED_DELIMETER + UPDATE_SETTINGS_FILE_CHANNEL;
+const STATE_FAILED_SERVICE_COULD_NOT_COPY_UPDATER =
+ STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_COULD_NOT_COPY_UPDATER;
+const STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR =
+ STATE_FAILED +
+ STATE_FAILED_DELIMETER +
+ SERVICE_INVALID_APPLYTO_DIR_STAGED_ERROR;
+const STATE_FAILED_SERVICE_INVALID_APPLYTO_DIR_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + SERVICE_INVALID_APPLYTO_DIR_ERROR;
+const STATE_FAILED_SERVICE_INVALID_INSTALL_DIR_PATH_ERROR =
+ STATE_FAILED +
+ STATE_FAILED_DELIMETER +
+ SERVICE_INVALID_INSTALL_DIR_PATH_ERROR;
+const STATE_FAILED_SERVICE_INVALID_WORKING_DIR_PATH_ERROR =
+ STATE_FAILED +
+ STATE_FAILED_DELIMETER +
+ SERVICE_INVALID_WORKING_DIR_PATH_ERROR;
+const STATE_FAILED_INVALID_APPLYTO_DIR_STAGED_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_STAGED_ERROR;
+const STATE_FAILED_INVALID_APPLYTO_DIR_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_APPLYTO_DIR_ERROR;
+const STATE_FAILED_INVALID_INSTALL_DIR_PATH_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_INSTALL_DIR_PATH_ERROR;
+const STATE_FAILED_INVALID_WORKING_DIR_PATH_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_WORKING_DIR_PATH_ERROR;
+const STATE_FAILED_INVALID_CALLBACK_PATH_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_CALLBACK_PATH_ERROR;
+const STATE_FAILED_INVALID_CALLBACK_DIR_ERROR =
+ STATE_FAILED + STATE_FAILED_DELIMETER + INVALID_CALLBACK_DIR_ERROR;
+const STATE_FAILED_WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION =
+ STATE_FAILED +
+ STATE_FAILED_DELIMETER +
+ WRITE_ERROR_BACKGROUND_TASK_SHARING_VIOLATION;
+
+const DEFAULT_UPDATE_VERSION = "999999.0";
+
+/**
+ * Constructs a string representing a remote update xml file.
+ *
+ * @param aUpdates
+ * The string representing the update elements.
+ * @return The string representing a remote update xml file.
+ */
+function getRemoteUpdatesXMLString(aUpdates) {
+ return '<?xml version="1.0"?><updates>' + aUpdates + "</updates>";
+}
+
+/**
+ * Constructs a string representing an update element for a remote update xml
+ * file. See getUpdateString for parameter information not provided below.
+ *
+ * @param aUpdateProps
+ * An object containing non default test values for an nsIUpdate.
+ * See updateProps names below for possible object names.
+ * @param aPatches
+ * String representing the application update patches.
+ * @return The string representing an update element for an update xml file.
+ */
+function getRemoteUpdateString(aUpdateProps, aPatches) {
+ const updateProps = {
+ appVersion: DEFAULT_UPDATE_VERSION,
+ buildID: "20080811053724",
+ custom1: null,
+ custom2: null,
+ detailsURL: URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS",
+ displayVersion: null,
+ name: "App Update Test",
+ promptWaitTime: null,
+ type: "major",
+ };
+
+ for (let name in aUpdateProps) {
+ updateProps[name] = aUpdateProps[name];
+ }
+
+ // To test that text nodes are handled properly the string returned contains
+ // spaces and newlines.
+ return getUpdateString(updateProps) + ">\n " + aPatches + "\n</update>\n";
+}
+
+/**
+ * Constructs a string representing a patch element for a remote update xml
+ * file. See getPatchString for parameter information not provided below.
+ *
+ * @param aPatchProps (optional)
+ * An object containing non default test values for an nsIUpdatePatch.
+ * See patchProps below for possible object names.
+ * @return The string representing a patch element for a remote update xml file.
+ */
+function getRemotePatchString(aPatchProps) {
+ const patchProps = {
+ type: "complete",
+ _url: null,
+ get url() {
+ if (this._url) {
+ return this._url;
+ }
+ if (gURLData) {
+ return gURLData + FILE_SIMPLE_MAR;
+ }
+ return null;
+ },
+ set url(val) {
+ this._url = val;
+ },
+ custom1: null,
+ custom2: null,
+ size: SIZE_SIMPLE_MAR,
+ };
+
+ for (let name in aPatchProps) {
+ patchProps[name] = aPatchProps[name];
+ }
+
+ return getPatchString(patchProps) + "/>";
+}
+
+/**
+ * Constructs a string representing a local update xml file.
+ *
+ * @param aUpdates
+ * The string representing the update elements.
+ * @return The string representing a local update xml file.
+ */
+function getLocalUpdatesXMLString(aUpdates) {
+ if (!aUpdates || aUpdates == "") {
+ return '<updates xmlns="http://www.mozilla.org/2005/app-update"/>';
+ }
+ return (
+ '<updates xmlns="http://www.mozilla.org/2005/app-update">' +
+ aUpdates +
+ "</updates>"
+ );
+}
+
+/**
+ * Constructs a string representing an update element for a local update xml
+ * file. See getUpdateString for parameter information not provided below.
+ *
+ * @param aUpdateProps
+ * An object containing non default test values for an nsIUpdate.
+ * See updateProps names below for possible object names.
+ * @param aPatches
+ * String representing the application update patches.
+ * @return The string representing an update element for an update xml file.
+ */
+function getLocalUpdateString(aUpdateProps, aPatches) {
+ const updateProps = {
+ _appVersion: null,
+ get appVersion() {
+ if (this._appVersion) {
+ return this._appVersion;
+ }
+ if (Services && Services.appinfo && Services.appinfo.version) {
+ return Services.appinfo.version;
+ }
+ return DEFAULT_UPDATE_VERSION;
+ },
+ set appVersion(val) {
+ this._appVersion = val;
+ },
+ buildID: "20080811053724",
+ channel: UpdateUtils ? UpdateUtils.getUpdateChannel() : "default",
+ custom1: null,
+ custom2: null,
+ detailsURL: URL_HTTP_UPDATE_SJS + "?uiURL=DETAILS",
+ displayVersion: null,
+ foregroundDownload: "true",
+ installDate: "1238441400314",
+ isCompleteUpdate: "true",
+ name: "App Update Test",
+ previousAppVersion: null,
+ promptWaitTime: null,
+ serviceURL: "http://test_service/",
+ statusText: "Install Pending",
+ type: "major",
+ };
+
+ for (let name in aUpdateProps) {
+ updateProps[name] = aUpdateProps[name];
+ }
+
+ let checkInterval = updateProps.checkInterval
+ ? 'checkInterval="' + updateProps.checkInterval + '" '
+ : "";
+ let channel = 'channel="' + updateProps.channel + '" ';
+ let isCompleteUpdate =
+ 'isCompleteUpdate="' + updateProps.isCompleteUpdate + '" ';
+ let foregroundDownload = updateProps.foregroundDownload
+ ? 'foregroundDownload="' + updateProps.foregroundDownload + '" '
+ : "";
+ let installDate = 'installDate="' + updateProps.installDate + '" ';
+ let previousAppVersion = updateProps.previousAppVersion
+ ? 'previousAppVersion="' + updateProps.previousAppVersion + '" '
+ : "";
+ let statusText = updateProps.statusText
+ ? 'statusText="' + updateProps.statusText + '" '
+ : "";
+ let serviceURL = 'serviceURL="' + updateProps.serviceURL + '">';
+
+ return (
+ getUpdateString(updateProps) +
+ " " +
+ checkInterval +
+ channel +
+ isCompleteUpdate +
+ foregroundDownload +
+ installDate +
+ previousAppVersion +
+ statusText +
+ serviceURL +
+ aPatches +
+ "</update>"
+ );
+}
+
+/**
+ * Constructs a string representing a patch element for a local update xml file.
+ * See getPatchString for parameter information not provided below.
+ *
+ * @param aPatchProps (optional)
+ * An object containing non default test values for an nsIUpdatePatch.
+ * See patchProps below for possible object names.
+ * @return The string representing a patch element for a local update xml file.
+ */
+function getLocalPatchString(aPatchProps) {
+ const patchProps = {
+ type: "complete",
+ url: gURLData + FILE_SIMPLE_MAR,
+ size: SIZE_SIMPLE_MAR,
+ custom1: null,
+ custom2: null,
+ selected: "true",
+ state: STATE_SUCCEEDED,
+ };
+
+ for (let name in aPatchProps) {
+ patchProps[name] = aPatchProps[name];
+ }
+
+ let selected = 'selected="' + patchProps.selected + '" ';
+ let state = 'state="' + patchProps.state + '"/>';
+ return getPatchString(patchProps) + " " + selected + state;
+}
+
+/**
+ * Constructs a string representing an update element for a remote update xml
+ * file.
+ *
+ * @param aUpdateProps (optional)
+ * An object containing non default test values for an nsIUpdate.
+ * See the aUpdateProps property names below for possible object names.
+ * @return The string representing an update element for an update xml file.
+ */
+function getUpdateString(aUpdateProps) {
+ let type = 'type="' + aUpdateProps.type + '" ';
+ let name = 'name="' + aUpdateProps.name + '" ';
+ let displayVersion = aUpdateProps.displayVersion
+ ? 'displayVersion="' + aUpdateProps.displayVersion + '" '
+ : "";
+ let appVersion = 'appVersion="' + aUpdateProps.appVersion + '" ';
+ // Not specifying a detailsURL will cause a leak due to bug 470244
+ let detailsURL = 'detailsURL="' + aUpdateProps.detailsURL + '" ';
+ let promptWaitTime = aUpdateProps.promptWaitTime
+ ? 'promptWaitTime="' + aUpdateProps.promptWaitTime + '" '
+ : "";
+ let disableBITS = aUpdateProps.disableBITS
+ ? 'disableBITS="' + aUpdateProps.disableBITS + '" '
+ : "";
+ let disableBackgroundUpdates = aUpdateProps.disableBackgroundUpdates
+ ? 'disableBackgroundUpdates="' +
+ aUpdateProps.disableBackgroundUpdates +
+ '" '
+ : "";
+ let custom1 = aUpdateProps.custom1 ? aUpdateProps.custom1 + " " : "";
+ let custom2 = aUpdateProps.custom2 ? aUpdateProps.custom2 + " " : "";
+ let buildID = 'buildID="' + aUpdateProps.buildID + '"';
+
+ return (
+ "<update " +
+ type +
+ name +
+ displayVersion +
+ appVersion +
+ detailsURL +
+ promptWaitTime +
+ disableBITS +
+ disableBackgroundUpdates +
+ custom1 +
+ custom2 +
+ buildID
+ );
+}
+
+/**
+ * Constructs a string representing a patch element for an update xml file.
+ *
+ * @param aPatchProps (optional)
+ * An object containing non default test values for an nsIUpdatePatch.
+ * See the patchProps property names below for possible object names.
+ * @return The string representing a patch element for an update xml file.
+ */
+function getPatchString(aPatchProps) {
+ let type = 'type="' + aPatchProps.type + '" ';
+ let url = 'URL="' + aPatchProps.url + '" ';
+ let size = 'size="' + aPatchProps.size + '"';
+ let custom1 = aPatchProps.custom1 ? aPatchProps.custom1 + " " : "";
+ let custom2 = aPatchProps.custom2 ? aPatchProps.custom2 + " " : "";
+ return "<patch " + type + url + custom1 + custom2 + size;
+}
+
+/**
+ * Reads the binary contents of a file and returns it as a string.
+ *
+ * @param aFile
+ * The file to read from.
+ * @return The contents of the file as a string.
+ */
+function readFileBytes(aFile) {
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ // Specifying -1 for ioFlags will open the file with the default of PR_RDONLY.
+ // Specifying -1 for perm will open the file with the default of 0.
+ fis.init(aFile, -1, -1, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+ let bis = Cc["@mozilla.org/binaryinputstream;1"].createInstance(
+ Ci.nsIBinaryInputStream
+ );
+ bis.setInputStream(fis);
+ let data = [];
+ let count = fis.available();
+ while (count > 0) {
+ let bytes = bis.readByteArray(Math.min(65535, count));
+ data.push(String.fromCharCode.apply(null, bytes));
+ count -= bytes.length;
+ if (!bytes.length) {
+ throw new Error("Nothing read from input stream!");
+ }
+ }
+ data = data.join("");
+ fis.close();
+ return data.toString();
+}
diff --git a/toolkit/mozapps/update/tests/data/simple.mar b/toolkit/mozapps/update/tests/data/simple.mar
new file mode 100644
index 0000000000..fd635b46bd
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/simple.mar
Binary files differ
diff --git a/toolkit/mozapps/update/tests/data/syncManagerTestChild.js b/toolkit/mozapps/update/tests/data/syncManagerTestChild.js
new file mode 100644
index 0000000000..1f52554e7c
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/syncManagerTestChild.js
@@ -0,0 +1,55 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/
+ */
+
+// This is the script that runs in the child xpcshell process for the test
+// unit_aus_update/updateSyncManager.js.
+// The main thing this script does is override the child's directory service
+// so that it ends up with the same fake binary path that the parent test runner
+// has opened its update lock with.
+// This requires that we have already been passed a constant on our command
+// line which contains the relevant fake binary path, which is called:
+/* global customExePath */
+
+print("child process is running");
+
+// This function is copied from xpcshellUtilsAUS.js so that we can have our
+// xpcshell subprocess call it without having to load that whole file, because
+// it turns out that needs a bunch of infrastructure that normally the testing
+// framework would provide, and that also requires a bunch of setup, and it's
+// just not worth all that. This is a cut down version that only includes the
+// directory provider functionality that the subprocess really needs.
+function adjustGeneralPaths() {
+ let dirProvider = {
+ getFile: function AGP_DP_getFile(aProp, aPersistent) {
+ // Set the value of persistent to false so when this directory provider is
+ // unregistered it will revert back to the original provider.
+ aPersistent.value = false;
+ // The sync manager only needs XREExeF, so that's all we provide.
+ if (aProp == "XREExeF") {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(customExePath);
+ return file;
+ }
+ return null;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]),
+ };
+ let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService);
+ ds.QueryInterface(Ci.nsIProperties).undefine("XREExeF");
+ ds.registerProvider(dirProvider);
+
+ // Now that we've overridden the directory provider, the name of the update
+ // lock needs to be changed to match the overridden path.
+ let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService(
+ Ci.nsIUpdateSyncManager
+ );
+ syncManager.resetLock();
+}
+
+adjustGeneralPaths();
+
+// Wait a few seconds for the parent to do what it needs to do, then exit.
+print("child process should now have the lock; will exit in 5 seconds");
+simulateNoScriptActivity(5);
+print("child process exiting now");
diff --git a/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js b/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js
new file mode 100644
index 0000000000..90880d96d9
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/xpcshellConstantsPP.js
@@ -0,0 +1,29 @@
+/* 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/. */
+
+#filter substitution
+
+/* Preprocessed constants used by xpcshell tests */
+
+// MOZ_APP_VENDOR is optional.
+#ifdef MOZ_APP_VENDOR
+const MOZ_APP_VENDOR = "@MOZ_APP_VENDOR@";
+#else
+const MOZ_APP_VENDOR = "";
+#endif
+
+// MOZ_APP_BASENAME is not optional for tests.
+const MOZ_APP_BASENAME = "@MOZ_APP_BASENAME@";
+
+#ifdef MOZ_VERIFY_MAR_SIGNATURE
+const MOZ_VERIFY_MAR_SIGNATURE = true;
+#else
+const MOZ_VERIFY_MAR_SIGNATURE = false;
+#endif
+
+#ifdef DISABLE_UPDATER_AUTHENTICODE_CHECK
+ const IS_AUTHENTICODE_CHECK_ENABLED = false;
+#else
+ const IS_AUTHENTICODE_CHECK_ENABLED = true;
+#endif
diff --git a/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
new file mode 100644
index 0000000000..111f0b1e8c
--- /dev/null
+++ b/toolkit/mozapps/update/tests/data/xpcshellUtilsAUS.js
@@ -0,0 +1,4803 @@
+/* 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/. */
+
+/**
+ * Test log warnings that happen before the test has started
+ * "Couldn't get the user appdata directory. Crash events may not be produced."
+ * in nsExceptionHandler.cpp (possibly bug 619104)
+ *
+ * Test log warnings that happen after the test has finished
+ * "OOPDeinit() without successful OOPInit()" in nsExceptionHandler.cpp
+ * (bug 619104)
+ * "XPCOM objects created/destroyed from static ctor/dtor" in nsTraceRefcnt.cpp
+ * (possibly bug 457479)
+ *
+ * Other warnings printed to the test logs
+ * "site security information will not be persisted" in
+ * nsSiteSecurityService.cpp and the error in nsSystemInfo.cpp preceding this
+ * error are due to not having a profile when running some of the xpcshell
+ * tests. Since most xpcshell tests also log these errors these tests don't
+ * call do_get_profile unless necessary for the test.
+ * "!mMainThread" in nsThreadManager.cpp are due to using timers and it might be
+ * possible to fix some or all of these in the test itself.
+ * "NS_FAILED(rv)" in nsThreadUtils.cpp are due to using timers and it might be
+ * possible to fix some or all of these in the test itself.
+ */
+
+"use strict";
+
+const { AppConstants } = ChromeUtils.importESModule(
+ "resource://gre/modules/AppConstants.sys.mjs"
+);
+const { TestUtils } = ChromeUtils.importESModule(
+ "resource://testing-common/TestUtils.sys.mjs"
+);
+
+ChromeUtils.defineESModuleGetters(this, {
+ MockRegistrar: "resource://testing-common/MockRegistrar.sys.mjs",
+ updateAppInfo: "resource://testing-common/AppInfo.sys.mjs",
+});
+
+const Cm = Components.manager;
+
+/* global MOZ_APP_VENDOR, MOZ_APP_BASENAME */
+/* global MOZ_VERIFY_MAR_SIGNATURE, IS_AUTHENTICODE_CHECK_ENABLED */
+load("../data/xpcshellConstantsPP.js");
+
+const DIR_MACOS = AppConstants.platform == "macosx" ? "Contents/MacOS/" : "";
+const DIR_RESOURCES =
+ AppConstants.platform == "macosx" ? "Contents/Resources/" : "";
+const TEST_FILE_SUFFIX = AppConstants.platform == "macosx" ? "_mac" : "";
+const FILE_COMPLETE_MAR = "complete" + TEST_FILE_SUFFIX + ".mar";
+const FILE_PARTIAL_MAR = "partial" + TEST_FILE_SUFFIX + ".mar";
+const FILE_COMPLETE_PRECOMPLETE = "complete_precomplete" + TEST_FILE_SUFFIX;
+const FILE_PARTIAL_PRECOMPLETE = "partial_precomplete" + TEST_FILE_SUFFIX;
+const FILE_COMPLETE_REMOVEDFILES = "complete_removed-files" + TEST_FILE_SUFFIX;
+const FILE_PARTIAL_REMOVEDFILES = "partial_removed-files" + TEST_FILE_SUFFIX;
+const FILE_UPDATE_IN_PROGRESS_LOCK = "updated.update_in_progress.lock";
+const COMPARE_LOG_SUFFIX = "_" + mozinfo.os;
+const LOG_COMPLETE_SUCCESS = "complete_log_success" + COMPARE_LOG_SUFFIX;
+const LOG_PARTIAL_SUCCESS = "partial_log_success" + COMPARE_LOG_SUFFIX;
+const LOG_PARTIAL_FAILURE = "partial_log_failure" + COMPARE_LOG_SUFFIX;
+const LOG_REPLACE_SUCCESS = "replace_log_success";
+
+const USE_EXECV = AppConstants.platform == "linux";
+
+const URL_HOST = "http://localhost";
+
+const APP_INFO_NAME = "XPCShell";
+const APP_INFO_VENDOR = "Mozilla";
+
+const APP_BIN_SUFFIX =
+ AppConstants.platform == "linux" ? "-bin" : mozinfo.bin_suffix;
+const FILE_APP_BIN = AppConstants.MOZ_APP_NAME + APP_BIN_SUFFIX;
+const FILE_COMPLETE_EXE = "complete.exe";
+const FILE_HELPER_BIN = "TestAUSHelper" + mozinfo.bin_suffix;
+const FILE_MAINTENANCE_SERVICE_BIN = "maintenanceservice.exe";
+const FILE_MAINTENANCE_SERVICE_INSTALLER_BIN =
+ "maintenanceservice_installer.exe";
+const FILE_OLD_VERSION_MAR = "old_version.mar";
+const FILE_PARTIAL_EXE = "partial.exe";
+const FILE_UPDATER_BIN = "updater" + mozinfo.bin_suffix;
+
+const PERFORMING_STAGED_UPDATE = "Performing a staged update";
+const CALL_QUIT = "calling QuitProgressUI";
+const ERR_UPDATE_IN_PROGRESS = "Update already in progress! Exiting";
+const ERR_RENAME_FILE = "rename_file: failed to rename file";
+const ERR_ENSURE_COPY = "ensure_copy: failed to copy the file";
+const ERR_UNABLE_OPEN_DEST = "unable to open destination file";
+const ERR_BACKUP_DISCARD = "backup_discard: unable to remove";
+const ERR_MOVE_DESTDIR_7 = "Moving destDir to tmpDir failed, err: 7";
+const ERR_BACKUP_CREATE_7 = "backup_create failed: 7";
+const ERR_LOADSOURCEFILE_FAILED = "LoadSourceFile failed";
+const ERR_PARENT_PID_PERSISTS =
+ "The parent process didn't exit! Continuing with update.";
+const ERR_BGTASK_EXCLUSIVE =
+ "failed to exclusively open executable file from background task: ";
+
+const LOG_SVC_SUCCESSFUL_LAUNCH = "Process was started... waiting on result.";
+const LOG_SVC_UNSUCCESSFUL_LAUNCH =
+ "The install directory path is not valid for this application.";
+
+// Typical end of a message when calling assert
+const MSG_SHOULD_EQUAL = " should equal the expected value";
+const MSG_SHOULD_EXIST = "the file or directory should exist";
+const MSG_SHOULD_NOT_EXIST = "the file or directory should not exist";
+
+// Time in seconds the helper application should sleep before exiting. The
+// helper can also be made to exit by writing |finish| to its input file.
+const HELPER_SLEEP_TIMEOUT = 180;
+
+// How many of do_timeout calls using FILE_IN_USE_TIMEOUT_MS to wait before the
+// test is aborted.
+const FILE_IN_USE_TIMEOUT_MS = 1000;
+
+const PIPE_TO_NULL =
+ AppConstants.platform == "win" ? ">nul" : "> /dev/null 2>&1";
+
+const LOG_FUNCTION = info;
+
+const gHTTPHandlerPath = "updates.xml";
+
+var gIsServiceTest;
+var gTestID;
+
+// This default value will be overridden when using the http server.
+var gURLData = URL_HOST + "/";
+var gTestserver;
+var gUpdateCheckCount = 0;
+
+var gIncrementalDownloadErrorType;
+
+var gResponseBody;
+
+var gProcess;
+var gAppTimer;
+var gHandle;
+
+var gGREDirOrig;
+var gGREBinDirOrig;
+
+var gPIDPersistProcess;
+
+// Variables are used instead of contants so tests can override these values if
+// necessary.
+var gCallbackBinFile = "callback_app" + mozinfo.bin_suffix;
+var gCallbackArgs = ["./", "callback.log", "Test Arg 2", "Test Arg 3"];
+var gPostUpdateBinFile = "postup_app" + mozinfo.bin_suffix;
+
+var gTimeoutRuns = 0;
+
+// Environment related globals
+var gShouldResetEnv = undefined;
+var gAddedEnvXRENoWindowsCrashDialog = false;
+var gEnvXPCOMDebugBreak;
+var gEnvXPCOMMemLeakLog;
+var gEnvForceServiceFallback = false;
+
+const URL_HTTP_UPDATE_SJS = "http://test_details/";
+const DATA_URI_SPEC = Services.io.newFileURI(do_get_file("", false)).spec;
+
+/* import-globals-from shared.js */
+load("shared.js");
+
+// Set to true to log additional information for debugging. To log additional
+// information for individual tests set gDebugTest to false here and to true in
+// the test's onload function.
+gDebugTest = true;
+
+// Setting gDebugTestLog to true will create log files for the tests in
+// <objdir>/_tests/xpcshell/toolkit/mozapps/update/tests/<testdir>/ except for
+// the service tests since they run sequentially. This can help when debugging
+// failures for the tests that intermittently fail when they run in parallel.
+// Never set gDebugTestLog to true except when running tests locally.
+var gDebugTestLog = false;
+// An empty array for gTestsToLog will log most of the output of all of the
+// update tests except for the service tests. To only log specific tests add the
+// test file name without the file extension to the array below.
+var gTestsToLog = [];
+var gRealDump;
+var gFOS;
+
+var gTestFiles = [];
+var gTestDirs = [];
+
+// Common files for both successful and failed updates.
+var gTestFilesCommon = [
+ {
+ description: "Should never change",
+ fileName: FILE_UPDATE_SETTINGS_INI,
+ relPathDir: DIR_RESOURCES,
+ originalContents: UPDATE_SETTINGS_CONTENTS,
+ compareContents: UPDATE_SETTINGS_CONTENTS,
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o767,
+ comparePerms: 0o767,
+ },
+ {
+ description: "Should never change",
+ fileName: "channel-prefs.js",
+ relPathDir: DIR_RESOURCES + "defaults/pref/",
+ originalContents: "ShouldNotBeReplaced\n",
+ compareContents: "ShouldNotBeReplaced\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o767,
+ comparePerms: 0o767,
+ },
+];
+
+// Files for a complete successful update. This can be used for a complete
+// failed update by calling setTestFilesAndDirsForFailure.
+var gTestFilesCompleteSuccess = [
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "precomplete",
+ relPathDir: DIR_RESOURCES,
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_PARTIAL_PRECOMPLETE,
+ compareFile: FILE_COMPLETE_PRECOMPLETE,
+ originalPerms: 0o666,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "searchpluginstext0",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: "ToBeReplacedWithFromComplete\n",
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o775,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "searchpluginspng1.png",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "complete.png",
+ originalPerms: null,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "searchpluginspng0.png",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "partial.png",
+ compareFile: "complete.png",
+ originalPerms: 0o666,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "removed-files",
+ relPathDir: DIR_RESOURCES,
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_PARTIAL_REMOVEDFILES,
+ compareFile: FILE_COMPLETE_REMOVEDFILES,
+ originalPerms: 0o666,
+ comparePerms: 0o644,
+ },
+ {
+ description:
+ "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions1text0",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: 0o644,
+ },
+ {
+ description:
+ "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions1png1.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "partial.png",
+ compareFile: "complete.png",
+ originalPerms: 0o666,
+ comparePerms: 0o644,
+ },
+ {
+ description:
+ "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions1png0.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "complete.png",
+ originalPerms: null,
+ comparePerms: 0o644,
+ },
+ {
+ description:
+ "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions0text0",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: "ToBeReplacedWithFromComplete\n",
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: 0o644,
+ },
+ {
+ description:
+ "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions0png1.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "complete.png",
+ originalPerms: null,
+ comparePerms: 0o644,
+ },
+ {
+ description:
+ "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions0png0.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "complete.png",
+ originalPerms: null,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "exe0.exe",
+ relPathDir: DIR_MACOS,
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_HELPER_BIN,
+ compareFile: FILE_COMPLETE_EXE,
+ originalPerms: 0o777,
+ comparePerms: 0o755,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "10text0",
+ relPathDir: DIR_RESOURCES + "1/10/",
+ originalContents: "ToBeReplacedWithFromComplete\n",
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o767,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "0exe0.exe",
+ relPathDir: DIR_RESOURCES + "0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_HELPER_BIN,
+ compareFile: FILE_COMPLETE_EXE,
+ originalPerms: 0o777,
+ comparePerms: 0o755,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "00text1",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: "ToBeReplacedWithFromComplete\n",
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o677,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "00text0",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: "ToBeReplacedWithFromComplete\n",
+ compareContents: "FromComplete\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o775,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "00png0.png",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "complete.png",
+ originalPerms: 0o776,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Removed by precomplete (remove)",
+ fileName: "20text0",
+ relPathDir: DIR_RESOURCES + "2/20/",
+ originalContents: "ToBeDeleted\n",
+ compareContents: null,
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: null,
+ },
+ {
+ description: "Removed by precomplete (remove)",
+ fileName: "20png0.png",
+ relPathDir: DIR_RESOURCES + "2/20/",
+ originalContents: "ToBeDeleted\n",
+ compareContents: null,
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: null,
+ },
+];
+
+// Concatenate the common files to the end of the array.
+gTestFilesCompleteSuccess = gTestFilesCompleteSuccess.concat(gTestFilesCommon);
+
+// Files for a partial successful update. This can be used for a partial failed
+// update by calling setTestFilesAndDirsForFailure.
+var gTestFilesPartialSuccess = [
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "precomplete",
+ relPathDir: DIR_RESOURCES,
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_COMPLETE_PRECOMPLETE,
+ compareFile: FILE_PARTIAL_PRECOMPLETE,
+ originalPerms: 0o666,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "searchpluginstext0",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: "ToBeReplacedWithFromPartial\n",
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o775,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Patched by update.manifest if the file exists (patch-if)",
+ fileName: "searchpluginspng1.png",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o666,
+ comparePerms: 0o666,
+ },
+ {
+ description: "Patched by update.manifest if the file exists (patch-if)",
+ fileName: "searchpluginspng0.png",
+ relPathDir: DIR_RESOURCES + "searchplugins/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o666,
+ comparePerms: 0o666,
+ },
+ {
+ description:
+ "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions1text0",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: 0o644,
+ },
+ {
+ description:
+ "Patched by update.manifest if the parent directory exists (patch-if)",
+ fileName: "extensions1png1.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o666,
+ comparePerms: 0o666,
+ },
+ {
+ description:
+ "Patched by update.manifest if the parent directory exists (patch-if)",
+ fileName: "extensions1png0.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions1/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o666,
+ comparePerms: 0o666,
+ },
+ {
+ description:
+ "Added by update.manifest if the parent directory exists (add-if)",
+ fileName: "extensions0text0",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: "ToBeReplacedWithFromPartial\n",
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o644,
+ comparePerms: 0o644,
+ },
+ {
+ description:
+ "Patched by update.manifest if the parent directory exists (patch-if)",
+ fileName: "extensions0png1.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o644,
+ comparePerms: 0o644,
+ },
+ {
+ description:
+ "Patched by update.manifest if the parent directory exists (patch-if)",
+ fileName: "extensions0png0.png",
+ relPathDir: DIR_RESOURCES + "distribution/extensions/extensions0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o644,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Patched by update.manifest (patch)",
+ fileName: "exe0.exe",
+ relPathDir: DIR_MACOS,
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_COMPLETE_EXE,
+ compareFile: FILE_PARTIAL_EXE,
+ originalPerms: 0o755,
+ comparePerms: 0o755,
+ },
+ {
+ description: "Patched by update.manifest (patch)",
+ fileName: "0exe0.exe",
+ relPathDir: DIR_RESOURCES + "0/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: FILE_COMPLETE_EXE,
+ compareFile: FILE_PARTIAL_EXE,
+ originalPerms: 0o755,
+ comparePerms: 0o755,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "00text0",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: "ToBeReplacedWithFromPartial\n",
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: 0o644,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Patched by update.manifest (patch)",
+ fileName: "00png0.png",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: "complete.png",
+ compareFile: "partial.png",
+ originalPerms: 0o666,
+ comparePerms: 0o666,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "20text0",
+ relPathDir: DIR_RESOURCES + "2/20/",
+ originalContents: null,
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "20png0.png",
+ relPathDir: DIR_RESOURCES + "2/20/",
+ originalContents: null,
+ compareContents: null,
+ originalFile: null,
+ compareFile: "partial.png",
+ originalPerms: null,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Added by update.manifest (add)",
+ fileName: "00text2",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: null,
+ compareContents: "FromPartial\n",
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: 0o644,
+ },
+ {
+ description: "Removed by update.manifest (remove)",
+ fileName: "10text0",
+ relPathDir: DIR_RESOURCES + "1/10/",
+ originalContents: "ToBeDeleted\n",
+ compareContents: null,
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: null,
+ },
+ {
+ description: "Removed by update.manifest (remove)",
+ fileName: "00text1",
+ relPathDir: DIR_RESOURCES + "0/00/",
+ originalContents: "ToBeDeleted\n",
+ compareContents: null,
+ originalFile: null,
+ compareFile: null,
+ originalPerms: null,
+ comparePerms: null,
+ },
+];
+
+// Concatenate the common files to the end of the array.
+gTestFilesPartialSuccess = gTestFilesPartialSuccess.concat(gTestFilesCommon);
+
+var gTestDirsCommon = [
+ {
+ relPathDir: DIR_RESOURCES + "3/",
+ dirRemoved: false,
+ files: ["3text0", "3text1"],
+ filesRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "4/",
+ dirRemoved: true,
+ files: ["4text0", "4text1"],
+ filesRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "5/",
+ dirRemoved: true,
+ files: ["5test.exe", "5text0", "5text1"],
+ filesRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "6/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "7/",
+ dirRemoved: true,
+ files: ["7text0", "7text1"],
+ subDirs: ["70/", "71/"],
+ subDirFiles: ["7xtest.exe", "7xtext0", "7xtext1"],
+ },
+ {
+ relPathDir: DIR_RESOURCES + "8/",
+ dirRemoved: false,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "8/80/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "8/81/",
+ dirRemoved: false,
+ files: ["81text0", "81text1"],
+ },
+ {
+ relPathDir: DIR_RESOURCES + "8/82/",
+ dirRemoved: false,
+ subDirs: ["820/", "821/"],
+ },
+ {
+ relPathDir: DIR_RESOURCES + "8/83/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "8/84/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "8/85/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "8/86/",
+ dirRemoved: true,
+ files: ["86text0", "86text1"],
+ },
+ {
+ relPathDir: DIR_RESOURCES + "8/87/",
+ dirRemoved: true,
+ subDirs: ["870/", "871/"],
+ subDirFiles: ["87xtext0", "87xtext1"],
+ },
+ {
+ relPathDir: DIR_RESOURCES + "8/88/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "8/89/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "9/90/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "9/91/",
+ dirRemoved: false,
+ files: ["91text0", "91text1"],
+ },
+ {
+ relPathDir: DIR_RESOURCES + "9/92/",
+ dirRemoved: false,
+ subDirs: ["920/", "921/"],
+ },
+ {
+ relPathDir: DIR_RESOURCES + "9/93/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "9/94/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "9/95/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "9/96/",
+ dirRemoved: true,
+ files: ["96text0", "96text1"],
+ },
+ {
+ relPathDir: DIR_RESOURCES + "9/97/",
+ dirRemoved: true,
+ subDirs: ["970/", "971/"],
+ subDirFiles: ["97xtext0", "97xtext1"],
+ },
+ {
+ relPathDir: DIR_RESOURCES + "9/98/",
+ dirRemoved: true,
+ },
+ {
+ relPathDir: DIR_RESOURCES + "9/99/",
+ dirRemoved: true,
+ },
+ {
+ description:
+ "Silences 'WARNING: Failed to resolve XUL App Dir.' in debug builds",
+ relPathDir: DIR_RESOURCES + "browser",
+ dirRemoved: false,
+ },
+];
+
+// Directories for a complete successful update. This array can be used for a
+// complete failed update by calling setTestFilesAndDirsForFailure.
+var gTestDirsCompleteSuccess = [
+ {
+ description: "Removed by precomplete (rmdir)",
+ relPathDir: DIR_RESOURCES + "2/20/",
+ dirRemoved: true,
+ },
+ {
+ description: "Removed by precomplete (rmdir)",
+ relPathDir: DIR_RESOURCES + "2/",
+ dirRemoved: true,
+ },
+];
+
+// Concatenate the common files to the beginning of the array.
+gTestDirsCompleteSuccess = gTestDirsCommon.concat(gTestDirsCompleteSuccess);
+
+// Directories for a partial successful update. This array can be used for a
+// partial failed update by calling setTestFilesAndDirsForFailure.
+var gTestDirsPartialSuccess = [
+ {
+ description: "Removed by update.manifest (rmdir)",
+ relPathDir: DIR_RESOURCES + "1/10/",
+ dirRemoved: true,
+ },
+ {
+ description: "Removed by update.manifest (rmdir)",
+ relPathDir: DIR_RESOURCES + "1/",
+ dirRemoved: true,
+ },
+];
+
+// Concatenate the common files to the beginning of the array.
+gTestDirsPartialSuccess = gTestDirsCommon.concat(gTestDirsPartialSuccess);
+
+/**
+ * Helper function for setting up the test environment.
+ *
+ * @param aAppUpdateAutoEnabled
+ * See setAppUpdateAutoSync in shared.js for details.
+ * @param aAllowBits
+ * If true, allow update downloads via the Windows BITS service.
+ * If false, this download mechanism will not be used.
+ */
+function setupTestCommon(aAppUpdateAutoEnabled = false, aAllowBits = false) {
+ debugDump("start - general test setup");
+
+ Assert.strictEqual(
+ gTestID,
+ undefined,
+ "gTestID should be 'undefined' (setupTestCommon should " +
+ "only be called once)"
+ );
+
+ let caller = Components.stack.caller;
+ gTestID = caller.filename.toString().split("/").pop().split(".")[0];
+
+ if (gDebugTestLog && !gIsServiceTest) {
+ if (!gTestsToLog.length || gTestsToLog.includes(gTestID)) {
+ let logFile = do_get_file(gTestID + ".log", true);
+ if (!logFile.exists()) {
+ logFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ }
+ gFOS = Cc["@mozilla.org/network/file-output-stream;1"].createInstance(
+ Ci.nsIFileOutputStream
+ );
+ gFOS.init(logFile, MODE_WRONLY | MODE_APPEND, PERMS_FILE, 0);
+
+ gRealDump = dump;
+ dump = dumpOverride;
+ }
+ }
+
+ createAppInfo("xpcshell@tests.mozilla.org", APP_INFO_NAME, "1.0", "2.0");
+
+ if (gIsServiceTest && !shouldRunServiceTest()) {
+ return false;
+ }
+
+ do_test_pending();
+
+ setDefaultPrefs();
+
+ gGREDirOrig = getGREDir();
+ gGREBinDirOrig = getGREBinDir();
+
+ let applyDir = getApplyDirFile().parent;
+
+ // Try to remove the directory used to apply updates and the updates directory
+ // on platforms other than Windows. This is non-fatal for the test since if
+ // this fails a different directory will be used.
+ if (applyDir.exists()) {
+ debugDump("attempting to remove directory. Path: " + applyDir.path);
+ try {
+ removeDirRecursive(applyDir);
+ } catch (e) {
+ logTestInfo(
+ "non-fatal error removing directory. Path: " +
+ applyDir.path +
+ ", Exception: " +
+ e
+ );
+ // When the application doesn't exit properly it can cause the test to
+ // fail again on the second run with an NS_ERROR_FILE_ACCESS_DENIED error
+ // along with no useful information in the test log. To prevent this use
+ // a different directory for the test when it isn't possible to remove the
+ // existing test directory (bug 1294196).
+ gTestID += "_new";
+ logTestInfo(
+ "using a new directory for the test by changing gTestID " +
+ "since there is an existing test directory that can't be " +
+ "removed, gTestID: " +
+ gTestID
+ );
+ }
+ }
+
+ if (AppConstants.platform == "win") {
+ Services.prefs.setBoolPref(
+ PREF_APP_UPDATE_SERVICE_ENABLED,
+ !!gIsServiceTest
+ );
+ }
+
+ if (gIsServiceTest) {
+ let exts = ["id", "log", "status"];
+ for (let i = 0; i < exts.length; ++i) {
+ let file = getSecureOutputFile(exts[i]);
+ if (file.exists()) {
+ try {
+ file.remove(false);
+ } catch (e) {}
+ }
+ }
+ }
+
+ adjustGeneralPaths();
+ createWorldWritableAppUpdateDir();
+
+ // Logged once here instead of in the mock directory provider to lessen test
+ // log spam.
+ debugDump("Updates Directory (UpdRootD) Path: " + getMockUpdRootD().path);
+
+ // This prevents a warning about not being able to find the greprefs.js file
+ // from being logged.
+ let grePrefsFile = getGREDir();
+ if (!grePrefsFile.exists()) {
+ grePrefsFile.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ }
+ grePrefsFile.append("greprefs.js");
+ if (!grePrefsFile.exists()) {
+ grePrefsFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ }
+
+ // The name of the update lock needs to be changed to match the path
+ // overridden in adjustGeneralPaths() above. Wait until now to reset
+ // because the GRE dir now exists, which may cause the "install
+ // path" to be normalized differently now that it can be resolved.
+ debugDump("resetting update lock");
+ let syncManager = Cc["@mozilla.org/updates/update-sync-manager;1"].getService(
+ Ci.nsIUpdateSyncManager
+ );
+ syncManager.resetLock();
+
+ // Remove the updates directory on Windows and Mac OS X which is located
+ // outside of the application directory after the call to adjustGeneralPaths
+ // has set it up. Since the test hasn't ran yet and the directory shouldn't
+ // exist this is non-fatal for the test.
+ if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
+ let updatesDir = getMockUpdRootD();
+ if (updatesDir.exists()) {
+ debugDump("attempting to remove directory. Path: " + updatesDir.path);
+ try {
+ removeDirRecursive(updatesDir);
+ } catch (e) {
+ logTestInfo(
+ "non-fatal error removing directory. Path: " +
+ updatesDir.path +
+ ", Exception: " +
+ e
+ );
+ }
+ }
+ }
+
+ setAppUpdateAutoSync(aAppUpdateAutoEnabled);
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_BITS_ENABLED, aAllowBits);
+
+ debugDump("finish - general test setup");
+ return true;
+}
+
+/**
+ * Nulls out the most commonly used global vars used by tests to prevent leaks
+ * as needed and attempts to restore the system to its original state.
+ */
+function cleanupTestCommon() {
+ debugDump("start - general test cleanup");
+
+ if (gChannel) {
+ gPrefRoot.removeObserver(PREF_APP_UPDATE_CHANNEL, observer);
+ }
+
+ gTestserver = null;
+
+ if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") {
+ // This will delete the launch script if it exists.
+ getLaunchScript();
+ }
+
+ if (gIsServiceTest) {
+ let exts = ["id", "log", "status"];
+ for (let i = 0; i < exts.length; ++i) {
+ let file = getSecureOutputFile(exts[i]);
+ if (file.exists()) {
+ try {
+ file.remove(false);
+ } catch (e) {}
+ }
+ }
+ }
+
+ if (AppConstants.platform == "win" && MOZ_APP_BASENAME) {
+ let appDir = getApplyDirFile();
+ let vendor = MOZ_APP_VENDOR ? MOZ_APP_VENDOR : "Mozilla";
+ const REG_PATH =
+ "SOFTWARE\\" + vendor + "\\" + MOZ_APP_BASENAME + "\\TaskBarIDs";
+ let key = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ try {
+ key.open(
+ Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ REG_PATH,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+ if (key.hasValue(appDir.path)) {
+ key.removeValue(appDir.path);
+ }
+ } catch (e) {}
+ try {
+ key.open(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ REG_PATH,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+ if (key.hasValue(appDir.path)) {
+ key.removeValue(appDir.path);
+ }
+ } catch (e) {}
+ }
+
+ // The updates directory is located outside of the application directory and
+ // needs to be removed on Windows and Mac OS X.
+ if (AppConstants.platform == "win" || AppConstants.platform == "macosx") {
+ let updatesDir = getMockUpdRootD();
+ // Try to remove the directory used to apply updates. Since the test has
+ // already finished this is non-fatal for the test.
+ if (updatesDir.exists()) {
+ debugDump("attempting to remove directory. Path: " + updatesDir.path);
+ try {
+ removeDirRecursive(updatesDir);
+ } catch (e) {
+ logTestInfo(
+ "non-fatal error removing directory. Path: " +
+ updatesDir.path +
+ ", Exception: " +
+ e
+ );
+ }
+ if (AppConstants.platform == "macosx") {
+ let updatesRootDir = gUpdatesRootDir.clone();
+ while (updatesRootDir.path != updatesDir.path) {
+ if (updatesDir.exists()) {
+ debugDump(
+ "attempting to remove directory. Path: " + updatesDir.path
+ );
+ try {
+ // Try to remove the directory without the recursive flag set
+ // since the top level directory has already had its contents
+ // removed and the parent directory might still be used by a
+ // different test.
+ updatesDir.remove(false);
+ } catch (e) {
+ logTestInfo(
+ "non-fatal error removing directory. Path: " +
+ updatesDir.path +
+ ", Exception: " +
+ e
+ );
+ if (e == Cr.NS_ERROR_FILE_DIR_NOT_EMPTY) {
+ break;
+ }
+ }
+ }
+ updatesDir = updatesDir.parent;
+ }
+ }
+ }
+ }
+
+ let applyDir = getApplyDirFile().parent;
+
+ // Try to remove the directory used to apply updates. Since the test has
+ // already finished this is non-fatal for the test.
+ if (applyDir.exists()) {
+ debugDump("attempting to remove directory. Path: " + applyDir.path);
+ try {
+ removeDirRecursive(applyDir);
+ } catch (e) {
+ logTestInfo(
+ "non-fatal error removing directory. Path: " +
+ applyDir.path +
+ ", Exception: " +
+ e
+ );
+ }
+ }
+
+ resetEnvironment();
+ Services.prefs.clearUserPref(PREF_APP_UPDATE_BITS_ENABLED);
+
+ debugDump("finish - general test cleanup");
+
+ if (gRealDump) {
+ dump = gRealDump;
+ gRealDump = null;
+ }
+
+ if (gFOS) {
+ gFOS.close();
+ }
+}
+
+/**
+ * Helper function to store the log output of calls to dump in a variable so the
+ * values can be written to a file for a parallel run of a test and printed to
+ * the log file when the test runs synchronously.
+ */
+function dumpOverride(aText) {
+ gFOS.write(aText, aText.length);
+ gRealDump(aText);
+}
+
+/**
+ * Helper function that calls do_test_finished that tracks whether a parallel
+ * run of a test passed when it runs synchronously so the log output can be
+ * inspected.
+ */
+function doTestFinish() {
+ if (gDebugTest) {
+ // This prevents do_print errors from being printed by the xpcshell test
+ // harness due to nsUpdateService.js logging to the console when the
+ // app.update.log preference is true.
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, false);
+ gAUS.observe(null, "nsPref:changed", PREF_APP_UPDATE_LOG);
+ }
+
+ reloadUpdateManagerData(true);
+
+ // Call app update's observe method passing quit-application to test that the
+ // shutdown of app update runs without throwing or leaking. The observer
+ // method is used directly instead of calling notifyObservers so components
+ // outside of the scope of this test don't assert and thereby cause app update
+ // tests to fail.
+ gAUS.observe(null, "quit-application", "");
+
+ executeSoon(do_test_finished);
+}
+
+/**
+ * Sets the most commonly used preferences used by tests
+ */
+function setDefaultPrefs() {
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_DISABLEDFORTESTING, false);
+ if (gDebugTest) {
+ // Enable Update logging
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, true);
+ } else {
+ // Some apps set this preference to true by default
+ Services.prefs.setBoolPref(PREF_APP_UPDATE_LOG, false);
+ }
+}
+
+/**
+ * Helper function for updater binary tests that sets the appropriate values
+ * to check for update failures.
+ */
+function setTestFilesAndDirsForFailure() {
+ gTestFiles.forEach(function STFADFF_Files(aTestFile) {
+ aTestFile.compareContents = aTestFile.originalContents;
+ aTestFile.compareFile = aTestFile.originalFile;
+ aTestFile.comparePerms = aTestFile.originalPerms;
+ });
+
+ gTestDirs.forEach(function STFADFF_Dirs(aTestDir) {
+ aTestDir.dirRemoved = false;
+ if (aTestDir.filesRemoved) {
+ aTestDir.filesRemoved = false;
+ }
+ });
+}
+
+/**
+ * Helper function for updater binary tests that prevents the distribution
+ * directory files from being created.
+ */
+function preventDistributionFiles() {
+ gTestFiles = gTestFiles.filter(function (aTestFile) {
+ return !aTestFile.relPathDir.includes("distribution/");
+ });
+
+ gTestDirs = gTestDirs.filter(function (aTestDir) {
+ return !aTestDir.relPathDir.includes("distribution/");
+ });
+}
+
+/**
+ * On Mac OS X this sets the last modified time for the app bundle directory to
+ * a date in the past to test that the last modified time is updated when an
+ * update has been successfully applied (bug 600098).
+ */
+function setAppBundleModTime() {
+ if (AppConstants.platform != "macosx") {
+ return;
+ }
+ let now = Date.now();
+ let yesterday = now - 1000 * 60 * 60 * 24;
+ let applyToDir = getApplyDirFile();
+ applyToDir.lastModifiedTime = yesterday;
+}
+
+/**
+ * On Mac OS X this checks that the last modified time for the app bundle
+ * directory has been updated when an update has been successfully applied
+ * (bug 600098).
+ */
+function checkAppBundleModTime() {
+ if (AppConstants.platform != "macosx") {
+ return;
+ }
+ // All we care about is that the last modified time has changed so that Mac OS
+ // X Launch Services invalidates its cache so the test allows up to one minute
+ // difference in the last modified time.
+ const MAC_MAX_TIME_DIFFERENCE = 60000;
+ let now = Date.now();
+ let applyToDir = getApplyDirFile();
+ let timeDiff = Math.abs(applyToDir.lastModifiedTime - now);
+ Assert.ok(
+ timeDiff < MAC_MAX_TIME_DIFFERENCE,
+ "the last modified time on the apply to directory should " +
+ "change after a successful update"
+ );
+}
+
+/**
+ * Performs Update Manager checks to verify that the update metadata is correct
+ * and that it is the same after the update xml files are reloaded.
+ *
+ * @param aStatusFileState
+ * The expected state of the status file.
+ * @param aHasActiveUpdate
+ * Should there be an active update.
+ * @param aUpdateStatusState
+ * The expected update's status state.
+ * @param aUpdateErrCode
+ * The expected update's error code.
+ * @param aUpdateCount
+ * The update history's update count.
+ */
+function checkUpdateManager(
+ aStatusFileState,
+ aHasActiveUpdate,
+ aUpdateStatusState,
+ aUpdateErrCode,
+ aUpdateCount
+) {
+ let activeUpdate =
+ aUpdateStatusState == STATE_DOWNLOADING
+ ? gUpdateManager.downloadingUpdate
+ : gUpdateManager.readyUpdate;
+ Assert.equal(
+ readStatusState(),
+ aStatusFileState,
+ "the status file state" + MSG_SHOULD_EQUAL
+ );
+ let msgTags = [" after startup ", " after a file reload "];
+ for (let i = 0; i < msgTags.length; ++i) {
+ logTestInfo(
+ "checking Update Manager updates" + msgTags[i] + "is performed"
+ );
+ if (aHasActiveUpdate) {
+ Assert.ok(
+ !!activeUpdate,
+ msgTags[i] + "the active update should be defined"
+ );
+ } else {
+ Assert.ok(
+ !activeUpdate,
+ msgTags[i] + "the active update should not be defined"
+ );
+ }
+ Assert.equal(
+ gUpdateManager.getUpdateCount(),
+ aUpdateCount,
+ msgTags[i] + "the update manager updateCount attribute" + MSG_SHOULD_EQUAL
+ );
+ if (aUpdateCount > 0) {
+ let update = gUpdateManager.getUpdateAt(0);
+ Assert.equal(
+ update.state,
+ aUpdateStatusState,
+ msgTags[i] + "the first update state" + MSG_SHOULD_EQUAL
+ );
+ Assert.equal(
+ update.errorCode,
+ aUpdateErrCode,
+ msgTags[i] + "the first update errorCode" + MSG_SHOULD_EQUAL
+ );
+ }
+ if (i != msgTags.length - 1) {
+ reloadUpdateManagerData();
+ }
+ }
+}
+
+/**
+ * Waits until the update files exist or not based on the parameters specified
+ * when calling this function or the default values if the parameters are not
+ * specified. This is necessary due to the update xml files being written
+ * asynchronously by nsIUpdateManager.
+ *
+ * @param aActiveUpdateExists (optional)
+ * Whether the active-update.xml file should exist (default is false).
+ * @param aUpdatesExists (optional)
+ * Whether the updates.xml file should exist (default is true).
+ */
+async function waitForUpdateXMLFiles(
+ aActiveUpdateExists = false,
+ aUpdatesExists = true
+) {
+ function areFilesStabilized() {
+ let file = getUpdateDirFile(FILE_ACTIVE_UPDATE_XML_TMP);
+ if (file.exists()) {
+ debugDump("file exists, Path: " + file.path);
+ return false;
+ }
+ file = getUpdateDirFile(FILE_UPDATES_XML_TMP);
+ if (file.exists()) {
+ debugDump("file exists, Path: " + file.path);
+ return false;
+ }
+ file = getUpdateDirFile(FILE_ACTIVE_UPDATE_XML);
+ if (file.exists() != aActiveUpdateExists) {
+ debugDump(
+ "file exists should equal: " +
+ aActiveUpdateExists +
+ ", Path: " +
+ file.path
+ );
+ return false;
+ }
+ file = getUpdateDirFile(FILE_UPDATES_XML);
+ if (file.exists() != aUpdatesExists) {
+ debugDump(
+ "file exists should equal: " +
+ aActiveUpdateExists +
+ ", Path: " +
+ file.path
+ );
+ return false;
+ }
+ return true;
+ }
+
+ await TestUtils.waitForCondition(
+ () => areFilesStabilized(),
+ "Waiting for update xml files to stabilize"
+ );
+}
+
+/**
+ * On Mac OS X and Windows this checks if the post update '.running' file exists
+ * to determine if the post update binary was launched.
+ *
+ * @param aShouldExist
+ * Whether the post update '.running' file should exist.
+ */
+function checkPostUpdateRunningFile(aShouldExist) {
+ if (AppConstants.platform == "linux") {
+ return;
+ }
+ let postUpdateRunningFile = getPostUpdateFile(".running");
+ if (aShouldExist) {
+ Assert.ok(
+ postUpdateRunningFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(postUpdateRunningFile.path)
+ );
+ } else {
+ Assert.ok(
+ !postUpdateRunningFile.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(postUpdateRunningFile.path)
+ );
+ }
+}
+
+/**
+ * Initializes the most commonly used settings and creates an instance of the
+ * update service stub.
+ */
+function standardInit() {
+ // Initialize the update service stub component
+ initUpdateServiceStub();
+}
+
+/**
+ * Helper function for getting the application version from the application.ini
+ * file. This will look in both the GRE and the application directories for the
+ * application.ini file.
+ *
+ * @return The version string from the application.ini file.
+ */
+function getAppVersion() {
+ // Read the application.ini and use its application version.
+ let iniFile = gGREDirOrig.clone();
+ iniFile.append(FILE_APPLICATION_INI);
+ if (!iniFile.exists()) {
+ iniFile = gGREBinDirOrig.clone();
+ iniFile.append(FILE_APPLICATION_INI);
+ }
+ Assert.ok(iniFile.exists(), MSG_SHOULD_EXIST + getMsgPath(iniFile.path));
+ let iniParser = Cc["@mozilla.org/xpcom/ini-parser-factory;1"]
+ .getService(Ci.nsIINIParserFactory)
+ .createINIParser(iniFile);
+ return iniParser.getString("App", "Version");
+}
+
+/**
+ * Helper function for getting the path to the directory where the
+ * application binary is located (e.g. <test_file_leafname>/dir.app/).
+ *
+ * Note: The dir.app subdirectory under <test_file_leafname> is needed for
+ * platforms other than Mac OS X so the tests can run in parallel due to
+ * update staging creating a lock file named moz_update_in_progress.lock in
+ * the parent directory of the installation directory.
+ * Note: For service tests with IS_AUTHENTICODE_CHECK_ENABLED we use an absolute
+ * path inside Program Files because the service itself will refuse to
+ * update an installation not located in Program Files.
+ *
+ * @return The path to the directory where application binary is located.
+ */
+function getApplyDirPath() {
+ if (gIsServiceTest && IS_AUTHENTICODE_CHECK_ENABLED) {
+ let dir = getMaintSvcDir();
+ dir.append(gTestID);
+ dir.append("dir.app");
+ return dir.path;
+ }
+ return gTestID + "/dir.app/";
+}
+
+/**
+ * Helper function for getting the nsIFile for a file in the directory where the
+ * update will be applied.
+ *
+ * The files for the update are located two directories below the apply to
+ * directory since Mac OS X sets the last modified time for the root directory
+ * to the current time and if the update changes any files in the root directory
+ * then it wouldn't be possible to test (bug 600098).
+ *
+ * @param aRelPath (optional)
+ * The relative path to the file or directory to get from the root of
+ * the test's directory. If not specified the test's directory will be
+ * returned.
+ * @return The nsIFile for the file in the directory where the update will be
+ * applied.
+ */
+function getApplyDirFile(aRelPath) {
+ // do_get_file only supports relative paths, but under these conditions we
+ // need to use an absolute path in Program Files instead.
+ if (gIsServiceTest && IS_AUTHENTICODE_CHECK_ENABLED) {
+ let file = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ file.initWithPath(getApplyDirPath());
+ if (aRelPath) {
+ if (aRelPath == "..") {
+ file = file.parent;
+ } else {
+ aRelPath = aRelPath.replace(/\//g, "\\");
+ file.appendRelativePath(aRelPath);
+ }
+ }
+ return file;
+ }
+ let relpath = getApplyDirPath() + (aRelPath ? aRelPath : "");
+ return do_get_file(relpath, true);
+}
+
+/**
+ * Helper function for getting the relative path to the directory where the
+ * test data files are located.
+ *
+ * @return The relative path to the directory where the test data files are
+ * located.
+ */
+function getTestDirPath() {
+ return "../data/";
+}
+
+/**
+ * Helper function for getting the nsIFile for a file in the test data
+ * directory.
+ *
+ * @param aRelPath (optional)
+ * The relative path to the file or directory to get from the root of
+ * the test's data directory. If not specified the test's data
+ * directory will be returned.
+ * @param aAllowNonExists (optional)
+ * Whether or not to throw an error if the path exists.
+ * If not specified, then false is used.
+ * @return The nsIFile for the file in the test data directory.
+ * @throws If the file or directory does not exist.
+ */
+function getTestDirFile(aRelPath, aAllowNonExists) {
+ let relpath = getTestDirPath() + (aRelPath ? aRelPath : "");
+ return do_get_file(relpath, !!aAllowNonExists);
+}
+
+/**
+ * Helper function for getting the nsIFile for the maintenance service
+ * directory on Windows.
+ *
+ * @return The nsIFile for the maintenance service directory.
+ * @throws If called from a platform other than Windows.
+ */
+function getMaintSvcDir() {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ const CSIDL_PROGRAM_FILES = 0x26;
+ const CSIDL_PROGRAM_FILESX86 = 0x2a;
+ // This will return an empty string on our Win XP build systems.
+ let maintSvcDir = getSpecialFolderDir(CSIDL_PROGRAM_FILESX86);
+ if (maintSvcDir) {
+ maintSvcDir.append("Mozilla Maintenance Service");
+ debugDump(
+ "using CSIDL_PROGRAM_FILESX86 - maintenance service install " +
+ "directory path: " +
+ maintSvcDir.path
+ );
+ }
+ if (!maintSvcDir || !maintSvcDir.exists()) {
+ maintSvcDir = getSpecialFolderDir(CSIDL_PROGRAM_FILES);
+ if (maintSvcDir) {
+ maintSvcDir.append("Mozilla Maintenance Service");
+ debugDump(
+ "using CSIDL_PROGRAM_FILES - maintenance service install " +
+ "directory path: " +
+ maintSvcDir.path
+ );
+ }
+ }
+ if (!maintSvcDir) {
+ do_throw("Unable to find the maintenance service install directory");
+ }
+
+ return maintSvcDir;
+}
+
+/**
+ * Reads the current update operation/state in the status file in the secure
+ * update log directory.
+ *
+ * @return The status value.
+ */
+function readSecureStatusFile() {
+ let file = getSecureOutputFile("status");
+ if (!file.exists()) {
+ debugDump("update status file does not exist, path: " + file.path);
+ return STATE_NONE;
+ }
+ return readFile(file).split("\n")[0];
+}
+
+/**
+ * Get an nsIFile for a file in the secure update log directory. The file name
+ * is always the value of gTestID and the file extension is specified by the
+ * aFileExt parameter.
+ *
+ * @param aFileExt
+ * The file extension.
+ * @return The nsIFile of the secure update file.
+ */
+function getSecureOutputFile(aFileExt) {
+ let file = getMaintSvcDir();
+ file.append("UpdateLogs");
+ file.append(gTestID + "." + aFileExt);
+ return file;
+}
+
+/**
+ * Get the nsIFile for a Windows special folder determined by the CSIDL
+ * passed.
+ *
+ * @param aCSIDL
+ * The CSIDL for the Windows special folder.
+ * @return The nsIFile for the Windows special folder.
+ * @throws If called from a platform other than Windows.
+ */
+function getSpecialFolderDir(aCSIDL) {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let lib = ctypes.open("shell32");
+ let SHGetSpecialFolderPath = lib.declare(
+ "SHGetSpecialFolderPathW",
+ ctypes.winapi_abi,
+ ctypes.bool /* bool(return) */,
+ ctypes.int32_t /* HWND hwndOwner */,
+ ctypes.char16_t.ptr /* LPTSTR lpszPath */,
+ ctypes.int32_t /* int csidl */,
+ ctypes.bool /* BOOL fCreate */
+ );
+
+ let aryPath = ctypes.char16_t.array()(260);
+ let rv = SHGetSpecialFolderPath(0, aryPath, aCSIDL, false);
+ if (!rv) {
+ do_throw(
+ "SHGetSpecialFolderPath failed to retrieve " +
+ aCSIDL +
+ " with Win32 error " +
+ ctypes.winLastError
+ );
+ }
+ lib.close();
+
+ let path = aryPath.readString(); // Convert the c-string to js-string
+ if (!path) {
+ return null;
+ }
+ debugDump("SHGetSpecialFolderPath returned path: " + path);
+ let dir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ dir.initWithPath(path);
+ return dir;
+}
+
+XPCOMUtils.defineLazyGetter(this, "gInstallDirPathHash", function test_gIDPH() {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ if (!MOZ_APP_BASENAME) {
+ return null;
+ }
+
+ let vendor = MOZ_APP_VENDOR ? MOZ_APP_VENDOR : "Mozilla";
+ let appDir = getApplyDirFile();
+
+ const REG_PATH =
+ "SOFTWARE\\" + vendor + "\\" + MOZ_APP_BASENAME + "\\TaskBarIDs";
+ let regKey = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ try {
+ regKey.open(
+ Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ REG_PATH,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+ regKey.writeStringValue(appDir.path, gTestID);
+ return gTestID;
+ } catch (e) {}
+
+ try {
+ regKey.create(
+ Ci.nsIWindowsRegKey.ROOT_KEY_CURRENT_USER,
+ REG_PATH,
+ Ci.nsIWindowsRegKey.ACCESS_ALL
+ );
+ regKey.writeStringValue(appDir.path, gTestID);
+ return gTestID;
+ } catch (e) {
+ logTestInfo(
+ "failed to create registry value. Registry Path: " +
+ REG_PATH +
+ ", Value Name: " +
+ appDir.path +
+ ", Value Data: " +
+ gTestID +
+ ", Exception " +
+ e
+ );
+ do_throw(
+ "Unable to write HKLM or HKCU TaskBarIDs registry value, key path: " +
+ REG_PATH
+ );
+ }
+ return null;
+});
+
+XPCOMUtils.defineLazyGetter(this, "gLocalAppDataDir", function test_gLADD() {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ const CSIDL_LOCAL_APPDATA = 0x1c;
+ return getSpecialFolderDir(CSIDL_LOCAL_APPDATA);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gCommonAppDataDir", function test_gCDD() {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ const CSIDL_COMMON_APPDATA = 0x0023;
+ return getSpecialFolderDir(CSIDL_COMMON_APPDATA);
+});
+
+XPCOMUtils.defineLazyGetter(this, "gProgFilesDir", function test_gPFD() {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ const CSIDL_PROGRAM_FILES = 0x26;
+ return getSpecialFolderDir(CSIDL_PROGRAM_FILES);
+});
+
+/**
+ * Helper function for getting the update root directory used by the tests. This
+ * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir
+ * in nsXREDirProvider.cpp so an application will be able to find the update
+ * when running a test that launches the application.
+ *
+ * The aGetOldLocation argument performs the same function that the argument
+ * with the same name in nsXREDirProvider::GetUpdateRootDir performs. If true,
+ * the old (pre-migration) update directory is returned.
+ */
+function getMockUpdRootD(aGetOldLocation = false) {
+ if (AppConstants.platform == "win") {
+ return getMockUpdRootDWin(aGetOldLocation);
+ }
+
+ if (AppConstants.platform == "macosx") {
+ return getMockUpdRootDMac();
+ }
+
+ return getApplyDirFile(DIR_MACOS);
+}
+
+/**
+ * Helper function for getting the update root directory used by the tests. This
+ * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir
+ * in nsXREDirProvider.cpp so an application will be able to find the update
+ * when running a test that launches the application.
+ *
+ * @throws If called from a platform other than Windows.
+ */
+function getMockUpdRootDWin(aGetOldLocation) {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let relPathUpdates = "";
+ let dataDirectory = gCommonAppDataDir.clone();
+ if (aGetOldLocation) {
+ relPathUpdates += "Mozilla";
+ } else {
+ relPathUpdates += "Mozilla-1de4eec8-1241-4177-a864-e594e8d1fb38";
+ }
+
+ relPathUpdates += "\\" + DIR_UPDATES + "\\" + gInstallDirPathHash;
+ let updatesDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ updatesDir.initWithPath(dataDirectory.path + "\\" + relPathUpdates);
+ return updatesDir;
+}
+
+function createWorldWritableAppUpdateDir() {
+ // This function is only necessary in Windows
+ if (AppConstants.platform == "win") {
+ let installDir = Services.dirsvc.get(
+ XRE_EXECUTABLE_FILE,
+ Ci.nsIFile
+ ).parent;
+ let exitValue = runTestHelperSync(["create-update-dir", installDir.path]);
+ Assert.equal(exitValue, 0, "The helper process exit value should be 0");
+ }
+}
+
+XPCOMUtils.defineLazyGetter(this, "gUpdatesRootDir", function test_gURD() {
+ if (AppConstants.platform != "macosx") {
+ do_throw("Mac OS X only function called by a different platform!");
+ }
+
+ let dir = Services.dirsvc.get("ULibDir", Ci.nsIFile);
+ dir.append("Caches");
+ if (MOZ_APP_VENDOR || MOZ_APP_BASENAME) {
+ dir.append(MOZ_APP_VENDOR ? MOZ_APP_VENDOR : MOZ_APP_BASENAME);
+ } else {
+ dir.append("Mozilla");
+ }
+ dir.append(DIR_UPDATES);
+ return dir;
+});
+
+/**
+ * Helper function for getting the update root directory used by the tests. This
+ * returns the same directory as returned by nsXREDirProvider::GetUpdateRootDir
+ * in nsXREDirProvider.cpp so an application will be able to find the update
+ * when running a test that launches the application.
+ */
+function getMockUpdRootDMac() {
+ if (AppConstants.platform != "macosx") {
+ do_throw("Mac OS X only function called by a different platform!");
+ }
+
+ let appDir = Services.dirsvc.get(XRE_EXECUTABLE_FILE, Ci.nsIFile).parent
+ .parent.parent;
+ let appDirPath = appDir.path;
+ appDirPath = appDirPath.substr(0, appDirPath.length - 4);
+
+ let pathUpdates = gUpdatesRootDir.path + appDirPath;
+ let updatesDir = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ updatesDir.initWithPath(pathUpdates);
+ return updatesDir;
+}
+
+/**
+ * Creates an update in progress lock file in the specified directory on
+ * Windows.
+ *
+ * @param aDir
+ * The nsIFile for the directory where the lock file should be created.
+ * @throws If called from a platform other than Windows.
+ */
+function createUpdateInProgressLockFile(aDir) {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let file = aDir.clone();
+ file.append(FILE_UPDATE_IN_PROGRESS_LOCK);
+ file.create(file.NORMAL_FILE_TYPE, 0o444);
+ file.QueryInterface(Ci.nsILocalFileWin);
+ file.readOnly = true;
+ Assert.ok(file.exists(), MSG_SHOULD_EXIST + getMsgPath(file.path));
+ Assert.ok(!file.isWritable(), "the lock file should not be writeable");
+}
+
+/**
+ * Removes an update in progress lock file in the specified directory on
+ * Windows.
+ *
+ * @param aDir
+ * The nsIFile for the directory where the lock file is located.
+ * @throws If called from a platform other than Windows.
+ */
+function removeUpdateInProgressLockFile(aDir) {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let file = aDir.clone();
+ file.append(FILE_UPDATE_IN_PROGRESS_LOCK);
+ file.QueryInterface(Ci.nsILocalFileWin);
+ file.readOnly = false;
+ file.remove(false);
+ Assert.ok(!file.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(file.path));
+}
+
+/**
+ * Copies the test updater to the GRE binary directory and returns the nsIFile
+ * for the copied test updater.
+ *
+ * @return nsIFIle for the copied test updater.
+ */
+function copyTestUpdaterToBinDir() {
+ let updaterLeafName =
+ AppConstants.platform == "macosx" ? "updater.app" : FILE_UPDATER_BIN;
+ let testUpdater = getTestDirFile(updaterLeafName);
+ let updater = getGREBinDir();
+ updater.append(updaterLeafName);
+ if (!updater.exists()) {
+ testUpdater.copyToFollowingLinks(updater.parent, updaterLeafName);
+ }
+ if (AppConstants.platform == "macosx") {
+ updater.append("Contents");
+ updater.append("MacOS");
+ updater.append("org.mozilla.updater");
+ }
+ return updater;
+}
+
+/**
+ * Logs the contents of an update log and for maintenance service tests this
+ * will log the contents of the latest maintenanceservice.log.
+ *
+ * @param aLogLeafName
+ * The leaf name of the update log.
+ */
+function logUpdateLog(aLogLeafName) {
+ let updateLog = getUpdateDirFile(aLogLeafName);
+ if (updateLog.exists()) {
+ // xpcshell tests won't display the entire contents so log each line.
+ let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n");
+ updateLogContents = removeTimeStamps(updateLogContents);
+ updateLogContents = replaceLogPaths(updateLogContents);
+ let aryLogContents = updateLogContents.split("\n");
+ logTestInfo("contents of " + updateLog.path + ":");
+ aryLogContents.forEach(function LU_ULC_FE(aLine) {
+ logTestInfo(aLine);
+ });
+ } else {
+ logTestInfo("update log doesn't exist, path: " + updateLog.path);
+ }
+
+ if (gIsServiceTest) {
+ let secureStatus = readSecureStatusFile();
+ logTestInfo("secure update status: " + secureStatus);
+
+ updateLog = getSecureOutputFile("log");
+ if (updateLog.exists()) {
+ // xpcshell tests won't display the entire contents so log each line.
+ let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n");
+ updateLogContents = removeTimeStamps(updateLogContents);
+ updateLogContents = replaceLogPaths(updateLogContents);
+ let aryLogContents = updateLogContents.split("\n");
+ logTestInfo("contents of " + updateLog.path + ":");
+ aryLogContents.forEach(function LU_SULC_FE(aLine) {
+ logTestInfo(aLine);
+ });
+ } else {
+ logTestInfo("secure update log doesn't exist, path: " + updateLog.path);
+ }
+
+ let serviceLog = getMaintSvcDir();
+ serviceLog.append("logs");
+ serviceLog.append("maintenanceservice.log");
+ if (serviceLog.exists()) {
+ // xpcshell tests won't display the entire contents so log each line.
+ let serviceLogContents = readFileBytes(serviceLog).replace(/\r\n/g, "\n");
+ serviceLogContents = replaceLogPaths(serviceLogContents);
+ let aryLogContents = serviceLogContents.split("\n");
+ logTestInfo("contents of " + serviceLog.path + ":");
+ aryLogContents.forEach(function LU_MSLC_FE(aLine) {
+ logTestInfo(aLine);
+ });
+ } else {
+ logTestInfo(
+ "maintenance service log doesn't exist, path: " + serviceLog.path
+ );
+ }
+ }
+}
+
+/**
+ * Gets the maintenance service log contents.
+ */
+function readServiceLogFile() {
+ let file = getMaintSvcDir();
+ file.append("logs");
+ file.append("maintenanceservice.log");
+ return readFile(file);
+}
+
+/**
+ * Launches the updater binary to apply an update for updater tests.
+ *
+ * @param aExpectedStatus
+ * The expected value of update.status when the update finishes. For
+ * service tests passing STATE_PENDING or STATE_APPLIED will change the
+ * value to STATE_PENDING_SVC and STATE_APPLIED_SVC respectively.
+ * @param aSwitchApp
+ * If true the update should switch the application with an updated
+ * staged application and if false the update should be applied to the
+ * installed application.
+ * @param aExpectedExitValue
+ * The expected exit value from the updater binary for non-service
+ * tests.
+ * @param aCheckSvcLog
+ * Whether the service log should be checked for service tests.
+ * @param aPatchDirPath (optional)
+ * When specified the patch directory path to use for invalid argument
+ * tests otherwise the normal path will be used.
+ * @param aInstallDirPath (optional)
+ * When specified the install directory path to use for invalid
+ * argument tests otherwise the normal path will be used.
+ * @param aApplyToDirPath (optional)
+ * When specified the apply to / working directory path to use for
+ * invalid argument tests otherwise the normal path will be used.
+ * @param aCallbackPath (optional)
+ * When specified the callback path to use for invalid argument tests
+ * otherwise the normal path will be used.
+ */
+function runUpdate(
+ aExpectedStatus,
+ aSwitchApp,
+ aExpectedExitValue,
+ aCheckSvcLog,
+ aPatchDirPath,
+ aInstallDirPath,
+ aApplyToDirPath,
+ aCallbackPath
+) {
+ let isInvalidArgTest =
+ !!aPatchDirPath ||
+ !!aInstallDirPath ||
+ !!aApplyToDirPath ||
+ !!aCallbackPath;
+
+ let svcOriginalLog;
+ if (gIsServiceTest) {
+ copyFileToTestAppDir(FILE_MAINTENANCE_SERVICE_BIN, false);
+ copyFileToTestAppDir(FILE_MAINTENANCE_SERVICE_INSTALLER_BIN, false);
+ if (aCheckSvcLog) {
+ svcOriginalLog = readServiceLogFile();
+ }
+ }
+
+ let pid = 0;
+ if (gPIDPersistProcess) {
+ pid = gPIDPersistProcess.pid;
+ Services.env.set("MOZ_TEST_SHORTER_WAIT_PID", "1");
+ }
+
+ let updateBin = copyTestUpdaterToBinDir();
+ Assert.ok(updateBin.exists(), MSG_SHOULD_EXIST + getMsgPath(updateBin.path));
+
+ let updatesDirPath = aPatchDirPath || getUpdateDirFile(DIR_PATCH).path;
+ let installDirPath = aInstallDirPath || getApplyDirFile().path;
+ let applyToDirPath = aApplyToDirPath || getApplyDirFile().path;
+ let stageDirPath = aApplyToDirPath || getStageDirFile().path;
+
+ let callbackApp = getApplyDirFile(DIR_RESOURCES + gCallbackBinFile);
+ Assert.ok(
+ callbackApp.exists(),
+ MSG_SHOULD_EXIST + ", path: " + callbackApp.path
+ );
+ callbackApp.permissions = PERMS_DIRECTORY;
+
+ setAppBundleModTime();
+
+ let args = [updatesDirPath, installDirPath];
+ if (aSwitchApp) {
+ args[2] = stageDirPath;
+ args[3] = pid + "/replace";
+ } else {
+ args[2] = applyToDirPath;
+ args[3] = pid;
+ }
+
+ let launchBin = gIsServiceTest && isInvalidArgTest ? callbackApp : updateBin;
+
+ if (!isInvalidArgTest) {
+ args = args.concat([callbackApp.parent.path, callbackApp.path]);
+ args = args.concat(gCallbackArgs);
+ } else if (gIsServiceTest) {
+ args = ["launch-service", updateBin.path].concat(args);
+ } else if (aCallbackPath) {
+ args = args.concat([callbackApp.parent.path, aCallbackPath]);
+ }
+
+ debugDump("launching the program: " + launchBin.path + " " + args.join(" "));
+
+ if (aSwitchApp && !isInvalidArgTest) {
+ // We want to set the env vars again
+ gShouldResetEnv = undefined;
+ }
+
+ setEnvironment();
+
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(launchBin);
+ process.run(true, args, args.length);
+
+ resetEnvironment();
+
+ if (gPIDPersistProcess) {
+ Services.env.set("MOZ_TEST_SHORTER_WAIT_PID", "");
+ }
+
+ let status = readStatusFile();
+ if (
+ (!gIsServiceTest && process.exitValue != aExpectedExitValue) ||
+ (status != aExpectedStatus && !gIsServiceTest && !isInvalidArgTest)
+ ) {
+ if (process.exitValue != aExpectedExitValue) {
+ logTestInfo(
+ "updater exited with unexpected value! Got: " +
+ process.exitValue +
+ ", Expected: " +
+ aExpectedExitValue
+ );
+ }
+ if (status != aExpectedStatus) {
+ logTestInfo(
+ "update status is not the expected status! Got: " +
+ status +
+ ", Expected: " +
+ aExpectedStatus
+ );
+ }
+ logUpdateLog(FILE_LAST_UPDATE_LOG);
+ }
+
+ if (gIsServiceTest && isInvalidArgTest) {
+ let secureStatus = readSecureStatusFile();
+ if (secureStatus != STATE_NONE) {
+ status = secureStatus;
+ }
+ }
+
+ if (!gIsServiceTest) {
+ Assert.equal(
+ process.exitValue,
+ aExpectedExitValue,
+ "the process exit value" + MSG_SHOULD_EQUAL
+ );
+ }
+
+ if (status != aExpectedStatus) {
+ logUpdateLog(FILE_UPDATE_LOG);
+ }
+ Assert.equal(status, aExpectedStatus, "the update status" + MSG_SHOULD_EQUAL);
+
+ Assert.ok(
+ !updateHasBinaryTransparencyErrorResult(),
+ "binary transparency is not being processed for now"
+ );
+
+ if (gIsServiceTest && aCheckSvcLog) {
+ let contents = readServiceLogFile();
+ Assert.notEqual(
+ contents,
+ svcOriginalLog,
+ "the contents of the maintenanceservice.log should not " +
+ "be the same as the original contents"
+ );
+ if (gEnvForceServiceFallback) {
+ // If we are forcing the service to fail and fall back to update without
+ // the service, the service log should reflect that we failed in that way.
+ Assert.ok(
+ contents.includes(LOG_SVC_UNSUCCESSFUL_LAUNCH),
+ "the contents of the maintenanceservice.log should " +
+ "contain the unsuccessful launch string"
+ );
+ } else if (!isInvalidArgTest) {
+ Assert.notEqual(
+ contents.indexOf(LOG_SVC_SUCCESSFUL_LAUNCH),
+ -1,
+ "the contents of the maintenanceservice.log should " +
+ "contain the successful launch string"
+ );
+ }
+ }
+}
+
+/**
+ * Launches the helper binary synchronously with the specified arguments for
+ * updater tests.
+ *
+ * @param aArgs
+ * The arguments to pass to the helper binary.
+ * @return the process exit value returned by the helper binary.
+ */
+function runTestHelperSync(aArgs) {
+ let helperBin = getTestDirFile(FILE_HELPER_BIN);
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ process.init(helperBin);
+ debugDump("Running " + helperBin.path + " " + aArgs.join(" "));
+ process.run(true, aArgs, aArgs.length);
+ return process.exitValue;
+}
+
+/**
+ * Creates a symlink for updater tests.
+ */
+function createSymlink() {
+ let args = [
+ "setup-symlink",
+ "moz-foo",
+ "moz-bar",
+ "target",
+ getApplyDirFile().path + "/" + DIR_RESOURCES + "link",
+ ];
+ let exitValue = runTestHelperSync(args);
+ Assert.equal(exitValue, 0, "the helper process exit value should be 0");
+ let file = getApplyDirFile(DIR_RESOURCES + "link");
+ Assert.ok(file.exists(), MSG_SHOULD_EXIST + ", path: " + file.path);
+ file.permissions = 0o666;
+ args = [
+ "setup-symlink",
+ "moz-foo2",
+ "moz-bar2",
+ "target2",
+ getApplyDirFile().path + "/" + DIR_RESOURCES + "link2",
+ "change-perm",
+ ];
+ exitValue = runTestHelperSync(args);
+ Assert.equal(exitValue, 0, "the helper process exit value should be 0");
+}
+
+/**
+ * Removes a symlink for updater tests.
+ */
+function removeSymlink() {
+ let args = [
+ "remove-symlink",
+ "moz-foo",
+ "moz-bar",
+ "target",
+ getApplyDirFile().path + "/" + DIR_RESOURCES + "link",
+ ];
+ let exitValue = runTestHelperSync(args);
+ Assert.equal(exitValue, 0, "the helper process exit value should be 0");
+ args = [
+ "remove-symlink",
+ "moz-foo2",
+ "moz-bar2",
+ "target2",
+ getApplyDirFile().path + "/" + DIR_RESOURCES + "link2",
+ ];
+ exitValue = runTestHelperSync(args);
+ Assert.equal(exitValue, 0, "the helper process exit value should be 0");
+}
+
+/**
+ * Checks a symlink for updater tests.
+ */
+function checkSymlink() {
+ let args = [
+ "check-symlink",
+ getApplyDirFile().path + "/" + DIR_RESOURCES + "link",
+ ];
+ let exitValue = runTestHelperSync(args);
+ Assert.equal(exitValue, 0, "the helper process exit value should be 0");
+}
+
+/**
+ * Sets the active update and related information for updater tests.
+ */
+function setupActiveUpdate() {
+ let pendingState = gIsServiceTest ? STATE_PENDING_SVC : STATE_PENDING;
+ let patchProps = { state: pendingState };
+ let patches = getLocalPatchString(patchProps);
+ let updates = getLocalUpdateString({}, patches);
+ writeUpdatesToXMLFile(getLocalUpdatesXMLString(updates), true);
+ writeVersionFile(DEFAULT_UPDATE_VERSION);
+ writeStatusFile(pendingState);
+ reloadUpdateManagerData();
+ Assert.ok(!!gUpdateManager.readyUpdate, "the ready update should be defined");
+}
+
+/**
+ * Stages an update using nsIUpdateProcessor:processUpdate for updater tests.
+ *
+ * @param aStateAfterStage
+ * The expected update state after the update has been staged.
+ * @param aCheckSvcLog
+ * Whether the service log should be checked for service tests.
+ * @param aUpdateRemoved (optional)
+ * Whether the update is removed after staging. This can happen when
+ * a staging failure occurs.
+ */
+async function stageUpdate(
+ aStateAfterStage,
+ aCheckSvcLog,
+ aUpdateRemoved = false
+) {
+ debugDump("start - attempting to stage update");
+
+ let svcLogOriginalContents;
+ if (gIsServiceTest && aCheckSvcLog) {
+ svcLogOriginalContents = readServiceLogFile();
+ }
+
+ setAppBundleModTime();
+ setEnvironment();
+ try {
+ // Stage the update.
+ Cc["@mozilla.org/updates/update-processor;1"]
+ .createInstance(Ci.nsIUpdateProcessor)
+ .processUpdate();
+ } catch (e) {
+ Assert.ok(
+ false,
+ "error thrown while calling processUpdate, Exception: " + e
+ );
+ }
+ await waitForEvent("update-staged", aStateAfterStage);
+ resetEnvironment();
+
+ if (AppConstants.platform == "win") {
+ if (gIsServiceTest) {
+ waitForServiceStop(false);
+ } else {
+ let updater = getApplyDirFile(FILE_UPDATER_BIN);
+ await TestUtils.waitForCondition(
+ () => !isFileInUse(updater),
+ "Waiting for the file tp not be in use, Path: " + updater.path
+ );
+ }
+ }
+
+ if (!aUpdateRemoved) {
+ Assert.equal(
+ readStatusState(),
+ aStateAfterStage,
+ "the status file state" + MSG_SHOULD_EQUAL
+ );
+
+ Assert.equal(
+ gUpdateManager.readyUpdate.state,
+ aStateAfterStage,
+ "the update state" + MSG_SHOULD_EQUAL
+ );
+ }
+
+ let log = getUpdateDirFile(FILE_LAST_UPDATE_LOG);
+ Assert.ok(log.exists(), MSG_SHOULD_EXIST + getMsgPath(log.path));
+
+ log = getUpdateDirFile(FILE_UPDATE_LOG);
+ Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ log = getUpdateDirFile(FILE_BACKUP_UPDATE_LOG);
+ Assert.ok(!log.exists(), MSG_SHOULD_NOT_EXIST + getMsgPath(log.path));
+
+ let stageDir = getStageDirFile();
+ if (
+ aStateAfterStage == STATE_APPLIED ||
+ aStateAfterStage == STATE_APPLIED_SVC
+ ) {
+ Assert.ok(stageDir.exists(), MSG_SHOULD_EXIST + getMsgPath(stageDir.path));
+ } else {
+ Assert.ok(
+ !stageDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(stageDir.path)
+ );
+ }
+
+ if (gIsServiceTest && aCheckSvcLog) {
+ let contents = readServiceLogFile();
+ Assert.notEqual(
+ contents,
+ svcLogOriginalContents,
+ "the contents of the maintenanceservice.log should not " +
+ "be the same as the original contents"
+ );
+ Assert.notEqual(
+ contents.indexOf(LOG_SVC_SUCCESSFUL_LAUNCH),
+ -1,
+ "the contents of the maintenanceservice.log should " +
+ "contain the successful launch string"
+ );
+ }
+
+ debugDump("finish - attempting to stage update");
+}
+
+/**
+ * Helper function to check whether the maintenance service updater tests should
+ * run. See bug 711660 for more details.
+ *
+ * @return true if the test should run and false if it shouldn't.
+ * @throws If called from a platform other than Windows.
+ */
+function shouldRunServiceTest() {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let binDir = getGREBinDir();
+ let updaterBin = binDir.clone();
+ updaterBin.append(FILE_UPDATER_BIN);
+ Assert.ok(
+ updaterBin.exists(),
+ MSG_SHOULD_EXIST + ", leafName: " + updaterBin.leafName
+ );
+
+ let updaterBinPath = updaterBin.path;
+ if (/ /.test(updaterBinPath)) {
+ updaterBinPath = '"' + updaterBinPath + '"';
+ }
+
+ let isBinSigned = isBinarySigned(updaterBinPath);
+
+ const REG_PATH =
+ "SOFTWARE\\Mozilla\\MaintenanceService\\" +
+ "3932ecacee736d366d6436db0f55bce4";
+ let key = Cc["@mozilla.org/windows-registry-key;1"].createInstance(
+ Ci.nsIWindowsRegKey
+ );
+ try {
+ key.open(
+ Ci.nsIWindowsRegKey.ROOT_KEY_LOCAL_MACHINE,
+ REG_PATH,
+ Ci.nsIWindowsRegKey.ACCESS_READ | key.WOW64_64
+ );
+ } catch (e) {
+ // The build system could sign the files and not have the test registry key
+ // in which case we should fail the test if the updater binary is signed so
+ // the build system can be fixed by adding the registry key.
+ if (IS_AUTHENTICODE_CHECK_ENABLED) {
+ Assert.ok(
+ !isBinSigned,
+ "the updater.exe binary should not be signed when the test " +
+ "registry key doesn't exist (if it is, build system " +
+ "configuration bug?)"
+ );
+ }
+
+ logTestInfo(
+ "this test can only run on the buildbot build system at this time"
+ );
+ return false;
+ }
+
+ // Check to make sure the service is installed
+ let args = ["wait-for-service-stop", "MozillaMaintenance", "10"];
+ let exitValue = runTestHelperSync(args);
+ Assert.notEqual(
+ exitValue,
+ 0xee,
+ "the maintenance service should be " +
+ "installed (if not, build system configuration bug?)"
+ );
+
+ if (IS_AUTHENTICODE_CHECK_ENABLED) {
+ // The test registry key exists and IS_AUTHENTICODE_CHECK_ENABLED is true
+ // so the binaries should be signed. To run the test locally
+ // DISABLE_UPDATER_AUTHENTICODE_CHECK can be defined.
+ Assert.ok(
+ isBinSigned,
+ "the updater.exe binary should be signed (if not, build system " +
+ "configuration bug?)"
+ );
+ }
+
+ // In case the machine is running an old maintenance service or if it
+ // is not installed, and permissions exist to install it. Then install
+ // the newer bin that we have since all of the other checks passed.
+ return attemptServiceInstall();
+}
+
+/**
+ * Helper function to check whether the a binary is signed.
+ *
+ * @param aBinPath
+ * The path to the file to check if it is signed.
+ * @return true if the file is signed and false if it isn't.
+ */
+function isBinarySigned(aBinPath) {
+ let args = ["check-signature", aBinPath];
+ let exitValue = runTestHelperSync(args);
+ if (exitValue != 0) {
+ logTestInfo(
+ "binary is not signed. " +
+ FILE_HELPER_BIN +
+ " returned " +
+ exitValue +
+ " for file " +
+ aBinPath
+ );
+ return false;
+ }
+ return true;
+}
+
+/**
+ * Helper function for setting up the application files required to launch the
+ * application for the updater tests by either copying or creating symlinks to
+ * the files.
+ *
+ * @param options.requiresOmnijar when true, copy or symlink omnijars as well.
+ * This may be required to launch the updated application and have non-trivial
+ * functionality available.
+ */
+function setupAppFiles({ requiresOmnijar = false } = {}) {
+ debugDump(
+ "start - copying or creating symlinks to application files " +
+ "for the test"
+ );
+
+ let destDir = getApplyDirFile();
+ if (!destDir.exists()) {
+ try {
+ destDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ } catch (e) {
+ logTestInfo(
+ "unable to create directory! Path: " +
+ destDir.path +
+ ", Exception: " +
+ e
+ );
+ do_throw(e);
+ }
+ }
+
+ // Required files for the application or the test that aren't listed in the
+ // dependentlibs.list file.
+ let appFiles = [
+ { relPath: FILE_APP_BIN, inGreDir: false },
+ { relPath: FILE_APPLICATION_INI, inGreDir: true },
+ { relPath: "dependentlibs.list", inGreDir: true },
+ ];
+
+ if (requiresOmnijar) {
+ appFiles.push({ relPath: AppConstants.OMNIJAR_NAME, inGreDir: true });
+
+ if (AppConstants.MOZ_BUILD_APP == "browser") {
+ // Only Firefox uses an app-specific omnijar.
+ appFiles.push({
+ relPath: "browser/" + AppConstants.OMNIJAR_NAME,
+ inGreDir: true,
+ });
+ }
+ }
+
+ // On Linux the updater.png must also be copied and libsoftokn3.so must be
+ // symlinked or copied.
+ if (AppConstants.platform == "linux") {
+ appFiles.push(
+ { relPath: "icons/updater.png", inGreDir: true },
+ { relPath: "libsoftokn3.so", inGreDir: true }
+ );
+ }
+
+ // Read the dependent libs file leafnames from the dependentlibs.list file
+ // into the array.
+ let deplibsFile = gGREDirOrig.clone();
+ deplibsFile.append("dependentlibs.list");
+ let fis = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
+ Ci.nsIFileInputStream
+ );
+ fis.init(deplibsFile, 0x01, 0o444, Ci.nsIFileInputStream.CLOSE_ON_EOF);
+ fis.QueryInterface(Ci.nsILineInputStream);
+
+ let hasMore;
+ let line = {};
+ do {
+ hasMore = fis.readLine(line);
+ appFiles.push({ relPath: line.value, inGreDir: false });
+ } while (hasMore);
+
+ fis.close();
+
+ appFiles.forEach(function CMAF_FLN_FE(aAppFile) {
+ copyFileToTestAppDir(aAppFile.relPath, aAppFile.inGreDir);
+ });
+
+ copyTestUpdaterToBinDir();
+
+ debugDump(
+ "finish - copying or creating symlinks to application files " +
+ "for the test"
+ );
+}
+
+/**
+ * Copies the specified files from the dist/bin directory into the test's
+ * application directory.
+ *
+ * @param aFileRelPath
+ * The relative path to the source and the destination of the file to
+ * copy.
+ * @param aInGreDir
+ * Whether the file is located in the GRE directory which is
+ * <bundle>/Contents/Resources on Mac OS X and is the installation
+ * directory on all other platforms. If false the file must be in the
+ * GRE Binary directory which is <bundle>/Contents/MacOS on Mac OS X
+ * and is the installation directory on on all other platforms.
+ */
+function copyFileToTestAppDir(aFileRelPath, aInGreDir) {
+ // gGREDirOrig and gGREBinDirOrig must always be cloned when changing its
+ // properties
+ let srcFile = aInGreDir ? gGREDirOrig.clone() : gGREBinDirOrig.clone();
+ let destFile = aInGreDir ? getGREDir() : getGREBinDir();
+ let fileRelPath = aFileRelPath;
+ let pathParts = fileRelPath.split("/");
+ for (let i = 0; i < pathParts.length; i++) {
+ if (pathParts[i]) {
+ srcFile.append(pathParts[i]);
+ destFile.append(pathParts[i]);
+ }
+ }
+
+ if (AppConstants.platform == "macosx" && !srcFile.exists()) {
+ debugDump(
+ "unable to copy file since it doesn't exist! Checking if " +
+ fileRelPath +
+ ".app exists. Path: " +
+ srcFile.path
+ );
+ // gGREDirOrig and gGREBinDirOrig must always be cloned when changing its
+ // properties
+ srcFile = aInGreDir ? gGREDirOrig.clone() : gGREBinDirOrig.clone();
+ destFile = aInGreDir ? getGREDir() : getGREBinDir();
+ for (let i = 0; i < pathParts.length; i++) {
+ if (pathParts[i]) {
+ srcFile.append(
+ pathParts[i] + (pathParts.length - 1 == i ? ".app" : "")
+ );
+ destFile.append(
+ pathParts[i] + (pathParts.length - 1 == i ? ".app" : "")
+ );
+ }
+ }
+ fileRelPath = fileRelPath + ".app";
+ }
+ Assert.ok(
+ srcFile.exists(),
+ MSG_SHOULD_EXIST + ", leafName: " + srcFile.leafName
+ );
+
+ // Symlink libraries. Note that the XUL library on Mac OS X doesn't have a
+ // file extension and shouldSymlink will always be false on Windows.
+ let shouldSymlink =
+ pathParts[pathParts.length - 1] == "XUL" ||
+ fileRelPath.substr(fileRelPath.length - 3) == ".so" ||
+ fileRelPath.substr(fileRelPath.length - 6) == ".dylib";
+ if (!shouldSymlink) {
+ if (!destFile.exists()) {
+ try {
+ srcFile.copyToFollowingLinks(destFile.parent, destFile.leafName);
+ } catch (e) {
+ // Just in case it is partially copied
+ if (destFile.exists()) {
+ try {
+ destFile.remove(true);
+ } catch (ex) {
+ logTestInfo(
+ "unable to remove file that failed to copy! Path: " +
+ destFile.path +
+ ", Exception: " +
+ ex
+ );
+ }
+ }
+ do_throw(
+ "Unable to copy file! Path: " + srcFile.path + ", Exception: " + e
+ );
+ }
+ }
+ } else {
+ try {
+ if (destFile.exists()) {
+ destFile.remove(false);
+ }
+ let ln = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ ln.initWithPath("/bin/ln");
+ let process = Cc["@mozilla.org/process/util;1"].createInstance(
+ Ci.nsIProcess
+ );
+ process.init(ln);
+ let args = ["-s", srcFile.path, destFile.path];
+ process.run(true, args, args.length);
+ Assert.ok(
+ destFile.isSymlink(),
+ destFile.leafName + " should be a symlink"
+ );
+ } catch (e) {
+ do_throw(
+ "Unable to create symlink for file! Path: " +
+ srcFile.path +
+ ", Exception: " +
+ e
+ );
+ }
+ }
+}
+
+/**
+ * Attempts to upgrade the maintenance service if permissions are allowed.
+ * This is useful for XP where we have permission to upgrade in case an
+ * older service installer exists. Also if the user manually installed into
+ * a unprivileged location.
+ *
+ * @return true if the installed service is from this build. If the installed
+ * service is not from this build the test will fail instead of
+ * returning false.
+ * @throws If called from a platform other than Windows.
+ */
+function attemptServiceInstall() {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ let maintSvcDir = getMaintSvcDir();
+ Assert.ok(
+ maintSvcDir.exists(),
+ MSG_SHOULD_EXIST + ", leafName: " + maintSvcDir.leafName
+ );
+ let oldMaintSvcBin = maintSvcDir.clone();
+ oldMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN);
+ Assert.ok(
+ oldMaintSvcBin.exists(),
+ MSG_SHOULD_EXIST + ", leafName: " + oldMaintSvcBin.leafName
+ );
+ let buildMaintSvcBin = getGREBinDir();
+ buildMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN);
+ if (readFileBytes(oldMaintSvcBin) == readFileBytes(buildMaintSvcBin)) {
+ debugDump(
+ "installed maintenance service binary is the same as the " +
+ "build's maintenance service binary"
+ );
+ return true;
+ }
+ let backupMaintSvcBin = maintSvcDir.clone();
+ backupMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN + ".backup");
+ try {
+ if (backupMaintSvcBin.exists()) {
+ backupMaintSvcBin.remove(false);
+ }
+ oldMaintSvcBin.moveTo(
+ maintSvcDir,
+ FILE_MAINTENANCE_SERVICE_BIN + ".backup"
+ );
+ buildMaintSvcBin.copyTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN);
+ backupMaintSvcBin.remove(false);
+ } catch (e) {
+ // Restore the original file in case the moveTo was successful.
+ if (backupMaintSvcBin.exists()) {
+ oldMaintSvcBin = maintSvcDir.clone();
+ oldMaintSvcBin.append(FILE_MAINTENANCE_SERVICE_BIN);
+ if (!oldMaintSvcBin.exists()) {
+ backupMaintSvcBin.moveTo(maintSvcDir, FILE_MAINTENANCE_SERVICE_BIN);
+ }
+ }
+ Assert.ok(
+ false,
+ "should be able copy the test maintenance service to " +
+ "the maintenance service directory (if not, build system " +
+ "configuration bug?), path: " +
+ maintSvcDir.path
+ );
+ }
+
+ return true;
+}
+
+/**
+ * Waits for the applications that are launched by the maintenance service to
+ * stop.
+ *
+ * @throws If called from a platform other than Windows.
+ */
+function waitServiceApps() {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ // maintenanceservice_installer.exe is started async during updates.
+ waitForApplicationStop("maintenanceservice_installer.exe");
+ // maintenanceservice_tmp.exe is started async from the service installer.
+ waitForApplicationStop("maintenanceservice_tmp.exe");
+ // In case the SCM thinks the service is stopped, but process still exists.
+ waitForApplicationStop("maintenanceservice.exe");
+}
+
+/**
+ * Waits for the maintenance service to stop.
+ *
+ * @throws If called from a platform other than Windows.
+ */
+function waitForServiceStop(aFailTest) {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ waitServiceApps();
+ debugDump("waiting for the maintenance service to stop if necessary");
+ // Use the helper bin to ensure the service is stopped. If not stopped, then
+ // wait for the service to stop (at most 120 seconds).
+ let args = ["wait-for-service-stop", "MozillaMaintenance", "120"];
+ let exitValue = runTestHelperSync(args);
+ Assert.notEqual(exitValue, 0xee, "the maintenance service should exist");
+ if (exitValue != 0) {
+ if (aFailTest) {
+ Assert.ok(
+ false,
+ "the maintenance service should stop, process exit " +
+ "value: " +
+ exitValue
+ );
+ }
+ logTestInfo(
+ "maintenance service did not stop which may cause test " +
+ "failures later, process exit value: " +
+ exitValue
+ );
+ } else {
+ debugDump("service stopped");
+ }
+ waitServiceApps();
+}
+
+/**
+ * Waits for the specified application to stop.
+ *
+ * @param aApplication
+ * The application binary name to wait until it has stopped.
+ * @throws If called from a platform other than Windows.
+ */
+function waitForApplicationStop(aApplication) {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ debugDump("waiting for " + aApplication + " to stop if necessary");
+ // Use the helper bin to ensure the application is stopped. If not stopped,
+ // then wait for it to stop (at most 120 seconds).
+ let args = ["wait-for-application-exit", aApplication, "120"];
+ let exitValue = runTestHelperSync(args);
+ Assert.equal(
+ exitValue,
+ 0,
+ "the process should have stopped, process name: " + aApplication
+ );
+}
+
+/**
+ * Gets the platform specific shell binary that is launched using nsIProcess and
+ * in turn launches a binary used for the test (e.g. application, updater,
+ * etc.). A shell is used so debug console output can be redirected to a file so
+ * it doesn't end up in the test log.
+ *
+ * @return nsIFile for the shell binary to launch using nsIProcess.
+ */
+function getLaunchBin() {
+ let launchBin;
+ if (AppConstants.platform == "win") {
+ launchBin = Services.dirsvc.get("WinD", Ci.nsIFile);
+ launchBin.append("System32");
+ launchBin.append("cmd.exe");
+ } else {
+ launchBin = Cc["@mozilla.org/file/local;1"].createInstance(Ci.nsIFile);
+ launchBin.initWithPath("/bin/sh");
+ }
+ Assert.ok(launchBin.exists(), MSG_SHOULD_EXIST + getMsgPath(launchBin.path));
+
+ return launchBin;
+}
+
+/**
+ * Locks a Windows directory.
+ *
+ * @param aDirPath
+ * The test file object that describes the file to make in use.
+ * @throws If called from a platform other than Windows.
+ */
+function lockDirectory(aDirPath) {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ debugDump("start - locking installation directory");
+ const LPCWSTR = ctypes.char16_t.ptr;
+ const DWORD = ctypes.uint32_t;
+ const LPVOID = ctypes.voidptr_t;
+ const GENERIC_READ = 0x80000000;
+ const FILE_SHARE_READ = 1;
+ const FILE_SHARE_WRITE = 2;
+ const OPEN_EXISTING = 3;
+ const FILE_FLAG_BACKUP_SEMANTICS = 0x02000000;
+ const INVALID_HANDLE_VALUE = LPVOID(0xffffffff);
+ let kernel32 = ctypes.open("kernel32");
+ let CreateFile = kernel32.declare(
+ "CreateFileW",
+ ctypes.winapi_abi,
+ LPVOID,
+ LPCWSTR,
+ DWORD,
+ DWORD,
+ LPVOID,
+ DWORD,
+ DWORD,
+ LPVOID
+ );
+ gHandle = CreateFile(
+ aDirPath,
+ GENERIC_READ,
+ FILE_SHARE_READ | FILE_SHARE_WRITE,
+ LPVOID(0),
+ OPEN_EXISTING,
+ FILE_FLAG_BACKUP_SEMANTICS,
+ LPVOID(0)
+ );
+ Assert.notEqual(
+ gHandle.toString(),
+ INVALID_HANDLE_VALUE.toString(),
+ "the handle should not equal INVALID_HANDLE_VALUE"
+ );
+ kernel32.close();
+ debugDump("finish - locking installation directory");
+}
+
+/**
+ * Launches the test helper binary to make it in use for updater tests.
+ *
+ * @param aRelPath
+ * The relative path in the apply to directory for the helper binary.
+ * @param aCopyTestHelper
+ * Whether to copy the test helper binary to the relative path in the
+ * apply to directory.
+ */
+async function runHelperFileInUse(aRelPath, aCopyTestHelper) {
+ debugDump("aRelPath: " + aRelPath);
+ // Launch an existing file so it is in use during the update.
+ let helperBin = getTestDirFile(FILE_HELPER_BIN);
+ let fileInUseBin = getApplyDirFile(aRelPath);
+ if (aCopyTestHelper) {
+ if (fileInUseBin.exists()) {
+ fileInUseBin.remove(false);
+ }
+ helperBin.copyTo(fileInUseBin.parent, fileInUseBin.leafName);
+ }
+ fileInUseBin.permissions = PERMS_DIRECTORY;
+ let args = [
+ getApplyDirPath() + DIR_RESOURCES,
+ "input",
+ "output",
+ "-s",
+ HELPER_SLEEP_TIMEOUT,
+ ];
+ let fileInUseProcess = Cc["@mozilla.org/process/util;1"].createInstance(
+ Ci.nsIProcess
+ );
+ fileInUseProcess.init(fileInUseBin);
+ fileInUseProcess.run(false, args, args.length);
+
+ await waitForHelperSleep();
+}
+
+/**
+ * Launches the test helper binary to provide a pid that is in use for updater
+ * tests.
+ *
+ * @param aRelPath
+ * The relative path in the apply to directory for the helper binary.
+ * @param aCopyTestHelper
+ * Whether to copy the test helper binary to the relative path in the
+ * apply to directory.
+ */
+async function runHelperPIDPersists(aRelPath, aCopyTestHelper) {
+ debugDump("aRelPath: " + aRelPath);
+ // Launch an existing file so it is in use during the update.
+ let helperBin = getTestDirFile(FILE_HELPER_BIN);
+ let pidPersistsBin = getApplyDirFile(aRelPath);
+ if (aCopyTestHelper) {
+ if (pidPersistsBin.exists()) {
+ pidPersistsBin.remove(false);
+ }
+ helperBin.copyTo(pidPersistsBin.parent, pidPersistsBin.leafName);
+ }
+ pidPersistsBin.permissions = PERMS_DIRECTORY;
+ let args = [
+ getApplyDirPath() + DIR_RESOURCES,
+ "input",
+ "output",
+ "-s",
+ HELPER_SLEEP_TIMEOUT,
+ ];
+ gPIDPersistProcess = Cc["@mozilla.org/process/util;1"].createInstance(
+ Ci.nsIProcess
+ );
+ gPIDPersistProcess.init(pidPersistsBin);
+ gPIDPersistProcess.run(false, args, args.length);
+
+ await waitForHelperSleep();
+ await TestUtils.waitForCondition(
+ () => !!gPIDPersistProcess.pid,
+ "Waiting for the process pid"
+ );
+}
+
+/**
+ * Launches the test helper binary and locks a file specified on the command
+ * line for updater tests.
+ *
+ * @param aTestFile
+ * The test file object that describes the file to lock.
+ */
+async function runHelperLockFile(aTestFile) {
+ // Exclusively lock an existing file so it is in use during the update.
+ let helperBin = getTestDirFile(FILE_HELPER_BIN);
+ let helperDestDir = getApplyDirFile(DIR_RESOURCES);
+ helperBin.copyTo(helperDestDir, FILE_HELPER_BIN);
+ helperBin = getApplyDirFile(DIR_RESOURCES + FILE_HELPER_BIN);
+ // Strip off the first two directories so the path has to be from the helper's
+ // working directory.
+ let lockFileRelPath = aTestFile.relPathDir.split("/");
+ if (AppConstants.platform == "macosx") {
+ lockFileRelPath = lockFileRelPath.slice(2);
+ }
+ lockFileRelPath = lockFileRelPath.join("/") + "/" + aTestFile.fileName;
+ let args = [
+ getApplyDirPath() + DIR_RESOURCES,
+ "input",
+ "output",
+ "-s",
+ HELPER_SLEEP_TIMEOUT,
+ lockFileRelPath,
+ ];
+ let helperProcess = Cc["@mozilla.org/process/util;1"].createInstance(
+ Ci.nsIProcess
+ );
+ helperProcess.init(helperBin);
+ helperProcess.run(false, args, args.length);
+
+ await waitForHelperSleep();
+}
+
+/**
+ * Helper function that waits until the helper has completed its operations.
+ */
+async function waitForHelperSleep() {
+ // Give the lock file process time to lock the file before updating otherwise
+ // this test can fail intermittently on Windows debug builds.
+ let file = getApplyDirFile(DIR_RESOURCES + "output");
+ await TestUtils.waitForCondition(
+ () => file.exists(),
+ "Waiting for file to exist, path: " + file.path
+ );
+
+ let expectedContents = "sleeping\n";
+ await TestUtils.waitForCondition(
+ () => readFile(file) == expectedContents,
+ "Waiting for expected file contents: " + expectedContents
+ );
+
+ await TestUtils.waitForCondition(() => {
+ try {
+ file.remove(false);
+ } catch (e) {
+ debugDump(
+ "failed to remove file. Path: " + file.path + ", Exception: " + e
+ );
+ }
+ return !file.exists();
+ }, "Waiting for file to be removed, Path: " + file.path);
+}
+
+/**
+ * Helper function to tell the helper to finish and exit its sleep state.
+ */
+async function waitForHelperExit() {
+ let file = getApplyDirFile(DIR_RESOURCES + "input");
+ writeFile(file, "finish\n");
+
+ // Give the lock file process time to lock the file before updating otherwise
+ // this test can fail intermittently on Windows debug builds.
+ file = getApplyDirFile(DIR_RESOURCES + "output");
+ await TestUtils.waitForCondition(
+ () => file.exists(),
+ "Waiting for file to exist, Path: " + file.path
+ );
+
+ let expectedContents = "finished\n";
+ await TestUtils.waitForCondition(
+ () => readFile(file) == expectedContents,
+ "Waiting for expected file contents: " + expectedContents
+ );
+
+ // Give the lock file process time to unlock the file before deleting the
+ // input and output files.
+ await TestUtils.waitForCondition(() => {
+ try {
+ file.remove(false);
+ } catch (e) {
+ debugDump(
+ "failed to remove file. Path: " + file.path + ", Exception: " + e
+ );
+ }
+ return !file.exists();
+ }, "Waiting for file to be removed, Path: " + file.path);
+
+ file = getApplyDirFile(DIR_RESOURCES + "input");
+ await TestUtils.waitForCondition(() => {
+ try {
+ file.remove(false);
+ } catch (e) {
+ debugDump(
+ "failed to remove file. Path: " + file.path + ", Exception: " + e
+ );
+ }
+ return !file.exists();
+ }, "Waiting for file to be removed, Path: " + file.path);
+}
+
+/**
+ * Helper function for updater binary tests that creates the files and
+ * directories used by the test.
+ *
+ * @param aMarFile
+ * The mar file for the update test.
+ * @param aPostUpdateAsync
+ * When null the updater.ini is not created otherwise this parameter
+ * is passed to createUpdaterINI.
+ * @param aPostUpdateExeRelPathPrefix
+ * When aPostUpdateAsync null this value is ignored otherwise it is
+ * passed to createUpdaterINI.
+ * @param aSetupActiveUpdate
+ * Whether to setup the active update.
+ *
+ * @param options.requiresOmnijar
+ * When true, copy or symlink omnijars as well. This may be required
+ * to launch the updated application and have non-trivial functionality
+ * available.
+ */
+async function setupUpdaterTest(
+ aMarFile,
+ aPostUpdateAsync,
+ aPostUpdateExeRelPathPrefix = "",
+ aSetupActiveUpdate = true,
+ { requiresOmnijar = false } = {}
+) {
+ debugDump("start - updater test setup");
+ let updatesPatchDir = getUpdateDirFile(DIR_PATCH);
+ if (!updatesPatchDir.exists()) {
+ updatesPatchDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ }
+ // Copy the mar that will be applied
+ let mar = getTestDirFile(aMarFile);
+ mar.copyToFollowingLinks(updatesPatchDir, FILE_UPDATE_MAR);
+
+ let helperBin = getTestDirFile(FILE_HELPER_BIN);
+ helperBin.permissions = PERMS_DIRECTORY;
+ let afterApplyBinDir = getApplyDirFile(DIR_RESOURCES);
+ helperBin.copyToFollowingLinks(afterApplyBinDir, gCallbackBinFile);
+ helperBin.copyToFollowingLinks(afterApplyBinDir, gPostUpdateBinFile);
+
+ gTestFiles.forEach(function SUT_TF_FE(aTestFile) {
+ debugDump("start - setup test file: " + aTestFile.fileName);
+ if (aTestFile.originalFile || aTestFile.originalContents) {
+ let testDir = getApplyDirFile(aTestFile.relPathDir);
+ // Somehow these create calls are failing with FILE_ALREADY_EXISTS even
+ // after checking .exists() first, so we just catch the exception.
+ try {
+ testDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ throw e;
+ }
+ }
+
+ let testFile;
+ if (aTestFile.originalFile) {
+ testFile = getTestDirFile(aTestFile.originalFile);
+ testFile.copyToFollowingLinks(testDir, aTestFile.fileName);
+ testFile = getApplyDirFile(aTestFile.relPathDir + aTestFile.fileName);
+ Assert.ok(
+ testFile.exists(),
+ MSG_SHOULD_EXIST + ", path: " + testFile.path
+ );
+ } else {
+ testFile = getApplyDirFile(aTestFile.relPathDir + aTestFile.fileName);
+ writeFile(testFile, aTestFile.originalContents);
+ }
+
+ // Skip these tests on Windows since chmod doesn't really set permissions
+ // on Windows.
+ if (AppConstants.platform != "win" && aTestFile.originalPerms) {
+ testFile.permissions = aTestFile.originalPerms;
+ // Store the actual permissions on the file for reference later after
+ // setting the permissions.
+ if (!aTestFile.comparePerms) {
+ aTestFile.comparePerms = testFile.permissions;
+ }
+ }
+ }
+ debugDump("finish - setup test file: " + aTestFile.fileName);
+ });
+
+ // Add the test directory that will be updated for a successful update or left
+ // in the initial state for a failed update.
+ gTestDirs.forEach(function SUT_TD_FE(aTestDir) {
+ debugDump("start - setup test directory: " + aTestDir.relPathDir);
+ let testDir = getApplyDirFile(aTestDir.relPathDir);
+ // Somehow these create calls are failing with FILE_ALREADY_EXISTS even
+ // after checking .exists() first, so we just catch the exception.
+ try {
+ testDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ } catch (e) {
+ if (e.result != Cr.NS_ERROR_FILE_ALREADY_EXISTS) {
+ throw e;
+ }
+ }
+
+ if (aTestDir.files) {
+ aTestDir.files.forEach(function SUT_TD_F_FE(aTestFile) {
+ let testFile = getApplyDirFile(aTestDir.relPathDir + aTestFile);
+ if (!testFile.exists()) {
+ testFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ }
+ });
+ }
+
+ if (aTestDir.subDirs) {
+ aTestDir.subDirs.forEach(function SUT_TD_SD_FE(aSubDir) {
+ let testSubDir = getApplyDirFile(aTestDir.relPathDir + aSubDir);
+ if (!testSubDir.exists()) {
+ testSubDir.create(Ci.nsIFile.DIRECTORY_TYPE, PERMS_DIRECTORY);
+ }
+
+ if (aTestDir.subDirFiles) {
+ aTestDir.subDirFiles.forEach(function SUT_TD_SDF_FE(aTestFile) {
+ let testFile = getApplyDirFile(
+ aTestDir.relPathDir + aSubDir + aTestFile
+ );
+ if (!testFile.exists()) {
+ testFile.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_FILE);
+ }
+ });
+ }
+ });
+ }
+ debugDump("finish - setup test directory: " + aTestDir.relPathDir);
+ });
+
+ if (aSetupActiveUpdate) {
+ setupActiveUpdate();
+ }
+
+ if (aPostUpdateAsync !== null) {
+ createUpdaterINI(aPostUpdateAsync, aPostUpdateExeRelPathPrefix);
+ }
+
+ await TestUtils.waitForCondition(() => {
+ try {
+ setupAppFiles({ requiresOmnijar });
+ return true;
+ } catch (e) {
+ logTestInfo("exception when calling setupAppFiles, Exception: " + e);
+ }
+ return false;
+ }, "Waiting to setup app files");
+
+ debugDump("finish - updater test setup");
+}
+
+/**
+ * Helper function for updater binary tests that creates the updater.ini
+ * file.
+ *
+ * @param aIsExeAsync
+ * True or undefined if the post update process should be async. If
+ * undefined ExeAsync will not be added to the updater.ini file in
+ * order to test the default launch behavior which is async.
+ * @param aExeRelPathPrefix
+ * A string to prefix the ExeRelPath values in the updater.ini.
+ */
+function createUpdaterINI(aIsExeAsync, aExeRelPathPrefix) {
+ let exeArg = "ExeArg=post-update-async\n";
+ let exeAsync = "";
+ if (aIsExeAsync !== undefined) {
+ if (aIsExeAsync) {
+ exeAsync = "ExeAsync=true\n";
+ } else {
+ exeArg = "ExeArg=post-update-sync\n";
+ exeAsync = "ExeAsync=false\n";
+ }
+ }
+
+ if (AppConstants.platform == "win" && aExeRelPathPrefix) {
+ aExeRelPathPrefix = aExeRelPathPrefix.replace("/", "\\");
+ }
+
+ let exeRelPathMac =
+ "ExeRelPath=" +
+ aExeRelPathPrefix +
+ DIR_RESOURCES +
+ gPostUpdateBinFile +
+ "\n";
+ let exeRelPathWin =
+ "ExeRelPath=" + aExeRelPathPrefix + gPostUpdateBinFile + "\n";
+ let updaterIniContents =
+ "[Strings]\n" +
+ "Title=Update Test\n" +
+ "Info=Running update test " +
+ gTestID +
+ "\n\n" +
+ "[PostUpdateMac]\n" +
+ exeRelPathMac +
+ exeArg +
+ exeAsync +
+ "\n" +
+ "[PostUpdateWin]\n" +
+ exeRelPathWin +
+ exeArg +
+ exeAsync;
+ let updaterIni = getApplyDirFile(DIR_RESOURCES + FILE_UPDATER_INI);
+ writeFile(updaterIni, updaterIniContents);
+}
+
+/**
+ * Gets the message log path used for assert checks to lessen the length printed
+ * to the log file.
+ *
+ * @param aPath
+ * The path to shorten for the log file.
+ * @return the message including the shortened path for the log file.
+ */
+function getMsgPath(aPath) {
+ return ", path: " + replaceLogPaths(aPath);
+}
+
+/**
+ * Helper function that replaces the common part of paths in the update log's
+ * contents with <test_dir_path> for paths to the the test directory and
+ * <update_dir_path> for paths to the update directory. This is needed since
+ * Assert.equal will truncate what it prints to the xpcshell log file.
+ *
+ * @param aLogContents
+ * The update log file's contents.
+ * @return the log contents with the paths replaced.
+ */
+function replaceLogPaths(aLogContents) {
+ let logContents = aLogContents;
+ // Remove the majority of the path up to the test directory. This is needed
+ // since Assert.equal won't print long strings to the test logs.
+ let testDirPath = getApplyDirFile().parent.path;
+ if (AppConstants.platform == "win") {
+ // Replace \\ with \\\\ so the regexp works.
+ testDirPath = testDirPath.replace(/\\/g, "\\\\");
+ }
+ logContents = logContents.replace(
+ new RegExp(testDirPath, "g"),
+ "<test_dir_path>/" + gTestID
+ );
+ let updatesDirPath = getMockUpdRootD().path;
+ if (AppConstants.platform == "win") {
+ // Replace \\ with \\\\ so the regexp works.
+ updatesDirPath = updatesDirPath.replace(/\\/g, "\\\\");
+ }
+ logContents = logContents.replace(
+ new RegExp(updatesDirPath, "g"),
+ "<update_dir_path>/" + gTestID
+ );
+ if (AppConstants.platform == "win") {
+ // Replace \ with /
+ logContents = logContents.replace(/\\/g, "/");
+ }
+ return logContents;
+}
+
+/**
+ * Helper function that removes the timestamps in the update log
+ *
+ * @param aLogContents
+ * The update log file's contents.
+ * @return the log contents without timestamps
+ */
+function removeTimeStamps(aLogContents) {
+ return aLogContents.replace(
+ /^\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}[+-]\d{4}: /gm,
+ ""
+ );
+}
+
+/**
+ * Helper function for updater binary tests for verifying the contents of the
+ * update log after a successful update.
+ *
+ * @param aCompareLogFile
+ * The log file to compare the update log with.
+ * @param aStaged
+ * If the update log file is for a staged update.
+ * @param aReplace
+ * If the update log file is for a replace update.
+ * @param aExcludeDistDir
+ * Removes lines containing the distribution directory from the log
+ * file to compare the update log with.
+ */
+function checkUpdateLogContents(
+ aCompareLogFile,
+ aStaged = false,
+ aReplace = false,
+ aExcludeDistDir = false
+) {
+ if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") {
+ // The order that files are returned when enumerating the file system on
+ // Linux and Mac is not deterministic so skip checking the logs.
+ return;
+ }
+
+ let updateLog = getUpdateDirFile(FILE_LAST_UPDATE_LOG);
+ let updateLogContents = readFileBytes(updateLog);
+
+ // Remove leading timestamps
+ updateLogContents = removeTimeStamps(updateLogContents);
+
+ // The channel-prefs.js is defined in gTestFilesCommon which will always be
+ // located to the end of gTestFiles when it is present.
+ if (
+ gTestFiles.length > 1 &&
+ gTestFiles[gTestFiles.length - 1].fileName == "channel-prefs.js" &&
+ !gTestFiles[gTestFiles.length - 1].originalContents
+ ) {
+ updateLogContents = updateLogContents.replace(/.*defaults\/.*/g, "");
+ }
+
+ if (
+ gTestFiles.length > 2 &&
+ gTestFiles[gTestFiles.length - 2].fileName == FILE_UPDATE_SETTINGS_INI &&
+ !gTestFiles[gTestFiles.length - 2].originalContents
+ ) {
+ updateLogContents = updateLogContents.replace(
+ /.*update-settings.ini.*/g,
+ ""
+ );
+ }
+
+ // Skip the source/destination lines since they contain absolute paths.
+ // These could be changed to relative paths using <test_dir_path> and
+ // <update_dir_path>
+ updateLogContents = updateLogContents.replace(/PATCH DIRECTORY.*/g, "");
+ updateLogContents = updateLogContents.replace(
+ /INSTALLATION DIRECTORY.*/g,
+ ""
+ );
+ updateLogContents = updateLogContents.replace(/WORKING DIRECTORY.*/g, "");
+ // Skip lines that log failed attempts to open the callback executable.
+ updateLogContents = updateLogContents.replace(
+ /NS_main: callback app file .*/g,
+ ""
+ );
+ // Remove carriage returns.
+ updateLogContents = updateLogContents.replace(/\r/g, "");
+
+ if (AppConstants.platform == "win") {
+ // The FindFile results when enumerating the filesystem on Windows is not
+ // determistic so the results matching the following need to be fixed.
+ let re = new RegExp(
+ // eslint-disable-next-line no-control-regex
+ "([^\n]* 7/7text1[^\n]*)\n([^\n]* 7/7text0[^\n]*)\n",
+ "g"
+ );
+ updateLogContents = updateLogContents.replace(re, "$2\n$1\n");
+ }
+
+ if (aReplace) {
+ // Remove the lines which contain absolute paths
+ updateLogContents = updateLogContents.replace(/^Begin moving.*$/gm, "");
+ updateLogContents = updateLogContents.replace(
+ /^ensure_remove: failed to remove file: .*$/gm,
+ ""
+ );
+ updateLogContents = updateLogContents.replace(
+ /^ensure_remove_recursive: unable to remove directory: .*$/gm,
+ ""
+ );
+ updateLogContents = updateLogContents.replace(
+ /^Removing tmpDir failed, err: -1$/gm,
+ ""
+ );
+ updateLogContents = updateLogContents.replace(
+ /^remove_recursive_on_reboot: .*$/gm,
+ ""
+ );
+ // Replace requests will retry renaming the installation directory 10 times
+ // when there are files still in use. The following will remove the
+ // additional entries from the log file when this happens so the log check
+ // passes.
+ let re = new RegExp(
+ ERR_RENAME_FILE +
+ "[^\n]*\n" +
+ "PerformReplaceRequest: destDir rename[^\n]*\n" +
+ "rename_file: proceeding to rename the directory\n",
+ "g"
+ );
+ updateLogContents = updateLogContents.replace(re, "");
+ }
+
+ // Replace error codes since they are different on each platform.
+ updateLogContents = updateLogContents.replace(/, err:.*\n/g, "\n");
+ // Replace to make the log parsing happy.
+ updateLogContents = updateLogContents.replace(/non-fatal error /g, "");
+ // Remove consecutive newlines
+ updateLogContents = updateLogContents.replace(/\n+/g, "\n");
+ // Remove leading and trailing newlines
+ updateLogContents = updateLogContents.replace(/^\n|\n$/g, "");
+ // Replace the log paths with <test_dir_path> and <update_dir_path>
+ updateLogContents = replaceLogPaths(updateLogContents);
+
+ let compareLogContents = "";
+ if (aCompareLogFile) {
+ compareLogContents = readFileBytes(getTestDirFile(aCompareLogFile));
+ }
+
+ if (aStaged) {
+ compareLogContents = PERFORMING_STAGED_UPDATE + "\n" + compareLogContents;
+ }
+
+ // Remove leading timestamps
+ compareLogContents = removeTimeStamps(compareLogContents);
+
+ // The channel-prefs.js is defined in gTestFilesCommon which will always be
+ // located to the end of gTestFiles.
+ if (
+ gTestFiles.length > 1 &&
+ gTestFiles[gTestFiles.length - 1].fileName == "channel-prefs.js" &&
+ !gTestFiles[gTestFiles.length - 1].originalContents
+ ) {
+ compareLogContents = compareLogContents.replace(/.*defaults\/.*/g, "");
+ }
+
+ if (
+ gTestFiles.length > 2 &&
+ gTestFiles[gTestFiles.length - 2].fileName == FILE_UPDATE_SETTINGS_INI &&
+ !gTestFiles[gTestFiles.length - 2].originalContents
+ ) {
+ compareLogContents = compareLogContents.replace(
+ /.*update-settings.ini.*/g,
+ ""
+ );
+ }
+
+ if (aExcludeDistDir) {
+ compareLogContents = compareLogContents.replace(/.*distribution\/.*/g, "");
+ }
+
+ // Remove leading and trailing newlines
+ compareLogContents = compareLogContents.replace(/\n+/g, "\n");
+ // Remove leading and trailing newlines
+ compareLogContents = compareLogContents.replace(/^\n|\n$/g, "");
+
+ // Don't write the contents of the file to the log to reduce log spam
+ // unless there is a failure.
+ if (compareLogContents == updateLogContents) {
+ Assert.ok(true, "the update log contents" + MSG_SHOULD_EQUAL);
+ } else {
+ logTestInfo("the update log contents are not correct");
+ logUpdateLog(FILE_LAST_UPDATE_LOG);
+ let aryLog = updateLogContents.split("\n");
+ let aryCompare = compareLogContents.split("\n");
+ // Pushing an empty string to both arrays makes it so either array's length
+ // can be used in the for loop below without going out of bounds.
+ aryLog.push("");
+ aryCompare.push("");
+ // xpcshell tests won't display the entire contents so log the first
+ // incorrect line.
+ for (let i = 0; i < aryLog.length; ++i) {
+ if (aryLog[i] != aryCompare[i]) {
+ logTestInfo(
+ "the first incorrect line is line #" +
+ i +
+ " and the " +
+ "value is: '" +
+ aryLog[i] +
+ "'"
+ );
+ Assert.equal(
+ aryLog[i],
+ aryCompare[i],
+ "the update log contents" + MSG_SHOULD_EQUAL
+ );
+ }
+ }
+ // This should never happen!
+ do_throw("Unable to find incorrect update log contents!");
+ }
+}
+
+/**
+ * Helper function to check if the update log contains a string.
+ *
+ * @param aCheckString
+ * The string to check if the update log contains.
+ */
+function checkUpdateLogContains(aCheckString) {
+ let updateLog = getUpdateDirFile(FILE_LAST_UPDATE_LOG);
+ let updateLogContents = readFileBytes(updateLog).replace(/\r\n/g, "\n");
+ updateLogContents = removeTimeStamps(updateLogContents);
+ updateLogContents = replaceLogPaths(updateLogContents);
+ Assert.notEqual(
+ updateLogContents.indexOf(aCheckString),
+ -1,
+ "the update log '" +
+ updateLog +
+ "' contents should contain value: '" +
+ aCheckString +
+ "'"
+ );
+}
+
+/**
+ * Helper function for updater binary tests for verifying the state of files and
+ * directories after a successful update.
+ *
+ * @param aGetFileFunc
+ * The function used to get the files in the directory to be checked.
+ * @param aStageDirExists
+ * If true the staging directory will be tested for existence and if
+ * false the staging directory will be tested for non-existence.
+ * @param aToBeDeletedDirExists
+ * On Windows, if true the tobedeleted directory will be tested for
+ * existence and if false the tobedeleted directory will be tested for
+ * non-existence. On all othere platforms it will be tested for
+ * non-existence.
+ */
+function checkFilesAfterUpdateSuccess(
+ aGetFileFunc,
+ aStageDirExists = false,
+ aToBeDeletedDirExists = false
+) {
+ debugDump("testing contents of files after a successful update");
+ gTestFiles.forEach(function CFAUS_TF_FE(aTestFile) {
+ let testFile = aGetFileFunc(
+ aTestFile.relPathDir + aTestFile.fileName,
+ true
+ );
+ debugDump("testing file: " + testFile.path);
+ if (aTestFile.compareFile || aTestFile.compareContents) {
+ Assert.ok(
+ testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path)
+ );
+
+ // Skip these tests on Windows since chmod doesn't really set permissions
+ // on Windows.
+ if (AppConstants.platform != "win" && aTestFile.comparePerms) {
+ // Check if the permssions as set in the complete mar file are correct.
+ Assert.equal(
+ "0o" + (testFile.permissions & 0xfff).toString(8),
+ "0o" + (aTestFile.comparePerms & 0xfff).toString(8),
+ "the file permissions" + MSG_SHOULD_EQUAL
+ );
+ }
+
+ let fileContents1 = readFileBytes(testFile);
+ let fileContents2 = aTestFile.compareFile
+ ? readFileBytes(getTestDirFile(aTestFile.compareFile))
+ : aTestFile.compareContents;
+ // Don't write the contents of the file to the log to reduce log spam
+ // unless there is a failure.
+ if (fileContents1 == fileContents2) {
+ Assert.ok(true, "the file contents" + MSG_SHOULD_EQUAL);
+ } else {
+ Assert.equal(
+ fileContents1,
+ fileContents2,
+ "the file contents" + MSG_SHOULD_EQUAL
+ );
+ }
+ } else {
+ Assert.ok(
+ !testFile.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path)
+ );
+ }
+ });
+
+ debugDump(
+ "testing operations specified in removed-files were performed " +
+ "after a successful update"
+ );
+ gTestDirs.forEach(function CFAUS_TD_FE(aTestDir) {
+ let testDir = aGetFileFunc(aTestDir.relPathDir, true);
+ debugDump("testing directory: " + testDir.path);
+ if (aTestDir.dirRemoved) {
+ Assert.ok(
+ !testDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(testDir.path)
+ );
+ } else {
+ Assert.ok(testDir.exists(), MSG_SHOULD_EXIST + getMsgPath(testDir.path));
+
+ if (aTestDir.files) {
+ aTestDir.files.forEach(function CFAUS_TD_F_FE(aTestFile) {
+ let testFile = aGetFileFunc(aTestDir.relPathDir + aTestFile, true);
+ if (aTestDir.filesRemoved) {
+ Assert.ok(
+ !testFile.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path)
+ );
+ } else {
+ Assert.ok(
+ testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path)
+ );
+ }
+ });
+ }
+
+ if (aTestDir.subDirs) {
+ aTestDir.subDirs.forEach(function CFAUS_TD_SD_FE(aSubDir) {
+ let testSubDir = aGetFileFunc(aTestDir.relPathDir + aSubDir, true);
+ Assert.ok(
+ testSubDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testSubDir.path)
+ );
+ if (aTestDir.subDirFiles) {
+ aTestDir.subDirFiles.forEach(function CFAUS_TD_SDF_FE(aTestFile) {
+ let testFile = aGetFileFunc(
+ aTestDir.relPathDir + aSubDir + aTestFile,
+ true
+ );
+ Assert.ok(
+ testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path)
+ );
+ });
+ }
+ });
+ }
+ }
+ });
+
+ checkFilesAfterUpdateCommon(aStageDirExists, aToBeDeletedDirExists);
+}
+
+/**
+ * Helper function for updater binary tests for verifying the state of files and
+ * directories after a failed update.
+ *
+ * @param aGetFileFunc
+ * The function used to get the files in the directory to be checked.
+ * @param aStageDirExists
+ * If true the staging directory will be tested for existence and if
+ * false the staging directory will be tested for non-existence.
+ * @param aToBeDeletedDirExists
+ * On Windows, if true the tobedeleted directory will be tested for
+ * existence and if false the tobedeleted directory will be tested for
+ * non-existence. On all othere platforms it will be tested for
+ * non-existence.
+ */
+function checkFilesAfterUpdateFailure(
+ aGetFileFunc,
+ aStageDirExists = false,
+ aToBeDeletedDirExists = false
+) {
+ debugDump("testing contents of files after a failed update");
+ gTestFiles.forEach(function CFAUF_TF_FE(aTestFile) {
+ let testFile = aGetFileFunc(
+ aTestFile.relPathDir + aTestFile.fileName,
+ true
+ );
+ debugDump("testing file: " + testFile.path);
+ if (aTestFile.compareFile || aTestFile.compareContents) {
+ Assert.ok(
+ testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path)
+ );
+
+ // Skip these tests on Windows since chmod doesn't really set permissions
+ // on Windows.
+ if (AppConstants.platform != "win" && aTestFile.comparePerms) {
+ // Check the original permssions are retained on the file.
+ Assert.equal(
+ testFile.permissions & 0xfff,
+ aTestFile.comparePerms & 0xfff,
+ "the file permissions" + MSG_SHOULD_EQUAL
+ );
+ }
+
+ let fileContents1 = readFileBytes(testFile);
+ let fileContents2 = aTestFile.compareFile
+ ? readFileBytes(getTestDirFile(aTestFile.compareFile))
+ : aTestFile.compareContents;
+ // Don't write the contents of the file to the log to reduce log spam
+ // unless there is a failure.
+ if (fileContents1 == fileContents2) {
+ Assert.ok(true, "the file contents" + MSG_SHOULD_EQUAL);
+ } else {
+ Assert.equal(
+ fileContents1,
+ fileContents2,
+ "the file contents" + MSG_SHOULD_EQUAL
+ );
+ }
+ } else {
+ Assert.ok(
+ !testFile.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(testFile.path)
+ );
+ }
+ });
+
+ debugDump(
+ "testing operations specified in removed-files were not " +
+ "performed after a failed update"
+ );
+ gTestDirs.forEach(function CFAUF_TD_FE(aTestDir) {
+ let testDir = aGetFileFunc(aTestDir.relPathDir, true);
+ Assert.ok(testDir.exists(), MSG_SHOULD_EXIST + getMsgPath(testDir.path));
+
+ if (aTestDir.files) {
+ aTestDir.files.forEach(function CFAUS_TD_F_FE(aTestFile) {
+ let testFile = aGetFileFunc(aTestDir.relPathDir + aTestFile, true);
+ Assert.ok(
+ testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path)
+ );
+ });
+ }
+
+ if (aTestDir.subDirs) {
+ aTestDir.subDirs.forEach(function CFAUS_TD_SD_FE(aSubDir) {
+ let testSubDir = aGetFileFunc(aTestDir.relPathDir + aSubDir, true);
+ Assert.ok(
+ testSubDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testSubDir.path)
+ );
+ if (aTestDir.subDirFiles) {
+ aTestDir.subDirFiles.forEach(function CFAUS_TD_SDF_FE(aTestFile) {
+ let testFile = aGetFileFunc(
+ aTestDir.relPathDir + aSubDir + aTestFile,
+ true
+ );
+ Assert.ok(
+ testFile.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(testFile.path)
+ );
+ });
+ }
+ });
+ }
+ });
+
+ checkFilesAfterUpdateCommon(aStageDirExists, aToBeDeletedDirExists);
+}
+
+/**
+ * Helper function for updater binary tests for verifying the state of common
+ * files and directories after a successful or failed update.
+ *
+ * @param aStageDirExists
+ * If true the staging directory will be tested for existence and if
+ * false the staging directory will be tested for non-existence.
+ * @param aToBeDeletedDirExists
+ * On Windows, if true the tobedeleted directory will be tested for
+ * existence and if false the tobedeleted directory will be tested for
+ * non-existence. On all othere platforms it will be tested for
+ * non-existence.
+ */
+function checkFilesAfterUpdateCommon(aStageDirExists, aToBeDeletedDirExists) {
+ debugDump("testing extra directories");
+ let stageDir = getStageDirFile();
+ if (aStageDirExists) {
+ Assert.ok(stageDir.exists(), MSG_SHOULD_EXIST + getMsgPath(stageDir.path));
+ } else {
+ Assert.ok(
+ !stageDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(stageDir.path)
+ );
+ }
+
+ let toBeDeletedDirExists =
+ AppConstants.platform == "win" ? aToBeDeletedDirExists : false;
+ let toBeDeletedDir = getApplyDirFile(DIR_TOBEDELETED);
+ if (toBeDeletedDirExists) {
+ Assert.ok(
+ toBeDeletedDir.exists(),
+ MSG_SHOULD_EXIST + getMsgPath(toBeDeletedDir.path)
+ );
+ } else {
+ Assert.ok(
+ !toBeDeletedDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(toBeDeletedDir.path)
+ );
+ }
+
+ let updatingDir = getApplyDirFile("updating");
+ Assert.ok(
+ !updatingDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(updatingDir.path)
+ );
+
+ if (stageDir.exists()) {
+ updatingDir = stageDir.clone();
+ updatingDir.append("updating");
+ Assert.ok(
+ !updatingDir.exists(),
+ MSG_SHOULD_NOT_EXIST + getMsgPath(updatingDir.path)
+ );
+ }
+
+ debugDump(
+ "testing backup files should not be left behind in the " +
+ "application directory"
+ );
+ let applyToDir = getApplyDirFile();
+ checkFilesInDirRecursive(applyToDir, checkForBackupFiles);
+
+ if (stageDir.exists()) {
+ debugDump(
+ "testing backup files should not be left behind in the " +
+ "staging directory"
+ );
+ checkFilesInDirRecursive(stageDir, checkForBackupFiles);
+ }
+}
+
+/**
+ * Helper function for updater binary tests for verifying the contents of the
+ * updater callback application log which should contain the arguments passed to
+ * the callback application.
+ *
+ * @param appLaunchLog (optional)
+ * The application log nsIFile to verify. Defaults to the second
+ * parameter passed to the callback executable (in the apply directory).
+ */
+function checkCallbackLog(
+ appLaunchLog = getApplyDirFile(DIR_RESOURCES + gCallbackArgs[1])
+) {
+ if (!appLaunchLog.exists()) {
+ // Uses do_timeout instead of do_execute_soon to lessen log spew.
+ do_timeout(FILE_IN_USE_TIMEOUT_MS, checkCallbackLog);
+ return;
+ }
+
+ let expectedLogContents = gCallbackArgs.join("\n") + "\n";
+ let logContents = readFile(appLaunchLog);
+ // It is possible for the log file contents check to occur before the log file
+ // contents are completely written so wait until the contents are the expected
+ // value. If the contents are never the expected value then the test will
+ // fail by timing out after gTimeoutRuns is greater than MAX_TIMEOUT_RUNS or
+ // the test harness times out the test.
+ const MAX_TIMEOUT_RUNS = 20000;
+ if (logContents != expectedLogContents) {
+ gTimeoutRuns++;
+ if (gTimeoutRuns > MAX_TIMEOUT_RUNS) {
+ logTestInfo("callback log contents are not correct");
+ // This file doesn't contain full paths so there is no need to call
+ // replaceLogPaths.
+ let aryLog = logContents.split("\n");
+ let aryCompare = expectedLogContents.split("\n");
+ // Pushing an empty string to both arrays makes it so either array's length
+ // can be used in the for loop below without going out of bounds.
+ aryLog.push("");
+ aryCompare.push("");
+ // xpcshell tests won't display the entire contents so log the incorrect
+ // line.
+ for (let i = 0; i < aryLog.length; ++i) {
+ if (aryLog[i] != aryCompare[i]) {
+ logTestInfo(
+ "the first incorrect line in the callback log is: " + aryLog[i]
+ );
+ Assert.equal(
+ aryLog[i],
+ aryCompare[i],
+ "the callback log contents" + MSG_SHOULD_EQUAL
+ );
+ }
+ }
+ // This should never happen!
+ do_throw("Unable to find incorrect callback log contents!");
+ }
+ // Uses do_timeout instead of do_execute_soon to lessen log spew.
+ do_timeout(FILE_IN_USE_TIMEOUT_MS, checkCallbackLog);
+ return;
+ }
+ Assert.ok(true, "the callback log contents" + MSG_SHOULD_EQUAL);
+
+ waitForFilesInUse();
+}
+
+/**
+ * Helper function for updater binary tests for getting the log and running
+ * files created by the test helper binary file when called with the post-update
+ * command line argument.
+ *
+ * @param aSuffix
+ * The string to append to the post update test helper binary path.
+ */
+function getPostUpdateFile(aSuffix) {
+ return getApplyDirFile(DIR_RESOURCES + gPostUpdateBinFile + aSuffix);
+}
+
+/**
+ * Checks the contents of the updater post update binary log. When completed
+ * checkPostUpdateAppLogFinished will be called.
+ */
+async function checkPostUpdateAppLog() {
+ // Only Mac OS X and Windows support post update.
+ if (AppConstants.platform == "macosx" || AppConstants.platform == "win") {
+ let file = getPostUpdateFile(".log");
+ await TestUtils.waitForCondition(
+ () => file.exists(),
+ "Waiting for file to exist, path: " + file.path
+ );
+
+ let expectedContents = "post-update\n";
+ await TestUtils.waitForCondition(
+ () => readFile(file) == expectedContents,
+ "Waiting for expected file contents: " + expectedContents
+ );
+ }
+}
+
+/**
+ * Helper function to check if a file is in use on Windows by making a copy of
+ * a file and attempting to delete the original file. If the deletion is
+ * successful the copy of the original file is renamed to the original file's
+ * name and if the deletion is not successful the copy of the original file is
+ * deleted.
+ *
+ * @param aFile
+ * An nsIFile for the file to be checked if it is in use.
+ * @return true if the file can't be deleted and false otherwise.
+ * @throws If called from a platform other than Windows.
+ */
+function isFileInUse(aFile) {
+ if (AppConstants.platform != "win") {
+ do_throw("Windows only function called by a different platform!");
+ }
+
+ if (!aFile.exists()) {
+ debugDump("file does not exist, path: " + aFile.path);
+ return false;
+ }
+
+ let fileBak = aFile.parent;
+ fileBak.append(aFile.leafName + ".bak");
+ try {
+ if (fileBak.exists()) {
+ fileBak.remove(false);
+ }
+ aFile.copyTo(aFile.parent, fileBak.leafName);
+ aFile.remove(false);
+ fileBak.moveTo(aFile.parent, aFile.leafName);
+ debugDump("file is not in use, path: " + aFile.path);
+ return false;
+ } catch (e) {
+ debugDump("file in use, path: " + aFile.path + ", Exception: " + e);
+ try {
+ if (fileBak.exists()) {
+ fileBak.remove(false);
+ }
+ } catch (ex) {
+ logTestInfo(
+ "unable to remove backup file, path: " +
+ fileBak.path +
+ ", Exception: " +
+ ex
+ );
+ }
+ }
+ return true;
+}
+
+/**
+ * Waits until files that are in use that break tests are no longer in use and
+ * then calls doTestFinish to end the test.
+ */
+function waitForFilesInUse() {
+ if (AppConstants.platform == "win") {
+ let fileNames = [
+ FILE_APP_BIN,
+ FILE_UPDATER_BIN,
+ FILE_MAINTENANCE_SERVICE_INSTALLER_BIN,
+ ];
+ for (let i = 0; i < fileNames.length; ++i) {
+ let file = getApplyDirFile(fileNames[i]);
+ if (isFileInUse(file)) {
+ do_timeout(FILE_IN_USE_TIMEOUT_MS, waitForFilesInUse);
+ return;
+ }
+ }
+ }
+
+ debugDump("calling doTestFinish");
+ doTestFinish();
+}
+
+/**
+ * Helper function for updater binary tests for verifying there are no update
+ * backup files left behind after an update.
+ *
+ * @param aFile
+ * An nsIFile to check if it has moz-backup for its extension.
+ */
+function checkForBackupFiles(aFile) {
+ Assert.notEqual(
+ getFileExtension(aFile),
+ "moz-backup",
+ "the file's extension should not equal moz-backup" + getMsgPath(aFile.path)
+ );
+}
+
+/**
+ * Helper function for updater binary tests for recursively enumerating a
+ * directory and calling a callback function with the file as a parameter for
+ * each file found.
+ *
+ * @param aDir
+ * A nsIFile for the directory to be deleted
+ * @param aCallback
+ * A callback function that will be called with the file as a
+ * parameter for each file found.
+ */
+function checkFilesInDirRecursive(aDir, aCallback) {
+ if (!aDir.exists()) {
+ do_throw("Directory must exist!");
+ }
+
+ let dirEntries = aDir.directoryEntries;
+ while (dirEntries.hasMoreElements()) {
+ let entry = dirEntries.nextFile;
+
+ if (entry.exists()) {
+ if (entry.isDirectory()) {
+ checkFilesInDirRecursive(entry, aCallback);
+ } else {
+ aCallback(entry);
+ }
+ }
+ }
+}
+
+/**
+ * Waits for an update check request to complete and asserts that the results
+ * are as-expected.
+ *
+ * @param aSuccess
+ * Whether the update check succeeds or not. If aSuccess is true then
+ * the check should succeed and if aSuccess is false then the check
+ * should fail.
+ * @param aExpectedValues
+ * An object with common values to check.
+ * @return A promise which will resolve with the nsIUpdateCheckResult object
+ * once the update check is complete.
+ */
+async function waitForUpdateCheck(aSuccess, aExpectedValues = {}) {
+ let check = gUpdateChecker.checkForUpdates(gUpdateChecker.FOREGROUND_CHECK);
+ let result = await check.result;
+ Assert.ok(result.checksAllowed, "We should be able to check for updates");
+ Assert.equal(
+ result.succeeded,
+ aSuccess,
+ "the update check should " + (aSuccess ? "succeed" : "error")
+ );
+ if (aExpectedValues.updateCount) {
+ Assert.equal(
+ aExpectedValues.updateCount,
+ result.updates.length,
+ "the update count" + MSG_SHOULD_EQUAL
+ );
+ }
+ if (aExpectedValues.url) {
+ Assert.equal(
+ aExpectedValues.url,
+ result.request.channel.originalURI.spec,
+ "the url" + MSG_SHOULD_EQUAL
+ );
+ }
+ return result;
+}
+
+/**
+ * Downloads an update and waits for the download onStopRequest.
+ *
+ * @param aUpdates
+ * An array of updates to select from to download an update.
+ * @param aUpdateCount
+ * The number of updates in the aUpdates array.
+ * @param aExpectedStatus
+ * The download onStopRequest expected status.
+ * @return A promise which will resolve the first time the update download
+ * onStopRequest occurs and returns the arguments from onStopRequest.
+ */
+async function waitForUpdateDownload(aUpdates, aExpectedStatus) {
+ let bestUpdate = gAUS.selectUpdate(aUpdates);
+ let success = await gAUS.downloadUpdate(bestUpdate, false);
+ if (!success) {
+ do_throw("nsIApplicationUpdateService:downloadUpdate returned " + success);
+ }
+ return new Promise(resolve =>
+ gAUS.addDownloadListener({
+ onStartRequest: aRequest => {},
+ onProgress: (aRequest, aContext, aProgress, aMaxProgress) => {},
+ onStatus: (aRequest, aStatus, aStatusText) => {},
+ onStopRequest(request, status) {
+ gAUS.removeDownloadListener(this);
+ Assert.equal(
+ aExpectedStatus,
+ status,
+ "the download status" + MSG_SHOULD_EQUAL
+ );
+ resolve(request, status);
+ },
+ QueryInterface: ChromeUtils.generateQI([
+ "nsIRequestObserver",
+ "nsIProgressEventSink",
+ ]),
+ })
+ );
+}
+
+/**
+ * Helper for starting the http server used by the tests
+ */
+function start_httpserver() {
+ let dir = getTestDirFile();
+ debugDump("http server directory path: " + dir.path);
+
+ if (!dir.isDirectory()) {
+ do_throw(
+ "A file instead of a directory was specified for HttpServer " +
+ "registerDirectory! Path: " +
+ dir.path
+ );
+ }
+
+ let { HttpServer } = ChromeUtils.import("resource://testing-common/httpd.js");
+ gTestserver = new HttpServer();
+ gTestserver.registerDirectory("/", dir);
+ gTestserver.registerPathHandler("/" + gHTTPHandlerPath, pathHandler);
+ gTestserver.start(-1);
+ let testserverPort = gTestserver.identity.primaryPort;
+ // eslint-disable-next-line no-global-assign
+ gURLData = URL_HOST + ":" + testserverPort + "/";
+ debugDump("http server port = " + testserverPort);
+}
+
+/**
+ * Custom path handler for the http server
+ *
+ * @param aMetadata
+ * The http metadata for the request.
+ * @param aResponse
+ * The http response for the request.
+ */
+function pathHandler(aMetadata, aResponse) {
+ gUpdateCheckCount += 1;
+ aResponse.setHeader("Content-Type", "text/xml", false);
+ aResponse.setStatusLine(aMetadata.httpVersion, 200, "OK");
+ aResponse.bodyOutputStream.write(gResponseBody, gResponseBody.length);
+}
+
+/**
+ * Helper for stopping the http server used by the tests
+ *
+ * @param aCallback
+ * The callback to call after stopping the http server.
+ */
+function stop_httpserver(aCallback) {
+ Assert.ok(!!aCallback, "the aCallback parameter should be defined");
+ gTestserver.stop(aCallback);
+}
+
+/**
+ * Creates an nsIXULAppInfo
+ *
+ * @param aID
+ * The ID of the test application
+ * @param aName
+ * A name for the test application
+ * @param aVersion
+ * The version of the application
+ * @param aPlatformVersion
+ * The gecko version of the application
+ */
+function createAppInfo(aID, aName, aVersion, aPlatformVersion) {
+ updateAppInfo({
+ vendor: APP_INFO_VENDOR,
+ name: aName,
+ ID: aID,
+ version: aVersion,
+ appBuildID: "2007010101",
+ platformVersion: aPlatformVersion,
+ platformBuildID: "2007010101",
+ inSafeMode: false,
+ logConsoleErrors: true,
+ OS: "XPCShell",
+ XPCOMABI: "noarch-spidermonkey",
+ });
+}
+
+/**
+ * Returns the platform specific arguments used by nsIProcess when launching
+ * the application.
+ *
+ * @param aExtraArgs (optional)
+ * An array of extra arguments to append to the default arguments.
+ * @return an array of arguments to be passed to nsIProcess.
+ *
+ * Note: a shell is necessary to pipe the application's console output which
+ * would otherwise pollute the xpcshell log.
+ *
+ * Command line arguments used when launching the application:
+ * -no-remote prevents shell integration from being affected by an existing
+ * application process.
+ * -test-process-updates makes the application exit after being relaunched by
+ * the updater.
+ * the platform specific string defined by PIPE_TO_NULL to output both stdout
+ * and stderr to null. This is needed to prevent output from the application
+ * from ending up in the xpchsell log.
+ */
+function getProcessArgs(aExtraArgs) {
+ if (!aExtraArgs) {
+ aExtraArgs = [];
+ }
+
+ let appBin = getApplyDirFile(DIR_MACOS + FILE_APP_BIN);
+ Assert.ok(appBin.exists(), MSG_SHOULD_EXIST + ", path: " + appBin.path);
+ let appBinPath = appBin.path;
+
+ // The profile must be specified for the tests that launch the application to
+ // run locally when the profiles.ini and installs.ini files already exist.
+ // We can't use getApplyDirFile to find the profile path because on Windows
+ // for service tests that would place the profile inside Program Files, and
+ // this test script has permission to write in Program Files, but the
+ // application may drop those permissions. So for Windows service tests we
+ // override that path with the per-test temp directory that xpcshell provides,
+ // which should be user writable.
+ let profileDir = appBin.parent.parent;
+ if (gIsServiceTest && IS_AUTHENTICODE_CHECK_ENABLED) {
+ profileDir = do_get_tempdir();
+ }
+ profileDir.append("profile");
+ let profilePath = profileDir.path;
+
+ let args;
+ if (AppConstants.platform == "macosx" || AppConstants.platform == "linux") {
+ let launchScript = getLaunchScript();
+ // Precreate the script with executable permissions
+ launchScript.create(Ci.nsIFile.NORMAL_FILE_TYPE, PERMS_DIRECTORY);
+
+ let scriptContents = "#! /bin/sh\n";
+ scriptContents += "export XRE_PROFILE_PATH=" + profilePath + "\n";
+ scriptContents +=
+ appBinPath +
+ " -no-remote -test-process-updates " +
+ aExtraArgs.join(" ") +
+ " " +
+ PIPE_TO_NULL;
+ writeFile(launchScript, scriptContents);
+ debugDump(
+ "created " + launchScript.path + " containing:\n" + scriptContents
+ );
+ args = [launchScript.path];
+ } else {
+ args = [
+ "/D",
+ "/Q",
+ "/C",
+ appBinPath,
+ "-profile",
+ profilePath,
+ "-no-remote",
+ "-test-process-updates",
+ "-wait-for-browser",
+ ]
+ .concat(aExtraArgs)
+ .concat([PIPE_TO_NULL]);
+ }
+ return args;
+}
+
+/**
+ * Gets a file path for the application to dump its arguments into. This is used
+ * to verify that a callback application is launched.
+ *
+ * @return the file for the application to dump its arguments into.
+ */
+function getAppArgsLogPath() {
+ let appArgsLog = do_get_file("/" + gTestID + "_app_args_log", true);
+ if (appArgsLog.exists()) {
+ appArgsLog.remove(false);
+ }
+ let appArgsLogPath = appArgsLog.path;
+ if (/ /.test(appArgsLogPath)) {
+ appArgsLogPath = '"' + appArgsLogPath + '"';
+ }
+ return appArgsLogPath;
+}
+
+/**
+ * Gets the nsIFile reference for the shell script to launch the application. If
+ * the file exists it will be removed by this function.
+ *
+ * @return the nsIFile for the shell script to launch the application.
+ */
+function getLaunchScript() {
+ let launchScript = do_get_file("/" + gTestID + "_launch.sh", true);
+ if (launchScript.exists()) {
+ launchScript.remove(false);
+ }
+ return launchScript;
+}
+
+/**
+ * Makes GreD, XREExeF, and UpdRootD point to unique file system locations so
+ * xpcshell tests can run in parallel and to keep the environment clean.
+ */
+function adjustGeneralPaths() {
+ let dirProvider = {
+ getFile: function AGP_DP_getFile(aProp, aPersistent) {
+ // Set the value of persistent to false so when this directory provider is
+ // unregistered it will revert back to the original provider.
+ aPersistent.value = false;
+ switch (aProp) {
+ case NS_GRE_DIR:
+ return getApplyDirFile(DIR_RESOURCES);
+ case NS_GRE_BIN_DIR:
+ return getApplyDirFile(DIR_MACOS);
+ case XRE_EXECUTABLE_FILE:
+ return getApplyDirFile(DIR_MACOS + FILE_APP_BIN);
+ case XRE_UPDATE_ROOT_DIR:
+ return getMockUpdRootD();
+ case XRE_OLD_UPDATE_ROOT_DIR:
+ return getMockUpdRootD(true);
+ }
+ return null;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIDirectoryServiceProvider"]),
+ };
+ let ds = Services.dirsvc.QueryInterface(Ci.nsIDirectoryService);
+ ds.QueryInterface(Ci.nsIProperties).undefine(NS_GRE_DIR);
+ ds.QueryInterface(Ci.nsIProperties).undefine(NS_GRE_BIN_DIR);
+ ds.QueryInterface(Ci.nsIProperties).undefine(XRE_EXECUTABLE_FILE);
+ ds.registerProvider(dirProvider);
+ registerCleanupFunction(function AGP_cleanup() {
+ debugDump("start - unregistering directory provider");
+
+ if (gAppTimer) {
+ debugDump("start - cancel app timer");
+ gAppTimer.cancel();
+ gAppTimer = null;
+ debugDump("finish - cancel app timer");
+ }
+
+ if (gProcess && gProcess.isRunning) {
+ debugDump("start - kill process");
+ try {
+ gProcess.kill();
+ } catch (e) {
+ debugDump("kill process failed, Exception: " + e);
+ }
+ gProcess = null;
+ debugDump("finish - kill process");
+ }
+
+ if (gPIDPersistProcess && gPIDPersistProcess.isRunning) {
+ debugDump("start - kill pid persist process");
+ try {
+ gPIDPersistProcess.kill();
+ } catch (e) {
+ debugDump("kill pid persist process failed, Exception: " + e);
+ }
+ gPIDPersistProcess = null;
+ debugDump("finish - kill pid persist process");
+ }
+
+ if (gHandle) {
+ try {
+ debugDump("start - closing handle");
+ let kernel32 = ctypes.open("kernel32");
+ let CloseHandle = kernel32.declare(
+ "CloseHandle",
+ ctypes.winapi_abi,
+ ctypes.bool /* return*/,
+ ctypes.voidptr_t /* handle*/
+ );
+ if (!CloseHandle(gHandle)) {
+ debugDump("call to CloseHandle failed");
+ }
+ kernel32.close();
+ gHandle = null;
+ debugDump("finish - closing handle");
+ } catch (e) {
+ debugDump("call to CloseHandle failed, Exception: " + e);
+ }
+ }
+
+ ds.unregisterProvider(dirProvider);
+ cleanupTestCommon();
+
+ // Now that our provider is unregistered, reset the lock a second time so
+ // that we know the lock we're interested in gets released (xpcshell
+ // doesn't always run a proper XPCOM shutdown sequence, which is where that
+ // would normally be happening).
+ let syncManager = Cc[
+ "@mozilla.org/updates/update-sync-manager;1"
+ ].getService(Ci.nsIUpdateSyncManager);
+ syncManager.resetLock();
+
+ debugDump("finish - unregistering directory provider");
+ });
+}
+
+/**
+ * The timer callback to kill the process if it takes too long.
+ */
+const gAppTimerCallback = {
+ notify: function TC_notify(aTimer) {
+ gAppTimer = null;
+ if (gProcess.isRunning) {
+ logTestInfo("attempting to kill process");
+ gProcess.kill();
+ }
+ Assert.ok(false, "launch application timer expired");
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsITimerCallback"]),
+};
+
+/**
+ * Launches an application to apply an update.
+ *
+ * @param aExpectedStatus
+ * The expected value of update.status when the update finishes.
+ */
+async function runUpdateUsingApp(aExpectedStatus) {
+ debugDump("start - launching application to apply update");
+
+ // The maximum number of milliseconds the process that is launched can run
+ // before the test will try to kill it.
+ const APP_TIMER_TIMEOUT = 120000;
+ let launchBin = getLaunchBin();
+ let args = getProcessArgs();
+ debugDump("launching " + launchBin.path + " " + args.join(" "));
+
+ gProcess = Cc["@mozilla.org/process/util;1"].createInstance(Ci.nsIProcess);
+ gProcess.init(launchBin);
+
+ gAppTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+ gAppTimer.initWithCallback(
+ gAppTimerCallback,
+ APP_TIMER_TIMEOUT,
+ Ci.nsITimer.TYPE_ONE_SHOT
+ );
+
+ setEnvironment();
+
+ debugDump("launching application");
+ gProcess.run(true, args, args.length);
+ debugDump("launched application exited");
+
+ resetEnvironment();
+
+ if (AppConstants.platform == "win") {
+ waitForApplicationStop(FILE_UPDATER_BIN);
+ }
+
+ let file = getUpdateDirFile(FILE_UPDATE_STATUS);
+ await TestUtils.waitForCondition(
+ () => file.exists(),
+ "Waiting for file to exist, path: " + file.path
+ );
+
+ await TestUtils.waitForCondition(
+ () => readStatusFile() == aExpectedStatus,
+ "Waiting for expected status file contents: " + aExpectedStatus
+ ).catch(e => {
+ // Instead of throwing let the check below fail the test so the status
+ // file's contents are logged.
+ logTestInfo(e);
+ });
+ Assert.equal(
+ readStatusFile(),
+ aExpectedStatus,
+ "the status file state" + MSG_SHOULD_EQUAL
+ );
+
+ // Don't check for an update log when the code in nsUpdateDriver.cpp skips
+ // updating.
+ if (
+ aExpectedStatus != STATE_PENDING &&
+ aExpectedStatus != STATE_PENDING_SVC &&
+ aExpectedStatus != STATE_APPLIED &&
+ aExpectedStatus != STATE_APPLIED_SVC
+ ) {
+ // Don't proceed until the update log has been created.
+ file = getUpdateDirFile(FILE_UPDATE_LOG);
+ await TestUtils.waitForCondition(
+ () => file.exists(),
+ "Waiting for file to exist, path: " + file.path
+ );
+ }
+
+ debugDump("finish - launching application to apply update");
+}
+
+/* This Mock incremental downloader is used to verify that connection interrupts
+ * work correctly in updater code. The implementation of the mock incremental
+ * downloader is very simple, it simply copies the file to the destination
+ * location.
+ */
+function initMockIncrementalDownload() {
+ const INC_CONTRACT_ID = "@mozilla.org/network/incremental-download;1";
+ let incrementalDownloadCID = MockRegistrar.register(
+ INC_CONTRACT_ID,
+ IncrementalDownload
+ );
+ registerCleanupFunction(() => {
+ MockRegistrar.unregister(incrementalDownloadCID);
+ });
+}
+
+function IncrementalDownload() {
+ this.wrappedJSObject = this;
+}
+
+IncrementalDownload.prototype = {
+ /* nsIIncrementalDownload */
+ init(uri, file, chunkSize, intervalInSeconds) {
+ this._destination = file;
+ this._URI = uri;
+ this._finalURI = uri;
+ },
+
+ start(observer, ctxt) {
+ // Do the actual operation async to give a chance for observers to add
+ // themselves.
+ Services.tm.dispatchToMainThread(() => {
+ this._observer = observer.QueryInterface(Ci.nsIRequestObserver);
+ this._ctxt = ctxt;
+ this._observer.onStartRequest(this);
+ let mar = getTestDirFile(FILE_SIMPLE_MAR);
+ mar.copyTo(this._destination.parent, this._destination.leafName);
+ let status = Cr.NS_OK;
+ switch (gIncrementalDownloadErrorType++) {
+ case 0:
+ status = Cr.NS_ERROR_NET_RESET;
+ break;
+ case 1:
+ status = Cr.NS_ERROR_CONNECTION_REFUSED;
+ break;
+ case 2:
+ status = Cr.NS_ERROR_NET_RESET;
+ break;
+ case 3:
+ status = Cr.NS_OK;
+ break;
+ case 4:
+ status = Cr.NS_ERROR_OFFLINE;
+ // After we report offline, we want to eventually show offline
+ // status being changed to online.
+ Services.tm.dispatchToMainThread(function () {
+ Services.obs.notifyObservers(
+ gAUS,
+ "network:offline-status-changed",
+ "online"
+ );
+ });
+ break;
+ }
+ this._observer.onStopRequest(this, status);
+ });
+ },
+
+ get URI() {
+ return this._URI;
+ },
+
+ get currentSize() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ get destination() {
+ return this._destination;
+ },
+
+ get finalURI() {
+ return this._finalURI;
+ },
+
+ get totalSize() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+
+ /* nsIRequest */
+ cancel(aStatus) {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ suspend() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ isPending() {
+ throw Components.Exception("", Cr.NS_ERROR_NOT_IMPLEMENTED);
+ },
+ _loadFlags: 0,
+ get loadFlags() {
+ return this._loadFlags;
+ },
+ set loadFlags(val) {
+ this._loadFlags = val;
+ },
+
+ _loadGroup: null,
+ get loadGroup() {
+ return this._loadGroup;
+ },
+ set loadGroup(val) {
+ this._loadGroup = val;
+ },
+
+ _name: "",
+ get name() {
+ return this._name;
+ },
+
+ _status: 0,
+ get status() {
+ return this._status;
+ },
+ QueryInterface: ChromeUtils.generateQI(["nsIIncrementalDownload"]),
+};
+
+/**
+ * Sets the environment that will be used by the application process when it is
+ * launched.
+ */
+function setEnvironment() {
+ if (AppConstants.platform == "win") {
+ // The tests use nsIProcess to launch the updater and it is simpler to just
+ // set an environment variable and have the test updater set the current
+ // working directory than it is to set the current working directory in the
+ // test itself.
+ Services.env.set("CURWORKDIRPATH", getApplyDirFile().path);
+ }
+
+ // Prevent setting the environment more than once.
+ if (gShouldResetEnv !== undefined) {
+ return;
+ }
+
+ gShouldResetEnv = true;
+
+ if (
+ AppConstants.platform == "win" &&
+ !Services.env.exists("XRE_NO_WINDOWS_CRASH_DIALOG")
+ ) {
+ gAddedEnvXRENoWindowsCrashDialog = true;
+ debugDump(
+ "setting the XRE_NO_WINDOWS_CRASH_DIALOG environment " +
+ "variable to 1... previously it didn't exist"
+ );
+ Services.env.set("XRE_NO_WINDOWS_CRASH_DIALOG", "1");
+ }
+
+ if (Services.env.exists("XPCOM_MEM_LEAK_LOG")) {
+ gEnvXPCOMMemLeakLog = Services.env.get("XPCOM_MEM_LEAK_LOG");
+ debugDump(
+ "removing the XPCOM_MEM_LEAK_LOG environment variable... " +
+ "previous value " +
+ gEnvXPCOMMemLeakLog
+ );
+ Services.env.set("XPCOM_MEM_LEAK_LOG", "");
+ }
+
+ if (Services.env.exists("XPCOM_DEBUG_BREAK")) {
+ gEnvXPCOMDebugBreak = Services.env.get("XPCOM_DEBUG_BREAK");
+ debugDump(
+ "setting the XPCOM_DEBUG_BREAK environment variable to " +
+ "warn... previous value " +
+ gEnvXPCOMDebugBreak
+ );
+ } else {
+ debugDump(
+ "setting the XPCOM_DEBUG_BREAK environment variable to " +
+ "warn... previously it didn't exist"
+ );
+ }
+
+ Services.env.set("XPCOM_DEBUG_BREAK", "warn");
+
+ if (gEnvForceServiceFallback) {
+ // This env variable forces the updater to use the service in an invalid
+ // way, so that it has to fall back to updating without the service.
+ debugDump("setting MOZ_FORCE_SERVICE_FALLBACK environment variable to 1");
+ Services.env.set("MOZ_FORCE_SERVICE_FALLBACK", "1");
+ } else if (gIsServiceTest) {
+ debugDump("setting MOZ_NO_SERVICE_FALLBACK environment variable to 1");
+ Services.env.set("MOZ_NO_SERVICE_FALLBACK", "1");
+ }
+}
+
+/**
+ * Sets the environment back to the original values after launching the
+ * application.
+ */
+function resetEnvironment() {
+ // Prevent resetting the environment more than once.
+ if (gShouldResetEnv !== true) {
+ return;
+ }
+
+ gShouldResetEnv = false;
+
+ if (gEnvXPCOMMemLeakLog) {
+ debugDump(
+ "setting the XPCOM_MEM_LEAK_LOG environment variable back to " +
+ gEnvXPCOMMemLeakLog
+ );
+ Services.env.set("XPCOM_MEM_LEAK_LOG", gEnvXPCOMMemLeakLog);
+ }
+
+ if (gEnvXPCOMDebugBreak) {
+ debugDump(
+ "setting the XPCOM_DEBUG_BREAK environment variable back to " +
+ gEnvXPCOMDebugBreak
+ );
+ Services.env.set("XPCOM_DEBUG_BREAK", gEnvXPCOMDebugBreak);
+ } else if (Services.env.exists("XPCOM_DEBUG_BREAK")) {
+ debugDump("clearing the XPCOM_DEBUG_BREAK environment variable");
+ Services.env.set("XPCOM_DEBUG_BREAK", "");
+ }
+
+ if (AppConstants.platform == "win" && gAddedEnvXRENoWindowsCrashDialog) {
+ debugDump("removing the XRE_NO_WINDOWS_CRASH_DIALOG environment variable");
+ Services.env.set("XRE_NO_WINDOWS_CRASH_DIALOG", "");
+ }
+
+ if (gEnvForceServiceFallback) {
+ debugDump("removing MOZ_FORCE_SERVICE_FALLBACK environment variable");
+ Services.env.set("MOZ_FORCE_SERVICE_FALLBACK", "");
+ } else if (gIsServiceTest) {
+ debugDump("removing MOZ_NO_SERVICE_FALLBACK environment variable");
+ Services.env.set("MOZ_NO_SERVICE_FALLBACK", "");
+ }
+}