OLD | NEW |
1 /* | 1 /* |
2 * This file is part of Adblock Plus <https://adblockplus.org/>, | 2 * This file is part of Adblock Plus <https://adblockplus.org/>, |
3 * Copyright (C) 2006-present eyeo GmbH | 3 * Copyright (C) 2006-present eyeo GmbH |
4 * | 4 * |
5 * Adblock Plus is free software: you can redistribute it and/or modify | 5 * Adblock Plus is free software: you can redistribute it and/or modify |
6 * it under the terms of the GNU General Public License version 3 as | 6 * it under the terms of the GNU General Public License version 3 as |
7 * published by the Free Software Foundation. | 7 * published by the Free Software Foundation. |
8 * | 8 * |
9 * Adblock Plus is distributed in the hope that it will be useful, | 9 * Adblock Plus is distributed in the hope that it will be useful, |
10 * but WITHOUT ANY WARRANTY; without even the implied warranty of | 10 * but WITHOUT ANY WARRANTY; without even the implied warranty of |
11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the | 11 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the |
12 * GNU General Public License for more details. | 12 * GNU General Public License for more details. |
13 * | 13 * |
14 * You should have received a copy of the GNU General Public License | 14 * You should have received a copy of the GNU General Public License |
15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. | 15 * along with Adblock Plus. If not, see <http://www.gnu.org/licenses/>. |
16 */ | 16 */ |
17 | 17 |
18 "use strict"; | 18 "use strict"; |
19 | 19 |
| 20 const assert = require("assert"); |
20 const {createSandbox, unexpectedError} = require("./_common"); | 21 const {createSandbox, unexpectedError} = require("./_common"); |
21 | 22 |
22 let Filter = null; | 23 let Filter = null; |
23 let filterStorage = null; | 24 let filterStorage = null; |
24 let IO = null; | 25 let IO = null; |
25 let Prefs = null; | 26 let Prefs = null; |
26 let ExternalSubscription = null; | 27 let ExternalSubscription = null; |
27 let SpecialSubscription = null; | 28 let SpecialSubscription = null; |
28 | 29 |
29 exports.setUp = function(callback) | 30 describe("Filter Storage Read/Write", () => |
30 { | 31 { |
31 let sandboxedRequire = createSandbox(); | 32 beforeEach(() => |
32 ( | 33 { |
33 {Filter} = sandboxedRequire("../lib/filterClasses"), | 34 let sandboxedRequire = createSandbox(); |
34 {filterStorage} = sandboxedRequire("../lib/filterStorage"), | 35 ( |
35 {IO} = sandboxedRequire("./stub-modules/io"), | 36 {Filter} = sandboxedRequire("../lib/filterClasses"), |
36 {Prefs} = sandboxedRequire("./stub-modules/prefs"), | 37 {filterStorage} = sandboxedRequire("../lib/filterStorage"), |
37 {ExternalSubscription, SpecialSubscription} = sandboxedRequire("../lib/subsc
riptionClasses") | 38 {IO} = sandboxedRequire("./stub-modules/io"), |
38 ); | 39 {Prefs} = sandboxedRequire("./stub-modules/prefs"), |
39 | 40 {ExternalSubscription, SpecialSubscription} = sandboxedRequire("../lib/sub
scriptionClasses") |
40 filterStorage.addFilter(Filter.fromText("foobar")); | 41 ); |
41 callback(); | 42 |
42 }; | 43 filterStorage.addFilter(Filter.fromText("foobar")); |
43 | 44 }); |
44 let testData = new Promise((resolve, reject) => | 45 |
45 { | 46 let testData = new Promise((resolve, reject) => |
46 const fs = require("fs"); | 47 { |
47 const path = require("path"); | 48 const fs = require("fs"); |
48 let datapath = path.resolve(__dirname, "data", "patterns.ini"); | 49 const path = require("path"); |
49 | 50 let datapath = path.resolve(__dirname, "data", "patterns.ini"); |
50 fs.readFile(datapath, "utf-8", (error, data) => | 51 |
51 { | 52 fs.readFile(datapath, "utf-8", (error, data) => |
52 if (error) | 53 { |
53 reject(error); | 54 if (error) |
54 else | 55 reject(error); |
55 resolve(data.split(/[\r\n]+/)); | 56 else |
| 57 resolve(data.split(/[\r\n]+/)); |
| 58 }); |
| 59 }); |
| 60 |
| 61 function canonize(data) |
| 62 { |
| 63 let curSection = null; |
| 64 let sections = []; |
| 65 for (let line of data) |
| 66 { |
| 67 if (/^\[.*\]$/.test(line)) |
| 68 { |
| 69 if (curSection) |
| 70 sections.push(curSection); |
| 71 |
| 72 curSection = {header: line, data: []}; |
| 73 } |
| 74 else if (curSection && /\S/.test(line)) |
| 75 curSection.data.push(line); |
| 76 } |
| 77 if (curSection) |
| 78 sections.push(curSection); |
| 79 |
| 80 for (let section of sections) |
| 81 { |
| 82 section.key = section.header + " " + section.data[0]; |
| 83 section.data.sort(); |
| 84 } |
| 85 sections.sort((a, b) => |
| 86 { |
| 87 if (a.key < b.key) |
| 88 return -1; |
| 89 else if (a.key > b.key) |
| 90 return 1; |
| 91 return 0; |
| 92 }); |
| 93 return sections; |
| 94 } |
| 95 |
| 96 async function testReadWrite(withExternal, withEmptySpecial) |
| 97 { |
| 98 assert.ok(!filterStorage.initialized, "Uninitialized before the first load")
; |
| 99 |
| 100 try |
| 101 { |
| 102 let data = await testData; |
| 103 |
| 104 IO._setFileContents(filterStorage.sourceFile, data); |
| 105 await filterStorage.loadFromDisk(); |
| 106 |
| 107 assert.ok(filterStorage.initialized, "Initialize after the first load"); |
| 108 assert.equal(filterStorage.fileProperties.version, filterStorage.formatVer
sion, "File format version"); |
| 109 |
| 110 if (withExternal) |
| 111 { |
| 112 { |
| 113 let subscription = new ExternalSubscription("~external~external subscr
iption ID", "External subscription"); |
| 114 subscription.addFilter(Filter.fromText("foo")); |
| 115 subscription.addFilter(Filter.fromText("bar")); |
| 116 filterStorage.addSubscription(subscription); |
| 117 } |
| 118 |
| 119 let externalSubscriptions = [...filterStorage.subscriptions()].filter(su
bscription => subscription instanceof ExternalSubscription); |
| 120 assert.equal(externalSubscriptions.length, 1, "Number of external subscr
iptions after updateExternalSubscription"); |
| 121 |
| 122 assert.equal(externalSubscriptions[0].url, "~external~external subscript
ion ID", "ID of external subscription"); |
| 123 assert.equal(externalSubscriptions[0].filterCount, 2, "Number of filters
in external subscription"); |
| 124 } |
| 125 |
| 126 if (withEmptySpecial) |
| 127 { |
| 128 let specialSubscription = |
| 129 SpecialSubscription.createForFilter(Filter.fromText("!foo")); |
| 130 filterStorage.addSubscription(specialSubscription); |
| 131 |
| 132 filterStorage.removeFilter(Filter.fromText("!foo"), specialSubscription)
; |
| 133 |
| 134 assert.equal(specialSubscription.filterCount, 0, |
| 135 "No filters in special subscription"); |
| 136 assert.ok(new Set(filterStorage.subscriptions()).has(specialSubscription
), |
| 137 "Empty special subscription still in storage"); |
| 138 } |
| 139 |
| 140 await filterStorage.saveToDisk(); |
| 141 |
| 142 let expected = await testData; |
| 143 |
| 144 assert.deepEqual(canonize(IO._getFileContents(filterStorage.sourceFile)), |
| 145 canonize(expected), "Read/write result"); |
| 146 } |
| 147 catch (error) |
| 148 { |
| 149 unexpectedError.call(assert, error); |
| 150 } |
| 151 } |
| 152 |
| 153 describe("Read and Save", () => |
| 154 { |
| 155 it("to File", () => |
| 156 { |
| 157 testReadWrite(false); |
| 158 }); |
| 159 |
| 160 it("to file With External Subscription", () => |
| 161 { |
| 162 testReadWrite(true); |
| 163 }); |
| 164 |
| 165 it("to file With Empty Special", () => |
| 166 { |
| 167 testReadWrite(false, true); |
| 168 }); |
| 169 }); |
| 170 |
| 171 it("Import / Export", async() => |
| 172 { |
| 173 try |
| 174 { |
| 175 let lines = await testData; |
| 176 |
| 177 if (lines.length && lines[lines.length - 1] == "") |
| 178 lines.pop(); |
| 179 |
| 180 let importer = filterStorage.importData(); |
| 181 for (let line of lines) |
| 182 importer(line); |
| 183 importer(null); |
| 184 |
| 185 assert.equal(filterStorage.fileProperties.version, filterStorage.formatVer
sion, "File format version"); |
| 186 |
| 187 let exported = Array.from(filterStorage.exportData()); |
| 188 assert.deepEqual(canonize(exported), canonize(lines), "Import/export resul
t"); |
| 189 |
| 190 // Make sure the relationships between filters and subscriptions are set |
| 191 // up. |
| 192 for (let subscription of filterStorage.subscriptions()) |
| 193 { |
| 194 for (let text of subscription.filterText()) |
| 195 { |
| 196 let found = false; |
| 197 |
| 198 for (let filterSubscription of Filter.fromText(text).subscriptions()) |
| 199 { |
| 200 if (filterSubscription == subscription) |
| 201 { |
| 202 found = true; |
| 203 break; |
| 204 } |
| 205 } |
| 206 |
| 207 assert.ok(found, `Filter ${text} should be linked to subscription ${su
bscription.url}`); |
| 208 } |
| 209 } |
| 210 } |
| 211 catch (error) |
| 212 { |
| 213 unexpectedError.call(assert, error); |
| 214 } |
| 215 }); |
| 216 |
| 217 describe("Backups", () => |
| 218 { |
| 219 it("Saving Without", async() => |
| 220 { |
| 221 Prefs.patternsbackups = 0; |
| 222 Prefs.patternsbackupinterval = 24; |
| 223 |
| 224 try |
| 225 { |
| 226 await filterStorage.saveToDisk(); |
| 227 await filterStorage.saveToDisk(); |
| 228 |
| 229 assert.ok(!IO._getFileContents(filterStorage.getBackupName(1)), |
| 230 "Backup shouldn't be created"); |
| 231 } |
| 232 catch (error) |
| 233 { |
| 234 unexpectedError.call(assert, error); |
| 235 } |
| 236 }); |
| 237 |
| 238 it("Saving With", async() => |
| 239 { |
| 240 Prefs.patternsbackups = 2; |
| 241 Prefs.patternsbackupinterval = 24; |
| 242 |
| 243 let backupFile = filterStorage.getBackupName(1); |
| 244 let backupFile2 = filterStorage.getBackupName(2); |
| 245 let backupFile3 = filterStorage.getBackupName(3); |
| 246 |
| 247 let oldModifiedTime; |
| 248 |
| 249 try |
| 250 { |
| 251 await filterStorage.saveToDisk(); |
| 252 |
| 253 // Save again immediately |
| 254 await filterStorage.saveToDisk(); |
| 255 |
| 256 assert.ok(IO._getFileContents(backupFile), "First backup created"); |
| 257 |
| 258 oldModifiedTime = IO._getModifiedTime(backupFile) - 10000; |
| 259 IO._setModifiedTime(backupFile, oldModifiedTime); |
| 260 await filterStorage.saveToDisk(); |
| 261 |
| 262 assert.equal(IO._getModifiedTime(backupFile), oldModifiedTime, "Backup n
ot overwritten if it is only 10 seconds old"); |
| 263 |
| 264 oldModifiedTime -= 40 * 60 * 60 * 1000; |
| 265 IO._setModifiedTime(backupFile, oldModifiedTime); |
| 266 await filterStorage.saveToDisk(); |
| 267 |
| 268 assert.notEqual(IO._getModifiedTime(backupFile), oldModifiedTime, "Backu
p overwritten if it is 40 hours old"); |
| 269 |
| 270 assert.ok(IO._getFileContents(backupFile2), "Second backup created when
first backup is overwritten"); |
| 271 |
| 272 IO._setModifiedTime(backupFile, IO._getModifiedTime(backupFile) - 20000)
; |
| 273 oldModifiedTime = IO._getModifiedTime(backupFile2); |
| 274 await filterStorage.saveToDisk(); |
| 275 |
| 276 assert.equal(IO._getModifiedTime(backupFile2), oldModifiedTime, "Second
backup not overwritten if first one is only 20 seconds old"); |
| 277 |
| 278 IO._setModifiedTime(backupFile, IO._getModifiedTime(backupFile) - 25 * 6
0 * 60 * 1000); |
| 279 oldModifiedTime = IO._getModifiedTime(backupFile2); |
| 280 await filterStorage.saveToDisk(); |
| 281 |
| 282 assert.notEqual(IO._getModifiedTime(backupFile2), oldModifiedTime, "Seco
nd backup overwritten if first one is 25 hours old"); |
| 283 |
| 284 assert.ok(!IO._getFileContents(backupFile3), "Third backup not created w
ith patternsbackups = 2"); |
| 285 } |
| 286 catch (error) |
| 287 { |
| 288 unexpectedError.call(assert, error); |
| 289 } |
| 290 }); |
| 291 |
| 292 it("Restoring", async() => |
| 293 { |
| 294 Prefs.patternsbackups = 2; |
| 295 Prefs.patternsbackupinterval = 24; |
| 296 |
| 297 try |
| 298 { |
| 299 await filterStorage.saveToDisk(); |
| 300 |
| 301 assert.equal([...filterStorage.subscriptions()][0].filterCount, 1, "Init
ial filter count"); |
| 302 filterStorage.addFilter(Filter.fromText("barfoo")); |
| 303 assert.equal([...filterStorage.subscriptions()][0].filterCount, 2, "Filt
er count after adding a filter"); |
| 304 await filterStorage.saveToDisk(); |
| 305 |
| 306 await filterStorage.loadFromDisk(); |
| 307 |
| 308 assert.equal([...filterStorage.subscriptions()][0].filterCount, 2, "Filt
er count after adding filter and reloading"); |
| 309 await filterStorage.restoreBackup(1); |
| 310 |
| 311 assert.equal([...filterStorage.subscriptions()][0].filterCount, 1, "Filt
er count after restoring backup"); |
| 312 await filterStorage.loadFromDisk(); |
| 313 |
| 314 assert.equal([...filterStorage.subscriptions()][0].filterCount, 1, "Filt
er count after reloading"); |
| 315 } |
| 316 catch (error) |
| 317 { |
| 318 unexpectedError.call(assert, error); |
| 319 } |
| 320 }); |
56 }); | 321 }); |
57 }); | 322 }); |
58 | |
59 function canonize(data) | |
60 { | |
61 let curSection = null; | |
62 let sections = []; | |
63 for (let line of data) | |
64 { | |
65 if (/^\[.*\]$/.test(line)) | |
66 { | |
67 if (curSection) | |
68 sections.push(curSection); | |
69 | |
70 curSection = {header: line, data: []}; | |
71 } | |
72 else if (curSection && /\S/.test(line)) | |
73 curSection.data.push(line); | |
74 } | |
75 if (curSection) | |
76 sections.push(curSection); | |
77 | |
78 for (let section of sections) | |
79 { | |
80 section.key = section.header + " " + section.data[0]; | |
81 section.data.sort(); | |
82 } | |
83 sections.sort((a, b) => | |
84 { | |
85 if (a.key < b.key) | |
86 return -1; | |
87 else if (a.key > b.key) | |
88 return 1; | |
89 return 0; | |
90 }); | |
91 return sections; | |
92 } | |
93 | |
94 async function testReadWrite(test, withExternal, withEmptySpecial) | |
95 { | |
96 test.ok(!filterStorage.initialized, "Uninitialized before the first load"); | |
97 | |
98 try | |
99 { | |
100 let data = await testData; | |
101 | |
102 IO._setFileContents(filterStorage.sourceFile, data); | |
103 await filterStorage.loadFromDisk(); | |
104 | |
105 test.ok(filterStorage.initialized, "Initialize after the first load"); | |
106 test.equal(filterStorage.fileProperties.version, filterStorage.formatVersion
, "File format version"); | |
107 | |
108 if (withExternal) | |
109 { | |
110 { | |
111 let subscription = new ExternalSubscription("~external~external subscrip
tion ID", "External subscription"); | |
112 subscription.addFilter(Filter.fromText("foo")); | |
113 subscription.addFilter(Filter.fromText("bar")); | |
114 filterStorage.addSubscription(subscription); | |
115 } | |
116 | |
117 let externalSubscriptions = [...filterStorage.subscriptions()].filter(subs
cription => subscription instanceof ExternalSubscription); | |
118 test.equal(externalSubscriptions.length, 1, "Number of external subscripti
ons after updateExternalSubscription"); | |
119 | |
120 test.equal(externalSubscriptions[0].url, "~external~external subscription
ID", "ID of external subscription"); | |
121 test.equal(externalSubscriptions[0].filterCount, 2, "Number of filters in
external subscription"); | |
122 } | |
123 | |
124 if (withEmptySpecial) | |
125 { | |
126 let specialSubscription = | |
127 SpecialSubscription.createForFilter(Filter.fromText("!foo")); | |
128 filterStorage.addSubscription(specialSubscription); | |
129 | |
130 filterStorage.removeFilter(Filter.fromText("!foo"), specialSubscription); | |
131 | |
132 test.equal(specialSubscription.filterCount, 0, | |
133 "No filters in special subscription"); | |
134 test.ok(new Set(filterStorage.subscriptions()).has(specialSubscription), | |
135 "Empty special subscription still in storage"); | |
136 } | |
137 | |
138 await filterStorage.saveToDisk(); | |
139 | |
140 let expected = await testData; | |
141 | |
142 test.deepEqual(canonize(IO._getFileContents(filterStorage.sourceFile)), | |
143 canonize(expected), "Read/write result"); | |
144 } | |
145 catch (error) | |
146 { | |
147 unexpectedError.call(test, error); | |
148 } | |
149 | |
150 test.done(); | |
151 } | |
152 | |
153 exports.testReadAndSaveToFile = function(test) | |
154 { | |
155 testReadWrite(test, false); | |
156 }; | |
157 | |
158 exports.testReadAndSaveToFileWithExternalSubscription = function(test) | |
159 { | |
160 testReadWrite(test, true); | |
161 }; | |
162 | |
163 exports.testReadAndSaveToFileWithEmptySpecial = function(test) | |
164 { | |
165 testReadWrite(test, false, true); | |
166 }; | |
167 | |
168 exports.testImportExport = async function(test) | |
169 { | |
170 try | |
171 { | |
172 let lines = await testData; | |
173 | |
174 if (lines.length && lines[lines.length - 1] == "") | |
175 lines.pop(); | |
176 | |
177 let importer = filterStorage.importData(); | |
178 for (let line of lines) | |
179 importer(line); | |
180 importer(null); | |
181 | |
182 test.equal(filterStorage.fileProperties.version, filterStorage.formatVersion
, "File format version"); | |
183 | |
184 let exported = Array.from(filterStorage.exportData()); | |
185 test.deepEqual(canonize(exported), canonize(lines), "Import/export result"); | |
186 | |
187 // Make sure the relationships between filters and subscriptions are set | |
188 // up. | |
189 for (let subscription of filterStorage.subscriptions()) | |
190 { | |
191 for (let text of subscription.filterText()) | |
192 { | |
193 let found = false; | |
194 | |
195 for (let filterSubscription of Filter.fromText(text).subscriptions()) | |
196 { | |
197 if (filterSubscription == subscription) | |
198 { | |
199 found = true; | |
200 break; | |
201 } | |
202 } | |
203 | |
204 test.ok(found, `Filter ${text} should be linked to subscription ${subscr
iption.url}`); | |
205 } | |
206 } | |
207 } | |
208 catch (error) | |
209 { | |
210 unexpectedError.call(test, error); | |
211 } | |
212 | |
213 test.done(); | |
214 }; | |
215 | |
216 exports.testSavingWithoutBackups = async function(test) | |
217 { | |
218 Prefs.patternsbackups = 0; | |
219 Prefs.patternsbackupinterval = 24; | |
220 | |
221 try | |
222 { | |
223 await filterStorage.saveToDisk(); | |
224 await filterStorage.saveToDisk(); | |
225 | |
226 test.ok(!IO._getFileContents(filterStorage.getBackupName(1)), | |
227 "Backup shouldn't be created"); | |
228 } | |
229 catch (error) | |
230 { | |
231 unexpectedError.call(test, error); | |
232 } | |
233 | |
234 test.done(); | |
235 }; | |
236 | |
237 exports.testSavingWithBackups = async function(test) | |
238 { | |
239 Prefs.patternsbackups = 2; | |
240 Prefs.patternsbackupinterval = 24; | |
241 | |
242 let backupFile = filterStorage.getBackupName(1); | |
243 let backupFile2 = filterStorage.getBackupName(2); | |
244 let backupFile3 = filterStorage.getBackupName(3); | |
245 | |
246 let oldModifiedTime; | |
247 | |
248 try | |
249 { | |
250 await filterStorage.saveToDisk(); | |
251 | |
252 // Save again immediately | |
253 await filterStorage.saveToDisk(); | |
254 | |
255 test.ok(IO._getFileContents(backupFile), "First backup created"); | |
256 | |
257 oldModifiedTime = IO._getModifiedTime(backupFile) - 10000; | |
258 IO._setModifiedTime(backupFile, oldModifiedTime); | |
259 await filterStorage.saveToDisk(); | |
260 | |
261 test.equal(IO._getModifiedTime(backupFile), oldModifiedTime, "Backup not ove
rwritten if it is only 10 seconds old"); | |
262 | |
263 oldModifiedTime -= 40 * 60 * 60 * 1000; | |
264 IO._setModifiedTime(backupFile, oldModifiedTime); | |
265 await filterStorage.saveToDisk(); | |
266 | |
267 test.notEqual(IO._getModifiedTime(backupFile), oldModifiedTime, "Backup over
written if it is 40 hours old"); | |
268 | |
269 test.ok(IO._getFileContents(backupFile2), "Second backup created when first
backup is overwritten"); | |
270 | |
271 IO._setModifiedTime(backupFile, IO._getModifiedTime(backupFile) - 20000); | |
272 oldModifiedTime = IO._getModifiedTime(backupFile2); | |
273 await filterStorage.saveToDisk(); | |
274 | |
275 test.equal(IO._getModifiedTime(backupFile2), oldModifiedTime, "Second backup
not overwritten if first one is only 20 seconds old"); | |
276 | |
277 IO._setModifiedTime(backupFile, IO._getModifiedTime(backupFile) - 25 * 60 *
60 * 1000); | |
278 oldModifiedTime = IO._getModifiedTime(backupFile2); | |
279 await filterStorage.saveToDisk(); | |
280 | |
281 test.notEqual(IO._getModifiedTime(backupFile2), oldModifiedTime, "Second bac
kup overwritten if first one is 25 hours old"); | |
282 | |
283 test.ok(!IO._getFileContents(backupFile3), "Third backup not created with pa
tternsbackups = 2"); | |
284 } | |
285 catch (error) | |
286 { | |
287 unexpectedError.call(test, error); | |
288 } | |
289 | |
290 test.done(); | |
291 }; | |
292 | |
293 exports.testRestoringBackup = async function(test) | |
294 { | |
295 Prefs.patternsbackups = 2; | |
296 Prefs.patternsbackupinterval = 24; | |
297 | |
298 try | |
299 { | |
300 await filterStorage.saveToDisk(); | |
301 | |
302 test.equal([...filterStorage.subscriptions()][0].filterCount, 1, "Initial fi
lter count"); | |
303 filterStorage.addFilter(Filter.fromText("barfoo")); | |
304 test.equal([...filterStorage.subscriptions()][0].filterCount, 2, "Filter cou
nt after adding a filter"); | |
305 await filterStorage.saveToDisk(); | |
306 | |
307 await filterStorage.loadFromDisk(); | |
308 | |
309 test.equal([...filterStorage.subscriptions()][0].filterCount, 2, "Filter cou
nt after adding filter and reloading"); | |
310 await filterStorage.restoreBackup(1); | |
311 | |
312 test.equal([...filterStorage.subscriptions()][0].filterCount, 1, "Filter cou
nt after restoring backup"); | |
313 await filterStorage.loadFromDisk(); | |
314 | |
315 test.equal([...filterStorage.subscriptions()][0].filterCount, 1, "Filter cou
nt after reloading"); | |
316 } | |
317 catch (error) | |
318 { | |
319 unexpectedError.call(test, error); | |
320 } | |
321 | |
322 test.done(); | |
323 }; | |
OLD | NEW |