head_storage.js (10451B)
1 /* This Source Code Form is subject to the terms of the Mozilla Public 2 * License, v. 2.0. If a copy of the MPL was not distributed with this 3 * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ 4 5 var { XPCOMUtils } = ChromeUtils.importESModule( 6 "resource://gre/modules/XPCOMUtils.sys.mjs" 7 ); 8 var { AppConstants } = ChromeUtils.importESModule( 9 "resource://gre/modules/AppConstants.sys.mjs" 10 ); 11 12 ChromeUtils.defineESModuleGetters(this, { 13 FileUtils: "resource://gre/modules/FileUtils.sys.mjs", 14 Sqlite: "resource://gre/modules/Sqlite.sys.mjs", 15 TelemetryTestUtils: "resource://testing-common/TelemetryTestUtils.sys.mjs", 16 TestUtils: "resource://testing-common/TestUtils.sys.mjs", 17 }); 18 19 const TELEMETRY_VALUES = { 20 success: 0, 21 failure: 1, 22 access: 2, 23 diskio: 3, 24 corrupt: 4, 25 busy: 5, 26 misuse: 6, 27 diskspace: 7, 28 }; 29 30 do_get_profile(); 31 var gDBConn = null; 32 33 const TEST_DB_NAME = "test_storage.sqlite"; 34 function getTestDB() { 35 var db = Services.dirsvc.get("ProfD", Ci.nsIFile); 36 db.append(TEST_DB_NAME); 37 return db; 38 } 39 40 /** 41 * Obtains a corrupt database to test against. 42 */ 43 function getCorruptDB() { 44 return do_get_file("corruptDB.sqlite"); 45 } 46 47 /** 48 * Obtains a fake (non-SQLite format) database to test against. 49 */ 50 function getFakeDB() { 51 return do_get_file("fakeDB.sqlite"); 52 } 53 54 /** 55 * Delete the test database file. 56 */ 57 function deleteTestDB() { 58 print("*** Storage Tests: Trying to remove file!"); 59 var dbFile = getTestDB(); 60 if (dbFile.exists()) { 61 try { 62 dbFile.remove(false); 63 } catch (e) { 64 /* stupid windows box */ 65 } 66 } 67 } 68 69 function cleanup() { 70 // close the connection 71 print("*** Storage Tests: Trying to close!"); 72 getOpenedDatabase().close(); 73 74 // we need to null out the database variable to get a new connection the next 75 // time getOpenedDatabase is called 76 gDBConn = null; 77 78 // removing test db 79 deleteTestDB(); 80 } 81 82 /** 83 * Use asyncClose to cleanup a connection. Synchronous by means of internally 84 * spinning an event loop. 85 */ 86 function asyncCleanup() { 87 let closed = false; 88 89 // close the connection 90 print("*** Storage Tests: Trying to asyncClose!"); 91 getOpenedDatabase().asyncClose(function () { 92 closed = true; 93 }); 94 95 let tm = Cc["@mozilla.org/thread-manager;1"].getService(); 96 tm.spinEventLoopUntil("Test(head_storage.js:asyncCleanup)", () => closed); 97 98 // we need to null out the database variable to get a new connection the next 99 // time getOpenedDatabase is called 100 gDBConn = null; 101 102 // removing test db 103 deleteTestDB(); 104 } 105 106 /** 107 * Get a connection to the test database. Creates and caches the connection 108 * if necessary, otherwise reuses the existing cached connection. This 109 * connection shares its cache. 110 * 111 * @returns the mozIStorageConnection for the file. 112 */ 113 function getOpenedDatabase(connectionFlags = 0) { 114 if (!gDBConn) { 115 gDBConn = Services.storage.openDatabase(getTestDB(), connectionFlags); 116 } 117 return gDBConn; 118 } 119 120 /** 121 * Get a connection to the test database. Creates and caches the connection 122 * if necessary, otherwise reuses the existing cached connection. This 123 * connection doesn't share its cache. 124 * 125 * @returns the mozIStorageConnection for the file. 126 */ 127 function getOpenedUnsharedDatabase() { 128 if (!gDBConn) { 129 gDBConn = Services.storage.openUnsharedDatabase(getTestDB()); 130 } 131 return gDBConn; 132 } 133 134 /** 135 * Obtains a specific database to use. 136 * 137 * @param aFile 138 * The nsIFile representing the db file to open. 139 * @returns the mozIStorageConnection for the file. 140 */ 141 function getDatabase(aFile) { 142 return Services.storage.openDatabase(aFile); 143 } 144 145 function createStatement(aSQL) { 146 return getOpenedDatabase().createStatement(aSQL); 147 } 148 149 /** 150 * Creates an asynchronous SQL statement. 151 * 152 * @param aSQL 153 * The SQL to parse into a statement. 154 * @returns a mozIStorageAsyncStatement from aSQL. 155 */ 156 function createAsyncStatement(aSQL) { 157 return getOpenedDatabase().createAsyncStatement(aSQL); 158 } 159 160 /** 161 * Invoke the given function and assert that it throws an exception expressing 162 * the provided error code in its 'result' attribute. JS function expressions 163 * can be used to do this concisely. 164 * 165 * Example: 166 * expectError(Cr.NS_ERROR_INVALID_ARG, () => explodingFunction()); 167 * 168 * @param aErrorCode 169 * The error code to expect from invocation of aFunction. 170 * @param aFunction 171 * The function to invoke and expect an XPCOM-style error from. 172 */ 173 function expectError(aErrorCode, aFunction) { 174 let exceptionCaught = false; 175 try { 176 aFunction(); 177 } catch (e) { 178 if (e.result != aErrorCode) { 179 do_throw( 180 "Got an exception, but the result code was not the expected " + 181 "one. Expected " + 182 aErrorCode + 183 ", got " + 184 e.result 185 ); 186 } 187 exceptionCaught = true; 188 } 189 if (!exceptionCaught) { 190 do_throw(aFunction + " should have thrown an exception but did not!"); 191 } 192 } 193 194 /** 195 * Run a query synchronously and verify that we get back the expected results. 196 * 197 * @param aSQLString 198 * The SQL string for the query. 199 * @param aBind 200 * The value to bind at index 0. 201 * @param aResults 202 * A list of the expected values returned in the sole result row. 203 * Express blobs as lists. 204 */ 205 function verifyQuery(aSQLString, aBind, aResults) { 206 let stmt = getOpenedDatabase().createStatement(aSQLString); 207 stmt.bindByIndex(0, aBind); 208 try { 209 Assert.ok(stmt.executeStep()); 210 let nCols = stmt.numEntries; 211 if (aResults.length != nCols) { 212 do_throw( 213 "Expected " + 214 aResults.length + 215 " columns in result but " + 216 "there are only " + 217 aResults.length + 218 "!" 219 ); 220 } 221 for (let iCol = 0; iCol < nCols; iCol++) { 222 let expectedVal = aResults[iCol]; 223 let valType = stmt.getTypeOfIndex(iCol); 224 if (expectedVal === null) { 225 Assert.equal(stmt.VALUE_TYPE_NULL, valType); 226 Assert.ok(stmt.getIsNull(iCol)); 227 } else if (typeof expectedVal == "number") { 228 if (Math.floor(expectedVal) == expectedVal) { 229 Assert.equal(stmt.VALUE_TYPE_INTEGER, valType); 230 Assert.equal(expectedVal, stmt.getInt32(iCol)); 231 } else { 232 Assert.equal(stmt.VALUE_TYPE_FLOAT, valType); 233 Assert.equal(expectedVal, stmt.getDouble(iCol)); 234 } 235 } else if (typeof expectedVal == "string") { 236 Assert.equal(stmt.VALUE_TYPE_TEXT, valType); 237 Assert.equal(expectedVal, stmt.getUTF8String(iCol)); 238 } else { 239 // blob 240 Assert.equal(stmt.VALUE_TYPE_BLOB, valType); 241 let count = { value: 0 }, 242 blob = { value: null }; 243 stmt.getBlob(iCol, count, blob); 244 Assert.equal(count.value, expectedVal.length); 245 for (let i = 0; i < count.value; i++) { 246 Assert.equal(expectedVal[i], blob.value[i]); 247 } 248 } 249 } 250 } finally { 251 stmt.finalize(); 252 } 253 } 254 255 /** 256 * Return the number of rows in the able with the given name using a synchronous 257 * query. 258 * 259 * @param aTableName 260 * The name of the table. 261 * @return The number of rows. 262 */ 263 function getTableRowCount(aTableName) { 264 var currentRows = 0; 265 var countStmt = getOpenedDatabase().createStatement( 266 "SELECT COUNT(1) AS count FROM " + aTableName 267 ); 268 try { 269 Assert.ok(countStmt.executeStep()); 270 currentRows = countStmt.row.count; 271 } finally { 272 countStmt.finalize(); 273 } 274 return currentRows; 275 } 276 277 // Promise-Returning Functions 278 279 function asyncClone(db, readOnly) { 280 return new Promise((resolve, reject) => { 281 db.asyncClone(readOnly, function (status, db2) { 282 if (Components.isSuccessCode(status)) { 283 resolve(db2.QueryInterface(Ci.mozIStorageAsyncConnection)); 284 } else { 285 reject(status); 286 } 287 }); 288 }); 289 } 290 291 function asyncClose(db) { 292 return new Promise((resolve, reject) => { 293 db.asyncClose(function (status) { 294 if (Components.isSuccessCode(status)) { 295 resolve(); 296 } else { 297 reject(status); 298 } 299 }); 300 }); 301 } 302 303 function mapOptionsToFlags(aOptions, aMapping) { 304 let result = aMapping.default; 305 Object.entries(aOptions || {}).forEach(([optionName, isTrue]) => { 306 if (aMapping.hasOwnProperty(optionName) && isTrue) { 307 result |= aMapping[optionName]; 308 } 309 }); 310 return result; 311 } 312 313 function getOpenFlagsMap() { 314 return { 315 default: Ci.mozIStorageService.OPEN_DEFAULT, 316 shared: Ci.mozIStorageService.OPEN_SHARED, 317 readOnly: Ci.mozIStorageService.OPEN_READONLY, 318 ignoreLockingMode: Ci.mozIStorageService.OPEN_IGNORE_LOCKING_MODE, 319 }; 320 } 321 322 function getConnectionFlagsMap() { 323 return { 324 default: Ci.mozIStorageService.CONNECTION_DEFAULT, 325 interruptible: Ci.mozIStorageService.CONNECTION_INTERRUPTIBLE, 326 }; 327 } 328 329 function openAsyncDatabase(file, options) { 330 return new Promise((resolve, reject) => { 331 const openFlags = mapOptionsToFlags(options, getOpenFlagsMap()); 332 const connectionFlags = mapOptionsToFlags(options, getConnectionFlagsMap()); 333 334 Services.storage.openAsyncDatabase( 335 file, 336 openFlags, 337 connectionFlags, 338 function (status, db) { 339 if (Components.isSuccessCode(status)) { 340 resolve(db.QueryInterface(Ci.mozIStorageAsyncConnection)); 341 } else { 342 reject(status); 343 } 344 } 345 ); 346 }); 347 } 348 349 function executeAsync(statement, onResult) { 350 return new Promise((resolve, reject) => { 351 statement.executeAsync({ 352 handleError(error) { 353 reject(error); 354 }, 355 handleResult(result) { 356 if (onResult) { 357 onResult(result); 358 } 359 }, 360 handleCompletion(result) { 361 resolve(result); 362 }, 363 }); 364 }); 365 } 366 367 function executeMultipleStatementsAsync(db, statements, onResult) { 368 return new Promise((resolve, reject) => { 369 db.executeAsync(statements, { 370 handleError(error) { 371 reject(error); 372 }, 373 handleResult(result) { 374 if (onResult) { 375 onResult(result); 376 } 377 }, 378 handleCompletion(result) { 379 resolve(result); 380 }, 381 }); 382 }); 383 } 384 385 function executeSimpleSQLAsync(db, query, onResult) { 386 return new Promise((resolve, reject) => { 387 db.executeSimpleSQLAsync(query, { 388 handleError(error) { 389 reject(error); 390 }, 391 handleResult(result) { 392 if (onResult) { 393 onResult(result); 394 } else { 395 do_throw("No results were expected"); 396 } 397 }, 398 handleCompletion(result) { 399 resolve(result); 400 }, 401 }); 402 }); 403 } 404 405 cleanup();