Commit c9beed86 authored by Victor Costan's avatar Victor Costan Committed by Commit Bot

IndexedDB: More WPT tests for reading from autoincrement stores.

Auto-increment keys represent a challenge in implementations where a
central browser process owns the databases' metadata, and multiple
renderer processes can access the same database.

Specifically, IndexedDB allows application code to start a transaction
and queue requests synchronously. So, there is no opportunity for the
browser process to pass the current autoincrement key to a renderer
process before the renderer processes requests. This situation can be
handled by queueing requests, which is complex, or by lazy key
injection, which looks easy and thus is quite popular, but has many
edge cases.

The tests here attempt to exercise all the code paths in lazy key
injection that work reasonably well today. The tests do not cover
compound indexes that include the primary (autoincrement) key, because
that is not handled well in any browser.

Bug: 701972
Change-Id: Ibbe38fd173d0821d329cafed449be359e2b33f6e
Reviewed-on: https://chromium-review.googlesource.com/c/1304067
Commit-Queue: Victor Costan <pwnall@chromium.org>
Reviewed-by: default avatarJoshua Bell <jsbell@chromium.org>
Cr-Commit-Position: refs/heads/master@{#603794}
parent 1de878d8
......@@ -39,9 +39,8 @@ promise_test(testCase => {
'versionchange transaction is aborted');
const request = indexedDB.open(dbName, 1);
return requestWatcher(testCase, request).wait_for('success');
}).then(event => {
const database = event.target.result;
return promiseForRequest(testCase, request);
}).then(database => {
const transaction = database.transaction('books', 'readonly');
const store = transaction.objectStore('books');
assert_array_equals(
......@@ -95,9 +94,8 @@ promise_test(testCase => {
'versionchange transaction is aborted');
const request = indexedDB.open(dbName, 1);
return requestWatcher(testCase, request).wait_for('success');
}).then(event => {
const database = event.target.result;
return promiseForRequest(testCase, request);
}).then(database => {
const transaction = database.transaction('not_books', 'readonly');
const store = transaction.objectStore('not_books');
assert_array_equals(
......
......@@ -40,9 +40,8 @@ promise_test(testCase => {
'IDBObjectStore.name should not reflect the rename any more ' +
'after the versionchange transaction is aborted');
const request = indexedDB.open(dbName, 1);
return requestWatcher(testCase, request).wait_for('success');
}).then(event => {
const database = event.target.result;
return promiseForRequest(testCase, request);
}).then(database => {
assert_array_equals(
database.objectStoreNames, ['books'],
'IDBDatabase.objectStoreNames should not reflect the rename ' +
......@@ -107,9 +106,8 @@ promise_test(testCase => {
'should be empty after the versionchange transaction is aborted ' +
'returns');
const request = indexedDB.open(dbName, 1);
return requestWatcher(testCase, request).wait_for('success');
}).then(event => {
const database = event.target.result;
return promiseForRequest(testCase, request);
}).then(database => {
assert_array_equals(
database.objectStoreNames, [],
'IDBDatabase.objectStoreNames should not reflect the creation or ' +
......
// Returns the "name" property written to the object with the given ID.
function nameForId(id) {
return `Object ${id}`;
}
// Initial database setup used by all the reading-autoincrement tests.
async function setupAutoincrementDatabase(testCase) {
const database = await createDatabase(testCase, database => {
const store = database.createObjectStore(
'store', { autoIncrement: true, keyPath: 'id' });
store.createIndex('by_name', 'name', { unique: true });
store.createIndex('by_id', 'id', { unique: true });
// Cover writing from the initial upgrade transaction.
for (let i = 1; i <= 16; ++i) {
if (i % 2 == 0) {
store.put({name: nameForId(i), id: i});
} else {
store.put({name: nameForId(i)});
}
}
});
// Cover writing from a subsequent transaction.
const transaction = database.transaction(['store'], 'readwrite');
const store = transaction.objectStore('store');
for (let i = 17; i <= 32; ++i) {
if (i % 2 == 0) {
store.put({name: nameForId(i), id: i});
} else {
store.put({name: nameForId(i)});
}
}
await promiseForTransaction(testCase, transaction);
return database;
}
// Returns the IDs used by the object store, sorted as strings.
//
// This is used to determine the correct order of records when retrieved from an
// index that uses stringified IDs.
function idsSortedByStringCompare() {
const stringIds = [];
for (let i = 1; i <= 32; ++i)
stringIds.push(i);
stringIds.sort((a, b) => indexedDB.cmp(`${a}`, `${b}`));
return stringIds;
}
async function iterateCursor(testCase, cursorRequest, callback) {
// This uses requestWatcher() directly instead of using promiseForRequest()
// inside the loop to avoid creating multiple EventWatcher instances. In turn,
// this avoids ending up with O(N) listeners for the request and O(N^2)
// dispatched events.
const eventWatcher = requestWatcher(testCase, cursorRequest);
while (true) {
const event = await eventWatcher.wait_for('success');
const cursor = event.target.result;
if (cursor === null)
return;
callback(cursor);
cursor.continue();
}
}
// Returns equivalent information to getAllKeys() by iterating a cursor.
//
// Returns an array with one dictionary per entry in the source. The dictionary
// has the properties "key" and "primaryKey".
async function getAllKeysViaCursor(testCase, cursorSource) {
const results = [];
await iterateCursor(testCase, cursorSource.openKeyCursor(), cursor => {
results.push({ key: cursor.key, primaryKey: cursor.primaryKey });
});
return results;
}
// Returns equivalent information to getAll() by iterating a cursor.
//
// Returns an array with one dictionary per entry in the source. The dictionary
// has the properties "key", "primaryKey" and "value".
async function getAllViaCursor(testCase, cursorSource) {
const results = [];
await iterateCursor(testCase, cursorSource.openCursor(), cursor => {
results.push({
key: cursor.key,
primaryKey: cursor.primaryKey,
value: cursor.value,
});
});
return results;
}
\ No newline at end of file
// META: global=window,dedicatedworker,sharedworker,serviceworker
// META: script=../support-promises.js
// META: script=./reading-autoincrement-common.js
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const index = store.index('by_id');
const result = await getAllViaCursor(testCase, index);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i) {
assert_equals(result[i - 1].key, i, 'Autoincrement index key');
assert_equals(result[i - 1].primaryKey, i, 'Autoincrement primary key');
assert_equals(result[i - 1].value.id, i, 'Autoincrement key in value');
assert_equals(result[i - 1].value.name, nameForId(i),
'String property in value');
}
database.close();
}, 'IDBIndex.openCursor() iterates over an index on the autoincrement key');
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const index = store.index('by_id');
const result = await getAllKeysViaCursor(testCase, index);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i) {
assert_equals(result[i - 1].key, i, 'Autoincrement index key');
assert_equals(result[i - 1].primaryKey, i, 'Autoincrement primary key');
}
database.close();
}, 'IDBIndex.openKeyCursor() iterates over an index on the autoincrement key');
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const index = store.index('by_name');
const stringSortedIds = idsSortedByStringCompare();
const result = await getAllViaCursor(testCase, index);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i) {
assert_equals(result[i - 1].key, nameForId(stringSortedIds[i - 1]),
'Index key');
assert_equals(result[i - 1].primaryKey, stringSortedIds[i - 1],
'Autoincrement primary key');
assert_equals(result[i - 1].value.id, stringSortedIds[i - 1],
'Autoincrement key in value');
assert_equals(result[i - 1].value.name, nameForId(stringSortedIds[i - 1]),
'String property in value');
}
database.close();
}, 'IDBIndex.openCursor() iterates over an index not covering the ' +
'autoincrement key');
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const index = store.index('by_name');
const stringSortedIds = idsSortedByStringCompare();
const result = await getAllKeysViaCursor(testCase, index);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i) {
assert_equals(result[i - 1].key, nameForId(stringSortedIds[i - 1]),
'Index key');
assert_equals(result[i - 1].primaryKey, stringSortedIds[i - 1],
'Autoincrement primary key');
}
database.close();
}, 'IDBIndex.openKeyCursor() iterates over an index not covering the ' +
'autoincrement key');
\ No newline at end of file
// META: global=window,dedicatedworker,sharedworker,serviceworker
// META: script=../support-promises.js
// META: script=./reading-autoincrement-common.js
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const index = store.index('by_id');
const request = index.getAll();
const result = await promiseForRequest(testCase, request);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i) {
assert_equals(result[i - 1].id, i, 'Autoincrement key');
assert_equals(result[i - 1].name, nameForId(i), 'String property');
}
database.close();
}, 'IDBIndex.getAll() for an index on the autoincrement key');
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const index = store.index('by_id');
const request = index.getAllKeys();
const result = await promiseForRequest(testCase, request);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i)
assert_equals(result[i - 1], i, 'Autoincrement key');
database.close();
}, 'IDBIndex.getAllKeys() for an index on the autoincrement key');
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const index = store.index('by_id');
for (let i = 1; i <= 32; ++i) {
const request = index.get(i);
const result = await promiseForRequest(testCase, request);
assert_equals(result.id, i, 'autoincrement key');
assert_equals(result.name, nameForId(i), 'string property');
}
database.close();
}, 'IDBIndex.get() for an index on the autoincrement key');
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const stringSortedIds = idsSortedByStringCompare();
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const index = store.index('by_name');
const request = index.getAll();
const result = await promiseForRequest(testCase, request);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i) {
assert_equals(result[i - 1].id, stringSortedIds[i - 1],
'autoincrement key');
assert_equals(result[i - 1].name, nameForId(stringSortedIds[i - 1]),
'string property');
}
database.close();
}, 'IDBIndex.getAll() for an index not covering the autoincrement key');
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const stringSortedIds = idsSortedByStringCompare();
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const index = store.index('by_name');
const request = index.getAllKeys();
const result = await promiseForRequest(testCase, request);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i)
assert_equals(result[i - 1], stringSortedIds[i - 1], 'String property');
database.close();
}, 'IDBIndex.getAllKeys() returns correct result for an index not covering ' +
'the autoincrement key');
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const index = store.index('by_name');
for (let i = 1; i <= 32; ++i) {
const request = index.get(nameForId(i));
const result = await promiseForRequest(testCase, request);
assert_equals(result.id, i, 'Autoincrement key');
assert_equals(result.name, nameForId(i), 'String property');
}
database.close();
}, 'IDBIndex.get() for an index not covering the autoincrement key');
// META: global=window,dedicatedworker,sharedworker,serviceworker
// META: script=../support-promises.js
// META: script=./reading-autoincrement-common.js
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const result = await getAllViaCursor(testCase, store);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i) {
assert_equals(result[i - 1].key, i, 'Autoincrement key');
assert_equals(result[i - 1].primaryKey, i, 'Autoincrement primary key');
assert_equals(result[i - 1].value.id, i, 'Autoincrement key in value');
assert_equals(result[i - 1].value.name, nameForId(i),
'string property in value');
}
database.close();
}, 'IDBObjectStore.openCursor() iterates over an autoincrement store');
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const result = await getAllKeysViaCursor(testCase, store);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i) {
assert_equals(result[i - 1].key, i, 'Incorrect autoincrement key');
assert_equals(result[i - 1].primaryKey, i, 'Incorrect primary key');
}
database.close();
}, 'IDBObjectStore.openKeyCursor() iterates over an autoincrement store');
\ No newline at end of file
// META: global=window,dedicatedworker,sharedworker,serviceworker
// META: script=../support-promises.js
// META: script=./reading-autoincrement-common.js
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const request = store.getAll();
const result = await promiseForRequest(testCase, request);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i) {
assert_equals(result[i - 1].id, i, 'Autoincrement key');
assert_equals(result[i - 1].name, nameForId(i), 'String property');
}
database.close();
}, 'IDBObjectStore.getAll() for an autoincrement store');
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
const request = store.getAllKeys();
const result = await promiseForRequest(testCase, request);
assert_equals(result.length, 32);
for (let i = 1; i <= 32; ++i)
assert_equals(result[i - 1], i, 'Autoincrement key');
database.close();
}, 'IDBObjectStore.getAllKeys() for an autoincrement store');
promise_test(async testCase => {
const database = await setupAutoincrementDatabase(testCase);
const transaction = database.transaction(['store'], 'readonly');
const store = transaction.objectStore('store');
for (let i = 1; i <= 32; ++i) {
const request = store.get(i);
const result = await promiseForRequest(testCase, request);
assert_equals(result.id, i, 'Autoincrement key');
assert_equals(result.name, nameForId(i), 'String property');
}
database.close();
}, 'IDBObjectStore.get() for an autoincrement store');
\ No newline at end of file
......@@ -5,11 +5,39 @@ function databaseName(testCase) {
return 'db' + self.location.pathname + '-' + testCase.name;
}
// Creates an EventWatcher covering all the events that can be issued by
// IndexedDB requests and transactions.
// EventWatcher covering all the events defined on IndexedDB requests.
//
// The events cover IDBRequest and IDBOpenDBRequest.
function requestWatcher(testCase, request) {
return new EventWatcher(testCase, request,
['abort', 'blocked', 'complete', 'error', 'success', 'upgradeneeded']);
['blocked', 'error', 'success', 'upgradeneeded']);
}
// EventWatcher covering all the events defined on IndexedDB transactions.
//
// The events cover IDBTransaction.
function transactionWatcher(testCase, request) {
return new EventWatcher(testCase, request, ['abort', 'complete', 'error']);
}
// Promise that resolves with an IDBRequest's result.
//
// The promise only resolves if IDBRequest receives the "success" event. Any
// other event causes the promise to reject with an error. This is correct in
// most cases, but insufficient for indexedDB.open(), which issues
// "upgradeneded" events under normal operation.
function promiseForRequest(testCase, request) {
const eventWatcher = requestWatcher(testCase, request);
return eventWatcher.wait_for('success').then(event => event.target.result);
}
// Promise that resolves when an IDBTransaction completes.
//
// The promise resolves with undefined if IDBTransaction receives the "complete"
// event, and rejects with an error for any other event.
function promiseForTransaction(testCase, request) {
const eventWatcher = transactionWatcher(testCase, request);
return eventWatcher.wait_for('complete').then(() => {});
}
// Migrates an IndexedDB database whose name is unique for the test case.
......@@ -64,7 +92,7 @@ function migrateNamedDatabase(
requestEventPromise = new Promise((resolve, reject) => {
request.onerror = event => {
event.preventDefault();
resolve(event);
resolve(event.target.error);
};
request.onsuccess = () => reject(new Error(
'indexedDB.open should not succeed for an aborted ' +
......@@ -79,8 +107,7 @@ function migrateNamedDatabase(
if (!shouldBeAborted) {
request.onerror = null;
request.onsuccess = null;
requestEventPromise =
requestWatcher(testCase, request).wait_for('success');
requestEventPromise = promiseForRequest(testCase, request);
}
// requestEventPromise needs to be the last promise in the chain, because
......@@ -95,12 +122,10 @@ function migrateNamedDatabase(
'indexedDB.open should not succeed without creating a ' +
'versionchange transaction'));
};
}).then(event => {
const database = event.target.result;
if (database) {
testCase.add_cleanup(() => { database.close(); });
}
return database || event.target.error;
}).then(databaseOrError => {
if (databaseOrError instanceof IDBDatabase)
testCase.add_cleanup(() => { databaseOrError.close(); });
return databaseOrError;
});
}
......@@ -126,9 +151,7 @@ function createDatabase(testCase, setupCallback) {
// close the database.
function createNamedDatabase(testCase, databaseName, setupCallback) {
const request = indexedDB.deleteDatabase(databaseName);
const eventWatcher = requestWatcher(testCase, request);
return eventWatcher.wait_for('success').then(event => {
return promiseForRequest(testCase, request).then(() => {
testCase.add_cleanup(() => { indexedDB.deleteDatabase(databaseName); });
return migrateNamedDatabase(testCase, databaseName, 1, setupCallback)
});
......@@ -152,9 +175,7 @@ function openDatabase(testCase, version) {
// close the database.
function openNamedDatabase(testCase, databaseName, version) {
const request = indexedDB.open(databaseName, version);
const eventWatcher = requestWatcher(testCase, request);
return eventWatcher.wait_for('success').then(() => {
const database = request.result;
return promiseForRequest(testCase, request).then(database => {
testCase.add_cleanup(() => { database.close(); });
return database;
});
......@@ -215,9 +236,7 @@ function checkStoreIndexes (testCase, store, errorMessage) {
function checkStoreGenerator(testCase, store, expectedKey, errorMessage) {
const request = store.put(
{ title: 'Bedrock Nights ' + expectedKey, author: 'Barney' });
const eventWatcher = requestWatcher(testCase, request);
return eventWatcher.wait_for('success').then(() => {
const result = request.result;
return promiseForRequest(testCase, request).then(result => {
assert_equals(result, expectedKey, errorMessage);
});
}
......@@ -230,9 +249,7 @@ function checkStoreGenerator(testCase, store, expectedKey, errorMessage) {
// is using it incorrectly.
function checkStoreContents(testCase, store, errorMessage) {
const request = store.get(123456);
const eventWatcher = requestWatcher(testCase, request);
return eventWatcher.wait_for('success').then(() => {
const result = request.result;
return promiseForRequest(testCase, request).then(result => {
assert_equals(result.isbn, BOOKS_RECORD_DATA[0].isbn, errorMessage);
assert_equals(result.author, BOOKS_RECORD_DATA[0].author, errorMessage);
assert_equals(result.title, BOOKS_RECORD_DATA[0].title, errorMessage);
......@@ -247,9 +264,7 @@ function checkStoreContents(testCase, store, errorMessage) {
// is using it incorrectly.
function checkAuthorIndexContents(testCase, index, errorMessage) {
const request = index.get(BOOKS_RECORD_DATA[2].author);
const eventWatcher = requestWatcher(testCase, request);
return eventWatcher.wait_for('success').then(() => {
const result = request.result;
return promiseForRequest(testCase, request).then(result => {
assert_equals(result.isbn, BOOKS_RECORD_DATA[2].isbn, errorMessage);
assert_equals(result.title, BOOKS_RECORD_DATA[2].title, errorMessage);
});
......@@ -263,9 +278,7 @@ function checkAuthorIndexContents(testCase, index, errorMessage) {
// is using it incorrectly.
function checkTitleIndexContents(testCase, index, errorMessage) {
const request = index.get(BOOKS_RECORD_DATA[2].title);
const eventWatcher = requestWatcher(testCase, request);
return eventWatcher.wait_for('success').then(() => {
const result = request.result;
return promiseForRequest(testCase, request).then(result => {
assert_equals(result.isbn, BOOKS_RECORD_DATA[2].isbn, errorMessage);
assert_equals(result.author, BOOKS_RECORD_DATA[2].author, errorMessage);
});
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment