Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix test regression in asyncHelpers.js #4197

Merged
merged 1 commit into from
Aug 19, 2024

Conversation

jedel1043
Copy link
Contributor

@jedel1043 jedel1043 commented Aug 15, 2024

Some recent changes to harness/asyncHelpers.js regressed some harness tests; test/harness/asyncHelpers-throwsAsync-func-throws-sync.js and test/harness/asyncHelpers-throwsAsync-no-arg.js to be more specific. The cause of this was fa3d024 and feb400c, which made some changes that broke those two tests.

This PR fixes this by slightly modifying asyncHelpers.js so that it passes the two tests, and also adds a new test to ensure #4186 is still fixed even with the new changes.

@jedel1043 jedel1043 requested a review from a team as a code owner August 15, 2024 05:22
harness/asyncHelpers.js Outdated Show resolved Hide resolved
Comment on lines 75 to 78
resolve(Promise.resolve(res).then(
thenable = res.then(
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't think I agree with this change... the provided func has correctly returned a thenable res, and I don't think we care about the value returned from res.then(onFulfilled, onRejected)—just that the appropriate callback will actually be invoked, and that is adequately covered by absorbing it with Promise.resolve.

However, I do see value in continuing to assert that res.then does not itself synchronously throw, which we can do by manually constructing the promise that follows res rather than using Promise.resolve.

Suggested change
resolve(Promise.resolve(res).then(
thenable = res.then(
var onResFulfilled, onResRejected;
var resSettlementP = new Promise(function (onFulfilled, onRejected) {
onResFulfilled = onFulfilled;
onResRejected = onRejected;
});
res.then(onResFulfilled, onResRejected);
resolve(resSettlementP.then(

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that's a much cleaner approach.

Comment on lines -53 to +67
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 res;
if (typeof expectedErrorConstructor !== "function") {
fail("assert.throwsAsync called with an argument that is not an error constructor");
}
if (typeof func !== "function") {
fail("assert.throwsAsync called with an argument that is not a function");
}
var expectedName = expectedErrorConstructor.name;
var expectation = "Expected a " + expectedName + " to be thrown asynchronously";
var res;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

👍

@@ -70,9 +73,9 @@ assert.throwsAsync = function (expectedErrorConstructor, func, message) {
if (res === null || typeof res !== "object" || typeof res.then !== "function") {
fail(expectation + " but result was not a thenable");
}

var thenable;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This would be better named something like secondaryThenable, but I'm pretty sure assert.throwsAsync is better without having it at all.

} catch (thrown) {
fail(expectation + " but .then threw synchronously");
}
if (thenable === null || typeof thenable !== "object" || typeof thenable.then !== "function") {
fail(expectation + " but result was not a conforming thenable");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I find this message is misleading; the result (res) is conforming as long as res.then(onFulfilled, onRejected) eventually invokes onFulfilled to indicate func() success or onRejected to indicate func() failure, even if res.then does not itself return a thenable—we only care about the callbacks, not about further chainability. But if we did care, I would suggest instead " but result thenable's "then" method did not return a thenable".

Comment on lines 14 to 18
then: function () {},
};
};
const p = assert.throwsAsync(TypeError, func);
assert(p instanceof Promise, "assert.throwsAsync should return a promise");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The problem with then: function () {} isn't that it fails to return a Promise, it's that it fails to invoke either its first or second argument, leaving this p permanently unsettled—and I don't think we can test for that. Put another way, then: function (_onFulfilled, _onRejected) { return Promise.resolve(); } is just as bad for actual use of asyncTest, but would not trip any assertions due to the very nature of asynchronous code.

Copy link
Contributor Author

@jedel1043 jedel1043 Aug 17, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah, that sounds fair. However, I was thinking of ways to test this and maybe it's possible to test with something like:

assert.throwsAsync(TypeError, func).then(
  function() { $DONE("the promise should not be resolved") },
  function() { $DONE("the promise should not be resolved") },
);
$DONE();

Though, I'm still unsure if we can assume that implementations will keep running jobs even if the print function already printed Test262:AsyncTestComplete, but that's maybe something other tests already assume?

Copy link
Contributor

@gibson042 gibson042 Aug 18, 2024

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Though, I'm still unsure if we can assume that implementations will keep running jobs even if the print function already printed Test262:AsyncTestComplete, but that's maybe something other tests already assume?

Presumably. If we're going to cover this, I guess it would be modeled on something like test/harness/asyncHelpers-asyncTest-returns-undefined.js and just overwrite $DONE plus document a timeliness assumption:

var realDone = $DONE;
var doneArgs = [];
globalThis.$DONE = function (arg) {
  doneArgs.push(arg);
};

function delay() {
  var later = Promise.resolve();
  for (var i = 0; i < 100; i++) {
    later = later.then();
  }
  return later;
}

(async function () {
  // Spy on the promise returned by an invocation of assert.throwsAsync with a
  // function that returns a thenable which never invokes provided callbacks.
  var blackHoleThenable = { then: function() {} };
  var p = assert.throwsAsync(TypeError, function() { return blackHoleThenable; });
  p.then($DONE, $DONE);
})()
  // Give it a long time to try.
  .then(delay, delay)
  .then(function () {
    assert.compareArray(doneArgs, [], "$DONE must not have be called");
  })
  .then(realDone, realDone);

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Sounds good! I'll change the test

@gibson042 gibson042 merged commit dde3050 into tc39:main Aug 19, 2024
8 checks passed
@jedel1043 jedel1043 deleted the fix-async-helpers branch August 19, 2024 16:56
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants