Bug 1359558 Part 2 Pass appropriate bootstrap reasons when temporarily installing an addon on top of an existing one draft
authorAndrew Swan <aswan@mozilla.com>
Mon, 31 Jul 2017 15:21:57 -0700
changeset 644582 966d78c37205f131b57eed461df75002873cdf86
parent 618695 9f52818807ff821079b22b6f622acb0211e8bcf5
child 644583 c986ab1ebb631d710900f0ce36bff41124b9d12d
push id73474
push useraswan@mozilla.com
push dateFri, 11 Aug 2017 04:22:18 +0000
bugs1359558
milestone56.0a1
Bug 1359558 Part 2 Pass appropriate bootstrap reasons when temporarily installing an addon on top of an existing one Prior to this patch we always passed APP_SHUTDOWN as the reason when shutting down and uninstall a temporary addon, even if the same addon was installed permanently. Now we send an appropriate reason. Also untangled a bunch of stuff that had been added to the test case for temporary-addon-installed-over-permanently-installed-addon and then extended the basic test to cover this scenario. MozReview-Commit-ID: 7rgfpiRYcFu
toolkit/mozapps/extensions/internal/XPIProvider.jsm
toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
--- a/toolkit/mozapps/extensions/internal/XPIProvider.jsm
+++ b/toolkit/mozapps/extensions/internal/XPIProvider.jsm
@@ -482,16 +482,31 @@ function findMatchingStaticBlocklistItem
         return item;
       }
     }
   }
   return null;
 }
 
 /**
+ * Determine the reason to pass to an extension's bootstrap methods when
+ * switch between versions.
+ *
+ * @param {string} oldVersion The version of the existing extension instance.
+ * @param {string} newVersion The version of the extension being installed.
+ *
+ * @return {BOOSTRAP_REASONS.ADDON_UPGRADE|BOOSTRAP_REASONS.ADDON_DOWNGRADE}
+ */
+function newVersionReason(oldVersion, newVersion) {
+  return Services.vc.compare(oldVersion, newVersion) <= 0 ?
+         BOOTSTRAP_REASONS.ADDON_UPGRADE :
+         BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
+}
+
+/**
  * Converts an iterable of addon objects into a map with the add-on's ID as key.
  */
 function addonMap(addons) {
   return new Map(addons.map(a => [a.id, a]));
 }
 
 /**
  * Helper function that determines whether an addon of a certain type is a
@@ -1677,22 +1692,23 @@ this.XPIStates = {
     let location = this.db.get(aLocation);
     return location && location.get(aId);
   },
 
   /**
    * Find the highest priority location of an add-on by ID and return the
    * location and the XPIState.
    * @param aId   The add-on ID
+   * @param aLocations If specified, the locations to search
    * @return {XPIState?}
    */
