summaryrefslogtreecommitdiffstats
path: root/storage/test/unit/test_vacuum.js
blob: a4cfdf714f62740e9919cef38f6c5867385538ce (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
/* 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`);
}