michael@0: const Cc = Components.classes; michael@0: const Ci = Components.interfaces; michael@0: const Cu = Components.utils; michael@0: const Cr = Components.results; michael@0: michael@0: function newCacheBackEndUsed() michael@0: { michael@0: var cache1srv = Components.classes["@mozilla.org/network/cache-service;1"] michael@0: .getService(Components.interfaces.nsICacheService); michael@0: var cache2srv = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] michael@0: .getService(Components.interfaces.nsICacheStorageService); michael@0: michael@0: return cache1srv.cacheIOTarget != cache2srv.ioTarget; michael@0: } michael@0: michael@0: var callbacks = new Array(); michael@0: michael@0: // Expect an existing entry michael@0: const NORMAL = 0; michael@0: // Expect a new entry michael@0: const NEW = 1 << 0; michael@0: // Return early from onCacheEntryCheck and set the callback to state it expects onCacheEntryCheck to happen michael@0: const NOTVALID = 1 << 1; michael@0: // Throw from onCacheEntryAvailable michael@0: const THROWAVAIL = 1 << 2; michael@0: // Open entry for reading-only michael@0: const READONLY = 1 << 3; michael@0: // Expect the entry to not be found michael@0: const NOTFOUND = 1 << 4; michael@0: // Return ENTRY_NEEDS_REVALIDATION from onCacheEntryCheck michael@0: const REVAL = 1 << 5; michael@0: // Return ENTRY_PARTIAL from onCacheEntryCheck, in combo with NEW or RECREATE bypasses check for emptiness of the entry michael@0: const PARTIAL = 1 << 6 michael@0: // Expect the entry is doomed, i.e. the output stream should not be possible to open michael@0: const DOOMED = 1 << 7; michael@0: // Don't trigger the go-on callback until the entry is written michael@0: const WAITFORWRITE = 1 << 8; michael@0: // Don't write data (i.e. don't open output stream) michael@0: const METAONLY = 1 << 9; michael@0: // Do recreation of an existing cache entry michael@0: const RECREATE = 1 << 10; michael@0: // Do not give me the entry michael@0: const NOTWANTED = 1 << 11; michael@0: // Tell the cache to wait for the entry to be completely written first michael@0: const COMPLETE = 1 << 12; michael@0: michael@0: var log_c2 = true; michael@0: function LOG_C2(o, m) michael@0: { michael@0: if (!log_c2) return; michael@0: if (!m) michael@0: dump("TEST-INFO | CACHE2: " + o + "\n"); michael@0: else michael@0: dump("TEST-INFO | CACHE2: callback #" + o.order + "(" + (o.workingData || "---") + ") " + m + "\n"); michael@0: } michael@0: michael@0: function pumpReadStream(inputStream, goon) michael@0: { michael@0: if (inputStream.isNonBlocking()) { michael@0: // non-blocking stream, must read via pump michael@0: var pump = Cc["@mozilla.org/network/input-stream-pump;1"] michael@0: .createInstance(Ci.nsIInputStreamPump); michael@0: pump.init(inputStream, -1, -1, 0, 0, true); michael@0: var data = ""; michael@0: pump.asyncRead({ michael@0: onStartRequest: function (aRequest, aContext) { }, michael@0: onDataAvailable: function (aRequest, aContext, aInputStream, aOffset, aCount) michael@0: { michael@0: var wrapper = Cc["@mozilla.org/scriptableinputstream;1"]. michael@0: createInstance(Ci.nsIScriptableInputStream); michael@0: wrapper.init(aInputStream); michael@0: var str = wrapper.read(wrapper.available()); michael@0: LOG_C2("reading data '" + str.substring(0,5) + "'"); michael@0: data += str; michael@0: }, michael@0: onStopRequest: function (aRequest, aContext, aStatusCode) michael@0: { michael@0: LOG_C2("done reading data: " + aStatusCode); michael@0: do_check_eq(aStatusCode, Cr.NS_OK); michael@0: goon(data); michael@0: }, michael@0: }, null); michael@0: } michael@0: else { michael@0: // blocking stream michael@0: var data = read_stream(inputStream, inputStream.available()); michael@0: goon(data); michael@0: } michael@0: } michael@0: michael@0: OpenCallback.prototype = michael@0: { michael@0: QueryInterface: function listener_qi(iid) { michael@0: if (iid.equals(Ci.nsISupports) || michael@0: iid.equals(Ci.nsICacheEntryOpenCallback)) { michael@0: return this; michael@0: } michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: onCacheEntryCheck: function(entry, appCache) michael@0: { michael@0: LOG_C2(this, "onCacheEntryCheck"); michael@0: do_check_true(!this.onCheckPassed); michael@0: this.onCheckPassed = true; michael@0: michael@0: if (this.behavior & NOTVALID) { michael@0: LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED"); michael@0: return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; michael@0: } michael@0: michael@0: if (this.behavior & NOTWANTED) { michael@0: LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NOT_WANTED"); michael@0: return Ci.nsICacheEntryOpenCallback.ENTRY_NOT_WANTED; michael@0: } michael@0: michael@0: do_check_eq(entry.getMetaDataElement("meto"), this.workingMetadata); michael@0: michael@0: // check for sane flag combination michael@0: do_check_neq(this.behavior & (REVAL|PARTIAL), REVAL|PARTIAL); michael@0: michael@0: if (this.behavior & (REVAL|PARTIAL)) { michael@0: LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NEEDS_REVALIDATION"); michael@0: return Ci.nsICacheEntryOpenCallback.ENTRY_NEEDS_REVALIDATION; michael@0: } michael@0: michael@0: if (this.behavior & COMPLETE) { michael@0: LOG_C2(this, "onCacheEntryCheck DONE, return RECHECK_AFTER_WRITE_FINISHED"); michael@0: if (newCacheBackEndUsed()) { michael@0: // Specific to the new backend because of concurrent read/write: michael@0: // when a consumer returns RECHECK_AFTER_WRITE_FINISHED from onCacheEntryCheck michael@0: // the cache calls this callback again after the entry write has finished. michael@0: // This gives the consumer a chance to recheck completeness of the entry michael@0: // again. michael@0: // Thus, we reset state as onCheck would have never been called. michael@0: this.onCheckPassed = false; michael@0: // Don't return RECHECK_AFTER_WRITE_FINISHED on second call of onCacheEntryCheck. michael@0: this.behavior &= ~COMPLETE; michael@0: } michael@0: return Ci.nsICacheEntryOpenCallback.RECHECK_AFTER_WRITE_FINISHED; michael@0: } michael@0: michael@0: LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED"); michael@0: return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; michael@0: }, michael@0: onCacheEntryAvailable: function(entry, isnew, appCache, status) michael@0: { michael@0: LOG_C2(this, "onCacheEntryAvailable, " + this.behavior); michael@0: do_check_true(!this.onAvailPassed); michael@0: this.onAvailPassed = true; michael@0: michael@0: do_check_eq(isnew, !!(this.behavior & NEW)); michael@0: michael@0: if (this.behavior & (NOTFOUND|NOTWANTED)) { michael@0: do_check_eq(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); michael@0: do_check_false(!!entry); michael@0: if (this.behavior & THROWAVAIL) michael@0: this.throwAndNotify(entry); michael@0: this.goon(entry); michael@0: } michael@0: else if (this.behavior & (NEW|RECREATE)) { michael@0: do_check_true(!!entry); michael@0: michael@0: if (this.behavior & RECREATE) { michael@0: entry = entry.recreate(); michael@0: do_check_true(!!entry); michael@0: } michael@0: michael@0: if (this.behavior & THROWAVAIL) michael@0: this.throwAndNotify(entry); michael@0: michael@0: if (!(this.behavior & WAITFORWRITE)) michael@0: this.goon(entry); michael@0: michael@0: if (!(this.behavior & PARTIAL)) { michael@0: try { michael@0: entry.getMetaDataElement("meto"); michael@0: do_check_true(false); michael@0: } michael@0: catch (ex) {} michael@0: } michael@0: michael@0: var self = this; michael@0: do_execute_soon(function() { // emulate network latency michael@0: entry.setMetaDataElement("meto", self.workingMetadata); michael@0: entry.metaDataReady(); michael@0: if (self.behavior & METAONLY) { michael@0: // Since forcing GC/CC doesn't trigger OnWriterClosed, we have to set the entry valid manually :( michael@0: entry.setValid(); michael@0: entry.close(); michael@0: return; michael@0: } michael@0: do_execute_soon(function() { // emulate more network latency michael@0: if (self.behavior & DOOMED) { michael@0: try { michael@0: var os = entry.openOutputStream(0); michael@0: do_check_true(false); michael@0: } catch (ex) { michael@0: do_check_true(true); michael@0: } michael@0: if (self.behavior & WAITFORWRITE) michael@0: self.goon(entry); michael@0: return; michael@0: } michael@0: michael@0: var offset = (self.behavior & PARTIAL) michael@0: ? entry.dataSize michael@0: : 0; michael@0: LOG_C2(self, "openOutputStream @ " + offset); michael@0: var os = entry.openOutputStream(offset); michael@0: LOG_C2(self, "writing data"); michael@0: var wrt = os.write(self.workingData, self.workingData.length); michael@0: do_check_eq(wrt, self.workingData.length); michael@0: os.close(); michael@0: if (self.behavior & WAITFORWRITE) michael@0: self.goon(entry); michael@0: michael@0: entry.close(); michael@0: }) michael@0: }) michael@0: } michael@0: else { // NORMAL michael@0: do_check_true(!!entry); michael@0: do_check_eq(entry.getMetaDataElement("meto"), this.workingMetadata); michael@0: if (this.behavior & THROWAVAIL) michael@0: this.throwAndNotify(entry); michael@0: michael@0: var wrapper = Cc["@mozilla.org/scriptableinputstream;1"]. michael@0: createInstance(Ci.nsIScriptableInputStream); michael@0: var self = this; michael@0: pumpReadStream(entry.openInputStream(0), function(data) { michael@0: do_check_eq(data, self.workingData); michael@0: self.onDataCheckPassed = true; michael@0: LOG_C2(self, "entry read done"); michael@0: self.goon(entry); michael@0: entry.close(); michael@0: }); michael@0: } michael@0: }, michael@0: selfCheck: function() michael@0: { michael@0: LOG_C2(this, "selfCheck"); michael@0: michael@0: do_check_true(this.onCheckPassed); michael@0: do_check_true(this.onAvailPassed); michael@0: do_check_true(this.onDataCheckPassed); michael@0: }, michael@0: throwAndNotify: function(entry) michael@0: { michael@0: LOG_C2(this, "Throwing"); michael@0: var self = this; michael@0: do_execute_soon(function() { michael@0: LOG_C2(self, "Notifying"); michael@0: self.goon(entry); michael@0: }); michael@0: throw Cr.NS_ERROR_FAILURE; michael@0: } michael@0: }; michael@0: michael@0: function OpenCallback(behavior, workingMetadata, workingData, goon) michael@0: { michael@0: this.behavior = behavior; michael@0: this.workingMetadata = workingMetadata; michael@0: this.workingData = workingData; michael@0: this.goon = goon; michael@0: this.onCheckPassed = (!!(behavior & (NEW|RECREATE)) || !workingMetadata) && !(behavior & NOTVALID); michael@0: this.onAvailPassed = false; michael@0: this.onDataCheckPassed = !!(behavior & (NEW|RECREATE|NOTWANTED)) || !workingMetadata; michael@0: callbacks.push(this); michael@0: this.order = callbacks.length; michael@0: } michael@0: michael@0: VisitCallback.prototype = michael@0: { michael@0: QueryInterface: function listener_qi(iid) { michael@0: if (iid.equals(Ci.nsISupports) || michael@0: iid.equals(Ci.nsICacheStorageVisitor)) { michael@0: return this; michael@0: } michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: onCacheStorageInfo: function(num, consumption) michael@0: { michael@0: LOG_C2(this, "onCacheStorageInfo: num=" + num + ", size=" + consumption); michael@0: do_check_eq(this.num, num); michael@0: if (newCacheBackEndUsed()) { michael@0: // Consumption is as expected only in the new backend michael@0: do_check_eq(this.consumption, consumption); michael@0: } michael@0: if (!this.entries) michael@0: this.notify(); michael@0: }, michael@0: onCacheEntryInfo: function(entry) michael@0: { michael@0: var key = entry.key; michael@0: LOG_C2(this, "onCacheEntryInfo: key=" + key); michael@0: michael@0: do_check_true(!!this.entries); michael@0: michael@0: var index = this.entries.indexOf(key); michael@0: do_check_true(index > -1); michael@0: michael@0: this.entries.splice(index, 1); michael@0: }, michael@0: onCacheEntryVisitCompleted: function() michael@0: { michael@0: LOG_C2(this, "onCacheEntryVisitCompleted"); michael@0: if (this.entries) michael@0: do_check_eq(this.entries.length, 0); michael@0: this.notify(); michael@0: }, michael@0: notify: function() michael@0: { michael@0: do_check_true(!!this.goon); michael@0: var goon = this.goon; michael@0: this.goon = null; michael@0: do_execute_soon(goon); michael@0: }, michael@0: selfCheck: function() michael@0: { michael@0: do_check_true(!this.entries || !this.entries.length); michael@0: } michael@0: }; michael@0: michael@0: function VisitCallback(num, consumption, entries, goon) michael@0: { michael@0: this.num = num; michael@0: this.consumption = consumption; michael@0: this.entries = entries; michael@0: this.goon = goon; michael@0: callbacks.push(this); michael@0: this.order = callbacks.length; michael@0: } michael@0: michael@0: EvictionCallback.prototype = michael@0: { michael@0: QueryInterface: function listener_qi(iid) { michael@0: if (iid.equals(Ci.nsISupports) || michael@0: iid.equals(Ci.nsICacheEntryDoomCallback)) { michael@0: return this; michael@0: } michael@0: throw Components.results.NS_ERROR_NO_INTERFACE; michael@0: }, michael@0: onCacheEntryDoomed: function(result) michael@0: { michael@0: do_check_eq(this.expectedSuccess, result == Cr.NS_OK); michael@0: this.goon(); michael@0: }, michael@0: selfCheck: function() {} michael@0: } michael@0: michael@0: function EvictionCallback(success, goon) michael@0: { michael@0: this.expectedSuccess = success; michael@0: this.goon = goon; michael@0: callbacks.push(this); michael@0: this.order = callbacks.length; michael@0: } michael@0: michael@0: MultipleCallbacks.prototype = michael@0: { michael@0: fired: function() michael@0: { michael@0: if (--this.pending == 0) michael@0: { michael@0: var self = this; michael@0: if (this.delayed) michael@0: do_execute_soon(function() { self.goon(); }); michael@0: else michael@0: this.goon(); michael@0: } michael@0: } michael@0: } michael@0: michael@0: function MultipleCallbacks(number, goon, delayed) michael@0: { michael@0: this.pending = number; michael@0: this.goon = goon; michael@0: this.delayed = delayed; michael@0: } michael@0: michael@0: function finish_cache2_test() michael@0: { michael@0: callbacks.forEach(function(callback, index) { michael@0: callback.selfCheck(); michael@0: }); michael@0: do_test_finished(); michael@0: }