/* Any copyright is dedicated to the Public Domain. * http://creativecommons.org/publicdomain/zero/1.0/ */ // This file tests the Vacuum Manager and asyncVacuum(). const { VacuumParticipant } = ChromeUtils.importESModule( "resource://testing-common/VacuumParticipant.sys.mjs" ); /** * Sends a fake idle-daily notification to the VACUUM Manager. */ function synthesize_idle_daily() { Cc["@mozilla.org/storage/vacuum;1"] .getService(Ci.nsIObserver) .observe(null, "idle-daily", null); } /** * Returns a new nsIFile reference for a profile database. * @param filename for the database, excluded the .sqlite extension. */ function new_db_file(name = "testVacuum") { let file = Services.dirsvc.get("ProfD", Ci.nsIFile); file.append(name + ".sqlite"); return file; } function reset_vacuum_date(name = "testVacuum") { let date = parseInt(Date.now() / 1000 - 31 * 86400); // Set last VACUUM to a date in the past. Services.prefs.setIntPref(`storage.vacuum.last.${name}.sqlite`, date); return date; } function get_vacuum_date(name = "testVacuum") { return Services.prefs.getIntPref(`storage.vacuum.last.${name}.sqlite`, 0); } add_setup(async function () { // turn on Cu.isInAutomation Services.prefs.setBoolPref( "security.turn_off_all_security_so_that_viruses_can_take_over_this_computer", true ); }); add_task(async function test_common_vacuum() { let last_vacuum_date = reset_vacuum_date(); info("Test that a VACUUM correctly happens and all notifications are fired."); let promiseTestVacuumBegin = TestUtils.topicObserved("test-begin-vacuum"); let promiseTestVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); let promiseVacuumBegin = TestUtils.topicObserved("vacuum-begin"); let promiseVacuumEnd = TestUtils.topicObserved("vacuum-end"); let participant = new VacuumParticipant( Services.storage.openDatabase(new_db_file()) ); await participant.promiseRegistered(); synthesize_idle_daily(); // Wait for notifications. await Promise.all([ promiseTestVacuumBegin, promiseTestVacuumEnd, promiseVacuumBegin, promiseVacuumEnd, ]); Assert.greater(get_vacuum_date(), last_vacuum_date); await participant.dispose(); }); add_task(async function test_skipped_if_recent_vacuum() { info("Test that a VACUUM is skipped if it was run recently."); Services.prefs.setIntPref( "storage.vacuum.last.testVacuum.sqlite", parseInt(Date.now() / 1000) ); // Wait for VACUUM skipped notification. let promiseSkipped = TestUtils.topicObserved("vacuum-skip"); let participant = new VacuumParticipant( Services.storage.openDatabase(new_db_file()) ); await participant.promiseRegistered(); synthesize_idle_daily(); // Check that VACUUM has been skipped. await promiseSkipped; await participant.dispose(); }); add_task(async function test_page_size_change() { info("Test that a VACUUM changes page_size"); reset_vacuum_date(); let conn = Services.storage.openDatabase(new_db_file()); info("Check initial page size."); let stmt = conn.createStatement("PRAGMA page_size"); Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.page_size, conn.defaultPageSize); stmt.finalize(); await populateFreeList(conn); let participant = new VacuumParticipant(conn, { expectedPageSize: 1024 }); await participant.promiseRegistered(); let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); synthesize_idle_daily(); await promiseVacuumEnd; info("Check that page size was updated."); stmt = conn.createStatement("PRAGMA page_size"); Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.page_size, 1024); stmt.finalize(); await participant.dispose(); }); add_task(async function test_skipped_optout_vacuum() { info("Test that a VACUUM is skipped if the participant wants to opt-out."); reset_vacuum_date(); let participant = new VacuumParticipant( Services.storage.openDatabase(new_db_file()), { grant: false } ); await participant.promiseRegistered(); // Wait for VACUUM skipped notification. let promiseSkipped = TestUtils.topicObserved("vacuum-skip"); synthesize_idle_daily(); // Check that VACUUM has been skipped. await promiseSkipped; await participant.dispose(); }); add_task(async function test_memory_database_crash() { info("Test that we don't crash trying to vacuum a memory database"); reset_vacuum_date(); let participant = new VacuumParticipant( Services.storage.openSpecialDatabase("memory") ); await participant.promiseRegistered(); // Wait for VACUUM skipped notification. let promiseSkipped = TestUtils.topicObserved("vacuum-skip"); synthesize_idle_daily(); // Check that VACUUM has been skipped. await promiseSkipped; await participant.dispose(); }); add_task(async function test_async_connection() { info("Test we can vacuum an async connection"); reset_vacuum_date(); let conn = await openAsyncDatabase(new_db_file()); await populateFreeList(conn); let participant = new VacuumParticipant(conn); await participant.promiseRegistered(); let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); synthesize_idle_daily(); await promiseVacuumEnd; await participant.dispose(); }); add_task(async function test_change_to_incremental_vacuum() { info("Test we can change to incremental vacuum"); reset_vacuum_date(); let conn = Services.storage.openDatabase(new_db_file()); info("Check initial vacuum."); let stmt = conn.createStatement("PRAGMA auto_vacuum"); Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.auto_vacuum, 0); stmt.finalize(); await populateFreeList(conn); let participant = new VacuumParticipant(conn, { useIncrementalVacuum: true }); await participant.promiseRegistered(); let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); synthesize_idle_daily(); await promiseVacuumEnd; info("Check that auto_vacuum was updated."); stmt = conn.createStatement("PRAGMA auto_vacuum"); Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.auto_vacuum, 2); stmt.finalize(); await participant.dispose(); }); add_task(async function test_change_from_incremental_vacuum() { info("Test we can change from incremental vacuum"); reset_vacuum_date(); let conn = Services.storage.openDatabase(new_db_file()); conn.executeSimpleSQL("PRAGMA auto_vacuum = 2"); info("Check initial vacuum."); let stmt = conn.createStatement("PRAGMA auto_vacuum"); Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.auto_vacuum, 2); stmt.finalize(); await populateFreeList(conn); let participant = new VacuumParticipant(conn, { useIncrementalVacuum: false, }); await participant.promiseRegistered(); let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); synthesize_idle_daily(); await promiseVacuumEnd; info("Check that auto_vacuum was updated."); stmt = conn.createStatement("PRAGMA auto_vacuum"); Assert.ok(stmt.executeStep()); Assert.equal(stmt.row.auto_vacuum, 0); stmt.finalize(); await participant.dispose(); }); add_task(async function test_attached_vacuum() { info("Test attached database is not a problem"); reset_vacuum_date(); let conn = Services.storage.openDatabase(new_db_file()); let conn2 = Services.storage.openDatabase(new_db_file("attached")); info("Attach " + conn2.databaseFile.path); conn.executeSimpleSQL( `ATTACH DATABASE '${conn2.databaseFile.path}' AS attached` ); await asyncClose(conn2); let stmt = conn.createStatement("PRAGMA database_list"); let schemas = []; while (stmt.executeStep()) { schemas.push(stmt.row.name); } Assert.deepEqual(schemas, ["main", "attached"]); stmt.finalize(); await populateFreeList(conn); await populateFreeList(conn, "attached"); let participant = new VacuumParticipant(conn); await participant.promiseRegistered(); let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-success"); synthesize_idle_daily(); await promiseVacuumEnd; await participant.dispose(); }); add_task(async function test_vacuum_fail() { info("Test a failed vacuum"); reset_vacuum_date(); let conn = Services.storage.openDatabase(new_db_file()); // Cannot vacuum in a transaction. conn.beginTransaction(); await populateFreeList(conn); let participant = new VacuumParticipant(conn); await participant.promiseRegistered(); let promiseVacuumEnd = TestUtils.topicObserved("test-end-vacuum-failure"); synthesize_idle_daily(); await promiseVacuumEnd; conn.commitTransaction(); await participant.dispose(); }); add_task(async function test_async_vacuum() { // Since previous tests already go through most cases, this only checks // the basics of directly calling asyncVacuum(). info("Test synchronous connection"); let conn = Services.storage.openDatabase(new_db_file()); await populateFreeList(conn); let rv = await new Promise(resolve => { conn.asyncVacuum(status => { resolve(status); }); }); Assert.ok(Components.isSuccessCode(rv)); await asyncClose(conn); info("Test asynchronous connection"); conn = await openAsyncDatabase(new_db_file()); await populateFreeList(conn); rv = await new Promise(resolve => { conn.asyncVacuum(status => { resolve(status); }); }); Assert.ok(Components.isSuccessCode(rv)); await asyncClose(conn); }); // Chunked growth is disabled on Android, so this test is pointless there. add_task( { skip_if: () => AppConstants.platform == "android" }, async function test_vacuum_growth() { // Tests vacuum doesn't nullify chunked growth. let conn = Services.storage.openDatabase(new_db_file("incremental")); conn.executeSimpleSQL("PRAGMA auto_vacuum = INCREMENTAL"); conn.setGrowthIncrement(2 * conn.defaultPageSize, ""); await populateFreeList(conn); let stmt = conn.createStatement("PRAGMA freelist_count"); let count = 0; Assert.ok(stmt.executeStep()); count = stmt.row.freelist_count; stmt.reset(); Assert.greater(count, 2, "There's more than 2 page in freelist"); let rv = await new Promise(resolve => { conn.asyncVacuum(status => { resolve(status); }, true); }); Assert.ok(Components.isSuccessCode(rv)); Assert.ok(stmt.executeStep()); Assert.equal( stmt.row.freelist_count, 2, "chunked growth space was preserved" ); stmt.reset(); // A full vacuuum should not be executed if there's less free pages than // chunked growth. rv = await new Promise(resolve => { conn.asyncVacuum(status => { resolve(status); }); }); Assert.ok(Components.isSuccessCode(rv)); Assert.ok(stmt.executeStep()); Assert.equal( stmt.row.freelist_count, 2, "chunked growth space was preserved" ); stmt.finalize(); await asyncClose(conn); } ); async function populateFreeList(conn, schema = "main") { await executeSimpleSQLAsync(conn, `CREATE TABLE ${schema}.test (id TEXT)`); await executeSimpleSQLAsync( conn, `INSERT INTO ${schema}.test VALUES ${Array.from({ length: 3000 }, () => Math.random()).map( v => "('" + v + "')" )}` ); await executeSimpleSQLAsync(conn, `DROP TABLE ${schema}.test`); }