From 8ca5bdd44cf682409d355a068068bec83752160b Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Sun, 19 May 2024 12:43:11 -0400 Subject: [PATCH 1/8] CONTRIBUTING.md: Improve asyncHelpers documentation * Only mention `$DONE` after `asyncTest`. * Document that `asyncTest` is not intended to be called multiple times per file. --- CONTRIBUTING.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 2c8e529a4fa..74118d65dea 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -209,8 +209,9 @@ This key is for boolean properties associated with the test. - **raw** - execute the test without any modification (no harness files will be included); necessary to test the behavior of directive prologue; implies `noStrict` -- **async** - defer interpretation of test results until after the invocation - of the global `$DONE` function +- **async** - defer interpretation of test results until settlement of an + `asyncTest` callback promise or manual invocation of `$DONE`; refer to + [Writing Asynchronous Tests](#writing-asynchronous-tests) for details - **generated** - informative flag used to denote test files that were created procedurally using the project's test generation tool; refer to [Procedurally-generated tests](#procedurally-generated-tests) @@ -346,7 +347,7 @@ Consumers that violate the spec by throwing exceptions for parsing errors at run An asynchronous test is any test that include the `async` frontmatter flag. -For most asynchronous tests, the `asyncHelpers.js` harness file includes an `asyncTest` method that precludes needing to interact with the test runner via the `$DONE` function. `asyncTest` takes an async function and will ensure that `$DONE` is called properly if the async function returns or throws an exception. For example, a test written using `asyncTest` might look like: +Most asynchronous tests should include the `asyncHelpers.js` harness file and call its `asyncTest` function **exactly once**, with a callback returning a promise that indicates test failure via rejection and otherwise fulfills upon test conclusion (such as an async function). ```js /*--- From 1b34a1c4847ef7b01a3899faed70729159b6002f Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Sun, 19 May 2024 12:43:58 -0400 Subject: [PATCH 2/8] harness/asyncHelpers.js: Add doc comments --- harness/asyncHelpers.js | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/harness/asyncHelpers.js b/harness/asyncHelpers.js index c8e58457fc1..e7eac06a68d 100644 --- a/harness/asyncHelpers.js +++ b/harness/asyncHelpers.js @@ -6,6 +6,14 @@ description: | defines: [asyncTest] ---*/ +/** + * Defines the **sole** asynchronous test of a file. + * @see {@link ../docs/rfcs/async-helpers.md} for background. + * + * @param {Function} testFunc a callback whose returned promise indicates test results + * (fulfillment for success, rejection for failure) + * @returns {void} + */ function asyncTest(testFunc) { if (!Object.hasOwn(globalThis, "$DONE")) { throw new Test262Error("asyncTest called without async flag"); @@ -28,6 +36,18 @@ function asyncTest(testFunc) { } } +/** + * Asserts that a callback asynchronously throws an instance of a particular + * error (i.e., returns a promise whose rejection value is an object referencing + * the constructor). + * + * @param {Function} expectedErrorConstructor the expected constructor of the + * rejection value + * @param {Function} func the callback + * @param {string} [message] the prefix to use for failure messages + * @returns {Promise} fulfills if the expected error is thrown, + * otherwise rejects + */ assert.throwsAsync = function (expectedErrorConstructor, func, message) { return new Promise(function (resolve) { var innerThenable; From b22b500f241a226b4c9e6b899638ec6bb5109269 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Sun, 19 May 2024 13:18:45 -0400 Subject: [PATCH 3/8] harness/asyncHelpers.js: Refactor assert.throwsAsync to fail fast --- harness/asyncHelpers.js | 37 ++++++++++++++++++------------------- 1 file changed, 18 insertions(+), 19 deletions(-) diff --git a/harness/asyncHelpers.js b/harness/asyncHelpers.js index e7eac06a68d..210cc4201f2 100644 --- a/harness/asyncHelpers.js +++ b/harness/asyncHelpers.js @@ -56,30 +56,29 @@ assert.throwsAsync = function (expectedErrorConstructor, func, message) { } else { message += " "; } - if (typeof func === "function") { - try { - innerThenable = func(); - if ( - innerThenable === null || - typeof innerThenable !== "object" || - typeof innerThenable.then !== "function" - ) { - message += - "Expected to obtain an inner promise that would reject with a" + - expectedErrorConstructor.name + - " but result was not a thenable"; - throw new Test262Error(message); - } - } catch (thrown) { + if (typeof func !== "function") { + message += + "assert.throwsAsync called with an argument that is not a function"; + throw new Test262Error(message); + } + try { + innerThenable = func(); + if ( + innerThenable === null || + typeof innerThenable !== "object" || + typeof innerThenable.then !== "function" + ) { message += - "Expected a " + + "Expected to obtain an inner promise that would reject with a" + expectedErrorConstructor.name + - " to be thrown asynchronously but an exception was thrown synchronously while obtaining the inner promise"; + " but result was not a thenable"; throw new Test262Error(message); } - } else { + } catch (thrown) { message += - "assert.throwsAsync called with an argument that is not a function"; + "Expected a " + + expectedErrorConstructor.name + + " to be thrown asynchronously but an exception was thrown synchronously while obtaining the inner promise"; throw new Test262Error(message); } From 0631e2111b23894103669bc7167023c12c20a3f4 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Sun, 19 May 2024 13:26:31 -0400 Subject: [PATCH 4/8] harness/asyncHelpers.js: Isolate assert.throwsAsync failure conditions --- harness/asyncHelpers.js | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/harness/asyncHelpers.js b/harness/asyncHelpers.js index 210cc4201f2..cd610bd7340 100644 --- a/harness/asyncHelpers.js +++ b/harness/asyncHelpers.js @@ -63,17 +63,6 @@ assert.throwsAsync = function (expectedErrorConstructor, func, message) { } try { innerThenable = func(); - if ( - innerThenable === null || - typeof innerThenable !== "object" || - typeof innerThenable.then !== "function" - ) { - message += - "Expected to obtain an inner promise that would reject with a" + - expectedErrorConstructor.name + - " but result was not a thenable"; - throw new Test262Error(message); - } } catch (thrown) { message += "Expected a " + @@ -81,6 +70,17 @@ assert.throwsAsync = function (expectedErrorConstructor, func, message) { " to be thrown asynchronously but an exception was thrown synchronously while obtaining the inner promise"; throw new Test262Error(message); } + if ( + innerThenable === null || + typeof innerThenable !== "object" || + typeof innerThenable.then !== "function" + ) { + message += + "Expected to obtain an inner promise that would reject with a" + + expectedErrorConstructor.name + + " but result was not a thenable"; + throw new Test262Error(message); + } try { resolve(innerThenable.then( From b87eaf9ac3e50b517cfef1de98e260a4f09fb353 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Sun, 19 May 2024 13:26:31 -0400 Subject: [PATCH 5/8] harness/asyncHelpers.js: Fix an assert.throwsAsync failure message typo --- harness/asyncHelpers.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/harness/asyncHelpers.js b/harness/asyncHelpers.js index cd610bd7340..fa4665b6217 100644 --- a/harness/asyncHelpers.js +++ b/harness/asyncHelpers.js @@ -76,7 +76,7 @@ assert.throwsAsync = function (expectedErrorConstructor, func, message) { typeof innerThenable.then !== "function" ) { message += - "Expected to obtain an inner promise that would reject with a" + + "Expected to obtain an inner promise that would reject with a " + expectedErrorConstructor.name + " but result was not a thenable"; throw new Test262Error(message); From 0b61e98564e3e7a84c0effc9641f4b9f9ba1683a Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Sun, 19 May 2024 13:33:08 -0400 Subject: [PATCH 6/8] harness/asyncHelpers.js: Clean up assert.throwsAsync failure messages * Remove mention of an "inner" promise/thenable. --- harness/asyncHelpers.js | 21 ++++++--------------- 1 file changed, 6 insertions(+), 15 deletions(-) diff --git a/harness/asyncHelpers.js b/harness/asyncHelpers.js index fa4665b6217..abd71ce8a42 100644 --- a/harness/asyncHelpers.js +++ b/harness/asyncHelpers.js @@ -67,7 +67,7 @@ assert.throwsAsync = function (expectedErrorConstructor, func, message) { message += "Expected a " + expectedErrorConstructor.name + - " to be thrown asynchronously but an exception was thrown synchronously while obtaining the inner promise"; + " to be thrown asynchronously but the function threw synchronously"; throw new Test262Error(message); } if ( @@ -76,7 +76,7 @@ assert.throwsAsync = function (expectedErrorConstructor, func, message) { typeof innerThenable.then !== "function" ) { message += - "Expected to obtain an inner promise that would reject with a " + + "Expected to obtain a promise that would reject with a " + expectedErrorConstructor.name + " but result was not a thenable"; throw new Test262Error(message); @@ -113,19 +113,10 @@ assert.throwsAsync = function (expectedErrorConstructor, func, message) { } )); } catch (thrown) { - if (typeof thrown !== "object" || thrown === null) { - message += - "Expected a " + - expectedErrorConstructor.name + - " to be thrown asynchronously but innerThenable synchronously threw a value that was not an object "; - } else { - message += - "Expected a " + - expectedErrorConstructor.name + - " to be thrown asynchronously but a " + - thrown.constructor.name + - " was thrown synchronously"; - } + message += + "Expected a " + + expectedErrorConstructor.name + + " to be thrown asynchronously but .then threw synchronously"; throw new Test262Error(message); } }); From fa3d0246e74da7844843ebfb4c71453c616b923a Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Sun, 19 May 2024 13:41:38 -0400 Subject: [PATCH 7/8] harness/asyncHelpers.js: Reduce the size of assert.throwsAsync --- harness/asyncHelpers.js | 64 ++++++++++++++++------------------------- 1 file changed, 25 insertions(+), 39 deletions(-) diff --git a/harness/asyncHelpers.js b/harness/asyncHelpers.js index abd71ce8a42..1bec9a0f2e6 100644 --- a/harness/asyncHelpers.js +++ b/harness/asyncHelpers.js @@ -50,74 +50,60 @@ function asyncTest(testFunc) { */ assert.throwsAsync = function (expectedErrorConstructor, func, message) { return new Promise(function (resolve) { + var expectedName = expectedErrorConstructor.name; + var fail = function (detail) { + if (message === undefined) { + throw new Test262Error(detail); + } + throw new Test262Error(message + " " + detail); + }; var innerThenable; - if (message === undefined) { - message = ""; - } else { - message += " "; - } if (typeof func !== "function") { - message += - "assert.throwsAsync called with an argument that is not a function"; - throw new Test262Error(message); + fail("assert.throwsAsync called with an argument that is not a function"); } try { innerThenable = func(); } catch (thrown) { - message += - "Expected a " + - expectedErrorConstructor.name + - " to be thrown asynchronously but the function threw synchronously"; - throw new Test262Error(message); + fail("Expected a " + + expectedName + + " to be thrown asynchronously but the function threw synchronously"); } if ( innerThenable === null || typeof innerThenable !== "object" || typeof innerThenable.then !== "function" ) { - message += - "Expected to obtain a promise that would reject with a " + - expectedErrorConstructor.name + - " but result was not a thenable"; - throw new Test262Error(message); + fail("Expected to obtain a promise that would reject with a " + + expectedName + + " but result was not a thenable"); } try { resolve(innerThenable.then( function () { - message += - "Expected a " + - expectedErrorConstructor.name + - " to be thrown asynchronously but no exception was thrown at all"; - throw new Test262Error(message); + fail("Expected a " + + expectedName + + " to be thrown asynchronously but no exception was thrown at all"); }, function (thrown) { - var expectedName, actualName; + var actualName; if (typeof thrown !== "object" || thrown === null) { - message += "Thrown value was not an object!"; - throw new Test262Error(message); + fail("Thrown value was not an object!"); } else if (thrown.constructor !== expectedErrorConstructor) { - expectedName = expectedErrorConstructor.name; actualName = thrown.constructor.name; if (expectedName === actualName) { - message += - "Expected a " + + fail("Expected a " + expectedName + - " but got a different error constructor with the same name"; - } else { - message += - "Expected a " + expectedName + " but got a " + actualName; + " but got a different error constructor with the same name"); } - throw new Test262Error(message); + fail("Expected a " + expectedName + " but got a " + actualName); } } )); } catch (thrown) { - message += - "Expected a " + - expectedErrorConstructor.name + - " to be thrown asynchronously but .then threw synchronously"; - throw new Test262Error(message); + fail("Expected a " + + expectedName + + " to be thrown asynchronously but .then threw synchronously"); } }); }; From f83c1b938757f8ef213aa00594f0460b2b8cfbb6 Mon Sep 17 00:00:00 2001 From: Richard Gibson Date: Sun, 19 May 2024 13:51:39 -0400 Subject: [PATCH 8/8] harness/asyncHelpers.js: Further reduce the size of assert.throwsAsync --- harness/asyncHelpers.js | 38 +++++++++++++------------------------- 1 file changed, 13 insertions(+), 25 deletions(-) diff --git a/harness/asyncHelpers.js b/harness/asyncHelpers.js index 1bec9a0f2e6..b2efdf8061b 100644 --- a/harness/asyncHelpers.js +++ b/harness/asyncHelpers.js @@ -51,59 +51,47 @@ function asyncTest(testFunc) { assert.throwsAsync = function (expectedErrorConstructor, func, message) { return new Promise(function (resolve) { var expectedName = expectedErrorConstructor.name; + var expectation = "Expected a " + expectedName + " to be thrown asynchronously"; var fail = function (detail) { if (message === undefined) { throw new Test262Error(detail); } throw new Test262Error(message + " " + detail); }; - var innerThenable; + var res; if (typeof func !== "function") { fail("assert.throwsAsync called with an argument that is not a function"); } try { - innerThenable = func(); + res = func(); } catch (thrown) { - fail("Expected a " + - expectedName + - " to be thrown asynchronously but the function threw synchronously"); + fail(expectation + " but the function threw synchronously"); } - if ( - innerThenable === null || - typeof innerThenable !== "object" || - typeof innerThenable.then !== "function" - ) { - fail("Expected to obtain a promise that would reject with a " + - expectedName + - " but result was not a thenable"); + if (res === null || typeof res !== "object" || typeof res.then !== "function") { + fail(expectation + " but result was not a thenable"); } try { - resolve(innerThenable.then( + resolve(res.then( function () { - fail("Expected a " + - expectedName + - " to be thrown asynchronously but no exception was thrown at all"); + fail(expectation + " but no exception was thrown at all"); }, function (thrown) { var actualName; - if (typeof thrown !== "object" || thrown === null) { - fail("Thrown value was not an object!"); + if (thrown === null || typeof thrown !== "object") { + fail(expectation + " but thrown value was not an object"); } else if (thrown.constructor !== expectedErrorConstructor) { actualName = thrown.constructor.name; if (expectedName === actualName) { - fail("Expected a " + - expectedName + + fail(expectation + " but got a different error constructor with the same name"); } - fail("Expected a " + expectedName + " but got a " + actualName); + fail(expectation + " but got a " + actualName); } } )); } catch (thrown) { - fail("Expected a " + - expectedName + - " to be thrown asynchronously but .then threw synchronously"); + fail(expectation + " but .then threw synchronously"); } }); };