test_l10nCache.js (17217B)
1 /* Any copyright is dedicated to the Public Domain. 2 * http://creativecommons.org/publicdomain/zero/1.0/ */ 3 4 // Tests L10nCache in UrlbarUtils.sys.mjs. 5 6 "use strict"; 7 8 ChromeUtils.defineESModuleGetters(this, { 9 L10nCache: "moz-src:///browser/components/urlbar/UrlbarUtils.sys.mjs", 10 }); 11 12 add_task(async function comprehensive() { 13 // Set up a mock localization. 14 let l10n = initL10n({ 15 args0a: "Zero args value", 16 args0b: "Another zero args value", 17 args1a: "One arg value is { $arg1 }", 18 args1b: "Another one arg value is { $arg1 }", 19 args2a: "Two arg values are { $arg1 } and { $arg2 }", 20 args2b: "More two arg values are { $arg1 } and { $arg2 }", 21 args3a: "Three arg values are { $arg1 }, { $arg2 }, and { $arg3 }", 22 args3b: "More three arg values are { $arg1 }, { $arg2 }, and { $arg3 }", 23 attrs1: [".label = attrs1 label has zero args"], 24 attrs2: [ 25 ".label = attrs2 label has zero args", 26 ".tooltiptext = attrs2 tooltiptext arg value is { $arg1 }", 27 ], 28 attrs3: [ 29 ".label = attrs3 label has zero args", 30 ".tooltiptext = attrs3 tooltiptext arg value is { $arg1 }", 31 ".alt = attrs3 alt arg values are { $arg1 } and { $arg2 }", 32 ], 33 }); 34 35 let tests = [ 36 // different strings with the same number of args and also the same strings 37 // with different args 38 { 39 obj: { 40 id: "args0a", 41 }, 42 expected: { 43 value: "Zero args value", 44 attributes: null, 45 }, 46 }, 47 { 48 obj: { 49 id: "args0b", 50 }, 51 expected: { 52 value: "Another zero args value", 53 attributes: null, 54 }, 55 }, 56 { 57 obj: { 58 id: "args1a", 59 args: { arg1: "foo1" }, 60 }, 61 expected: { 62 value: "One arg value is foo1", 63 attributes: null, 64 }, 65 }, 66 { 67 obj: { 68 id: "args1a", 69 args: { arg1: "foo2" }, 70 }, 71 expected: { 72 value: "One arg value is foo2", 73 attributes: null, 74 }, 75 }, 76 { 77 obj: { 78 id: "args1b", 79 args: { arg1: "foo1" }, 80 }, 81 expected: { 82 value: "Another one arg value is foo1", 83 attributes: null, 84 }, 85 }, 86 { 87 obj: { 88 id: "args1b", 89 args: { arg1: "foo2" }, 90 }, 91 expected: { 92 value: "Another one arg value is foo2", 93 attributes: null, 94 }, 95 }, 96 { 97 obj: { 98 id: "args2a", 99 args: { arg1: "foo1", arg2: "bar1" }, 100 }, 101 expected: { 102 value: "Two arg values are foo1 and bar1", 103 attributes: null, 104 }, 105 }, 106 { 107 obj: { 108 id: "args2a", 109 args: { arg1: "foo2", arg2: "bar2" }, 110 }, 111 expected: { 112 value: "Two arg values are foo2 and bar2", 113 attributes: null, 114 }, 115 }, 116 { 117 obj: { 118 id: "args2b", 119 args: { arg1: "foo1", arg2: "bar1" }, 120 }, 121 expected: { 122 value: "More two arg values are foo1 and bar1", 123 attributes: null, 124 }, 125 }, 126 { 127 obj: { 128 id: "args2b", 129 args: { arg1: "foo2", arg2: "bar2" }, 130 }, 131 expected: { 132 value: "More two arg values are foo2 and bar2", 133 attributes: null, 134 }, 135 }, 136 { 137 obj: { 138 id: "args3a", 139 args: { arg1: "foo1", arg2: "bar1", arg3: "baz1" }, 140 }, 141 expected: { 142 value: "Three arg values are foo1, bar1, and baz1", 143 attributes: null, 144 }, 145 }, 146 { 147 obj: { 148 id: "args3a", 149 args: { arg1: "foo2", arg2: "bar2", arg3: "baz2" }, 150 }, 151 expected: { 152 value: "Three arg values are foo2, bar2, and baz2", 153 attributes: null, 154 }, 155 }, 156 { 157 obj: { 158 id: "args3b", 159 args: { arg1: "foo1", arg2: "bar1", arg3: "baz1" }, 160 }, 161 expected: { 162 value: "More three arg values are foo1, bar1, and baz1", 163 attributes: null, 164 }, 165 }, 166 { 167 obj: { 168 id: "args3b", 169 args: { arg1: "foo2", arg2: "bar2", arg3: "baz2" }, 170 }, 171 expected: { 172 value: "More three arg values are foo2, bar2, and baz2", 173 attributes: null, 174 }, 175 }, 176 177 // two instances of the same string with their args swapped 178 { 179 obj: { 180 id: "args2a", 181 args: { arg1: "arg A", arg2: "arg B" }, 182 }, 183 expected: { 184 value: "Two arg values are arg A and arg B", 185 attributes: null, 186 }, 187 }, 188 { 189 obj: { 190 id: "args2a", 191 args: { arg1: "arg B", arg2: "arg A" }, 192 }, 193 expected: { 194 value: "Two arg values are arg B and arg A", 195 attributes: null, 196 }, 197 }, 198 199 // strings with attributes 200 { 201 obj: { 202 id: "attrs1", 203 }, 204 expected: { 205 value: null, 206 attributes: { 207 label: "attrs1 label has zero args", 208 }, 209 }, 210 }, 211 { 212 obj: { 213 id: "attrs2", 214 args: { 215 arg1: "arg A", 216 }, 217 }, 218 expected: { 219 value: null, 220 attributes: { 221 label: "attrs2 label has zero args", 222 tooltiptext: "attrs2 tooltiptext arg value is arg A", 223 }, 224 }, 225 }, 226 { 227 obj: { 228 id: "attrs3", 229 args: { 230 arg1: "arg A", 231 arg2: "arg B", 232 }, 233 }, 234 expected: { 235 value: null, 236 attributes: { 237 label: "attrs3 label has zero args", 238 tooltiptext: "attrs3 tooltiptext arg value is arg A", 239 alt: "attrs3 alt arg values are arg A and arg B", 240 }, 241 }, 242 }, 243 ]; 244 245 let cache = new L10nCache(l10n); 246 247 // Get some non-cached strings. 248 Assert.ok(!cache.get({ id: "uncached1" }), "Uncached string 1"); 249 Assert.ok(!cache.get({ id: "uncached2", args: "foo" }), "Uncached string 2"); 250 251 // Add each test string and get it back. 252 for (let { obj, expected } of tests) { 253 await cache.add(obj); 254 let message = cache.get(obj); 255 Assert.deepEqual( 256 message, 257 expected, 258 "Expected message for obj: " + JSON.stringify(obj) 259 ); 260 } 261 262 // Get each string again to make sure each add didn't somehow mess up the 263 // previously added strings. 264 for (let { obj, expected } of tests) { 265 Assert.deepEqual( 266 cache.get(obj), 267 expected, 268 "Expected message for obj: " + JSON.stringify(obj) 269 ); 270 } 271 272 // Delete some of the strings. We'll delete every other one to mix it up. 273 for (let i = 0; i < tests.length; i++) { 274 if (i % 2 == 0) { 275 let { obj } = tests[i]; 276 cache.delete(obj); 277 Assert.ok(!cache.get(obj), "Deleted from cache: " + JSON.stringify(obj)); 278 } 279 } 280 281 // Get each remaining string. 282 for (let i = 0; i < tests.length; i++) { 283 if (i % 2 != 0) { 284 let { obj, expected } = tests[i]; 285 Assert.deepEqual( 286 cache.get(obj), 287 expected, 288 "Expected message for obj: " + JSON.stringify(obj) 289 ); 290 } 291 } 292 293 // Clear the cache. 294 cache.clear(); 295 for (let { obj } of tests) { 296 Assert.ok(!cache.get(obj), "After cache clear: " + JSON.stringify(obj)); 297 } 298 299 // `ensure` each test string and get it back. 300 for (let { obj, expected } of tests) { 301 await cache.ensure(obj); 302 let message = cache.get(obj); 303 Assert.deepEqual( 304 message, 305 expected, 306 "Expected message for obj: " + JSON.stringify(obj) 307 ); 308 309 // Call `ensure` again. This time, `add` should not be called. 310 let originalAdd = cache.add; 311 cache.add = () => Assert.ok(false, "add erroneously called"); 312 await cache.ensure(obj); 313 cache.add = originalAdd; 314 } 315 316 // Clear the cache again. 317 cache.clear(); 318 for (let { obj } of tests) { 319 Assert.ok(!cache.get(obj), "After cache clear: " + JSON.stringify(obj)); 320 } 321 322 // `ensureAll` the test strings and get them back. 323 let objects = tests.map(({ obj }) => obj); 324 await cache.ensureAll(objects); 325 for (let { obj, expected } of tests) { 326 let message = cache.get(obj); 327 Assert.deepEqual( 328 message, 329 expected, 330 "Expected message for obj: " + JSON.stringify(obj) 331 ); 332 } 333 334 // Ensure the cache is cleared after the app locale changes 335 Assert.greater(cache.size(), 0, "The cache has messages in it."); 336 Services.obs.notifyObservers(null, "intl:app-locales-changed"); 337 Assert.equal(cache.size(), 0, "The cache is empty on app locale change"); 338 }); 339 340 // Tests cache eviction. 341 add_task(async function eviction() { 342 // Set up a mock localization. 343 let l10n = initL10n({ 344 args0: "Zero args value", 345 args1: "One arg value is { $arg1 }", 346 args2: "Two arg values are { $arg1 } and { $arg2 }", 347 args3: "Three arg values are { $arg1 }, { $arg2 }, and { $arg3 }", 348 349 attrs0: [".label = attrs0 label has zero args"], 350 attrs1: [ 351 ".label = attrs1 label has zero args", 352 ".tooltiptext = attrs1 tooltiptext arg value is { $arg1 }", 353 ], 354 attrs2: [ 355 ".label = attrs2 label has zero args", 356 ".tooltiptext = attrs2 tooltiptext arg value is { $arg1 }", 357 ".alt = attrs2 alt arg values are { $arg1 } and { $arg2 }", 358 ], 359 }); 360 361 let cache = new L10nCache(l10n); 362 363 // Get the max cache entries per l10n ID. 364 let maxEntriesPerId = L10nCache.MAX_ENTRIES_PER_ID; 365 Assert.equal( 366 typeof maxEntriesPerId, 367 "number", 368 "MAX_ENTRIES_PER_ID should be a number" 369 ); 370 Assert.greater(maxEntriesPerId, 0, "MAX_ENTRIES_PER_ID should be > 0"); 371 372 // Cache enough l10n objects with the same ID but different args to fill up 373 // the ID's cache entries. The args will be "aaa-0", "aaa-1", etc. 374 for (let i = 0; i < maxEntriesPerId; i++) { 375 let arg1 = "aaa-" + i; 376 let l10nObj = { 377 id: "args1", 378 args: { arg1 }, 379 }; 380 await cache.add(l10nObj); 381 382 // The message should be cached. 383 Assert.deepEqual( 384 cache.get(l10nObj), 385 { 386 value: `One arg value is ${arg1}`, 387 attributes: null, 388 }, 389 "Message should be cached: " + JSON.stringify(l10nObj) 390 ); 391 392 // The cache size should be incremented. 393 Assert.equal( 394 cache.size(), 395 i + 1, 396 "Expected cache size after adding l10n obj: " + JSON.stringify(l10nObj) 397 ); 398 } 399 400 // Check some l10n objects we did not cache. 401 for (let arg1 of [`aaa-${maxEntriesPerId}`, "some other value"]) { 402 let l10nObj = { 403 id: "args1", 404 args: { arg1 }, 405 }; 406 Assert.ok( 407 !cache.get(l10nObj), 408 "Message should not be cached since it wasn't added: " + 409 JSON.stringify(l10nObj) 410 ); 411 } 412 413 // Now cache more l10n objects with the same ID as before but with new args: 414 // "bbb-0", "bbb-1", etc. Each time we cache a new object, the oldest "aaa" 415 // entry should be evicted since the ID's cache entries are filled up. 416 for (let i = 0; i < maxEntriesPerId; i++) { 417 let arg1 = "bbb-" + i; 418 let l10nObj = { 419 id: "args1", 420 args: { arg1 }, 421 }; 422 await cache.add(l10nObj); 423 424 // The message should be cached. 425 Assert.deepEqual( 426 cache.get(l10nObj), 427 { 428 value: `One arg value is ${arg1}`, 429 attributes: null, 430 }, 431 "Message should be cached: " + JSON.stringify(l10nObj) 432 ); 433 434 // The cache size should remain maxed out. 435 Assert.equal( 436 cache.size(), 437 maxEntriesPerId, 438 "Cache size should remain maxed out after caching l10n obj: " + 439 JSON.stringify(l10nObj) 440 ); 441 442 // The oldest "aaa" entry should have been evicted, and all previous oldest 443 // entries in prior iterations of this loop should remain evicted. 444 for (let j = 0; j < maxEntriesPerId; j++) { 445 let oldArg1 = "aaa-" + j; 446 let oldL10nObj = { 447 id: "args1", 448 args: { arg1: oldArg1 }, 449 }; 450 if (j <= i) { 451 Assert.deepEqual( 452 cache.get(oldL10nObj), 453 null, 454 "Message should be evicted for old l10n obj: " + 455 JSON.stringify(oldL10nObj) 456 ); 457 } else { 458 Assert.deepEqual( 459 cache.get(oldL10nObj), 460 { 461 value: `One arg value is ${oldArg1}`, 462 attributes: null, 463 }, 464 "Message should not yet be evicted for old l10n obj: " + 465 JSON.stringify(oldL10nObj) 466 ); 467 } 468 } 469 } 470 471 // Now cache more l10n objects just like before but with a different ID. Since 472 // the ID is new, we should be able to fill up its cache entries. 473 for (let i = 0; i < maxEntriesPerId; i++) { 474 let arg1 = "yyy-" + i; 475 let arg2 = "zzz-" + i; 476 let l10nObj = { 477 id: "args2", 478 args: { arg1, arg2 }, 479 }; 480 await cache.add(l10nObj); 481 482 // The message should be cached. 483 Assert.deepEqual( 484 cache.get(l10nObj), 485 { 486 value: `Two arg values are ${arg1} and ${arg2}`, 487 attributes: null, 488 }, 489 "Message should be cached: " + JSON.stringify(l10nObj) 490 ); 491 492 // The cache size should start increasing again since we're caching l10n 493 // objects with a different ID from before. 494 Assert.equal( 495 cache.size(), 496 maxEntriesPerId + i + 1, 497 "Cache size should start increasing again: " + JSON.stringify(l10nObj) 498 ); 499 500 // All the messages with the "args1" ID from above should remain cached. 501 for (let j = 0; j < maxEntriesPerId; j++) { 502 let prevArg1 = "bbb-" + j; 503 let prevL10nObj = { 504 id: "args1", 505 args: { arg1: prevArg1 }, 506 }; 507 Assert.deepEqual( 508 cache.get(prevL10nObj), 509 { 510 value: `One arg value is ${prevArg1}`, 511 attributes: null, 512 }, 513 "Previous message should remain cached: " + JSON.stringify(prevL10nObj) 514 ); 515 } 516 } 517 518 // Now re-cache some of the previously cached "args1" messages. This should 519 // reorder the "args1" cache entries so that these re-cached messages are most 520 // recently used. We'll re-cache messages with even-numbered args values. 521 for (let i = 0; i < maxEntriesPerId; i++) { 522 if (i % 2 == 0) { 523 let arg1 = "bbb-" + i; 524 let l10nObj = { 525 id: "args1", 526 args: { arg1 }, 527 }; 528 Assert.ok( 529 await cache.get(l10nObj), 530 "Sanity check: Message should still be cached: " + 531 JSON.stringify(l10nObj) 532 ); 533 await cache.add(l10nObj); 534 535 // The cache size should remain maxed out. 536 Assert.equal( 537 cache.size(), 538 2 * maxEntriesPerId, 539 "Cache size should remain maxed out after caching l10n obj: " + 540 JSON.stringify(l10nObj) 541 ); 542 } 543 } 544 545 // Build a list of args in the expected cached "args1" entries sorted from 546 // least recently used to most recently used. Since we just re-cached messages 547 // with even-numbered args, they should be at the end of this list, and 548 // messages with odd-numbered args should be at the front. 549 let expected = []; 550 for (let i = 0; i < maxEntriesPerId; i++) { 551 if (i % 2) { 552 // odd 553 expected.push("bbb-" + i); 554 } 555 } 556 for (let i = 0; i < maxEntriesPerId; i++) { 557 if (i % 2 == 0) { 558 // even 559 expected.push("bbb-" + i); 560 } 561 } 562 563 // Now cache more l10n objects with the same "args1" ID but with new args. 564 // The old "bbb" entries should be evicted in the expected order. 565 for (let i = 0; i < maxEntriesPerId; i++) { 566 let arg1 = "ccc-" + i; 567 let l10nObj = { 568 id: "args1", 569 args: { arg1 }, 570 }; 571 await cache.add(l10nObj); 572 573 // The message should be cached. 574 Assert.deepEqual( 575 cache.get(l10nObj), 576 { 577 value: `One arg value is ${arg1}`, 578 attributes: null, 579 }, 580 "Message should be cached: " + JSON.stringify(l10nObj) 581 ); 582 583 // The cache size should remain maxed out. 584 Assert.equal( 585 cache.size(), 586 2 * maxEntriesPerId, 587 "Cache size should remain maxed out after caching l10n obj: " + 588 JSON.stringify(l10nObj) 589 ); 590 591 // The oldest entry should have been evicted, and all previous oldest 592 // entries in prior iterations of this loop should remain evicted. 593 for (let j = 0; j < expected.length; j++) { 594 let oldArg1 = expected[j]; 595 let oldL10nObj = { 596 id: "args1", 597 args: { arg1: oldArg1 }, 598 }; 599 if (j <= i) { 600 Assert.deepEqual( 601 cache.get(oldL10nObj), 602 null, 603 "Message should be evicted for old l10n obj: " + 604 JSON.stringify(oldL10nObj) 605 ); 606 } else { 607 Assert.deepEqual( 608 cache.get(oldL10nObj), 609 { 610 value: `One arg value is ${oldArg1}`, 611 attributes: null, 612 }, 613 "Message should not yet be evicted for old l10n obj: " + 614 JSON.stringify(oldL10nObj) 615 ); 616 } 617 } 618 } 619 }); 620 621 /** 622 * Sets up a mock localization. 623 * 624 * @param {object} pairs 625 * Fluent strings as key-value pairs. 626 * @returns {Localization} 627 * The mock Localization object. 628 */ 629 function initL10n(pairs) { 630 let source = Object.entries(pairs) 631 .map(([key, value]) => { 632 if (Array.isArray(value)) { 633 value = value.map(s => " \n" + s).join(""); 634 } 635 return `${key} = ${value}`; 636 }) 637 .join("\n"); 638 let registry = new L10nRegistry(); 639 registry.registerSources([ 640 L10nFileSource.createMock( 641 "test", 642 "app", 643 ["en-US"], 644 "/localization/{locale}", 645 [{ source, path: "/localization/en-US/test.ftl" }] 646 ), 647 ]); 648 return new Localization(["/test.ftl"], true, registry, ["en-US"]); 649 }