summaryrefslogtreecommitdiffstats
path: root/storage/test/unit/test_connection_online_backup.js
blob: ca8e27e9a21e4e6f1931c2bbea37f1ed6d866b89 (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
/* Any copyright is dedicated to the Public Domain.
http://creativecommons.org/publicdomain/zero/1.0/ */

"use strict";

/**
 * This test file tests the backupToFileAsync function on
 * mozIStorageAsyncConnection, which is implemented for mozStorageConnection.
 * (but not implemented for mozilla::dom::cache::Connection).
 */

// The name of the backup database file that will be created.
const BACKUP_FILE_NAME = "test_storage.sqlite.backup";
// The number of rows to insert into the test table in the source
// database.
const TEST_ROWS = 10;
// The page size to set on the source database. During setup, we assert that
// this does not match the default page size.
const TEST_PAGE_SIZE = 512;

/**
 * This setup function creates a table inside of the test database and inserts
 * some test rows. Critically, it keeps the connection to the database _open_
 * so that we can test the scenario where a database is copied with existing
 * open connections.
 *
 * The database is closed in a cleanup function.
 */
add_setup(async () => {
  let conn = await openAsyncDatabase(getTestDB());

  Assert.notEqual(
    conn.defaultPageSize,
    TEST_PAGE_SIZE,
    "Should not default to having the TEST_PAGE_SIZE"
  );

  await executeSimpleSQLAsync(conn, "PRAGMA page_size = " + TEST_PAGE_SIZE);

  let createStmt = conn.createAsyncStatement("CREATE TABLE test(name TEXT)");
  await executeAsync(createStmt);
  createStmt.finalize();

  registerCleanupFunction(async () => {
    await asyncClose(conn);
  });
});

/**
 * Erases the test table and inserts TEST_ROWS rows into it.
 *
 * @param {mozIStorageAsyncConnection} connection
 *   The connection to use to prepare the database.
 * @returns {Promise<undefined>}
 */
async function prepareSourceDatabase(connection) {
  await executeSimpleSQLAsync(connection, "DELETE from test");
  for (let i = 0; i < TEST_ROWS; ++i) {
    let name = `Database row #${i}`;
    let stmt = connection.createAsyncStatement(
      "INSERT INTO test (name) VALUES (:name)"
    );
    stmt.params.name = name;
    let result = await executeAsync(stmt);
    stmt.finalize();
    Assert.ok(Components.isSuccessCode(result), `Inserted test row #${i}`);
  }
}

/**
 * Gets the test DB prepared with the testing table and rows.
 *
 * @returns {Promise<mozIStorageAsyncConnection>}
 */
async function getPreparedAsyncDatabase() {
  let connection = await openAsyncDatabase(getTestDB());
  await prepareSourceDatabase(connection);
  return connection;
}

/**
 * Creates a copy of the database connected to via connection, and
 * returns an nsIFile pointing at the created copy file once the
 * copy is complete.
 *
 * @param {mozIStorageAsyncConnection} connection
 *   A connection to a database that should be copied.
 * @param {number} [pagesPerStep]
 *   The number of pages to copy per step. If not supplied or is 0, falls back
 *   to the platform default which is currently 5.
 * @param {number} [stepDelayMs]
 *   The number of milliseconds to wait between copying step. If not supplied
 *   or is 0, falls back to the platform default which is currently 250.
 * @returns {Promise<nsIFile>}
 */
async function createCopy(connection, pagesPerStep, stepDelayMs) {
  let destFilePath = PathUtils.join(PathUtils.profileDir, BACKUP_FILE_NAME);
  let destFile = await IOUtils.getFile(destFilePath);
  Assert.ok(
    !(await IOUtils.exists(destFilePath)),
    "Backup file shouldn't exist yet."
  );

  await new Promise(resolve => {
    connection.backupToFileAsync(
      destFile,
      result => {
        Assert.ok(Components.isSuccessCode(result));
        resolve(result);
      },
      pagesPerStep,
      stepDelayMs
    );
  });

  return destFile;
}

/**
 * Opens up the database at file, asserts that the page_size matches
 * TEST_PAGE_SIZE, and that the number of rows in the test table matches
 * expectedEntries. Closes the connection after these assertions.
 *
 * @param {nsIFile} file
 *   The database file to be opened and queried.
 * @param {number} [expectedEntries=TEST_ROWS]
 *   The expected number of rows in the test table. Defaults to TEST_ROWS.
 * @returns {Promise<undefined>}
 */
async function assertSuccessfulCopy(file, expectedEntries = TEST_ROWS) {
  let conn = await openAsyncDatabase(file);

  await executeSimpleSQLAsync(conn, "PRAGMA page_size", resultSet => {
    let result = resultSet.getNextRow();
    Assert.equal(TEST_PAGE_SIZE, result.getResultByIndex(0));
  });

  let stmt = conn.createAsyncStatement("SELECT COUNT(*) FROM test");
  let results = await new Promise(resolve => {
    executeAsync(stmt, resolve);
  });
  stmt.finalize();
  let row = results.getNextRow();
  let count = row.getResultByName("COUNT(*)");
  Assert.equal(count, expectedEntries, "Got the expected entries");

  Assert.ok(
    !file.leafName.endsWith(".tmp"),
    "Should not end in .tmp extension"
  );

  await asyncClose(conn);
}

/**
 * Test the basic behaviour of backupToFileAsync, and ensure that the copied
 * database has the same characteristics and contents as the source database.
 */
add_task(async function test_backupToFileAsync() {
  let newConnection = await getPreparedAsyncDatabase();
  let copyFile = await createCopy(newConnection);
  Assert.ok(
    await IOUtils.exists(copyFile.path),
    "A new file was created by backupToFileAsync"
  );

  await assertSuccessfulCopy(copyFile);
  await IOUtils.remove(copyFile.path);
  await asyncClose(newConnection);
});

/**
 * Tests that if insertions are underway during a copy, that those insertions
 * show up in the copied database.
 */
add_task(async function test_backupToFileAsync_during_insert() {
  let newConnection = await getPreparedAsyncDatabase();
  const NEW_ENTRIES = 5;

  let copyFilePromise = createCopy(newConnection);
  let inserts = [];
  for (let i = 0; i < NEW_ENTRIES; ++i) {
    let name = `New database row #${i}`;
    let stmt = newConnection.createAsyncStatement(
      "INSERT INTO test (name) VALUES (:name)"
    );
    stmt.params.name = name;
    inserts.push(executeAsync(stmt));
    stmt.finalize();
  }
  await Promise.all(inserts);
  let copyFile = await copyFilePromise;

  Assert.ok(
    await IOUtils.exists(copyFile.path),
    "A new file was created by backupToFileAsync"
  );

  await assertSuccessfulCopy(copyFile, TEST_ROWS + NEW_ENTRIES);
  await IOUtils.remove(copyFile.path);
  await asyncClose(newConnection);
});

/**
 * Tests that alternative pages-per-step and step delay values can be set when
 * calling backupToFileAsync.
 */
add_task(async function test_backupToFileAsync_during_insert() {
  let newConnection = await getPreparedAsyncDatabase();

  // Let's try some higher values...
  let copyFile = await createCopy(newConnection, 15, 500);
  Assert.ok(
    await IOUtils.exists(copyFile.path),
    "A new file was created by backupToFileAsync"
  );

  await assertSuccessfulCopy(copyFile);
  await IOUtils.remove(copyFile.path);

  // And now we'll try some lower values...
  copyFile = await createCopy(newConnection, 1, 25);
  Assert.ok(
    await IOUtils.exists(copyFile.path),
    "A new file was created by backupToFileAsync"
  );

  await assertSuccessfulCopy(copyFile);
  await IOUtils.remove(copyFile.path);

  await asyncClose(newConnection);
});

/**
 * Tests the behaviour of backupToFileAsync as exposed through Sqlite.sys.mjs.
 */
add_task(async function test_backupToFileAsync_via_Sqlite_module() {
  let xpcomConnection = await getPreparedAsyncDatabase();
  let moduleConnection = await Sqlite.openConnection({
    path: xpcomConnection.databaseFile.path,
  });

  let copyFilePath = PathUtils.join(PathUtils.profileDir, BACKUP_FILE_NAME);
  await moduleConnection.backup(copyFilePath);
  let copyFile = await IOUtils.getFile(copyFilePath);
  Assert.ok(await IOUtils.exists(copyFilePath), "A new file was created");

  await assertSuccessfulCopy(copyFile);
  await IOUtils.remove(copyFile.path);

  // Also check that we can plumb through pagesPerStep and stepDelayMs.
  await moduleConnection.backup(copyFilePath, 15, 500);
  Assert.ok(await IOUtils.exists(copyFilePath), "A new file was created");
  await assertSuccessfulCopy(copyFile);
  await IOUtils.remove(copyFile.path);

  await moduleConnection.close();
  await asyncClose(xpcomConnection);
});