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

Promise.prototype.finally: add tests #1156

Merged
merged 3 commits into from
Aug 11, 2017
Merged

Promise.prototype.finally: add tests #1156

merged 3 commits into from
Aug 11, 2017

Conversation

ljharb
Copy link
Member

@ljharb ljharb commented Jul 26, 2017

(The first two commits can and should be cherry-picked in separately; i'll rebase this PR ASAP if that happens)

Promise.prototype.finally reached stage 3 today at TC39; here's my first crack at the tests.

See tc39/proposal-promise-finally#18, https://tc39.github.io/proposal-promise-finally/, and https://github.com/es-shims/Promise.prototype.finally/blob/master/test/tests.js for more info.

Copy link
Member

@littledan littledan left a comment

Choose a reason for hiding this comment

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

These tests look good--it's nice to see edge cases tested.

One interaction that I'm wondering about is the interaction with the HostPromiseRejectionTracker hook. Seems like the Promise there will be the result of finally, rather than the Promise used as input to it.

It'd be nice to have a test for this behavior, as I imagine some implementers might intuit to go the other way. That could be through host hooks, or if that's too tough, in a web-platform-test.

@benjamingr
Copy link

One interaction that I'm wondering about is the interaction with the HostPromiseRejectionTracker hook. Seems like the Promise there will be the result of finally, rather than the Promise used as input to it.

That's a good point, the promise caught should be the result of the finally and not the result of the original promise. More specifically:

let p = Promise.reject(new Error());
let p2 = p.finally(() => {});

The unhandled rejection should be p2 and not p.

@littledan
Copy link
Member

@benjamingr To be clear, I believe the semantics you're saying are the right ones are what the specification implies. I was just wondering about tests for this property. Thanks for spelling it out better.

@ljharb
Copy link
Member Author

ljharb commented Jul 26, 2017

If there are existing tests for this hook, then I should be able to utilize them to test it here - if not, I'd need some guidance.

@benjamingr
Copy link

@ljharb Node tracks them with process.on("unhandledRejection", (e, p) => {}) and the browser with window.addEventListener("unhandledrejection", e => {... }).

If you'd like help I can send you links to the PRs adding the hooks which should shed light on how one might test them.

@ljharb
Copy link
Member Author

ljharb commented Jul 26, 2017

@benjamingr i'm aware of how a specific implementation tests them; but how would test262 - which is supposed to be host-agnostic - test those? It's run on more engines than browsers and node, and the spec permits unhandled rejection tracking, but does not mandate it.

@ljharb
Copy link
Member Author

ljharb commented Jul 26, 2017

I don't see any mention of HostPromiseRejectionTracker in the entire codebase; it seems like testing this isn't yet a solved problem.

@littledan
Copy link
Member

Test262 would have to add a host hook to test this. I don't think it has a hook like this currently. If test262 maintainers don't like the idea of adding such a hook, another method of testing would be to add something to web-platform-tests.

Copy link
Member

@leobalter leobalter left a comment

Choose a reason for hiding this comment

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

I don't have the proper time to do a complete review but I left some comments. Some are about whitespace or conventions.

Something I miss a lot reviewing the files is the info tag with at least some minimal spec part as a reference for the tests.

author: Jordan Harband
description: Promise.prototype.finally invokes `then` method
esid: sec-promise.prototype.finally
features: [Promise, Promise.prototype.finally]
Copy link
Member

Choose a reason for hiding this comment

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

Promise.prototype.finally is sufficient.

Copy link
Member Author

Choose a reason for hiding this comment

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

Would nobody want to run tests against an engine without Promise?

features.txt Outdated
@@ -64,12 +68,19 @@ default-arg
default-parameters
destructuring-binding
Float64Array
Function.prototype.call
Copy link
Member

Choose a reason for hiding this comment

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

I believe we should not add these features in this PR.

We can't assure this flag is enough here to capture every test using the feature. The same goes for any other new features added that are not Promise.prototype.finally.


verifyNotEnumerable(Promise.prototype.finally, 'length');
verifyNotWritable(Promise.prototype.finally, 'length');
verifyConfigurable(Promise.prototype.finally, 'length');
Copy link
Member

Choose a reason for hiding this comment

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

propertyHelper as a new function that we should use instead:

verifyProperty(Promise.prototype.finally, "length", {
  value: 1,
  enumerable: false,
  configurable: true,
  writable: false,
});

The order of the properties are not relevant in this function.

description: Promise.prototype.finally `name` property
esid: sec-promise.prototype.finally
info: >
ES6 Section 17:
Copy link
Member

Choose a reason for hiding this comment

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

We are over ES6 :)

