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 }