-  findAddon(aId) {
+  findAddon(aId, aLocations = this.db.values()) {
     // Fortunately the Map iterator returns in order of insertion, which is
     // also our highest -> lowest priority order.
-    for (let location of this.db.values()) {
+    for (let location of aLocations) {
       if (location.has(aId)) {
         return location.get(aId);
       }
     }
     return undefined;
   },
 
   /**
@@ -2250,23 +2266,30 @@ this.XPIProvider = {
             // pending enable)
             if (!XPIProvider.activeAddons.has(addon.id))
               continue;
 
             let addonDetails = createAddonDetails(addon.id, addon);
 
             // If the add-on was pending disable then shut it down and remove it
             // from the persisted data.
+            let reason = BOOTSTRAP_REASONS.APP_SHUTDOWN;
             if (addon.disable) {
-              XPIProvider.callBootstrapMethod(addonDetails, addon.file, "shutdown",
-                                              BOOTSTRAP_REASONS.ADDON_DISABLE);
-            } else {
-              XPIProvider.callBootstrapMethod(addonDetails, addon.file, "shutdown",
-                                              BOOTSTRAP_REASONS.APP_SHUTDOWN);
+              reason = BOOTSTRAP_REASONS.ADDON_DISABLE;
+            } else if (addon.location.name == KEY_APP_TEMPORARY) {
+              reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
+              let locations = Array.from(XPIStates.db.values())
+                                   .filter(loc => loc.name != TemporaryInstallLocation.name);
+              let existing = XPIStates.findAddon(addon.id, locations);
+              if (existing) {
+                reason = newVersionReason(addon.version, existing.version);
+              }
             }
+            XPIProvider.callBootstrapMethod(addonDetails, addon.file,
+                                            "shutdown", reason);
           }
           Services.obs.removeObserver(this, "quit-application-granted");
         }
       }, "quit-application-granted");
 
       // Detect final-ui-startup for telemetry reporting
       Services.obs.addObserver({
         observe(aSubject, aTopic, aData) {
@@ -2342,31 +2365,38 @@ this.XPIProvider = {
     this.cancelAll();
 
     // Uninstall any temporary add-ons.
     let tempLocation = XPIStates.getLocation(TemporaryInstallLocation.name);
     if (tempLocation) {
       for (let [id, addon] of tempLocation.entries()) {
         tempLocation.delete(id);
 
+        let reason = BOOTSTRAP_REASONS.ADDON_UNINSTALL;
+
+        let locations = Array.from(XPIStates.db.values())
+                             .filter(loc => loc != tempLocation);
+        let existing = XPIStates.findAddon(id, locations);
+        if (existing) {
+          reason = newVersionReason(addon.version, existing.version);
+        }
+
         this.callBootstrapMethod(createAddonDetails(id, addon),
-                                 addon.file, "uninstall",
-                                 BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+                                 addon.file, "uninstall", reason);
         this.unloadBootstrapScope(id);
         TemporaryInstallLocation.uninstallAddon(id);
 
-        let state = XPIStates.findAddon(id);
-        if (state) {
-          let newAddon = XPIDatabase.makeAddonLocationVisible(id, state.location.name);
+        if (existing) {
+          let newAddon = XPIDatabase.makeAddonLocationVisible(id, existing.location.name);
 
           let file = new nsIFile(newAddon.path);
 
+          let data = {oldVersion: addon.version};
           this.callBootstrapMethod(createAddonDetails(id, newAddon),
-                                   file, "install",
-                                   BOOTSTRAP_REASONS.ADDON_INSTALL);
+                                   file, "install", reason, data);
         }
       }
     }
 
     this.activeAddons.clear();
     this.allAppGlobal = true;
 
     // If there are pending operations then we must update the list of active
@@ -2851,19 +2881,17 @@ this.XPIProvider = {
             var file = existingAddon.file;
             if (file.exists()) {
               oldBootstrap = existingAddon;
 
               // We'll be replacing a currently active bootstrapped add-on so
               // call its uninstall method
               let newVersion = addon.version;
               let oldVersion = existingAddon;
-              let uninstallReason = Services.vc.compare(oldVersion, newVersion) < 0 ?
-                                    BOOTSTRAP_REASONS.ADDON_UPGRADE :
-                                    BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
+              let uninstallReason = newVersionReason(oldVersion, newVersion);
 
               this.callBootstrapMethod(createAddonDetails(existingAddonID, existingAddon),
                                        file, "uninstall", uninstallReason,
                                        { newVersion });
               this.unloadBootstrapScope(existingAddonID);
               flushChromeCaches();
             }
           } catch (e) {
@@ -3474,21 +3502,17 @@ this.XPIProvider = {
         let existingAddonID = oldAddon.id;
         let existingAddon = oldAddon._sourceBundle;
 
         // We'll be replacing a currently active bootstrapped add-on so
         // call its uninstall method
         let newVersion = addon.version;
         let oldVersion = oldAddon.version;
 
-        if (Services.vc.compare(newVersion, oldVersion) >= 0) {
-          installReason = BOOTSTRAP_REASONS.ADDON_UPGRADE;
-        } else {
-          installReason = BOOTSTRAP_REASONS.ADDON_DOWNGRADE;
-        }
+        installReason = newVersionReason(oldVersion, newVersion);
         let uninstallReason = installReason;
 
         extraParams.newVersion = newVersion;
         extraParams.oldVersion = oldVersion;
 
         if (oldAddon.active) {
           XPIProvider.callBootstrapMethod(oldAddon, existingAddon,
                                           "shutdown", uninstallReason,
--- a/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
+++ b/toolkit/mozapps/extensions/test/xpcshell/test_temporary.js
@@ -1,12 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/
  */
 
+Components.utils.import("resource://gre/modules/AppConstants.jsm");
+
 const ID = "bootstrap1@tests.mozilla.org";
 const sampleRDFManifest = {
   id: ID,
   version: "1.0",
   bootstrap: true,
   targetApplications: [{
     id: "xpcshell@tests.mozilla.org",
     minVersion: "1",
@@ -17,17 +19,19 @@ const sampleRDFManifest = {
 
 createAppInfo("xpcshell@tests.mozilla.org", "XPCShell", "1", "42");
 startupManager();
 
 BootstrapMonitor.init();
 
 // Partial list of bootstrap reasons from XPIProvider.jsm
 const BOOTSTRAP_REASONS = {
+  APP_STARTUP: 1,
   ADDON_INSTALL: 5,
+  ADDON_UNINSTALL: 6,
   ADDON_UPGRADE: 7,
   ADDON_DOWNGRADE: 8,
 };
 
 function waitForBootstrapEvent(expectedEvent, addonId) {
   return new Promise(resolve => {
     const observer = {
       observe: (subject, topic, data) => {
@@ -95,240 +99,266 @@ add_task(async function() {
   do_check_eq(addon.version, "1.0");
   do_check_eq(addon.name, "Test Bootstrap 1");
   do_check_true(addon.isCompatible);
   do_check_false(addon.appDisabled);
   do_check_true(addon.isActive);
   do_check_eq(addon.type, "extension");
   do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
 
+  let onShutdown = waitForBootstrapEvent("shutdown", ID);
+  let onUninstall = waitForBootstrapEvent("uninstall", ID);
+
   await promiseRestartManager();
 
+  let shutdown = await onShutdown;
+  equal(shutdown.reason, BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+
+  let uninstall = await onUninstall;
+  equal(uninstall.reason, BOOTSTRAP_REASONS.ADDON_UNINSTALL);
+
   BootstrapMonitor.checkAddonNotInstalled(ID);
   BootstrapMonitor.checkAddonNotStarted(ID);
 
   addon = await promiseAddonByID(ID);
   do_check_eq(addon, null);
 
   await promiseRestartManager();
 });
 
 // Install a temporary add-on over the top of an existing add-on.
 // Restart and make sure the existing add-on comes back.
 add_task(async function() {
-  await promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")], true);
+  await promiseInstallAllFiles([do_get_addon("test_bootstrap1_2")], true);
 
-  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
-  BootstrapMonitor.checkAddonStarted(ID, "1.0");
+  BootstrapMonitor.checkAddonInstalled(ID, "2.0");
+  BootstrapMonitor.checkAddonStarted(ID, "2.0");
 
   let addon = await promiseAddonByID(ID);
 
   do_check_neq(addon, null);
-  do_check_eq(addon.version, "1.0");
+  do_check_eq(addon.version, "2.0");
   do_check_eq(addon.name, "Test Bootstrap 1");
   do_check_true(addon.isCompatible);
   do_check_false(addon.appDisabled);
   do_check_true(addon.isActive);
   do_check_eq(addon.type, "extension");
   do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
 
   let tempdir = gTmpD.clone();
 
-  // test that an unpacked add-on works too
-  writeInstallRDFToDir({
-    id: ID,
-    version: "3.0",
-    bootstrap: true,
-    targetApplications: [{
-      id: "xpcshell@tests.mozilla.org",
-      minVersion: "1",
-      maxVersion: "1"
-    }],
-    name: "Test Bootstrap 1 (temporary)",
-  }, tempdir, "bootstrap1@tests.mozilla.org", "bootstrap.js");
+  let bootstrapJS = await OS.File.read("data/test_temporary/bootstrap.js", {encoding: "utf-8"});
 
-  let unpacked_addon = tempdir.clone();
-  unpacked_addon.append(ID);
-  do_get_file("data/test_temporary/bootstrap.js")
-    .copyTo(unpacked_addon, "bootstrap.js");
-
-  await AddonManager.installTemporaryAddon(unpacked_addon);
+  for (let newversion of ["1.0", "3.0"]) {
+    for (let packed of [false, true]) {
+      // ugh, file locking issues with xpis on windows
+      if (packed && AppConstants.platform == "win") {
+        continue;
+      }
 
-  BootstrapMonitor.checkAddonInstalled(ID, "3.0");
-  BootstrapMonitor.checkAddonStarted(ID, "3.0");
-
-  addon = await promiseAddonByID(ID);
+      let files = {
+        "install.rdf": AddonTestUtils.createInstallRDF({
+          id: ID,
+          version: newversion,
+          bootstrap: true,
+          targetApplications: [{
+            id: "xpcshell@tests.mozilla.org",
+            minVersion: "1",
+            maxVersion: "1"
+          }],
+          name: "Test Bootstrap 1 (temporary)",
+        }),
+        "bootstrap.js": bootstrapJS,
+      };
 
-  // temporary add-on is installed and started
-  do_check_neq(addon, null);
-  do_check_eq(addon.version, "3.0");
-  do_check_eq(addon.name, "Test Bootstrap 1 (temporary)");
-  do_check_true(addon.isCompatible);
-  do_check_false(addon.appDisabled);
-  do_check_true(addon.isActive);
-  do_check_eq(addon.type, "extension");
-  do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
-
-  restartManager();
-
-  BootstrapMonitor.checkAddonInstalled(ID, "1.0");
-  BootstrapMonitor.checkAddonStarted(ID, "1.0");
-
-  addon = await promiseAddonByID(ID);
+      let target;
+      if (packed) {
+        target = tempdir.clone();
+        target.append(ID);
 
-  // existing add-on is back
-  do_check_neq(addon, null);
-  do_check_eq(addon.version, "1.0");
-  do_check_eq(addon.name, "Test Bootstrap 1");
-  do_check_true(addon.isCompatible);
-  do_check_false(addon.appDisabled);
-  do_check_true(addon.isActive);
-  do_check_eq(addon.type, "extension");
-  do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+        await AddonTestUtils.promiseWriteFilesToDir(target.path, files);
+      } else {
+        target = tempdir.clone();
+        target.append(`${ID}.xpi`);
 
-  unpacked_addon.remove(true);
+        await AddonTestUtils.promiseWriteFilesToZip(target.path, files);
+      }
+
+      let onShutdown = waitForBootstrapEvent("shutdown", ID);
+      let onUninstall = waitForBootstrapEvent("uninstall", ID);
+      let onInstall = waitForBootstrapEvent("install", ID);
+      let onStartup = waitForBootstrapEvent("startup", ID);
+
+      await AddonManager.installTemporaryAddon(target);
 
-  // on Windows XPI files will be locked by the JAR cache, skip this test there.
-  if (!("nsIWindowsRegKey" in Components.interfaces)) {
-    // test that a packed (XPI) add-on works
-    writeInstallRDFToXPI({
-      id: ID,
-      version: "2.0",
-      bootstrap: true,
-      targetApplications: [{
-        id: "xpcshell@tests.mozilla.org",
-        minVersion: "1",
-        maxVersion: "1"
-      }],
-      name: "Test Bootstrap 1 (temporary)",
-    }, tempdir, "bootstrap1@tests.mozilla.org");
+      let reason = Services.vc.compare(newversion, "2.0") < 0 ?
+                   BOOTSTRAP_REASONS.ADDON_DOWNGRADE :
+                   BOOTSTRAP_REASONS.ADDON_UPGRADE;
 
-    let packed_addon = tempdir.clone();
-    packed_addon.append(ID + ".xpi");
+      let shutdown = await onShutdown;
+      equal(shutdown.data.version, "2.0");
+      equal(shutdown.reason, reason);
 
-    await AddonManager.installTemporaryAddon(packed_addon);
-
-    addon = await promiseAddonByID(ID);
+      let uninstall = await onUninstall;
+      equal(uninstall.data.version, "2.0");
+      equal(uninstall.reason, reason);
 
-    // temporary add-on is installed and started
-    do_check_neq(addon, null);
-    do_check_eq(addon.version, "2.0");
-    do_check_eq(addon.name, "Test Bootstrap 1 (temporary)");
-    do_check_true(addon.isCompatible);
-    do_check_false(addon.appDisabled);
-    do_check_true(addon.isActive);
-    do_check_eq(addon.type, "extension");
-    do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
-
-    restartManager();
-
-    BootstrapMonitor.checkAddonInstalled(ID, "1.0");
-    BootstrapMonitor.checkAddonStarted(ID, "1.0");
+      let install = await onInstall;
+      equal(install.data.version, newversion);
+      equal(install.reason, reason);
+      equal(install.data.oldVersion, "2.0");
 
-    addon = await promiseAddonByID(ID);
+      let startup = await onStartup;
+      equal(startup.data.version, newversion);
+      equal(startup.reason, reason);
+      equal(startup.data.oldVersion, "2.0");
 
-    // existing add-on is back
-    do_check_neq(addon, null);
-    do_check_eq(addon.version, "1.0");
-    do_check_eq(addon.name, "Test Bootstrap 1");
-    do_check_true(addon.isCompatible);
-    do_check_false(addon.appDisabled);
-    do_check_true(addon.isActive);
-    do_check_eq(addon.type, "extension");
-    do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
-
-    packed_addon.remove(false);
+      addon = await promiseAddonByID(ID);
 
-    // test that a webextension works
-    let webext = createTempWebExtensionFile({
-      manifest: {
-        version: "4.0",
-        name: "Test WebExtension 1 (temporary)",
-        applications: {
-          gecko: {
-            id: ID
-          }
-        }
-      }
-    });
+      // temporary add-on is installed and started
+      do_check_neq(addon, null);
+      do_check_eq(addon.version, newversion);
+      do_check_eq(addon.name, "Test Bootstrap 1 (temporary)");
+      do_check_true(addon.isCompatible);
+      do_check_false(addon.appDisabled);
+      do_check_true(addon.isActive);
+      do_check_eq(addon.type, "extension");
+      do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
 
-    await Promise.all([
-      AddonManager.installTemporaryAddon(webext),
-      promiseWebExtensionStartup(),
-    ]);
-    addon = await promiseAddonByID(ID);
+      // Now restart, the temporary addon will go away which should
+      // be the opposite action (ie, if the temporary addon was an
+      // upgrade, then removing it is a downgrade and vice versa)
+      reason = reason == BOOTSTRAP_REASONS.ADDON_UPGRADE ?
+               BOOTSTRAP_REASONS.ADDON_DOWNGRADE :
+               BOOTSTRAP_REASONS.ADDON_UPGRADE;
 
-    // temporary add-on is installed and started
-    do_check_neq(addon, null);
-    do_check_eq(addon.version, "4.0");
-    do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
-    do_check_true(addon.isCompatible);
-    do_check_false(addon.appDisabled);
-    do_check_true(addon.isActive);
-    do_check_eq(addon.type, "extension");
-    do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+      onShutdown = waitForBootstrapEvent("shutdown", ID);
+      onUninstall = waitForBootstrapEvent("uninstall", ID);
+      onInstall = waitForBootstrapEvent("install", ID);
+      onStartup = waitForBootstrapEvent("startup", ID);
+
+      await promiseRestartManager();
 
-    // test that re-loading a webextension works, using the same filename
-    webext.remove(false);
-    webext = createTempWebExtensionFile({
-      manifest: {
-        version: "5.0",
-        name: "Test WebExtension 1 (temporary)",
-        applications: {
-          gecko: {
-            id: ID
-          }
-        }
-      }
-    });
+      shutdown = await onShutdown;
+      equal(shutdown.data.version, newversion);
+      equal(shutdown.reason, reason);
 
-    await Promise.all([
-      AddonManager.installTemporaryAddon(webext),
-      promiseWebExtensionStartup(),
-    ]);
-    addon = await promiseAddonByID(ID);
+      uninstall = await onUninstall;
+      equal(uninstall.data.version, newversion);
+      equal(uninstall.reason, reason);
+
+      install = await onInstall;
+      equal(install.data.version, "2.0");
+      equal(install.reason, reason);
+      equal(install.data.oldVersion, newversion);
 
-    // temporary add-on is installed and started
-    do_check_neq(addon, null);
-    do_check_eq(addon.version, "5.0");
-    do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
-    do_check_true(addon.isCompatible);
-    do_check_false(addon.appDisabled);
-    do_check_true(addon.isActive);
-    do_check_eq(addon.type, "extension");
-    do_check_true(addon.isWebExtension);
-    do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+      startup = await onStartup;
+      equal(startup.data.version, "2.0");
+      // We don't actually propagate the upgrade/downgrade reason across
+      // the browser restart when a temporary addon is removed.  See
+      // bug 1359558 for detailed reasoning.
+      equal(startup.reason, BOOTSTRAP_REASONS.APP_STARTUP);
 
-    restartManager();
+      BootstrapMonitor.checkAddonInstalled(ID, "2.0");
+      BootstrapMonitor.checkAddonStarted(ID, "2.0");
+
+      addon = await promiseAddonByID(ID);
 
-    BootstrapMonitor.checkAddonInstalled(ID, "1.0");
-    BootstrapMonitor.checkAddonStarted(ID, "1.0");
-
-    addon = await promiseAddonByID(ID);
+      // existing add-on is back
+      do_check_neq(addon, null);
+      do_check_eq(addon.version, "2.0");
+      do_check_eq(addon.name, "Test Bootstrap 1");
+      do_check_true(addon.isCompatible);
+      do_check_false(addon.appDisabled);
+      do_check_true(addon.isActive);
+      do_check_eq(addon.type, "extension");
+      do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
 
-    // existing add-on is back
-    do_check_neq(addon, null);
-    do_check_eq(addon.version, "1.0");
-    do_check_eq(addon.name, "Test Bootstrap 1");
-    do_check_true(addon.isCompatible);
-    do_check_false(addon.appDisabled);
-    do_check_true(addon.isActive);
-    do_check_eq(addon.type, "extension");
-    do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+      target.remove(true);
+    }
   }
 
   // remove original add-on
   addon.uninstall();
 
   BootstrapMonitor.checkAddonNotInstalled(ID);
   BootstrapMonitor.checkAddonNotStarted(ID);
 
   await promiseRestartManager();
 });
 
+// Test that loading from the same path multiple times work
+add_task(async function test_samefile() {
+  // File locking issues on Windows, ugh
+  if (AppConstants.platform == "win") {
+    return;
+  }
+
+  // test that a webextension works
+  let webext = createTempWebExtensionFile({
+    manifest: {
+      version: "1.0",
+      name: "Test WebExtension 1 (temporary)",
+      applications: {
+        gecko: {
+          id: ID
+        }
+      }
+    }
+  });
+
+  await Promise.all([
+    AddonManager.installTemporaryAddon(webext),
+    promiseWebExtensionStartup(),
+  ]);
+  let addon = await promiseAddonByID(ID);
+
+  // temporary add-on is installed and started
+  do_check_neq(addon, null);
+  do_check_eq(addon.version, "1.0");
+  do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
+  do_check_true(addon.isCompatible);
+  do_check_false(addon.appDisabled);
+  do_check_true(addon.isActive);
+  do_check_eq(addon.type, "extension");
+  do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+  webext.remove(false);
+  webext = createTempWebExtensionFile({
+    manifest: {
+      version: "2.0",
+      name: "Test WebExtension 1 (temporary)",
+      applications: {
+        gecko: {
+          id: ID
+        }
+      }
+    }
+  });
+
+  await Promise.all([
+    AddonManager.installTemporaryAddon(webext),
+    promiseWebExtensionStartup(),
+  ]);
+  addon = await promiseAddonByID(ID);
+
+  // temporary add-on is installed and started
+  do_check_neq(addon, null);
+  do_check_eq(addon.version, "2.0");
+  do_check_eq(addon.name, "Test WebExtension 1 (temporary)");
+  do_check_true(addon.isCompatible);
+  do_check_false(addon.appDisabled);
+  do_check_true(addon.isActive);
+  do_check_eq(addon.type, "extension");
+  do_check_true(addon.isWebExtension);
+  do_check_eq(addon.signedState, mozinfo.addon_signing ? AddonManager.SIGNEDSTATE_PRIVILEGED : AddonManager.SIGNEDSTATE_NOT_REQUIRED);
+
+  addon.uninstall();
+});
+
 // Install a temporary add-on over the top of an existing add-on.
 // Uninstall it and make sure the existing add-on comes back.
 add_task(async function() {
   await promiseInstallAllFiles([do_get_addon("test_bootstrap1_1")], true);
 
   BootstrapMonitor.checkAddonInstalled(ID, "1.0");
   BootstrapMonitor.checkAddonStarted(ID, "1.0");