From 790baeafc86a766b8de82474191b76bef0f2b410 Mon Sep 17 00:00:00 2001 From: Blink WPT Bot Date: Thu, 5 Mar 2020 21:33:20 +0000 Subject: [PATCH] Bug 1619038 [wpt PR 22027] - WPT: Upstream and add some transaction scheduling tests, a=testonly Automatic update from web-platform-tests WPT: Upstream and add some transaction scheduling tests (#22027) In service of https://github.com/w3c/IndexedDB/issues/253 move some transaction scheduling tests from Blink to WPT. This involved converting them from js-test.js to testharness.js, but the overall logic of each test was retained. This also adds one new test which verifies the change described in https://github.com/w3c/IndexedDB/pull/319 - all browsers implicitly block R-O transactions behind overlapping R/W transactions. Change-Id: I596aaa75b79bf3bf3e17a2553abb4e11329d59ab Bug: 921193 Reviewed-on: https://chromium-review.googlesource.com/c/chromium/src/+/2081237 Reviewed-by: Daniel Murphy Commit-Queue: Joshua Bell Auto-Submit: Joshua Bell Cr-Commit-Position: refs/heads/master@{#746553} Co-authored-by: Joshua Bell -- wpt-commits: 5e1160e543827227c05b10c7660795436a76b307 wpt-pr: 22027 --- .../web-platform/tests/IndexedDB/support.js | 20 ++++++ ...ction-scheduling-across-connections.any.js | 72 +++++++++++++++++++ ...saction-scheduling-across-databases.any.js | 72 +++++++++++++++++++ ...transaction-scheduling-mixed-scopes.any.js | 63 ++++++++++++++++ .../transaction-scheduling-ordering.any.js | 40 +++++++++++ ...nsaction-scheduling-ro-waits-for-rw.any.js | 26 +++++++ .../transaction-scheduling-rw-scopes.any.js | 63 ++++++++++++++++ ...nsaction-scheduling-within-database.any.js | 55 ++++++++++++++ 8 files changed, 411 insertions(+) create mode 100644 testing/web-platform/tests/IndexedDB/transaction-scheduling-across-connections.any.js create mode 100644 testing/web-platform/tests/IndexedDB/transaction-scheduling-across-databases.any.js create mode 100644 testing/web-platform/tests/IndexedDB/transaction-scheduling-mixed-scopes.any.js create mode 100644 testing/web-platform/tests/IndexedDB/transaction-scheduling-ordering.any.js create mode 100644 testing/web-platform/tests/IndexedDB/transaction-scheduling-ro-waits-for-rw.any.js create mode 100644 testing/web-platform/tests/IndexedDB/transaction-scheduling-rw-scopes.any.js create mode 100644 testing/web-platform/tests/IndexedDB/transaction-scheduling-within-database.any.js diff --git a/testing/web-platform/tests/IndexedDB/support.js b/testing/web-platform/tests/IndexedDB/support.js index d3b2bb7d1212d..8dbfa6f1e37d7 100644 --- a/testing/web-platform/tests/IndexedDB/support.js +++ b/testing/web-platform/tests/IndexedDB/support.js @@ -101,6 +101,16 @@ function assert_key_equals(actual, expected, description) { assert_equals(indexedDB.cmp(actual, expected), 0, description); } +// Usage: +// indexeddb_test( +// (test_object, db_connection, upgrade_tx, open_request) => { +// // Database creation logic. +// }, +// (test_object, db_connection, open_request) => { +// // Test logic. +// test_object.done(); +// }, +// 'Test case description'); function indexeddb_test(upgrade_func, open_func, description, options) { async_test(function(t) { options = Object.assign({upgrade_will_abort: false}, options); @@ -189,3 +199,13 @@ function keep_alive(tx, store_name) { keepSpinning = false; }; } + +// Returns a new function. After it is called |count| times, |func| +// will be called. +function barrier_func(count, func) { + let n = 0; + return () => { + if (++n === count) + func(); + }; +} diff --git a/testing/web-platform/tests/IndexedDB/transaction-scheduling-across-connections.any.js b/testing/web-platform/tests/IndexedDB/transaction-scheduling-across-connections.any.js new file mode 100644 index 0000000000000..92d098d29c937 --- /dev/null +++ b/testing/web-platform/tests/IndexedDB/transaction-scheduling-across-connections.any.js @@ -0,0 +1,72 @@ +// META: script=support.js + +indexeddb_test( + (t, db) => { + const store = db.createObjectStore('store'); + }, + + (t, db1) => { + // Open a second connection to the same database. + const open_request = indexedDB.open(db1.name); + open_request.onerror = t.unreached_func('open() should succeed'); + open_request.onupgradeneeded = + t.unreached_func('second connection should not upgrade'); + open_request.onsuccess = t.step_func(() => { + const db2 = open_request.result; + t.add_cleanup(() => { db2.close(); }); + + const transaction1 = db1.transaction('store', 'readwrite'); + transaction1.onabort = t.unreached_func('transaction1 should complete'); + + const transaction2 = db2.transaction('store', 'readwrite'); + transaction2.onabort = t.unreached_func('transaction2 should complete'); + + let transaction1PutSuccess = false; + let transaction1Complete = false; + let transaction2PutSuccess = false; + + // Keep transaction1 alive for a while and ensure transaction2 + // doesn't start. + + let count = 0; + (function doTransaction1Put() { + const request = transaction1.objectStore('store').put(1, count++); + request.onerror = t.unreached_func('request should succeed'); + request.onsuccess = t.step_func(evt => { + transaction1PutSuccess = true; + if (count < 5) { + doTransaction1Put(); + } + }); + }()); + + transaction1.oncomplete = t.step_func(evt => { + transaction1Complete = true; + assert_false( + transaction2PutSuccess, + 'transaction1 should complete before transaction2 put succeeds'); + }); + + const request = transaction2.objectStore('store').put(2, 0); + request.onerror = t.unreached_func('request should succeed'); + request.onsuccess = t.step_func(evt => { + transaction2PutSuccess = true; + assert_true( + transaction1Complete, + 'transaction2 put should not succeed before transaction1 completes'); + }); + + transaction2.oncomplete = t.step_func_done(evt => { + assert_true( + transaction1PutSuccess, + 'transaction1 put should succeed before transaction2 runs'); + assert_true( + transaction1Complete, + 'transaction1 should complete before transaction2 runs'); + assert_true( + transaction2PutSuccess, + 'transaction2 put should succeed before transaction2 completes'); + }); + }); + }, + "Check that readwrite transactions with overlapping scopes do not run in parallel."); diff --git a/testing/web-platform/tests/IndexedDB/transaction-scheduling-across-databases.any.js b/testing/web-platform/tests/IndexedDB/transaction-scheduling-across-databases.any.js new file mode 100644 index 0000000000000..064444175867c --- /dev/null +++ b/testing/web-platform/tests/IndexedDB/transaction-scheduling-across-databases.any.js @@ -0,0 +1,72 @@ +// META: script=support.js + +indexeddb_test( + (t, db) => { + const store = db.createObjectStore('store'); + }, + + (t, db1) => { + // Open a second database. + const db2name = db1.name + '-2'; + const delete_request = indexedDB.deleteDatabase(db2name); + delete_request.onerror = t.unreached_func('deleteDatabase() should succeed'); + const open_request = indexedDB.open(db2name, 1); + open_request.onerror = t.unreached_func('open() should succeed'); + open_request.onupgradeneeded = t.step_func(() => { + const db2 = open_request.result; + const store = db2.createObjectStore('store'); + }); + open_request.onsuccess = t.step_func(() => { + const db2 = open_request.result; + t.add_cleanup(() => { + db2.close(); + indexedDB.deleteDatabase(db2.name); + }); + + let transaction1PutSuccess = false; + let transaction2PutSuccess = false; + + const onTransactionComplete = barrier_func(2, t.step_func_done(() => { + assert_true(transaction1PutSuccess, + 'transaction1 should have executed at least one request'); + assert_true(transaction2PutSuccess, + 'transaction1 should have executed at least one request'); + })); + + + const transaction1 = db1.transaction('store', 'readwrite'); + transaction1.onabort = t.unreached_func('transaction1 should complete'); + transaction1.oncomplete = t.step_func(onTransactionComplete); + + const transaction2 = db2.transaction('store', 'readwrite'); + transaction2.onabort = t.unreached_func('transaction2 should complete'); + transaction2.oncomplete = t.step_func(onTransactionComplete); + + // Keep both transactions alive until each has reported at least one + // successful operation. + + function doTransaction1Put() { + const request = transaction1.objectStore('store').put(0, 0); + request.onerror = t.unreached_func('put request should succeed'); + request.onsuccess = t.step_func(() => { + transaction1PutSuccess = true; + if (!transaction2PutSuccess) + doTransaction1Put(); + }); + } + + function doTransaction2Put() { + const request = transaction2.objectStore('store').put(0, 0); + request.onerror = t.unreached_func('put request should succeed'); + request.onsuccess = t.step_func(() => { + transaction2PutSuccess = true; + if (!transaction1PutSuccess) + doTransaction2Put(); + }); + } + + doTransaction1Put(); + doTransaction2Put(); + }); + }, + "Check that transactions in different databases can run in parallel."); diff --git a/testing/web-platform/tests/IndexedDB/transaction-scheduling-mixed-scopes.any.js b/testing/web-platform/tests/IndexedDB/transaction-scheduling-mixed-scopes.any.js new file mode 100644 index 0000000000000..5f04a6a288d23 --- /dev/null +++ b/testing/web-platform/tests/IndexedDB/transaction-scheduling-mixed-scopes.any.js @@ -0,0 +1,63 @@ +// META: script=support.js + +indexeddb_test( + (t, db) => { + const store = db.createObjectStore('store'); + db.createObjectStore('a'); + db.createObjectStore('b'); + db.createObjectStore('c'); + }, + + (t, db) => { + let transaction1Started = false; + let transaction1Complete = false; + let transaction2Started = false; + let transaction2Complete = false; + let transaction3Started = false; + let transaction3Complete = false; + + const transaction1 = db.transaction(['a'], 'readonly'); + let request = transaction1.objectStore('a').get(0); + request.onerror = t.unreached_func('request should succeed'); + request.onsuccess = t.step_func(() => { + transaction1Started = true; + }); + transaction1.onabort = t.unreached_func('transaction1 should complete'); + transaction1.oncomplete = t.step_func(() => { + transaction1Complete = true; + assert_false(transaction2Started); + assert_false(transaction3Started); + }); + + + // transaction2 overlaps with transaction1, so must wait until transaction1 + // completes. + const transaction2 = db.transaction(['a', 'b'], 'readwrite'); + request = transaction2.objectStore('a').get(0); + request.onerror = t.unreached_func('request should succeed'); + request.onsuccess = t.step_func(() => { + assert_true(transaction1Complete); + transaction2Started = true; + }); + transaction2.onabort = t.unreached_func('transaction2 should complete'); + transaction2.oncomplete = t.step_func(() => { + transaction2Complete = true; + assert_false(transaction3Started); + }); + + // transaction3 overlaps with transaction2, so must wait until transaction2 + // completes even though it does not overlap with transaction1. + const transaction3 = db.transaction(['b', 'c'], 'readonly'); + request = transaction3.objectStore('b').get(0); + request.onerror = t.unreached_func('request should succeed'); + request.onsuccess = t.step_func(() => { + assert_true(transaction1Complete); + assert_true(transaction2Complete); + transaction3Started = true; + }); + transaction3.onabort = t.unreached_func('transaction3 should complete'); + transaction3.oncomplete = t.step_func_done(() => { + transaction3Complete = true; + }); + }, + "Check that scope restrictions on mixed transactions are enforced."); diff --git a/testing/web-platform/tests/IndexedDB/transaction-scheduling-ordering.any.js b/testing/web-platform/tests/IndexedDB/transaction-scheduling-ordering.any.js new file mode 100644 index 0000000000000..9f47e5c58ca39 --- /dev/null +++ b/testing/web-platform/tests/IndexedDB/transaction-scheduling-ordering.any.js @@ -0,0 +1,40 @@ +// META: script=support.js + +indexeddb_test( + (t, db) => { + const store = db.createObjectStore('store'); + }, + + (t, db) => { + // Create in order tx1, tx2. + const tx1 = db.transaction('store', 'readwrite'); + const tx2 = db.transaction('store', 'readwrite'); + + // Use in order tx2, tx1. + tx2.objectStore('store').get(0); + tx1.objectStore('store').get(0); + + const order = []; + const done = barrier_func(2, t.step_func_done(() => { + // IndexedDB Spec: + // https://w3c.github.io/IndexedDB/#transaction-scheduling + // + // If multiple "readwrite" transactions are attempting to + // access the same object store (i.e. if they have overlapping + // scope), the transaction that was created first must be the + // transaction which gets access to the object store first. + // + assert_array_equals(order, [1, 2]); + })); + + tx1.oncomplete = t.step_func(e => { + order.push(1); + done(); + }); + + tx2.oncomplete = t.step_func(e => { + order.push(2); + done(); + }); + }, + "Verify Indexed DB transactions are ordered per spec"); diff --git a/testing/web-platform/tests/IndexedDB/transaction-scheduling-ro-waits-for-rw.any.js b/testing/web-platform/tests/IndexedDB/transaction-scheduling-ro-waits-for-rw.any.js new file mode 100644 index 0000000000000..dca08b820888f --- /dev/null +++ b/testing/web-platform/tests/IndexedDB/transaction-scheduling-ro-waits-for-rw.any.js @@ -0,0 +1,26 @@ +// META: script=support.js + +indexeddb_test( + (t, db) => { + const store = db.createObjectStore('store'); + store.put('value', 'key'); + }, + + (t, db) => { + const transaction1 = db.transaction('store', 'readwrite'); + transaction1.onabort = t.unreached_func('transaction1 should not abort'); + + const transaction2 = db.transaction('store', 'readonly'); + transaction2.onabort = t.unreached_func('transaction2 should not abort'); + + const request = transaction1.objectStore('store').put('new value', 'key'); + request.onerror = t.unreached_func('request should not fail'); + + const request2 = transaction2.objectStore('store').get('key'); + request2.onerror = t.unreached_func('request2 should not fail'); + request2.onsuccess = t.step_func_done(evt => { + assert_equals(request2.result, 'new value', + 'Request should see new value.'); + }); + }, + "readonly transaction should see the result of a previous readwrite transaction"); diff --git a/testing/web-platform/tests/IndexedDB/transaction-scheduling-rw-scopes.any.js b/testing/web-platform/tests/IndexedDB/transaction-scheduling-rw-scopes.any.js new file mode 100644 index 0000000000000..7c6f61614b068 --- /dev/null +++ b/testing/web-platform/tests/IndexedDB/transaction-scheduling-rw-scopes.any.js @@ -0,0 +1,63 @@ +// META: script=support.js + +indexeddb_test( + (t, db) => { + const store = db.createObjectStore('store'); + db.createObjectStore('a'); + db.createObjectStore('b'); + db.createObjectStore('c'); + }, + + (t, db) => { + let transaction1Started = false; + let transaction1Complete = false; + let transaction2Started = false; + let transaction2Complete = false; + let transaction3Started = false; + let transaction3Complete = false; + + const transaction1 = db.transaction(['a'], 'readwrite'); + let request = transaction1.objectStore('a').get(0); + request.onerror = t.unreached_func('request should succeed'); + request.onsuccess = t.step_func(() => { + transaction1Started = true; + }); + transaction1.onabort = t.unreached_func('transaction1 should complete'); + transaction1.oncomplete = t.step_func(() => { + transaction1Complete = true; + assert_false(transaction2Started); + assert_false(transaction3Started); + }); + + + // transaction2 overlaps with transaction1, so must wait until transaction1 + // completes. + const transaction2 = db.transaction(['a', 'b'], 'readwrite'); + request = transaction2.objectStore('a').get(0); + request.onerror = t.unreached_func('request should succeed'); + request.onsuccess = t.step_func(() => { + assert_true(transaction1Complete); + transaction2Started = true; + }); + transaction2.onabort = t.unreached_func('transaction2 should complete'); + transaction2.oncomplete = t.step_func(() => { + transaction2Complete = true; + assert_false(transaction3Started); + }); + + // transaction3 overlaps with transaction2, so must wait until transaction2 + // completes even though it does not overlap with transaction1. + const transaction3 = db.transaction(['b', 'c'], 'readwrite'); + request = transaction3.objectStore('b').get(0); + request.onerror = t.unreached_func('request should succeed'); + request.onsuccess = t.step_func(() => { + assert_true(transaction1Complete); + assert_true(transaction2Complete); + transaction3Started = true; + }); + transaction3.onabort = t.unreached_func('transaction3 should complete'); + transaction3.oncomplete = t.step_func_done(() => { + transaction3Complete = true; + }); + }, + "Check that scope restrictions on read-write transactions are enforced."); diff --git a/testing/web-platform/tests/IndexedDB/transaction-scheduling-within-database.any.js b/testing/web-platform/tests/IndexedDB/transaction-scheduling-within-database.any.js new file mode 100644 index 0000000000000..10dd8b6d7ac5e --- /dev/null +++ b/testing/web-platform/tests/IndexedDB/transaction-scheduling-within-database.any.js @@ -0,0 +1,55 @@ +// META: script=support.js + +indexeddb_test( + (t, db) => { + const store = db.createObjectStore('store'); + store.put('value', 'key'); + }, + + (t, db) => { + let transaction1GetSuccess = false; + let transaction2GetSuccess = false; + + const onTransactionComplete = barrier_func(2, t.step_func_done(() => { + assert_true(transaction1GetSuccess, + 'transaction1 should have executed at least one request'); + assert_true(transaction2GetSuccess, + 'transaction1 should have executed at least one request'); + })); + + const transaction1 = db.transaction('store', 'readonly'); + transaction1.onabort = t.unreached_func('transaction1 should not abort'); + transaction1.oncomplete = t.step_func(onTransactionComplete); + + const transaction2 = db.transaction('store', 'readonly'); + transaction2.onabort = t.unreached_func('transaction2 should not abort'); + transaction2.oncomplete = t.step_func(onTransactionComplete); + + // Keep both transactions alive until each has reported at least one + // successful operation + + function doTransaction1Get() { + const request = transaction1.objectStore('store').get('key'); + request.onerror = t.unreached_func('request should not fail'); + request.onsuccess = t.step_func(() => { + transaction1GetSuccess = true; + if (!transaction2GetSuccess) + doTransaction1Get(); + }); + } + + function doTransaction2Get() { + // NOTE: No logging since execution order is not deterministic. + const request = transaction2.objectStore('store').get('key'); + request.onerror = t.unreached_func('request should not fail'); + request.onsuccess = t.step_func(() => { + transaction2GetSuccess = true; + if (!transaction1GetSuccess) + doTransaction2Get(); + }); + } + + doTransaction1Get(); + doTransaction2Get(); + }, + 'Check that read-only transactions within a database can run in parallel.');