tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

head_helpers.js (21730B)


      1 /* Any copyright is dedicated to the Public Domain.
      2   http://creativecommons.org/publicdomain/zero/1.0/ */
      3 
      4 /* import-globals-from head_appinfo.js */
      5 /* import-globals-from ../../../common/tests/unit/head_helpers.js */
      6 /* import-globals-from head_errorhandler_common.js */
      7 /* import-globals-from head_http_server.js */
      8 
      9 // This file expects Service to be defined in the global scope when EHTestsCommon
     10 // is used (from service.js).
     11 /* global Service */
     12 
     13 var { AddonTestUtils, MockAsyncShutdown } = ChromeUtils.importESModule(
     14  "resource://testing-common/AddonTestUtils.sys.mjs"
     15 );
     16 var { Async } = ChromeUtils.importESModule(
     17  "resource://services-common/async.sys.mjs"
     18 );
     19 var { CommonUtils } = ChromeUtils.importESModule(
     20  "resource://services-common/utils.sys.mjs"
     21 );
     22 var { PlacesTestUtils } = ChromeUtils.importESModule(
     23  "resource://testing-common/PlacesTestUtils.sys.mjs"
     24 );
     25 var { sinon } = ChromeUtils.importESModule(
     26  "resource://testing-common/Sinon.sys.mjs"
     27 );
     28 var { SerializableSet, Svc, Utils, getChromeWindow } =
     29  ChromeUtils.importESModule("resource://services-sync/util.sys.mjs");
     30 var { XPCOMUtils } = ChromeUtils.importESModule(
     31  "resource://gre/modules/XPCOMUtils.sys.mjs"
     32 );
     33 var { PlacesUtils } = ChromeUtils.importESModule(
     34  "resource://gre/modules/PlacesUtils.sys.mjs"
     35 );
     36 var { PlacesSyncUtils } = ChromeUtils.importESModule(
     37  "resource://gre/modules/PlacesSyncUtils.sys.mjs"
     38 );
     39 var { ObjectUtils } = ChromeUtils.importESModule(
     40  "resource://gre/modules/ObjectUtils.sys.mjs"
     41 );
     42 var {
     43  MockFxaStorageManager,
     44  SyncTestingInfrastructure,
     45  configureFxAccountIdentity,
     46  configureIdentity,
     47  encryptPayload,
     48  getLoginTelemetryScalar,
     49  makeFxAccountsInternalMock,
     50  makeIdentityConfig,
     51  promiseNamedTimer,
     52  promiseZeroTimer,
     53  sumHistogram,
     54  syncTestLogging,
     55  waitForZeroTimer,
     56 } = ChromeUtils.importESModule(
     57  "resource://testing-common/services/sync/utils.sys.mjs"
     58 );
     59 ChromeUtils.defineESModuleGetters(this, {
     60  AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
     61 });
     62 
     63 add_setup(async function head_setup() {
     64  // Initialize logging. This will sometimes be reset by a pref reset,
     65  // so it's also called as part of SyncTestingInfrastructure().
     66  syncTestLogging();
     67  // If a test imports Service, make sure it is initialized first.
     68  if (typeof Service !== "undefined") {
     69    await Service.promiseInitialized;
     70  }
     71 });
     72 
     73 ChromeUtils.defineLazyGetter(this, "SyncPingSchema", function () {
     74  let { FileUtils } = ChromeUtils.importESModule(
     75    "resource://gre/modules/FileUtils.sys.mjs"
     76  );
     77  let { NetUtil } = ChromeUtils.importESModule(
     78    "resource://gre/modules/NetUtil.sys.mjs"
     79  );
     80  let stream = Cc["@mozilla.org/network/file-input-stream;1"].createInstance(
     81    Ci.nsIFileInputStream
     82  );
     83  let schema;
     84  try {
     85    let schemaFile = do_get_file("sync_ping_schema.json");
     86    stream.init(schemaFile, FileUtils.MODE_RDONLY, FileUtils.PERMS_FILE, 0);
     87 
     88    let bytes = NetUtil.readInputStream(stream, stream.available());
     89    schema = JSON.parse(new TextDecoder().decode(bytes));
     90  } finally {
     91    stream.close();
     92  }
     93 
     94  // Allow tests to make whatever engines they want, this shouldn't cause
     95  // validation failure.
     96  schema.definitions.engine.properties.name = { type: "string" };
     97  return schema;
     98 });
     99 
    100 ChromeUtils.defineLazyGetter(this, "SyncPingValidator", function () {
    101  const { JsonSchema } = ChromeUtils.importESModule(
    102    "resource://gre/modules/JsonSchema.sys.mjs"
    103  );
    104  return new JsonSchema.Validator(SyncPingSchema);
    105 });
    106 
    107 // This is needed for loadAddonTestFunctions().
    108 var gGlobalScope = this;
    109 
    110 function ExtensionsTestPath(path) {
    111  if (path[0] != "/") {
    112    throw Error("Path must begin with '/': " + path);
    113  }
    114 
    115  return "../../../../toolkit/mozapps/extensions/test/xpcshell" + path;
    116 }
    117 
    118 function webExtensionsTestPath(path) {
    119  if (path[0] != "/") {
    120    throw Error("Path must begin with '/': " + path);
    121  }
    122 
    123  return "../../../../toolkit/components/extensions/test/xpcshell" + path;
    124 }
    125 
    126 /**
    127 * Loads the WebExtension test functions by importing its test file.
    128 */
    129 function loadWebExtensionTestFunctions() {
    130  /* import-globals-from ../../../../toolkit/components/extensions/test/xpcshell/head_sync.js */
    131  const path = webExtensionsTestPath("/head_sync.js");
    132  let file = do_get_file(path);
    133  let uri = Services.io.newFileURI(file);
    134  Services.scriptloader.loadSubScript(uri.spec, gGlobalScope);
    135 }
    136 
    137 /**
    138 * Installs an add-on from an addonInstall
    139 *
    140 * @param  install addonInstall instance to install
    141 */
    142 async function installAddonFromInstall(install) {
    143  await install.install();
    144 
    145  Assert.notEqual(null, install.addon);
    146  Assert.notEqual(null, install.addon.syncGUID);
    147 
    148  return install.addon;
    149 }
    150 
    151 /**
    152 * Convenience function to install an add-on from the extensions unit tests.
    153 *
    154 * @param  file
    155 *         Add-on file to install.
    156 * @param  reconciler
    157 *         addons reconciler, if passed we will wait on the events to be
    158 *         processed before resolving
    159 * @return addon object that was installed
    160 */
    161 async function installAddon(file, reconciler = null) {
    162  let install = await AddonManager.getInstallForFile(file);
    163  Assert.notEqual(null, install);
    164  const addon = await installAddonFromInstall(install);
    165  if (reconciler) {
    166    await reconciler.queueCaller.promiseCallsComplete();
    167  }
    168  return addon;
    169 }
    170 
    171 /**
    172 * Convenience function to uninstall an add-on.
    173 *
    174 * @param addon
    175 *        Addon instance to uninstall
    176 * @param reconciler
    177 *        addons reconciler, if passed we will wait on the events to be
    178 *        processed before resolving
    179 */
    180 async function uninstallAddon(addon, reconciler = null) {
    181  const uninstallPromise = new Promise(res => {
    182    let listener = {
    183      onUninstalled(uninstalled) {
    184        if (uninstalled.id == addon.id) {
    185          AddonManager.removeAddonListener(listener);
    186          res(uninstalled);
    187        }
    188      },
    189    };
    190    AddonManager.addAddonListener(listener);
    191  });
    192  addon.uninstall();
    193  await uninstallPromise;
    194  if (reconciler) {
    195    await reconciler.queueCaller.promiseCallsComplete();
    196  }
    197 }
    198 
    199 async function generateNewKeys(collectionKeys, collections = null) {
    200  let wbo = await collectionKeys.generateNewKeysWBO(collections);
    201  let modified = new_timestamp();
    202  collectionKeys.setContents(wbo.cleartext, modified);
    203 }
    204 
    205 // Helpers for testing open tabs.
    206 // These reflect part of the internal structure of TabEngine,
    207 // and stub part of Service.wm.
    208 
    209 function mockShouldSkipWindow(win) {
    210  return win.closed || win.mockIsPrivate;
    211 }
    212 
    213 function mockGetTabState(tab) {
    214  return tab;
    215 }
    216 
    217 function mockGetWindowEnumerator(urls) {
    218  let elements = [];
    219 
    220  const numWindows = 1;
    221  for (let w = 0; w < numWindows; ++w) {
    222    let tabs = [];
    223    let win = {
    224      closed: false,
    225      mockIsPrivate: false,
    226      gBrowser: {
    227        tabs,
    228      },
    229    };
    230    elements.push(win);
    231 
    232    let lastAccessed = 2000;
    233    for (let url of urls) {
    234      tabs.push({
    235        linkedBrowser: {
    236          currentURI: Services.io.newURI(url),
    237          contentTitle: "title",
    238        },
    239        lastAccessed,
    240      });
    241      lastAccessed += 1000;
    242    }
    243  }
    244 
    245  // Always include a closed window and a private window.
    246  elements.push({
    247    closed: true,
    248    mockIsPrivate: false,
    249    gBrowser: {
    250      tabs: [],
    251    },
    252  });
    253 
    254  elements.push({
    255    closed: false,
    256    mockIsPrivate: true,
    257    gBrowser: {
    258      tabs: [],
    259    },
    260  });
    261 
    262  return elements.values();
    263 }
    264 
    265 // Helper function to get the sync telemetry and add the typically used test
    266 // engine names to its list of allowed engines.
    267 function get_sync_test_telemetry() {
    268  let { SyncTelemetry } = ChromeUtils.importESModule(
    269    "resource://services-sync/telemetry.sys.mjs"
    270  );
    271  SyncTelemetry.tryRefreshDevices = function () {};
    272  let testEngines = ["rotary", "steam", "sterling", "catapult", "nineties"];
    273  for (let engineName of testEngines) {
    274    SyncTelemetry.allowedEngines.add(engineName);
    275  }
    276  SyncTelemetry.submissionInterval = -1;
    277  return SyncTelemetry;
    278 }
    279 
    280 function assert_valid_ping(record) {
    281  // Our JSON validator does not like `undefined` values, even though they will
    282  // be skipped when we serialize to JSON.
    283  record = JSON.parse(JSON.stringify(record));
    284 
    285  // This is called as the test harness tears down due to shutdown. This
    286  // will typically have no recorded syncs, and the validator complains about
    287  // it. So ignore such records (but only ignore when *both* shutdown and
    288  // no Syncs - either of them not being true might be an actual problem)
    289  if (record && (record.why != "shutdown" || !!record.syncs.length)) {
    290    const result = SyncPingValidator.validate(record);
    291    if (!result.valid) {
    292      if (result.errors.length) {
    293        // validation failed - using a simple |deepEqual([], errors)| tends to
    294        // truncate the validation errors in the output and doesn't show that
    295        // the ping actually was - so be helpful.
    296        info("telemetry ping validation failed");
    297        info("the ping data is: " + JSON.stringify(record, undefined, 2));
    298        info(
    299          "the validation failures: " +
    300            JSON.stringify(result.errors, undefined, 2)
    301        );
    302        ok(
    303          false,
    304          "Sync telemetry ping validation failed - see output above for details"
    305        );
    306      }
    307    }
    308    equal(record.version, 1);
    309    record.syncs.forEach(p => {
    310      lessOrEqual(p.when, Date.now());
    311    });
    312  }
    313 }
    314 
    315 function assert_success_sync(record) {
    316  ok(!record.failureReason, JSON.stringify(record.failureReason));
    317  equal(undefined, record.status);
    318  greater(record.engines.length, 0);
    319  for (let e of record.engines) {
    320    ok(!e.failureReason);
    321    equal(undefined, e.status);
    322    if (e.validation) {
    323      equal(undefined, e.validation.problems);
    324      equal(undefined, e.validation.failureReason);
    325    }
    326    if (e.outgoing) {
    327      for (let o of e.outgoing) {
    328        equal(undefined, o.failed);
    329        notEqual(undefined, o.sent);
    330      }
    331    }
    332    if (e.incoming) {
    333      equal(undefined, e.incoming.failed);
    334      equal(undefined, e.incoming.newFailed);
    335      notEqual(undefined, e.incoming.applied || e.incoming.reconciled);
    336    }
    337  }
    338 }
    339 
    340 // Asserts that `ping` is a ping that doesn't contain any failure information
    341 function assert_success_ping(ping) {
    342  ok(!!ping);
    343  assert_valid_ping(ping);
    344  ping.syncs.forEach(assert_success_sync);
    345 }
    346 
    347 // Hooks into telemetry to validate all pings after calling.
    348 function validate_all_future_pings() {
    349  let telem = get_sync_test_telemetry();
    350  telem.submit = assert_valid_ping;
    351 }
    352 
    353 function wait_for_pings(expectedPings) {
    354  return new Promise(resolve => {
    355    let telem = get_sync_test_telemetry();
    356    let oldSubmit = telem.submit;
    357    let pings = [];
    358    telem.submit = function (record) {
    359      pings.push(record);
    360      if (pings.length == expectedPings) {
    361        telem.submit = oldSubmit;
    362        resolve(pings);
    363      }
    364    };
    365  });
    366 }
    367 
    368 async function wait_for_ping(callback, allowErrorPings, getFullPing = false) {
    369  let pingsPromise = wait_for_pings(1);
    370  await callback();
    371  let [record] = await pingsPromise;
    372  if (allowErrorPings) {
    373    assert_valid_ping(record);
    374  } else {
    375    assert_success_ping(record);
    376  }
    377  if (getFullPing) {
    378    return record;
    379  }
    380  equal(record.syncs.length, 1);
    381  return record.syncs[0];
    382 }
    383 
    384 // Perform a sync and validate all telemetry caused by the sync. If fnValidate
    385 // is null, we just check the ping records success. If fnValidate is specified,
    386 // then the sync must have recorded just a single sync, and that sync will be
    387 // passed to the function to be checked.
    388 async function sync_and_validate_telem(
    389  fnValidate = null,
    390  wantFullPing = false
    391 ) {
    392  let numErrors = 0;
    393  let telem = get_sync_test_telemetry();
    394  let oldSubmit = telem.submit;
    395  try {
    396    telem.submit = function (record) {
    397      // This is called via an observer, so failures here don't cause the test
    398      // to fail :(
    399      try {
    400        // All pings must be valid.
    401        assert_valid_ping(record);
    402        if (fnValidate) {
    403          // for historical reasons most of these callbacks expect a "sync"
    404          // record, not the entire ping.
    405          if (wantFullPing) {
    406            fnValidate(record);
    407          } else {
    408            Assert.equal(record.syncs.length, 1);
    409            fnValidate(record.syncs[0]);
    410          }
    411        } else {
    412          // no validation function means it must be a "success" ping.
    413          assert_success_ping(record);
    414        }
    415      } catch (ex) {
    416        print("Failure in ping validation callback", ex, "\n", ex.stack);
    417        numErrors += 1;
    418      }
    419    };
    420    await Service.sync();
    421    Assert.equal(numErrors, 0, "There were telemetry validation errors");
    422  } finally {
    423    telem.submit = oldSubmit;
    424  }
    425 }
    426 
    427 // Used for the (many) cases where we do a 'partial' sync, where only a single
    428 // engine is actually synced, but we still want to ensure we're generating a
    429 // valid ping. Returns a promise that resolves to the ping, or rejects with the
    430 // thrown error after calling an optional callback.
    431 async function sync_engine_and_validate_telem(
    432  engine,
    433  allowErrorPings,
    434  onError,
    435  wantFullPing = false
    436 ) {
    437  let telem = get_sync_test_telemetry();
    438  let caughtError = null;
    439  // Clear out status, so failures from previous syncs won't show up in the
    440  // telemetry ping.
    441  let { Status } = ChromeUtils.importESModule(
    442    "resource://services-sync/status.sys.mjs"
    443  );
    444  Status._engines = {};
    445  Status.partial = false;
    446  // Ideally we'd clear these out like we do with engines, (probably via
    447  // Status.resetSync()), but this causes *numerous* tests to fail, so we just
    448  // assume that if no failureReason or engine failures are set, and the
    449  // status properties are the same as they were initially, that it's just
    450  // a leftover.
    451  // This is only an issue since we're triggering the sync of just one engine,
    452  // without doing any other parts of the sync.
    453  let initialServiceStatus = Status._service;
    454  let initialSyncStatus = Status._sync;
    455 
    456  let oldSubmit = telem.submit;
    457  let submitPromise = new Promise((resolve, reject) => {
    458    telem.submit = function (ping) {
    459      telem.submit = oldSubmit;
    460      ping.syncs.forEach(record => {
    461        if (record && record.status) {
    462          // did we see anything to lead us to believe that something bad actually happened
    463          let realProblem =
    464            record.failureReason ||
    465            record.engines.some(e => {
    466              if (e.failureReason || e.status) {
    467                return true;
    468              }
    469              if (e.outgoing && e.outgoing.some(o => o.failed > 0)) {
    470                return true;
    471              }
    472              return e.incoming && e.incoming.failed;
    473            });
    474          if (!realProblem) {
    475            // no, so if the status is the same as it was initially, just assume
    476            // that its leftover and that we can ignore it.
    477            if (record.status.sync && record.status.sync == initialSyncStatus) {
    478              delete record.status.sync;
    479            }
    480            if (
    481              record.status.service &&
    482              record.status.service == initialServiceStatus
    483            ) {
    484              delete record.status.service;
    485            }
    486            if (!record.status.sync && !record.status.service) {
    487              delete record.status;
    488            }
    489          }
    490        }
    491      });
    492      if (allowErrorPings) {
    493        assert_valid_ping(ping);
    494      } else {
    495        assert_success_ping(ping);
    496      }
    497      equal(ping.syncs.length, 1);
    498      if (caughtError) {
    499        if (onError) {
    500          onError(ping.syncs[0], ping);
    501        }
    502        reject(caughtError);
    503      } else if (wantFullPing) {
    504        resolve(ping);
    505      } else {
    506        resolve(ping.syncs[0]);
    507      }
    508    };
    509  });
    510  // neuter the scheduler as it interacts badly with some of the tests - the
    511  // engine being synced usually isn't the registered engine, so we see
    512  // scored incremented and not removed, which schedules unexpected syncs.
    513  let oldObserve = Service.scheduler.observe;
    514  Service.scheduler.observe = () => {};
    515  try {
    516    Svc.Obs.notify("weave:service:sync:start");
    517    try {
    518      await engine.sync();
    519    } catch (e) {
    520      caughtError = e;
    521    }
    522    if (caughtError) {
    523      Svc.Obs.notify("weave:service:sync:error", caughtError);
    524    } else {
    525      Svc.Obs.notify("weave:service:sync:finish");
    526    }
    527  } finally {
    528    Service.scheduler.observe = oldObserve;
    529  }
    530  return submitPromise;
    531 }
    532 
    533 // Returns a promise that resolves once the specified observer notification
    534 // has fired.
    535 function promiseOneObserver(topic) {
    536  return new Promise(resolve => {
    537    let observer = function (subject, data) {
    538      Svc.Obs.remove(topic, observer);
    539      resolve({ subject, data });
    540    };
    541    Svc.Obs.add(topic, observer);
    542  });
    543 }
    544 
    545 async function registerRotaryEngine() {
    546  let { RotaryEngine } = ChromeUtils.importESModule(
    547    "resource://testing-common/services/sync/rotaryengine.sys.mjs"
    548  );
    549  await Service.engineManager.clear();
    550 
    551  await Service.engineManager.register(RotaryEngine);
    552  let engine = Service.engineManager.get("rotary");
    553  let syncID = await engine.resetLocalSyncID();
    554  engine.enabled = true;
    555 
    556  return { engine, syncID, tracker: engine._tracker };
    557 }
    558 
    559 // Set the validation prefs to attempt validation every time to avoid non-determinism.
    560 function enableValidationPrefs(engines = ["bookmarks"]) {
    561  for (let engine of engines) {
    562    Svc.PrefBranch.setIntPref(`engine.${engine}.validation.interval`, 0);
    563    Svc.PrefBranch.setIntPref(
    564      `engine.${engine}.validation.percentageChance`,
    565      100
    566    );
    567    Svc.PrefBranch.setIntPref(`engine.${engine}.validation.maxRecords`, -1);
    568    Svc.PrefBranch.setBoolPref(`engine.${engine}.validation.enabled`, true);
    569  }
    570 }
    571 
    572 async function serverForEnginesWithKeys(users, engines, callback) {
    573  // Generate and store a fake default key bundle to avoid resetting the client
    574  // before the first sync.
    575  let wbo = await Service.collectionKeys.generateNewKeysWBO();
    576  let modified = new_timestamp();
    577  Service.collectionKeys.setContents(wbo.cleartext, modified);
    578 
    579  let allEngines = [Service.clientsEngine].concat(engines);
    580 
    581  let globalEngines = {};
    582  for (let engine of allEngines) {
    583    let syncID = await engine.resetLocalSyncID();
    584    globalEngines[engine.name] = { version: engine.version, syncID };
    585  }
    586 
    587  let contents = {
    588    meta: {
    589      global: {
    590        syncID: Service.syncID,
    591        storageVersion: STORAGE_VERSION,
    592        engines: globalEngines,
    593      },
    594    },
    595    crypto: {
    596      keys: encryptPayload(wbo.cleartext),
    597    },
    598  };
    599  for (let engine of allEngines) {
    600    contents[engine.name] = {};
    601  }
    602 
    603  return serverForUsers(users, contents, callback);
    604 }
    605 
    606 async function serverForFoo(engine, callback) {
    607  // The bookmarks engine *always* tracks changes, meaning we might try
    608  // and sync due to the bookmarks we ourselves create! Worse, because we
    609  // do an engine sync only, there's no locking - so we end up with multiple
    610  // syncs running. Neuter that by making the threshold very large.
    611  Service.scheduler.syncThreshold = 10000000;
    612  return serverForEnginesWithKeys({ foo: "password" }, engine, callback);
    613 }
    614 
    615 // Places notifies history observers asynchronously, so `addVisits` might return
    616 // before the tracker receives the notification. This helper registers an
    617 // observer that resolves once the expected notification fires.
    618 async function promiseVisit(expectedType, expectedURI) {
    619  return new Promise(resolve => {
    620    function done(type, uri) {
    621      if (uri == expectedURI.spec && type == expectedType) {
    622        PlacesObservers.removeListener(
    623          ["page-visited", "page-removed"],
    624          observer.handlePlacesEvents
    625        );
    626        resolve();
    627      }
    628    }
    629    let observer = {
    630      handlePlacesEvents(events) {
    631        Assert.equal(events.length, 1);
    632 
    633        if (events[0].type === "page-visited") {
    634          done("added", events[0].url);
    635        } else if (events[0].type === "page-removed") {
    636          Assert.ok(events[0].isRemovedFromStore);
    637          done("removed", events[0].url);
    638        }
    639      },
    640    };
    641    PlacesObservers.addListener(
    642      ["page-visited", "page-removed"],
    643      observer.handlePlacesEvents
    644    );
    645  });
    646 }
    647 
    648 async function addVisit(
    649  suffix,
    650  referrer = null,
    651  transition = PlacesUtils.history.TRANSITION_LINK
    652 ) {
    653  let uriString = "http://getfirefox.com/" + suffix;
    654  let uri = CommonUtils.makeURI(uriString);
    655  _("Adding visit for URI " + uriString);
    656 
    657  let visitAddedPromise = promiseVisit("added", uri);
    658  await PlacesTestUtils.addVisits({
    659    uri,
    660    visitDate: Date.now() * 1000,
    661    transition,
    662    referrer,
    663  });
    664  await visitAddedPromise;
    665 
    666  return uri;
    667 }
    668 
    669 function bookmarkNodesToInfos(nodes) {
    670  return nodes.map(node => {
    671    let info = {
    672      guid: node.guid,
    673      index: node.index,
    674    };
    675    if (node.children) {
    676      info.children = bookmarkNodesToInfos(node.children);
    677    }
    678    return info;
    679  });
    680 }
    681 
    682 async function assertBookmarksTreeMatches(rootGuid, expected, message) {
    683  let root = await PlacesUtils.promiseBookmarksTree(rootGuid, {
    684    includeItemIds: true,
    685  });
    686  let actual = bookmarkNodesToInfos(root.children);
    687 
    688  if (!ObjectUtils.deepEqual(actual, expected)) {
    689    _(`Expected structure for ${rootGuid}`, JSON.stringify(expected));
    690    _(`Actual structure for ${rootGuid}`, JSON.stringify(actual));
    691    throw new Assert.constructor.AssertionError({ actual, expected, message });
    692  }
    693 }
    694 
    695 function add_bookmark_test(task) {
    696  const { BookmarksEngine } = ChromeUtils.importESModule(
    697    "resource://services-sync/engines/bookmarks.sys.mjs"
    698  );
    699 
    700  add_task(async function () {
    701    _(`Running bookmarks test ${task.name}`);
    702    let engine = new BookmarksEngine(Service);
    703    await engine.initialize();
    704    await engine._resetClient();
    705    try {
    706      await task(engine);
    707    } finally {
    708      await engine.finalize();
    709    }
    710  });
    711 }