head_cache2.js (12777B)
1 /* import-globals-from head_cache.js */ 2 /* import-globals-from head_channels.js */ 3 4 "use strict"; 5 6 var callbacks = []; 7 8 // Expect an existing entry 9 const NORMAL = 0; 10 // Expect a new entry 11 const NEW = 1 << 0; 12 // Return early from onCacheEntryCheck and set the callback to state it expects onCacheEntryCheck to happen 13 const NOTVALID = 1 << 1; 14 // Throw from onCacheEntryAvailable 15 const THROWAVAIL = 1 << 2; 16 // Open entry for reading-only 17 const READONLY = 1 << 3; 18 // Expect the entry to not be found 19 const NOTFOUND = 1 << 4; 20 // Return ENTRY_NEEDS_REVALIDATION from onCacheEntryCheck 21 const REVAL = 1 << 5; 22 // Return ENTRY_PARTIAL from onCacheEntryCheck, in combo with NEW or RECREATE bypasses check for emptiness of the entry 23 const PARTIAL = 1 << 6; 24 // Expect the entry is doomed, i.e. the output stream should not be possible to open 25 const DOOMED = 1 << 7; 26 // Don't trigger the go-on callback until the entry is written 27 const WAITFORWRITE = 1 << 8; 28 // Don't write data (i.e. don't open output stream) 29 const METAONLY = 1 << 9; 30 // Do recreation of an existing cache entry 31 const RECREATE = 1 << 10; 32 // Do not give me the entry 33 const NOTWANTED = 1 << 11; 34 // Tell the cache to wait for the entry to be completely written first 35 const COMPLETE = 1 << 12; 36 // Don't write meta/data and don't set valid in the callback, consumer will do it manually 37 const DONTFILL = 1 << 13; 38 // Used in combination with METAONLY, don't call setValid() on the entry after metadata has been set 39 const DONTSETVALID = 1 << 14; 40 // Notify before checking the data, useful for proper callback ordering checks 41 const NOTIFYBEFOREREAD = 1 << 15; 42 // It's allowed to not get an existing entry (result of opening is undetermined) 43 const MAYBE_NEW = 1 << 16; 44 45 var log_c2 = true; 46 function LOG_C2(o, m) { 47 if (!log_c2) { 48 return; 49 } 50 if (!m) { 51 dump("TEST-INFO | CACHE2: " + o + "\n"); 52 } else { 53 dump( 54 "TEST-INFO | CACHE2: callback #" + 55 o.order + 56 "(" + 57 (o.workingData ? o.workingData.substr(0, 10) : "---") + 58 ") " + 59 m + 60 "\n" 61 ); 62 } 63 } 64 65 function pumpReadStream(inputStream, goon) { 66 if (inputStream.isNonBlocking()) { 67 // non-blocking stream, must read via pump 68 var pump = Cc["@mozilla.org/network/input-stream-pump;1"].createInstance( 69 Ci.nsIInputStreamPump 70 ); 71 pump.init(inputStream, 0, 0, true); 72 let data = ""; 73 pump.asyncRead({ 74 onStartRequest() {}, 75 onDataAvailable(aRequest, aInputStream) { 76 var wrapper = Cc["@mozilla.org/scriptableinputstream;1"].createInstance( 77 Ci.nsIScriptableInputStream 78 ); 79 wrapper.init(aInputStream); 80 var str = wrapper.read(wrapper.available()); 81 LOG_C2("reading data '" + str.substring(0, 5) + "'"); 82 data += str; 83 }, 84 onStopRequest(aRequest, aStatusCode) { 85 LOG_C2("done reading data: " + aStatusCode); 86 Assert.equal(aStatusCode, Cr.NS_OK); 87 goon(data); 88 }, 89 }); 90 } else { 91 // blocking stream 92 let data = read_stream(inputStream, inputStream.available()); 93 goon(data); 94 } 95 } 96 97 OpenCallback.prototype = { 98 QueryInterface: ChromeUtils.generateQI(["nsICacheEntryOpenCallback"]), 99 onCacheEntryCheck(entry) { 100 LOG_C2(this, "onCacheEntryCheck"); 101 Assert.ok(!this.onCheckPassed); 102 this.onCheckPassed = true; 103 104 if (this.behavior & NOTVALID) { 105 LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED"); 106 return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; 107 } 108 109 if (this.behavior & NOTWANTED) { 110 LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NOT_WANTED"); 111 return Ci.nsICacheEntryOpenCallback.ENTRY_NOT_WANTED; 112 } 113 114 Assert.equal(entry.getMetaDataElement("meto"), this.workingMetadata); 115 116 // check for sane flag combination 117 Assert.notEqual(this.behavior & (REVAL | PARTIAL), REVAL | PARTIAL); 118 119 if (this.behavior & (REVAL | PARTIAL)) { 120 LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NEEDS_REVALIDATION"); 121 return Ci.nsICacheEntryOpenCallback.ENTRY_NEEDS_REVALIDATION; 122 } 123 124 if (this.behavior & COMPLETE) { 125 LOG_C2( 126 this, 127 "onCacheEntryCheck DONE, return RECHECK_AFTER_WRITE_FINISHED" 128 ); 129 // Specific to the new backend because of concurrent read/write: 130 // when a consumer returns RECHECK_AFTER_WRITE_FINISHED from onCacheEntryCheck 131 // the cache calls this callback again after the entry write has finished. 132 // This gives the consumer a chance to recheck completeness of the entry 133 // again. 134 // Thus, we reset state as onCheck would have never been called. 135 this.onCheckPassed = false; 136 // Don't return RECHECK_AFTER_WRITE_FINISHED on second call of onCacheEntryCheck. 137 this.behavior &= ~COMPLETE; 138 return Ci.nsICacheEntryOpenCallback.RECHECK_AFTER_WRITE_FINISHED; 139 } 140 141 LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED"); 142 return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; 143 }, 144 onCacheEntryAvailable(entry, isnew, status) { 145 if (this.behavior & MAYBE_NEW && isnew) { 146 this.behavior |= NEW; 147 } 148 149 LOG_C2(this, "onCacheEntryAvailable, " + this.behavior); 150 Assert.ok(!this.onAvailPassed); 151 this.onAvailPassed = true; 152 153 Assert.equal(isnew, !!(this.behavior & NEW)); 154 155 if (this.behavior & (NOTFOUND | NOTWANTED)) { 156 Assert.equal(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); 157 Assert.ok(!entry); 158 if (this.behavior & THROWAVAIL) { 159 this.throwAndNotify(entry); 160 } 161 this.goon(entry); 162 } else if (this.behavior & (NEW | RECREATE)) { 163 Assert.ok(!!entry); 164 165 if (this.behavior & RECREATE) { 166 entry = entry.recreate(); 167 Assert.ok(!!entry); 168 } 169 170 if (this.behavior & THROWAVAIL) { 171 this.throwAndNotify(entry); 172 } 173 174 if (!(this.behavior & WAITFORWRITE)) { 175 this.goon(entry); 176 } 177 178 if (!(this.behavior & PARTIAL)) { 179 try { 180 entry.getMetaDataElement("meto"); 181 Assert.ok(false); 182 } catch (ex) {} 183 } 184 185 if (this.behavior & DONTFILL) { 186 Assert.equal(false, this.behavior & WAITFORWRITE); 187 return; 188 } 189 190 let self = this; 191 executeSoon(function () { 192 // emulate network latency 193 entry.setMetaDataElement("meto", self.workingMetadata); 194 entry.metaDataReady(); 195 if (self.behavior & METAONLY) { 196 // Since forcing GC/CC doesn't trigger OnWriterClosed, we have to set the entry valid manually :( 197 if (!(self.behavior & DONTSETVALID)) { 198 entry.setValid(); 199 } 200 201 if (self.behavior & WAITFORWRITE) { 202 self.goon(entry); 203 } 204 205 return; 206 } 207 executeSoon(function () { 208 // emulate more network latency 209 if (self.behavior & DOOMED) { 210 LOG_C2(self, "checking doom state"); 211 try { 212 let os = entry.openOutputStream(0, -1); 213 // Unfortunately, in the undetermined state we cannot even check whether the entry 214 // is actually doomed or not. 215 os.close(); 216 Assert.ok(!!(self.behavior & MAYBE_NEW)); 217 } catch (ex) { 218 Assert.ok(true); 219 } 220 if (self.behavior & WAITFORWRITE) { 221 self.goon(entry); 222 } 223 return; 224 } 225 226 var offset = self.behavior & PARTIAL ? entry.dataSize : 0; 227 LOG_C2(self, "openOutputStream @ " + offset); 228 let os = entry.openOutputStream(offset, -1); 229 LOG_C2(self, "writing data"); 230 var wrt = os.write(self.workingData, self.workingData.length); 231 Assert.equal(wrt, self.workingData.length); 232 os.close(); 233 if (self.behavior & WAITFORWRITE) { 234 self.goon(entry); 235 } 236 }); 237 }); 238 } else { 239 // NORMAL 240 Assert.ok(!!entry); 241 Assert.equal(entry.getMetaDataElement("meto"), this.workingMetadata); 242 if (this.behavior & THROWAVAIL) { 243 this.throwAndNotify(entry); 244 } 245 if (this.behavior & NOTIFYBEFOREREAD) { 246 this.goon(entry, true); 247 } 248 249 let self = this; 250 pumpReadStream(entry.openInputStream(0), function (data) { 251 Assert.equal(data, self.workingData); 252 self.onDataCheckPassed = true; 253 LOG_C2(self, "entry read done"); 254 self.goon(entry); 255 }); 256 } 257 }, 258 selfCheck() { 259 LOG_C2(this, "selfCheck"); 260 261 Assert.ok(this.onCheckPassed || this.behavior & MAYBE_NEW); 262 Assert.ok(this.onAvailPassed); 263 Assert.ok(this.onDataCheckPassed || this.behavior & MAYBE_NEW); 264 }, 265 throwAndNotify(entry) { 266 LOG_C2(this, "Throwing"); 267 var self = this; 268 executeSoon(function () { 269 LOG_C2(self, "Notifying"); 270 self.goon(entry); 271 }); 272 throw Components.Exception("", Cr.NS_ERROR_FAILURE); 273 }, 274 }; 275 276 function OpenCallback(behavior, workingMetadata, workingData, goon) { 277 this.behavior = behavior; 278 this.workingMetadata = workingMetadata; 279 this.workingData = workingData; 280 this.goon = goon; 281 this.onCheckPassed = 282 (!!(behavior & (NEW | RECREATE)) || !workingMetadata) && 283 !(behavior & NOTVALID); 284 this.onAvailPassed = false; 285 this.onDataCheckPassed = 286 !!(behavior & (NEW | RECREATE | NOTWANTED)) || !workingMetadata; 287 callbacks.push(this); 288 this.order = callbacks.length; 289 } 290 291 VisitCallback.prototype = { 292 QueryInterface: ChromeUtils.generateQI(["nsICacheStorageVisitor"]), 293 onCacheStorageInfo(num, consumption) { 294 LOG_C2(this, "onCacheStorageInfo: num=" + num + ", size=" + consumption); 295 Assert.equal(this.num, num); 296 Assert.equal(this.consumption, consumption); 297 if (!this.entries) { 298 this.notify(); 299 } 300 }, 301 onCacheEntryInfo( 302 aURI, 303 aIdEnhance, 304 aDataSize, 305 aAltDataSize, 306 aFetchCount, 307 aLastModifiedTime, 308 aExpirationTime, 309 aPinned, 310 aInfo 311 ) { 312 var key = (aIdEnhance ? aIdEnhance + ":" : "") + aURI.asciiSpec; 313 LOG_C2(this, "onCacheEntryInfo: key=" + key); 314 315 function findCacheIndex(element) { 316 if (typeof element === "string") { 317 return element === key; 318 } else if (typeof element === "object") { 319 return ( 320 element.uri === key && 321 element.lci.isAnonymous === aInfo.isAnonymous && 322 ChromeUtils.isOriginAttributesEqual( 323 element.lci.originAttributes, 324 aInfo.originAttributes 325 ) 326 ); 327 } 328 329 return false; 330 } 331 332 Assert.ok( 333 !!this.entries, 334 "Ensure that the fact that we found cache entries matches expectations." 335 ); 336 337 var index = this.entries.findIndex(findCacheIndex); 338 Assert.greater(index, -1, "Cache entry should exist"); 339 340 this.entries.splice(index, 1); 341 }, 342 onCacheEntryVisitCompleted() { 343 LOG_C2(this, "onCacheEntryVisitCompleted"); 344 if (this.entries) { 345 Assert.equal( 346 this.entries.length, 347 0, 348 "Visited all expected cache entries." 349 ); 350 } 351 this.notify(); 352 }, 353 notify() { 354 Assert.ok(!!this.goon, "goon should not be null"); 355 var goon = this.goon; 356 this.goon = null; 357 executeSoon(goon); 358 }, 359 selfCheck() { 360 Assert.ok(!this.entries || !this.entries.length, "entries should be empty"); 361 }, 362 }; 363 364 function VisitCallback(num, consumption, entries, goon) { 365 this.num = num; 366 this.consumption = consumption; 367 this.entries = entries; 368 this.goon = goon; 369 callbacks.push(this); 370 this.order = callbacks.length; 371 } 372 373 EvictionCallback.prototype = { 374 QueryInterface: ChromeUtils.generateQI(["nsICacheEntryDoomCallback"]), 375 onCacheEntryDoomed(result) { 376 Assert.equal(this.expectedSuccess, result == Cr.NS_OK); 377 this.goon(); 378 }, 379 selfCheck() {}, 380 }; 381 382 function EvictionCallback(success, goon) { 383 this.expectedSuccess = success; 384 this.goon = goon; 385 callbacks.push(this); 386 this.order = callbacks.length; 387 } 388 389 MultipleCallbacks.prototype = { 390 fired() { 391 if (--this.pending == 0) { 392 var self = this; 393 if (this.delayed) { 394 executeSoon(function () { 395 self.goon(); 396 }); 397 } else { 398 this.goon(); 399 } 400 } 401 }, 402 add() { 403 ++this.pending; 404 }, 405 }; 406 407 function MultipleCallbacks(number, goon, delayed) { 408 this.pending = number; 409 this.goon = goon; 410 this.delayed = delayed; 411 } 412 413 function wait_for_cache_index(continue_func) { 414 // This callback will not fire before the index is in the ready state. nsICacheStorage.exists() will 415 // no longer throw after this point. 416 Services.cache2.asyncGetDiskConsumption({ 417 onNetworkCacheDiskConsumption() { 418 continue_func(); 419 }, 420 // eslint-disable-next-line mozilla/use-chromeutils-generateqi 421 QueryInterface() { 422 return this; 423 }, 424 }); 425 } 426 427 function finish_cache2_test() { 428 callbacks.forEach(function (callback) { 429 callback.selfCheck(); 430 }); 431 do_test_finished(); 432 }