|
1 const Cc = Components.classes; |
|
2 const Ci = Components.interfaces; |
|
3 const Cu = Components.utils; |
|
4 const Cr = Components.results; |
|
5 |
|
6 function newCacheBackEndUsed() |
|
7 { |
|
8 var cache1srv = Components.classes["@mozilla.org/network/cache-service;1"] |
|
9 .getService(Components.interfaces.nsICacheService); |
|
10 var cache2srv = Components.classes["@mozilla.org/netwerk/cache-storage-service;1"] |
|
11 .getService(Components.interfaces.nsICacheStorageService); |
|
12 |
|
13 return cache1srv.cacheIOTarget != cache2srv.ioTarget; |
|
14 } |
|
15 |
|
16 var callbacks = new Array(); |
|
17 |
|
18 // Expect an existing entry |
|
19 const NORMAL = 0; |
|
20 // Expect a new entry |
|
21 const NEW = 1 << 0; |
|
22 // Return early from onCacheEntryCheck and set the callback to state it expects onCacheEntryCheck to happen |
|
23 const NOTVALID = 1 << 1; |
|
24 // Throw from onCacheEntryAvailable |
|
25 const THROWAVAIL = 1 << 2; |
|
26 // Open entry for reading-only |
|
27 const READONLY = 1 << 3; |
|
28 // Expect the entry to not be found |
|
29 const NOTFOUND = 1 << 4; |
|
30 // Return ENTRY_NEEDS_REVALIDATION from onCacheEntryCheck |
|
31 const REVAL = 1 << 5; |
|
32 // Return ENTRY_PARTIAL from onCacheEntryCheck, in combo with NEW or RECREATE bypasses check for emptiness of the entry |
|
33 const PARTIAL = 1 << 6 |
|
34 // Expect the entry is doomed, i.e. the output stream should not be possible to open |
|
35 const DOOMED = 1 << 7; |
|
36 // Don't trigger the go-on callback until the entry is written |
|
37 const WAITFORWRITE = 1 << 8; |
|
38 // Don't write data (i.e. don't open output stream) |
|
39 const METAONLY = 1 << 9; |
|
40 // Do recreation of an existing cache entry |
|
41 const RECREATE = 1 << 10; |
|
42 // Do not give me the entry |
|
43 const NOTWANTED = 1 << 11; |
|
44 // Tell the cache to wait for the entry to be completely written first |
|
45 const COMPLETE = 1 << 12; |
|
46 |
|
47 var log_c2 = true; |
|
48 function LOG_C2(o, m) |
|
49 { |
|
50 if (!log_c2) return; |
|
51 if (!m) |
|
52 dump("TEST-INFO | CACHE2: " + o + "\n"); |
|
53 else |
|
54 dump("TEST-INFO | CACHE2: callback #" + o.order + "(" + (o.workingData || "---") + ") " + m + "\n"); |
|
55 } |
|
56 |
|
57 function pumpReadStream(inputStream, goon) |
|
58 { |
|
59 if (inputStream.isNonBlocking()) { |
|
60 // non-blocking stream, must read via pump |
|
61 var pump = Cc["@mozilla.org/network/input-stream-pump;1"] |
|
62 .createInstance(Ci.nsIInputStreamPump); |
|
63 pump.init(inputStream, -1, -1, 0, 0, true); |
|
64 var data = ""; |
|
65 pump.asyncRead({ |
|
66 onStartRequest: function (aRequest, aContext) { }, |
|
67 onDataAvailable: function (aRequest, aContext, aInputStream, aOffset, aCount) |
|
68 { |
|
69 var wrapper = Cc["@mozilla.org/scriptableinputstream;1"]. |
|
70 createInstance(Ci.nsIScriptableInputStream); |
|
71 wrapper.init(aInputStream); |
|
72 var str = wrapper.read(wrapper.available()); |
|
73 LOG_C2("reading data '" + str.substring(0,5) + "'"); |
|
74 data += str; |
|
75 }, |
|
76 onStopRequest: function (aRequest, aContext, aStatusCode) |
|
77 { |
|
78 LOG_C2("done reading data: " + aStatusCode); |
|
79 do_check_eq(aStatusCode, Cr.NS_OK); |
|
80 goon(data); |
|
81 }, |
|
82 }, null); |
|
83 } |
|
84 else { |
|
85 // blocking stream |
|
86 var data = read_stream(inputStream, inputStream.available()); |
|
87 goon(data); |
|
88 } |
|
89 } |
|
90 |
|
91 OpenCallback.prototype = |
|
92 { |
|
93 QueryInterface: function listener_qi(iid) { |
|
94 if (iid.equals(Ci.nsISupports) || |
|
95 iid.equals(Ci.nsICacheEntryOpenCallback)) { |
|
96 return this; |
|
97 } |
|
98 throw Components.results.NS_ERROR_NO_INTERFACE; |
|
99 }, |
|
100 onCacheEntryCheck: function(entry, appCache) |
|
101 { |
|
102 LOG_C2(this, "onCacheEntryCheck"); |
|
103 do_check_true(!this.onCheckPassed); |
|
104 this.onCheckPassed = true; |
|
105 |
|
106 if (this.behavior & NOTVALID) { |
|
107 LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED"); |
|
108 return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; |
|
109 } |
|
110 |
|
111 if (this.behavior & NOTWANTED) { |
|
112 LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NOT_WANTED"); |
|
113 return Ci.nsICacheEntryOpenCallback.ENTRY_NOT_WANTED; |
|
114 } |
|
115 |
|
116 do_check_eq(entry.getMetaDataElement("meto"), this.workingMetadata); |
|
117 |
|
118 // check for sane flag combination |
|
119 do_check_neq(this.behavior & (REVAL|PARTIAL), REVAL|PARTIAL); |
|
120 |
|
121 if (this.behavior & (REVAL|PARTIAL)) { |
|
122 LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_NEEDS_REVALIDATION"); |
|
123 return Ci.nsICacheEntryOpenCallback.ENTRY_NEEDS_REVALIDATION; |
|
124 } |
|
125 |
|
126 if (this.behavior & COMPLETE) { |
|
127 LOG_C2(this, "onCacheEntryCheck DONE, return RECHECK_AFTER_WRITE_FINISHED"); |
|
128 if (newCacheBackEndUsed()) { |
|
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 } |
|
139 return Ci.nsICacheEntryOpenCallback.RECHECK_AFTER_WRITE_FINISHED; |
|
140 } |
|
141 |
|
142 LOG_C2(this, "onCacheEntryCheck DONE, return ENTRY_WANTED"); |
|
143 return Ci.nsICacheEntryOpenCallback.ENTRY_WANTED; |
|
144 }, |
|
145 onCacheEntryAvailable: function(entry, isnew, appCache, status) |
|
146 { |
|
147 LOG_C2(this, "onCacheEntryAvailable, " + this.behavior); |
|
148 do_check_true(!this.onAvailPassed); |
|
149 this.onAvailPassed = true; |
|
150 |
|
151 do_check_eq(isnew, !!(this.behavior & NEW)); |
|
152 |
|
153 if (this.behavior & (NOTFOUND|NOTWANTED)) { |
|
154 do_check_eq(status, Cr.NS_ERROR_CACHE_KEY_NOT_FOUND); |
|
155 do_check_false(!!entry); |
|
156 if (this.behavior & THROWAVAIL) |
|
157 this.throwAndNotify(entry); |
|
158 this.goon(entry); |
|
159 } |
|
160 else if (this.behavior & (NEW|RECREATE)) { |
|
161 do_check_true(!!entry); |
|
162 |
|
163 if (this.behavior & RECREATE) { |
|
164 entry = entry.recreate(); |
|
165 do_check_true(!!entry); |
|
166 } |
|
167 |
|
168 if (this.behavior & THROWAVAIL) |
|
169 this.throwAndNotify(entry); |
|
170 |
|
171 if (!(this.behavior & WAITFORWRITE)) |
|
172 this.goon(entry); |
|
173 |
|
174 if (!(this.behavior & PARTIAL)) { |
|
175 try { |
|
176 entry.getMetaDataElement("meto"); |
|
177 do_check_true(false); |
|
178 } |
|
179 catch (ex) {} |
|
180 } |
|
181 |
|
182 var self = this; |
|
183 do_execute_soon(function() { // emulate network latency |
|
184 entry.setMetaDataElement("meto", self.workingMetadata); |
|
185 entry.metaDataReady(); |
|
186 if (self.behavior & METAONLY) { |
|
187 // Since forcing GC/CC doesn't trigger OnWriterClosed, we have to set the entry valid manually :( |
|
188 entry.setValid(); |
|
189 entry.close(); |
|
190 return; |
|
191 } |
|
192 do_execute_soon(function() { // emulate more network latency |
|
193 if (self.behavior & DOOMED) { |
|
194 try { |
|
195 var os = entry.openOutputStream(0); |
|
196 do_check_true(false); |
|
197 } catch (ex) { |
|
198 do_check_true(true); |
|
199 } |
|
200 if (self.behavior & WAITFORWRITE) |
|
201 self.goon(entry); |
|
202 return; |
|
203 } |
|
204 |
|
205 var offset = (self.behavior & PARTIAL) |
|
206 ? entry.dataSize |
|
207 : 0; |
|
208 LOG_C2(self, "openOutputStream @ " + offset); |
|
209 var os = entry.openOutputStream(offset); |
|
210 LOG_C2(self, "writing data"); |
|
211 var wrt = os.write(self.workingData, self.workingData.length); |
|
212 do_check_eq(wrt, self.workingData.length); |
|
213 os.close(); |
|
214 if (self.behavior & WAITFORWRITE) |
|
215 self.goon(entry); |
|
216 |
|
217 entry.close(); |
|
218 }) |
|
219 }) |
|
220 } |
|
221 else { // NORMAL |
|
222 do_check_true(!!entry); |
|
223 do_check_eq(entry.getMetaDataElement("meto"), this.workingMetadata); |
|
224 if (this.behavior & THROWAVAIL) |
|
225 this.throwAndNotify(entry); |
|
226 |
|
227 var wrapper = Cc["@mozilla.org/scriptableinputstream;1"]. |
|
228 createInstance(Ci.nsIScriptableInputStream); |
|
229 var self = this; |
|
230 pumpReadStream(entry.openInputStream(0), function(data) { |
|
231 do_check_eq(data, self.workingData); |
|
232 self.onDataCheckPassed = true; |
|
233 LOG_C2(self, "entry read done"); |
|
234 self.goon(entry); |
|
235 entry.close(); |
|
236 }); |
|
237 } |
|
238 }, |
|
239 selfCheck: function() |
|
240 { |
|
241 LOG_C2(this, "selfCheck"); |
|
242 |
|
243 do_check_true(this.onCheckPassed); |
|
244 do_check_true(this.onAvailPassed); |
|
245 do_check_true(this.onDataCheckPassed); |
|
246 }, |
|
247 throwAndNotify: function(entry) |
|
248 { |
|
249 LOG_C2(this, "Throwing"); |
|
250 var self = this; |
|
251 do_execute_soon(function() { |
|
252 LOG_C2(self, "Notifying"); |
|
253 self.goon(entry); |
|
254 }); |
|
255 throw Cr.NS_ERROR_FAILURE; |
|
256 } |
|
257 }; |
|
258 |
|
259 function OpenCallback(behavior, workingMetadata, workingData, goon) |
|
260 { |
|
261 this.behavior = behavior; |
|
262 this.workingMetadata = workingMetadata; |
|
263 this.workingData = workingData; |
|
264 this.goon = goon; |
|
265 this.onCheckPassed = (!!(behavior & (NEW|RECREATE)) || !workingMetadata) && !(behavior & NOTVALID); |
|
266 this.onAvailPassed = false; |
|
267 this.onDataCheckPassed = !!(behavior & (NEW|RECREATE|NOTWANTED)) || !workingMetadata; |
|
268 callbacks.push(this); |
|
269 this.order = callbacks.length; |
|
270 } |
|
271 |
|
272 VisitCallback.prototype = |
|
273 { |
|
274 QueryInterface: function listener_qi(iid) { |
|
275 if (iid.equals(Ci.nsISupports) || |
|
276 iid.equals(Ci.nsICacheStorageVisitor)) { |
|
277 return this; |
|
278 } |
|
279 throw Components.results.NS_ERROR_NO_INTERFACE; |
|
280 }, |
|
281 onCacheStorageInfo: function(num, consumption) |
|
282 { |
|
283 LOG_C2(this, "onCacheStorageInfo: num=" + num + ", size=" + consumption); |
|
284 do_check_eq(this.num, num); |
|
285 if (newCacheBackEndUsed()) { |
|
286 // Consumption is as expected only in the new backend |
|
287 do_check_eq(this.consumption, consumption); |
|
288 } |
|
289 if (!this.entries) |
|
290 this.notify(); |
|
291 }, |
|
292 onCacheEntryInfo: function(entry) |
|
293 { |
|
294 var key = entry.key; |
|
295 LOG_C2(this, "onCacheEntryInfo: key=" + key); |
|
296 |
|
297 do_check_true(!!this.entries); |
|
298 |
|
299 var index = this.entries.indexOf(key); |
|
300 do_check_true(index > -1); |
|
301 |
|
302 this.entries.splice(index, 1); |
|
303 }, |
|
304 onCacheEntryVisitCompleted: function() |
|
305 { |
|
306 LOG_C2(this, "onCacheEntryVisitCompleted"); |
|
307 if (this.entries) |
|
308 do_check_eq(this.entries.length, 0); |
|
309 this.notify(); |
|
310 }, |
|
311 notify: function() |
|
312 { |
|
313 do_check_true(!!this.goon); |
|
314 var goon = this.goon; |
|
315 this.goon = null; |
|
316 do_execute_soon(goon); |
|
317 }, |
|
318 selfCheck: function() |
|
319 { |
|
320 do_check_true(!this.entries || !this.entries.length); |
|
321 } |
|
322 }; |
|
323 |
|
324 function VisitCallback(num, consumption, entries, goon) |
|
325 { |
|
326 this.num = num; |
|
327 this.consumption = consumption; |
|
328 this.entries = entries; |
|
329 this.goon = goon; |
|
330 callbacks.push(this); |
|
331 this.order = callbacks.length; |
|
332 } |
|
333 |
|
334 EvictionCallback.prototype = |
|
335 { |
|
336 QueryInterface: function listener_qi(iid) { |
|
337 if (iid.equals(Ci.nsISupports) || |
|
338 iid.equals(Ci.nsICacheEntryDoomCallback)) { |
|
339 return this; |
|
340 } |
|
341 throw Components.results.NS_ERROR_NO_INTERFACE; |
|
342 }, |
|
343 onCacheEntryDoomed: function(result) |
|
344 { |
|
345 do_check_eq(this.expectedSuccess, result == Cr.NS_OK); |
|
346 this.goon(); |
|
347 }, |
|
348 selfCheck: function() {} |
|
349 } |
|
350 |
|
351 function EvictionCallback(success, goon) |
|
352 { |
|
353 this.expectedSuccess = success; |
|
354 this.goon = goon; |
|
355 callbacks.push(this); |
|
356 this.order = callbacks.length; |
|
357 } |
|
358 |
|
359 MultipleCallbacks.prototype = |
|
360 { |
|
361 fired: function() |
|
362 { |
|
363 if (--this.pending == 0) |
|
364 { |
|
365 var self = this; |
|
366 if (this.delayed) |
|
367 do_execute_soon(function() { self.goon(); }); |
|
368 else |
|
369 this.goon(); |
|
370 } |
|
371 } |
|
372 } |
|
373 |
|
374 function MultipleCallbacks(number, goon, delayed) |
|
375 { |
|
376 this.pending = number; |
|
377 this.goon = goon; |
|
378 this.delayed = delayed; |
|
379 } |
|
380 |
|
381 function finish_cache2_test() |
|
382 { |
|
383 callbacks.forEach(function(callback, index) { |
|
384 callback.selfCheck(); |
|
385 }); |
|
386 do_test_finished(); |
|
387 } |