ES should be fine

@@ -7,6 +7,10 @@
#
# https://github.com/tc39/process-document

# Promise.prototype.finally
# https://github.com/tc39/proposal-promise-finally
Promise.prototype.finally
Copy link
Member

Choose a reason for hiding this comment

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

Perfect, thanks!

author: Jordan Harband
description: >
Promise.prototype.finally called with a `this` value whose `then` property is
an accessor property that returns an abrupt completion
Copy link
Member

Choose a reason for hiding this comment

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

thanks for the clarification here.

});

assert.throws(Test262Error, function() {
Promise.prototype.finally.call(poisonedThen);
Copy link
Member

Choose a reason for hiding this comment

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

this would like calling poisonedThen.finally(), right? We can add a new assert.throws in this file.


assert.throws(Test262Error, function() {
Promise.prototype.finally.call(thrower);
});
Copy link
Member

Choose a reason for hiding this comment

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

this would like calling thrower.finally(), right? We can add a new assert.throws in this file.


p.then = undefined;
assert.throws(TypeError, function() {
Promise.prototype.finally.call(p);
Copy link
Member

Choose a reason for hiding this comment

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

we could poison finally to make sure TypeError happens at the expected time:

Correct me if I'm not using the API properly:

p.finally(function() { throw new Test262Error('this should not run'); });

Copy link
Member Author

Choose a reason for hiding this comment

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

ah, interesting. sure, i can do that (no need to poison the finally method, but i can certainly pass a poisoned onFinally)


assert.throws(TypeError, function() {
Promise.prototype.finally.call(undefined);
}, 'undefined');
Copy link
Member

Choose a reason for hiding this comment

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

here you can poison a promise's then:

const p = new Promise(function() {})

p.then = function() { throw new Test262Error('poison'); };

assert.throws(TypeError, function() {
  p.finally.call(undefined);
}, 'undefined');

Copy link
Member Author

Choose a reason for hiding this comment

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

I'm not sure what value this adds; it we use .call then the function has no more link to p, so p.then couldn't be reached even if i wanted it to.

@leobalter
Copy link
Member

Test262 would have to add a host hook to test this. I don't think it has a hook like this currently. If test262 maintainers don't like the idea of adding such a hook, another method of testing would be to add something to web-platform-tests.

I would need more time to review this, maybe @rwaldron is interested.

@ljharb
Copy link
Member Author

ljharb commented Jul 27, 2017

Thanks, updated! I haven't added "info" since I'm not sure where to link to prior to actually merging with the spec.

@gsathya
Copy link
Member

gsathya commented Jul 27, 2017

Quick question: do these tests pass when running V8 with --harmony-promise-finally flag?

FYI, there's a lot of V8 tests as well if you want to import them: https://cs.chromium.org/chromium/src/v8/test/mjsunit/harmony/promise-prototype-finally.js

@ljharb
Copy link
Member Author

ljharb commented Jul 27, 2017

@gsathya running test262 tests isn't easy; i can definitely give that a shot :-)

in general tho, wouldn't it make sense to never write v8-only tests, when something could go in test262? :-p

@gsathya
Copy link
Member

gsathya commented Jul 27, 2017

@gsathya running test262 tests isn't easy; i can definitely give that a shot :-)

Sounds good. Thanks! Node 8 should have this feature behind the flag, no need to build d8.

in general tho, wouldn't it make sense to never write v8-only tests, when something could go in test262? :-p

Until quite recently it wasn't possible to write test262 tests as part of a v8 patch -- we had to do out of band test262 code review dance and then roll that change into v8. @littledan has done some great work in making this possible now, so going forward we'll be writing more test262 tests.

@ljharb
Copy link
Member Author

ljharb commented Jul 27, 2017

Tests pass with both my promise.prototype.finally npm module, but I can't get test262-harness to run with the --harmony-promise-finally flag on node 8 latest (altho the flag works by itself) :-/

@ljharb
Copy link
Member Author

ljharb commented Jul 27, 2017

However, when I run promise.prototype.finally's tests against node --harmony-promise-finally, there is one of my own module's tests that fails in v8 - specifically around observable then calls with a Promise subclass.

Needless to say we'll have to make sure both implementations as well as the spec and the test262 tests are all in line :-)

@rwaldron
Copy link
Contributor

tests pass with both my promise.prototype.finally npm module, but I can't get test262-harness to run with the --harmony-promise-finally flag on node 8 latest (altho the flag works by itself) :-/

I'll dig into this.

@rwaldron
Copy link
Contributor

@ljharb the flag is --harmony_promise_finally (according to d8 built from source)

@rwaldron
Copy link
Contributor

I wouldn't bother using Node.js to test anything until the Node.js patch to eshost has landed. Also, I am experiencing the same problem with --harmony_promise_finally and it's because the order of arguments is wrong:

(inside ConsoleAgent.js, which is in eshost)

  createChildProcess(args) {
    args = args || [];
    args = args.concat(this.args);
    return cp.spawn(this.hostPath, args);
  }

Which results in the args coming out as (for example):

[ '/var/folders/xz/k2lxgs3s43180_tjwn1v_kwc0000gn/T/117627-60511-189e99u.f98kg.js',
  '--harmony_promise_finally' ]

The flag needs to be before the file:

rwaldron in ~/codez/javascript
$ node -v
v8.2.1
rwaldron in ~/codez/javascript
$ cat promise.prototype.finally.js
console.log(Promise.prototype.finally);
$ node --harmony_promise_finally promise.prototype.finally.js
[Function: finally]
rwaldron in ~/codez/javascript
$ node promise.prototype.finally.js --harmony_promise_finally
undefined

So I will make a PR to eshost that re-arranges the args for NodeAgent.


Moving on to my local run results:

d8

$ test262-harness --hostType=d8 --hostPath=`which d8` --hostArgs='--harmony_promise_finally' test/built-ins/Promise/prototype/finally/**/*.js
Ran 32 tests
32 passed
0 failed

@ljharb
Copy link
Member Author

ljharb commented Jul 27, 2017

@rwaldron ah! thanks. with node --harmony-promise-finally it works in the repl, perhaps node normalizes dashes and the v8 api doesn't?

@rwaldron
Copy link
Contributor

Using my patch, I was able to confirm these tests on Node.js 8.2.1

Nice work @ljharb!

We can do the eshost dependent tests in a follow up PR.

@ljharb
Copy link
Member Author

ljharb commented Jul 27, 2017

figured it out; it's a bug in eshost via test262-harness. When patched, node --harmony_promise_finally passes this PR's tests!

(edit: lol, github didn't update so i missed you figuring this out half an hour ago)

@rwaldron
Copy link
Contributor

perhaps node normalizes dashes and the v8 api doesn't?

Ah, that's probably very likely. There's no reason why NodeAgent couldn't/shouldn't try to do that as well.

@rwaldron
Copy link
Contributor

@ljharb

Ah, that's probably very likely.

I was wrong. Either dash or underscore work just fine, the issue was always just the order of the args. Patch to eshost is coming.


no.catch(function (e) {
assert.sameValue(e, noReason);
throw e;

Choose a reason for hiding this comment

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

I would add cases with thow e, within fulfillment and rejection handlers i.e.:

Promise.resolve(1)
    .then(function () { throw new Error('');})
    .finally(/* Check if finally is reached */);

Promise.reject(1)
    .then(function () {}, function () { throw new Error('');})
    .finally(/* Check if finally is reached */)

Choose a reason for hiding this comment

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

It seems that we need test that we raise TypeError when we use finally with new i.e.

const p = new Promise((resolve)=>{});

assert.throws(TypeError, function() {
  const f = new p.finally();
}, 'no constructor');

Copy link
Member Author

Choose a reason for hiding this comment

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

Hmm - I can add that test, but p.finally() returns a Promise, so I have no idea why it'd ever work with new, and .catch() lacks the same test. I'm pretty sure we don't test every returned object value to ensure it doesn't work with new - would checking that typeof wasn't "function" suffice?

Choose a reason for hiding this comment

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

Ohh, I see. You're right.
So it will be enough to check if finally returns Promise. Not sure that I can find such test, or I missed something.
If there is no such test we can use Symbol.toStringTag:

const p = Promise.resolve(1);
assert.sameValue(p.finally()[Symbol.toStringTag], 'Promise');

Copy link
Member Author

Choose a reason for hiding this comment

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

I'd think chaining a .then or .catch on the end would be sufficient, and I think some of the tests already do that.

@rwaldron
Copy link
Contributor

rwaldron commented Aug 2, 2017

The patch to eshost is in final stages now, so I don't consider this "blocked" anymore (not that it ever was, I just hoped to get that fixed first... you know how it is).

@leobalter leobalter mentioned this pull request Aug 3, 2017
1 task
@rwaldron
Copy link
Contributor

eshost and test262-harness are both updated now and I ran these tests locally against d8:

$ test262-harness --hostType d8 --hostPath `which d8` --hostArgs="--harmony_promise_finally" test/built-ins/Promise/prototype/finally/*.js
Ran 32 tests
32 passed
0 failed

Success!

@leobalter leobalter merged commit e467c83 into tc39:master Aug 11, 2017
@ljharb ljharb deleted the finally branch August 11, 2017 17:35
assert.sameValue(arguments.length, 0, 'onFinally receives zero args');
return {};
}).then(function (x) {
assert.sameValue(x, obj, 'onFinally can not override the resolution value');
Copy link
Member

Choose a reason for hiding this comment

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

If the onFinally and the onFulfilled callbacks are skipped, the tests would still pass right?

Copy link
Member

Choose a reason for hiding this comment

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

(I see other tests also that don't verify if the onFulfilled/onRejected/onFinally has actually run.)

Copy link
Member Author

Choose a reason for hiding this comment

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

yes, that's true. i can make a followup PR that adds counters to ensure they're called; that's a good idea

@gsathya
Copy link
Member

gsathya commented Aug 11, 2017

We are missing tests that call the SpeciesConstructor. The following test fails in V8:

// Flags: --allow-natives-syntax --harmony-promise-finally

var thenCalled = 0;

class FooPromise extends Promise {
  then(onFullfilled, onRejected) {
    thenCalled++;
    return super.then(onFullfilled, onRejected);
  }
}

var x = new FooPromise(r => r()).finally(() => {});
%RunMicrotasks();

// (1) Promise.prototype.finally
// 1. 7. Return ? Invoke(promise, "then", thenFinally, catchFinally).
// (2) ThenFinally function
// 1.1. 8. Return ? Invoke(promise, "then", valueThunk>
// (3) Promise Resolve Functions
// 25.4.1.3.2. 8. Let then be Get(resolution, "then").

print(thenCalled == 3);

@ljharb
Copy link
Member Author

ljharb commented Aug 11, 2017

Thanks, I'll write a followup PR soon that strengthens the tests!

@leobalter
Copy link
Member

leobalter commented Aug 12, 2017 via email

@ljharb
Copy link
Member Author

ljharb commented Jan 31, 2018

#1398 is merged; but more followup is still ideal.

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.

7 participants