tor-browser

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

test_rust_ingest.js (12133B)


      1 /* This Source Code Form is subject to the terms of the Mozilla Public
      2 * License, v. 2.0. If a copy of the MPL was not distributed with this
      3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
      4 
      5 // Tests ingest in the Rust backend.
      6 
      7 "use strict";
      8 
      9 ChromeUtils.defineESModuleGetters(this, {
     10  AsyncShutdown: "resource://gre/modules/AsyncShutdown.sys.mjs",
     11  InterruptKind:
     12    "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs",
     13  setTimeout: "resource://gre/modules/Timer.sys.mjs",
     14  SuggestIngestionMetrics:
     15    "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs",
     16  SuggestionProvider:
     17    "moz-src:///toolkit/components/uniffi-bindgen-gecko-js/components/generated/RustSuggest.sys.mjs",
     18 });
     19 
     20 // These consts are copied from the update timer manager test. See
     21 // `initUpdateTimerManager()`.
     22 const PREF_APP_UPDATE_TIMERMINIMUMDELAY = "app.update.timerMinimumDelay";
     23 const PREF_APP_UPDATE_TIMERFIRSTINTERVAL = "app.update.timerFirstInterval";
     24 const MAIN_TIMER_INTERVAL = 1000; // milliseconds
     25 const CATEGORY_UPDATE_TIMER = "update-timer";
     26 
     27 const REMOTE_SETTINGS_SUGGESTION = QuickSuggestTestUtils.ampRemoteSettings();
     28 
     29 add_setup(async function () {
     30  initUpdateTimerManager();
     31 
     32  await QuickSuggestTestUtils.ensureQuickSuggestInit({
     33    remoteSettingsRecords: [
     34      {
     35        type: "data",
     36        attachment: [REMOTE_SETTINGS_SUGGESTION],
     37      },
     38    ],
     39    prefs: [
     40      ["suggest.quicksuggest.all", true],
     41      ["suggest.quicksuggest.sponsored", true],
     42    ],
     43  });
     44 });
     45 
     46 // The backend should ingest when it's disabled and then re-enabled.
     47 add_task(async function disableEnable() {
     48  Assert.strictEqual(
     49    UrlbarPrefs.get("quicksuggest.rustEnabled"),
     50    true,
     51    "Sanity check: Rust pref is initially true"
     52  );
     53  Assert.strictEqual(
     54    QuickSuggest.rustBackend.isEnabled,
     55    true,
     56    "Sanity check: Rust backend is initially enabled"
     57  );
     58 
     59  let enabledTypes = QuickSuggest.rustBackend._test_enabledSuggestionTypes;
     60  Assert.greater(
     61    enabledTypes.length,
     62    0,
     63    "This test expects some Rust suggestion types to be enabled"
     64  );
     65 
     66  UrlbarPrefs.set("quicksuggest.rustEnabled", false);
     67  UrlbarPrefs.set("quicksuggest.rustEnabled", true);
     68 
     69  // `ingest()` must be stubbed only after re-enabling the backend since the
     70  // `SuggestStore` is recreated then.
     71  await withIngestStub(async ({ stub, rustBackend }) => {
     72    info("Awaiting ingest promise");
     73    await rustBackend.ingestPromise;
     74 
     75    checkIngestCounts({
     76      stub,
     77      expected: Object.fromEntries(
     78        enabledTypes.map(({ provider }) => [provider, 1])
     79      ),
     80    });
     81  });
     82 });
     83 
     84 // For a feature whose suggestion type provider has constraints, ingest should
     85 // happen when the constraints change.
     86 add_task(async function providerConstraintsChanged() {
     87  // We'll use the Dynamic feature since it has provider constraints. Make sure
     88  // it exists.
     89  let feature = QuickSuggest.getFeature("DynamicSuggestions");
     90  Assert.ok(
     91    !!feature,
     92    "This test expects the DynamicSuggestions feature to exist"
     93  );
     94  Assert.equal(
     95    feature.rustSuggestionType,
     96    "Dynamic",
     97    "This test expects Dynamic suggestions to exist"
     98  );
     99 
    100  let providersFilter = [SuggestionProvider.DYNAMIC];
    101  await withIngestStub(async ({ stub, rustBackend }) => {
    102    // Set the pref to a few non-empty string values. Each time, a dynamic
    103    // ingest should be triggered.
    104    for (let type of ["aaa", "bbb", "aaa,bbb"]) {
    105      UrlbarPrefs.set("quicksuggest.dynamicSuggestionTypes", type);
    106      info("Awaiting ingest promise after setting dynamicSuggestionTypes");
    107      await rustBackend.ingestPromise;
    108 
    109      checkIngestCounts({
    110        stub,
    111        providersFilter,
    112        expected: {
    113          [SuggestionProvider.DYNAMIC]: 1,
    114        },
    115      });
    116    }
    117 
    118    // Set the pref to an empty string. The feature should become disabled and
    119    // it shouldn't trigger ingest since no dynamic suggestions are enabled.
    120    UrlbarPrefs.set("quicksuggest.dynamicSuggestionTypes", "");
    121    info(
    122      "Awaiting ingest promise after setting dynamicSuggestionTypes to empty string"
    123    );
    124    await rustBackend.ingestPromise;
    125 
    126    Assert.ok(
    127      !feature.isEnabled,
    128      "Dynamic feature should be disabled after setting dynamicSuggestionTypes to empty string"
    129    );
    130    checkIngestCounts({
    131      stub,
    132      providersFilter,
    133      expected: {},
    134    });
    135  });
    136 
    137  UrlbarPrefs.clear("quicksuggest.dynamicSuggestionTypes");
    138  await QuickSuggest.rustBackend.ingestPromise;
    139 });
    140 
    141 // Ingestion should be performed according to the defined interval.
    142 add_task(async function interval() {
    143  // Re-enable the backend with a small ingest interval. A new ingest will
    144  // immediately start.
    145  let intervalSecs = 3;
    146  UrlbarPrefs.set("quicksuggest.rustIngestIntervalSeconds", intervalSecs);
    147  UrlbarPrefs.set("quicksuggest.rustEnabled", false);
    148  UrlbarPrefs.set("quicksuggest.rustEnabled", true);
    149 
    150  info("Awaiting initial ingest promise");
    151  let { ingestPromise } = QuickSuggest.rustBackend;
    152  await ingestPromise;
    153 
    154  let enabledTypes = QuickSuggest.rustBackend._test_enabledSuggestionTypes;
    155  Assert.greater(
    156    enabledTypes.length,
    157    0,
    158    "This test expects some Rust suggestion types to be enabled"
    159  );
    160 
    161  await withIngestStub(async ({ stub }) => {
    162    // Wait for a few ingests to happen due to the timer firing.
    163    for (let i = 0; i < 3; i++) {
    164      info(`Waiting ${intervalSecs}s for ingest to start at index ${i}`);
    165      ({ ingestPromise } = await waitForIngestStart(ingestPromise));
    166      info("Waiting for ingest to finish at index " + i);
    167      await ingestPromise;
    168      info("Ingest finished at index " + i);
    169 
    170      checkIngestCounts({
    171        stub,
    172        expected: Object.fromEntries(
    173          enabledTypes.map(({ provider }) => [provider, 1])
    174        ),
    175      });
    176    }
    177  });
    178 
    179  info("Disabling the backend");
    180  UrlbarPrefs.set("quicksuggest.rustEnabled", false);
    181 
    182  // At this point, ingests should stop with two caveats. (1) There may be one
    183  // ongoing ingest that started immediately after `ingestPromise` resolved in
    184  // the final iteration of the loop above. (2) The timer manager sometimes
    185  // fires our ingest timer even after it was unregistered by the backend (when
    186  // the backend was disabled), maybe because the interval is so small in this
    187  // test. These two things mean that up to two more ingests may finish now.
    188  // We'll simply wait for a few seconds up to two times until no new ingests
    189  // start.
    190 
    191  let waitSecs = 2 * intervalSecs;
    192  // eslint-disable-next-line mozilla/no-arbitrary-setTimeout
    193  let wait = () => new Promise(r => setTimeout(r, 1000 * waitSecs));
    194 
    195  let waitedAtEndOfLoop = false;
    196  for (let i = 0; i < 2; i++) {
    197    info(`Waiting ${waitSecs}s after disabling backend, i=${i}...`);
    198    await wait();
    199 
    200    let { ingestPromise: newIngestPromise } = QuickSuggest.rustBackend;
    201    if (ingestPromise == newIngestPromise) {
    202      info(`No new ingest started, i=${i}`);
    203      waitedAtEndOfLoop = true;
    204      break;
    205    }
    206 
    207    info(`New ingest started, now awaiting, i=${i}`);
    208    ingestPromise = newIngestPromise;
    209    await ingestPromise;
    210  }
    211 
    212  if (!waitedAtEndOfLoop) {
    213    info(`Waiting a final ${waitSecs}s...`);
    214    await wait();
    215  }
    216 
    217  // No new ingests should have started.
    218  Assert.equal(
    219    QuickSuggest.rustBackend.ingestPromise,
    220    ingestPromise,
    221    "No new ingest started after disabling the backend"
    222  );
    223 
    224  // Clean up for later tasks: Reset the interval and enable the backend again.
    225  UrlbarPrefs.clear("quicksuggest.rustIngestIntervalSeconds");
    226  UrlbarPrefs.set("quicksuggest.rustEnabled", true);
    227 
    228  info("Awaiting cleanup ingest promise");
    229  await QuickSuggest.rustBackend.ingestPromise;
    230  info("Done awaiting cleanup ingest promise");
    231 });
    232 
    233 // `SuggestStore.interrupt()` should be called on shutdown.
    234 add_task(async function shutdown() {
    235  let sandbox = sinon.createSandbox();
    236  let spy = sandbox.spy(QuickSuggest.rustBackend._test_store, "interrupt");
    237 
    238  Services.prefs.setBoolPref("toolkit.asyncshutdown.testing", true);
    239  AsyncShutdown.profileChangeTeardown._trigger();
    240 
    241  let calls = spy.getCalls();
    242  Assert.equal(
    243    calls.length,
    244    1,
    245    "store.interrupt() should have been called once on simulated shutdown"
    246  );
    247  Assert.deepEqual(
    248    calls[0].args,
    249    [InterruptKind.READ_WRITE],
    250    "store.interrupt() should have been called with InterruptKind.READ_WRITE"
    251  );
    252  Assert.ok(
    253    InterruptKind.READ_WRITE,
    254    "Sanity check: InterruptKind.READ_WRITE is defined"
    255  );
    256 
    257  Services.prefs.clearUserPref("toolkit.asyncshutdown.testing");
    258  sandbox.restore();
    259 });
    260 
    261 /**
    262 * Stubs `SuggestStore.ingest()` and calls your callback.
    263 *
    264 * @param {Function} callback
    265 *   Callback
    266 */
    267 async function withIngestStub(callback) {
    268  let sandbox = sinon.createSandbox();
    269  let { rustBackend } = QuickSuggest;
    270  let stub = sandbox.stub(rustBackend._test_store, "ingest");
    271 
    272  // `ingest()` returns a `SuggestIngestionMetrics` object.
    273  stub.returns(
    274    new SuggestIngestionMetrics({ ingestionTimes: [], downloadTimes: [] })
    275  );
    276 
    277  await callback({ stub, rustBackend });
    278  sandbox.restore();
    279 }
    280 
    281 /**
    282 * Gets `ingest()` call counts per Rust suggestion provider. Also resets the
    283 * call counts before returning.
    284 *
    285 * @param {stub} stub
    286 *   Sinon `ingest()` stub.
    287 * @param {Array} providersFilter
    288 *   Array of provider integers to filter in. If null, ingest counts from all
    289 *   providers will be returned.
    290 * @returns {object}
    291 *   An plain JS object that maps provider integers to ingest counts.
    292 */
    293 function getIngestCounts(stub, providersFilter = null) {
    294  let countsByProvider = {};
    295  for (let call of stub.getCalls()) {
    296    let ingestConstraints = call.args[0];
    297    for (let p of ingestConstraints.providers) {
    298      if (!providersFilter || providersFilter.includes(p)) {
    299        if (!countsByProvider.hasOwnProperty(p)) {
    300          countsByProvider[p] = 0;
    301        }
    302        countsByProvider[p]++;
    303      }
    304    }
    305  }
    306 
    307  info("Got ingest counts: " + JSON.stringify(countsByProvider));
    308 
    309  stub.resetHistory();
    310  return countsByProvider;
    311 }
    312 
    313 function checkIngestCounts({ stub, providersFilter, expected }) {
    314  Assert.deepEqual(
    315    getIngestCounts(stub, providersFilter),
    316    expected,
    317    "Actual ingest counts should match expected counts"
    318  );
    319 }
    320 
    321 async function waitForIngestStart(oldIngestPromise) {
    322  let newIngestPromise;
    323  await TestUtils.waitForCondition(() => {
    324    let { ingestPromise } = QuickSuggest.rustBackend;
    325    if (
    326      (oldIngestPromise && ingestPromise != oldIngestPromise) ||
    327      (!oldIngestPromise && ingestPromise)
    328    ) {
    329      newIngestPromise = ingestPromise;
    330      return true;
    331    }
    332    return false;
    333  }, "Waiting for a new ingest to start");
    334 
    335  Assert.equal(
    336    QuickSuggest.rustBackend.ingestPromise,
    337    newIngestPromise,
    338    "Sanity check: ingestPromise hasn't changed since waitForCondition returned"
    339  );
    340 
    341  // A bare promise can't be returned because it will cause the awaiting caller
    342  // to await that promise! We're simply trying to return the promise, which the
    343  // caller can later await.
    344  return { ingestPromise: newIngestPromise };
    345 }
    346 
    347 /**
    348 * Sets up the update timer manager for testing: makes it fire more often,
    349 * removes all existing timers, and initializes it for testing. The body of this
    350 * function is copied from:
    351 * https://searchfox.org/mozilla-central/source/toolkit/components/timermanager/tests/unit/consumerNotifications.js
    352 */
    353 function initUpdateTimerManager() {
    354  // Set the timer to fire every second
    355  Services.prefs.setIntPref(
    356    PREF_APP_UPDATE_TIMERMINIMUMDELAY,
    357    MAIN_TIMER_INTERVAL / 1000
    358  );
    359  Services.prefs.setIntPref(
    360    PREF_APP_UPDATE_TIMERFIRSTINTERVAL,
    361    MAIN_TIMER_INTERVAL
    362  );
    363 
    364  // Remove existing update timers to prevent them from being notified
    365  for (let { data: entry } of Services.catMan.enumerateCategory(
    366    CATEGORY_UPDATE_TIMER
    367  )) {
    368    Services.catMan.deleteCategoryEntry(CATEGORY_UPDATE_TIMER, entry, false);
    369  }
    370 
    371  Cc["@mozilla.org/updates/timer-manager;1"]
    372    .getService(Ci.nsIUpdateTimerManager)
    373    .QueryInterface(Ci.nsIObserver)
    374    .observe(null, "utm-test-init", "");
    375 }