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

Introducing "it.yield", a convenient way to test promise code #9601

Merged
merged 4 commits into from Jun 4, 2017

Conversation

lannka
Copy link
Contributor

@lannka lannka commented May 27, 2017

yield-sign-clipart-1

We're often facing tests like this:
Example A

verify(false);
resolver();  // this resolve a promise
verify(true); // this will fail because the promise chain is not executed yet.

Hack A: We are smart, so we figured we can:

verify(false);
resolver();  // this resolve a promise
// wait promise to resolve 
return widget.promise_.then(() => {
  verify(true);
});

Example B

verify(false);
clock.tick(100);  // this triggers a timer and then a promise chain
verify(true); // this will fail because the promise chain is not resolved yet.

Hack B:

verify(false);
clock.tick(100);  // this triggers a timer and then a promise chain

setTimeout(() => {
  verify(true); 
  done();
})

Hack B+: This can easily go out of control:

verify(false);
clock.tick(99); 

setTimeout(() => {
  verify(false); 
  clock.tick(1);

  setTimeout(() => {
    verify(true);
    done();
  });
})

Introducing it.yield, no more callback hell! :

verify(false);
clock.tick(99);
yield;
verify(false);
clock.tick(1);
yield;
verify(true);

/cc @cramforce @dvoytenko

(One caveat: Safari 9 does not support Generator)

@lannka lannka requested a review from zhouyx May 27, 2017 03:57
expect(is3pThrottled(win)).to.be.false;
incrementLoadingAds(win);
expect(is3pThrottled(win)).to.be.true;
clock.tick(999);
yield;
Copy link
Member

Choose a reason for hiding this comment

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

This is genius, but I don't fully understand how it works. Or this is a bit awkward.

Wouldn't it be better to always yield a promise instead of relying on the magic setInterval shortcircuiting in the trivial case?

If you return a promise from incrementLoadingAds that would work for this test.

Copy link
Contributor Author

Choose a reason for hiding this comment

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

When code runs to ayield statement in a Generator, it stops and yield to other tasks/micro tasks in the event loop to run. It will resume once the Generator.next() is called.

Calling Generator.next() in a setInterval makes sure the remaining work always get queued to the end of event loop.

Choose a reason for hiding this comment

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

Super cool, just learned about generators and yield. 😄

Could await and Promises could achieve the same goal (but perhaps read more naturally)? E.g. in the test below:

let loadAds = incrementLoadingAds(win); // returns Promise
expect(is3pThrottled(win)).to.be.true;
await loadAds; // replaces invocation of resolver and `yield`
expect(is3pThrottled(win)).to.be.false;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

depending on the use cases. here we do have a handle of the promise, but in many real scenario, we don't.

also, await is not ES6 feature.

yield is actually nature to me. it means to yield to other code (other tasks in the event loop) to run first.

}
return realIt(message, done => {
const iterator = runnable();
const interval = setInterval(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

This is awesome! Would fix tests with callback hell and can simplify many promise related tests!
One question I have: I know in our code we use setTimeout to delay callback. are the callback in setTimeout micro task as well?
Does the setInterval work in the following scenario:

param = false
promise = new Promise(resolve => {resolver = resolve});
promise.then(() => {param = true})
doSomething = () => {setTimeout(resolver)}
doSomething();
yield
expect(param).to.be.true

Copy link
Contributor Author

Choose a reason for hiding this comment

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

setTimeout is not a microtask.
but your example should work. because yield flushes the eventloop, including resolver.

Copy link
Contributor

Choose a reason for hiding this comment

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

Hmmm I tried

it('should work ', function* () {
    let param = true;
    let resolver;
    const promise = new Promise(resolve => {resolver = resolve;});
    promise.then(() => {
      param = false;
    });
    const doSomething = () => {setTimeout(resolver);};
    doSomething();
    yield;
    expect(param).to.be.false;
  });

and it won't pass.

Copy link
Contributor

@jridgewell jridgewell Jun 1, 2017

Choose a reason for hiding this comment

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

@zhouyx That's the thread @lannka and I have been going back and forth over. The only proper fix to this is to yield the promise (this ensure its then blocks have been called).

Copy link
Contributor Author

Choose a reason for hiding this comment

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

@zhouyx I added your test case into my test set, it works.

|| runnable.constructor.name !== 'GeneratorFunction') {
return realIt(message, runnable);
}
return realIt(message, done => {
Copy link
Contributor

Choose a reason for hiding this comment

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

Using a done parameter prevents these tests from returning a promise as a final resolution value.

Copy link
Contributor

@jridgewell jridgewell left a comment

Choose a reason for hiding this comment

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

I'm going to block this until I'm overruled by @dvoytenko or @cramforce. This is going to fail in a large majority of cases (a promise chain, not just a simple, single promise). In general, this works because setInterval happens after promise resolution. But that's an internal operation and not defined by the browser.

From what I can tell, there's two motivations behind this PR:

  1. Callback hell
  2. Async resolution

This kind of solves callback hell. But it does nothing to guarantee async resolution happens properly (my main objection). We can use this, but we have to add a bit more code to support. @cramforce's suggestion is actually the optimal solution (so is @choumx's, because async/await can be handled by Generators), and we can use this to get there.

If we yield a promise instance, we can wait upon the promise's resolution to happen and send back the value (this is what co does). This handles callback hell (we no longer need nested then blocks), and it solves async resolution (the yielded promise is guaranteed to resolve before the test code is run again). If the promise we yield is the promise the src code is waiting on, then we get perfection.

it('co like', function* () {
  const promise = srcCode.method();
  const result = yield promise;
  expect(result).to.be(something);
});

}
return realIt(message, done => {
const iterator = runnable();
const interval = setInterval(() => {
Copy link
Contributor

Choose a reason for hiding this comment

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

setTimeout every time the function yields would be more appropriate. This can end up causing the test code to run again before it's really intended.

@dvoytenko
Copy link
Contributor

@lannka I think this is really cool and I don't see much in terms of possibility of flakes, except for one case that I don't fully yet understand.

Imagine, this example:

it('test with promise', function* () {
  let value;
  Promise.resolve().then(function() {
    return Promise.resolve.then(function() {
      value = true;
    });
  });
  yield;
  expect(value).to.be.true;
});

This should be guaranteed to be always stable with native promises. But there's a possibility that, if a polyfill implemented via event queue (or native implemented incorrectly via event queue) we'd get the following situation:

  1. First promise is scheduled as next in queue, i.e. analogous to setTimeout(0).
  2. Next comes yield, it's essentially scheduled also as setTimeout(0). So it now follows (1).
  3. Then the setTimeout(0) in (1) is executed and it schedules again via setTimeout(0).

So it's possible that yield will be out of sync?

My second comment - it'd be cool if yield also flushed vsync in tests. I recommend we do this via describes like this:

  1. When describes instance has started, it creates a global sandbox object. We maintain a list of vsyncs = [] on the global sandbox object.
  2. On yield, we go over global sandbox.vsyncs and flush them all.

@lannka
Copy link
Contributor Author

lannka commented May 30, 2017

In general, this works because setInterval happens after promise resolution.

@jridgewell no. This works because setInterval will append remaining work into the tail of the event queue. thinking it as a way to flush the event queue.

Can you come up with a test case that will fail this?

@jridgewell
Copy link
Contributor

@jridgewell no. This works because setInterval will append remaining work into the tail of the event queue. thinking it as a way to flush the event queue.

That's my point. This is an internal procedure, not defined by the spec. Any promise that waits on XHR, or itself uses setTimeout (see anything that uses our Timer#promise), or any other optimization the browser makes (When do they start pausing resolution chains based on frame rate?) could break this.

@dvoytenko's code above is not defined even for the native Promise implementation. It could be result in true or false depending on the browser or polyfill.

@cramforce
Copy link
Member

Lets make it a standard to always yield to an actual promise. We can make a helper like macroTask() that yields to the event loop like setInterval does.

@lannka
Copy link
Contributor Author

lannka commented May 31, 2017

I added tests. They run successfully under latest Chrome, Safari, FF. Saucelabs is somehow not working for me right now, so didn't check on older browsers. PTAL

@cramforce not quite get what you mean by the macroTask() helper.
@jridgewell let me know if you have any test case you think it will be flaky on some browsers.

@dreamofabear
Copy link

I think he meant you can create a Promise that resolves after a macro-task:

function macroTask() {
  return new Promise(resolve => {
    setTimeout(resolve, 0);
  });
}

You can avoid setTimeout callback hell with this.

@lannka
Copy link
Contributor Author

lannka commented May 31, 2017

verify(false);
clock.tick(99); 

setTimeout(() => {
  verify(false); 
  clock.tick(1);

  setTimeout(() => {
    verify(true);
    done();
  });
})

@choumx how would you refactor the above callback hell using your macroTask?

@dreamofabear
Copy link

return macroTask().then(() => {
  verify(false); 
  clock.tick(1);
  return macroTask();
}).then(() => {
  verify(true);
});

yield is prettier than this though. 😄

@lannka
Copy link
Contributor Author

lannka commented May 31, 2017

@choumx this seems like the same :-) just a more manual way.

Talked with @jridgewell offline. His concern is about the potential undocumented race condition between promise.resolve and setTimeout/setInterval. But in fact, without introducing this helper, the problem exists if we ever want to use setTimeout to do a later check.

verify(false);
clock.tick(100);  // this triggers a timer and then a promise chain

setTimeout(() => {
  verify(true); 
  done();
})

@jridgewell
Copy link
Contributor

This would be fine:

yield macroTask();
verify(false); 
clock.tick(1);
yield macroTask();
verify(true);

That almost solves my objections because we're awaiting a promise. It's still a race condition (and so async resolution is not guaranteed) but we're at least on the right track.

The best solution is still going to be getting access to the source code's promise.

@jridgewell
Copy link
Contributor

But in fact, without introducing this helper, the problem exists if we ever want to use setTimeout to do a later check.

I don't want either of those (the current or the proposed). Using setTimeout to wait on promise resolution is the reason we have flake in the current code base.

@lannka
Copy link
Contributor Author

lannka commented May 31, 2017

Using setTimeout to wait on promise resolution is the reason we have flake in the current code base.

I'd love to see a live example of those flakes. If I understand correctly, your idea is to have both promise + setTimeout to make sure remaining work is append at the end of the loop, no matter how browser treats promise and setTimeout. If so, I can add additional Promise.resolve into my setInterval to keep the neat syntax.

@jridgewell
Copy link
Contributor

@lannka
Copy link
Contributor Author

lannka commented Jun 1, 2017

https://github.com/ampproject/amphtml/pull/9384/files

@jridgewell as we discussed offline previously, that is totally a different story. That test was flaky because the promise chain has a real XHR over the network in it. Timeout 5ms obviously isn't enough for some of the times. This test should be categorized as an integration test, where a "polling", or RequestBank (a concept I introduced to test HTTP requests) should be used.

Again, I also don't like the fact that we fixed the test by exposing an "test only" private field. Ideally, we'd keep those implementation details away from an integration test.

This introduced helper is not planed to be used in such case. If we really want to, we can change it to a unit test, where we mock out the XHR and resolve it explicitly:

xhrResolver();
yield;
check();

That way, we separated the concerns, which is the key of a unit test.

@jridgewell
Copy link
Contributor

That test was flaky because the promise chain has a real XHR over the network in it.

It's using a stubbed fetch. It's flakey because the race between setTimeout and Promise resolution is not defined, as I've said over and over again. It's even using a setTimeout(..., 5) (which even more macro-task than the one proposed in this PR) and it was still flakey.

@jridgewell
Copy link
Contributor

To be very specific, the problem is this:

Once execution of a Job is initiated, the Job always executes to completion. No other Job may be initiated until the currently running Job completes. However, the currently running Job or external events may cause the enqueuing of additional PendingJobs that may be initiated sometime after completion of the currently running Job. http://www.ecma-international.org/ecma-262/6.0/#sec-jobs-and-job-queues

A Job is a micro-task, and Promise#then enqueues a Job. Each browser is free to choose how they want to handle "enqueuing of additional PendingJobs that may be initiated sometime after completion of the currently running Job". The Job Queue does not have to be exhausted during this tick. So the browser does not have to evaluate an entire promise chain full of then blocks in one tick. They can choose to pause the Queue's evaluation for whatever reason.

To demonstrate in code, take my PJs Promise implementation (which AMP uses to polyfill):

// https://github.com/jridgewell/PJs/blob/b265fded45dbd06ae8a64ee0ccde29b1e3b8f760/promise.js#L423-L430
function flush() {
  // notice this rechecks `queue.length` every time,
  // meaning anything enqueued keeps it going
  for (var i = 0; i < queue.length; i++) {
    var fn = queue[i];
    queue[i] = null;
    fn(); // `fn` is a job
  }
}

I use exhaustive evaluation because it's easy, and it makes feel like promises are just as fast as nodebacks. But I could have easily done:

function flush() {
  var current = queue;
  // queue has been redefined, so anything enqueued is appended to this new array,
  // and not the one I'm currently iterating.
  queue = [];
  for (var i = 0; i < current.length; i++) {
    var fn = queue[i];
    fn();
  }
}

In this case, the queue is not exhausted and must wait till the next tick before anything the next promise in the chain is resolved. It's up to the implementation to decide on what "initiated sometime after completion" means. It's not even defined that the current job queue has to be exhausted, I could pause mid queue:

function flush() {
  // I'll arbitrarily stop at 1000 jobs,
  // because we're spending too much time here.
  // Maybe I'll implement a `performance.now` comparison to keep this under 16ms?
  // Totally up to me.
  for (var i = 0; i < 1000; i++) {
    var fn = queue[i];
    fn();
  }
  queue.splice(0, 1000);
}

With this, my problem with Async resolution is undefined. And this means the use of setTimeout to wait till after a promise chain is resolved is undefined (or racey in the best case). And this means we'd be building our test code on completely undefined behavior. That's why I'm objecting so strongly to this PR, we're codifying an undefined race condition.

If we instead yield the promise we want to wait on, we solve everything this PR wants.

  1. Yield return values means callback hell go away. That's awesome.
  2. The yielded promise can be waited on until it resolves, guaranteeing that race conditions are eliminated. That's even better.

@@ -214,14 +214,35 @@ it = function(message, runnable) { // eslint-disable-line no-native-reassign, no
return realIt(message, runnable);
}
return realIt(message, done => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We can refactor this so Promises take care of everything:

// Adapted from Babel's asyncToGenerator
return realIt(message, () => {
  return new Promise((resolve, reject) => {
    const gen = runnable();
    function step(key, arg) {
      let info;
      try {
        info = gen[key](arg);
      } catch (error) {
        reject(error);
        return;
      }

      const { value, done } = info;
      if (done) {
        resolve(value);
      } else {
        Promise.resolve(value).then(_next, _throw);
      }
    }
    function _next(value) {
      step("next", value);
    }
    function _throw(err) {
      step("throw", err);
    }
    _next();
  });
});

Differences include:

// With this, we can do: `yield 50`, which blocks the test for 50ms
// We should rarely need this in unit test, use with caution, as it
// usually brings test flackiness.
const timeout = (typeof state.value === 'number') ? state.value : 0;
Copy link
Contributor

Choose a reason for hiding this comment

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

We are we running a timeout if there's no timeout?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

yes, i should probably document that.

return fn(message, runnable);
}
return fn(message, done => {
const runner = (iterator, result) => {
Copy link
Contributor

Choose a reason for hiding this comment

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

We can refactor this so Promises take care of everything:

// Adapted from Babel's asyncToGenerator
return realIt(message, () => {
  return new Promise((resolve, reject) => {
    const gen = runnable();
    function step(key, arg) {
      let info;
      try {
        info = gen[key](arg);
      } catch (error) {
        reject(error);
        return;
      }

      const { value, done } = info;
      if (done) {
        resolve(value);
      } else {
        Promise.resolve(value).then(_next, _throw);
      }
    }
    function _next(value) {
      step("next", value);
    }
    function _throw(err) {
      step("throw", err);
    }
    _next();
  });
});

Differences include:

Copy link
Contributor Author

Choose a reason for hiding this comment

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

just want to understand the benefit better:

Promises handle async (through assimilation)

Sorry, what does that mean? :-)

Yielded promises can reject, going into the generators throw path

This is not a problem. I should have added anyway.

We avoid using done #8966

I handled this by using a try-catch block. But in your impl, there's also a try-catch, is that there for the same reason?

Less closures

If I count correctly, all the 3 functions are closures step, _next, _throw. Not sure how it is less than the done impl.

Also, I still think there is value to support setTimeout. We will add boilerplate code to turn a setTimeout to promise.

Copy link
Contributor

Choose a reason for hiding this comment

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

Sorry, what does that mean? :-)

Promise assimilation is:

Promise.resolve(1).then(value => assert(value, 1));
Promise.resolve(somePromiseThatResolvesTo1).then(value => assert(value, 1));

Promises are designed to be interoperable through assimilation (they do almost exactly what you do with the value.then === function check), but in a overspecified-spec way.

I handled this by using a try-catch block

Done is actually not handled by the try-catch. If the generator threw, that's handled by it. But the problem is the final result value (when iterator.done = true) isn't waited on (it could be a promise).

But in your impl, there's also a try-catch, is that there for the same reason?

My try-catch and yours are equivalent, but neither handles the done callback (see above).

not sure how it is less than the done impl.

Multiple closures are used when the yielded value is a promise (the then blocks). This one's really a nit-pick.

Also, I still think there is value to support setTimeout

#9601 (comment)

@lannka
Copy link
Contributor Author

lannka commented Jun 1, 2017

I split off the changes on concurrent-load.js and test-concurrent-load-js, leaving only yield related things in this PR.

A recap of the discussions so far:

We agreed:

  • Use yield promise to avoid callback hell in tests.

Still debating:

  • When there is no easy way to get the promise, can we use setTimeout? I.e., can we rely on the "vaguely documented fact" that promise resolves in microtask, which queues before setTimeout.
  • Whether make the impl a promise based or done based. The decision will be highly affected by the decision of the previous item.

My arguments are:

  • Exposing internal promise state only for test violates test principals. The more we couple the implementation code with test code, the more technical debt we'll be paying off later on.
  • Microtask seems to be a widely accepted concept. I also created tests and they all works on my local browsers. Ultimately, our unit tests are not to be run in a random polyfill.

ECMAScript has the concept of "jobs" which are similar to microtasks, but the relationship isn't explicit aside from vague mailing list discussions. However, the general consensus is that promises should be part of the microtask queue, and for good reason.
Treating promises as tasks leads to performance problems, as callbacks may be unnecessarily delayed by task-related things such as rendering. It also causes non-determinism due to interaction with other task sources, and can break interactions with other APIs, but more on that later.

I suggest we get this PR in and monitor the flakiness of the tests introduced from this PR on Travis.

@everyone thoughts?

Copy link
Contributor

@jridgewell jridgewell left a comment

Choose a reason for hiding this comment

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

Whether make the impl a promise based or done based. The decision will be highly affected by the decision of the previous item.

We can support the setTimeout API and remove done. It's really about duplication.

Microtask seems to be a widely accepted concept. I also created tests and they all works on my local browsers

See #9601 (comment).

Exposing internal promise state only for test violates test principals. The more we couple the implementation code with test code

I wonder if we can just accumulate all Promise instances. Something like:

it(() => {
  collectPromises();
  src.somethingThatsAsync();
  const allDone = Promises.all(stopCollectingPromises());
  yield allDone;
  // async stuff done
});

testing/yield.js Outdated
* Check test-yield.js for how-to.
*/
export function installYieldIt(realIt) {
it = enableYield.bind(null, realIt); // eslint-disable-line no-native-reassign, no-undef
Copy link
Contributor

Choose a reason for hiding this comment

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

global.it = .... we could do it.yields?

Copy link
Contributor Author

Choose a reason for hiding this comment

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

we can do either:

it.yields('message', function*() {
});

or

it('message', function*() {
});

I'm in favor of the latter because it makes the other syntax like it.only and it.skip unchanged.

testing/yield.js Outdated
return done(e);
}
if (state.done) {
return done();
Copy link
Contributor

Choose a reason for hiding this comment

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

We need to wait for the state's value to complete.

if (state.done) {
  Promise.resolve(state.value).then(() => done(), done);
  return;

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good catch

testing/yield.js Outdated
const _runner = runner.bind(null, iterator);
if (isPromise(state.value)) {
// With this, we can do: `const result = yield promise;`
state.value.then(_runner).catch(done);
Copy link
Contributor

Choose a reason for hiding this comment

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

This catch clause is incorrect. If a Promise is yielded, and it rejects, it should be passed into Generator#throw. Also, it can be in the same then block, not after in a catch block:

state.value.then(_runner, (error) => {
  // this will basically duplicate everything in runner
  try {
    result = iterator.throw(error);
  } catch (e) {
    return done(e);
  }
  //...
});

Copy link
Contributor Author

Choose a reason for hiding this comment

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

Sorry, I still don't quite understand this part. Why is it necessary to throw the error to iterator, instead of just finish the test case immediately?

I think it's harmless to have an iterator hang there without iterating to the end, no? Or is it for better error reporting?

Copy link
Contributor

Choose a reason for hiding this comment

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

It's not for error reporting, but so we can take full advantage of the generator's functionality. Imagine:

it('regular code', function() {
  return somePromiseThatRejects.then(() => {
    throw new Error('UNREACHABLE');
  }, err => {
    expect(err.message).to.contain('something');
  });
});

As it is now, this cannot be written into our yield style, because the promise would reject and kill the test. Instead, we want something like:

it('generator yield', function*() {
  try {
    yield somePromiseThatRejects;
    throw new Error('UNREACHABLE');
  } catch (e) {
    expect(e.message).to.contain('something');
  }
});

That's throwing the error into the iterator, allowing us to test all our promise tests as sync generators.

testing/yield.js Outdated
// We should rarely need this in unit test, use with caution, as it
// usually brings test flakiness.
const timeout = (typeof state.value === 'number') ? state.value : 0;
setTimeout(_runner, timeout);
Copy link
Contributor

Choose a reason for hiding this comment

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

Still strongly against this.

@lannka
Copy link
Contributor Author

lannka commented Jun 2, 2017

@jridgewell
after talking to Blink team, queuing promise resolution in microtask is a standard:
https://html.spec.whatwg.org/multipage/webappapis.html#enqueuejob(queuename,-job,-arguments)

So it's not a "undefined behavior", we should be able to use it, at least on modern browsers and well implemented polyfills.

Nevertheless, I will go with your previous suggestion, since it's more explicit and less magic behind the scene.

yield macroTask();
verify(false); 
clock.tick(1);
yield macroTask();
verify(true);

Any other concerns?

@jridgewell
Copy link
Contributor

after talking to Blink team, queuing promise resolution in microtask is a standard:

I mentioned that in #9601 (comment). The undefined part is not that it's a microtask, but that running the microtasks (especially newly queued ones) isn't guaranteed in this tick.

since it's more explicit and less magic behind the scene.

Awesome. I'm on board with this, as long as it's explicit and ugly in test code. The yield 10 still makes it easy to write flakey code.

Any other concerns?

Nope, I look forward to using yield!

@lannka
Copy link
Contributor Author

lannka commented Jun 2, 2017

@jridgewell I've updated the code. Please take a look.

Didn't get it to return a promise because:

  • so i can still do catch in for "done" state.
  • one less function

Copy link
Contributor

@jridgewell jridgewell left a comment

Choose a reason for hiding this comment

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

One issue, then LGTM.

testing/yield.js Outdated
} catch (e) {
// catch any assertion errors and pass to `done`
// otherwise the messages are swallowed
return done(e);
}
if (state.done) {
return done();
Promise.resolve(state.value).then(() => done(), _throw);
Copy link
Contributor

Choose a reason for hiding this comment

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

This is incorrect, the iterator is done and pushing the error into it won't be handled. This should be then(() => done(), done)

Copy link
Contributor Author

Choose a reason for hiding this comment

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

good point. fixed.

@jridgewell
Copy link
Contributor

PS. Why didn't we just use async/await? We could avoid the entire it = yield(it), it.only = yield(only) stuff...

it('async', async () => {
  try {
    await somePromise;
  } catch (e) {
    // promise rejected...
  }

  const value = await anythingEvenIfItsNotAPromise;
  assert(value);
});

@lannka
Copy link
Contributor Author

lannka commented Jun 2, 2017

@jridgewell well, essentially we wrote a polyfill of async/await using generator to be able to run in our current project setup.

@jridgewell
Copy link
Contributor

But it should just work because we compile our test code with Babel.

@lannka
Copy link
Contributor Author

lannka commented Jun 3, 2017

@jridgewell it does not work right away. maybe doable with preset-es2017.

But I was hesitating: is it good to have different babel setting in production vs test? Or in other words, are we ready to moving to es2017?

@lannka lannka merged commit 6f4e6f2 into ampproject:master Jun 4, 2017
torch2424 pushed a commit to torch2424/amphtml that referenced this pull request Jun 6, 2017
…ject#9601)

* Introduce yield.

* Address comments.

* fix done state error handling
torch2424 pushed a commit to torch2424/amphtml that referenced this pull request Jun 6, 2017
* Bidtellect amp implementation… (ampproject#9518)

* Bidtellect amp implementation…

* removing renderStartImplemented…

* spaces?

* adding example

* Fix test-amp-ad-network-doubleclick-impl (ampproject#9645)

* Optimizations to speed up Travis PR builds (ampproject#9626)

Travis builds have become slow during busy periods of the day due to various reasons. This PR does the following:

Reorganizes build tasks across fewer build shards
Reduces the number of VMs used by PR and master builds to 2
No longer does a full gulp dist for unit tests (Possible to do after ampproject#9404)
Reduces the total CPU time used for PR builds to < 20 mins
Fixes ampproject#9500
Fixes ampproject#9387

* Animations: full on-action API (ampproject#9641)

* Animations: full on-action API

* review fixes

* Fix Validator tests on Travis, which now uses Node v4 (ampproject#9654)

* Amp-imgur : Implement imgur embed (ampproject#9405) (ampproject#9434)

* Amp-imgur : Implement imgur embed (ampproject#9405)

* amp-imgur test code modified

* amp-imgur validation code changed

* fix amp-imgur lint check

* Remove unused Layour variables

* Test code modified

* Ingegration amp-imgur extension

* [amp-imgur] add resize logic

* Remove 3p iframes and add resize message listener

* Fix some code with reviews
* change some syntax
* change validation rules
* add find event source

* [amp-imgur] remove unnecessary code and add object check

* remove spec_name from validation code

* Remove unnecessary code and add owners file

* Change required tag name  to  in validation file

* data-imgurid -> data-imgur-id

* Add whitelist for amp-imgur

* remove unused AmpImgur import in test file

* remove unused preconnect callback

* Update amp-cors-requests.md (ampproject#9636)

Added updates and corrections per Dima's feedback.

* Expose login url building to amp access service (ampproject#9300)

* Animations: support style keyframes (ampproject#9647)

* Animations: support style keyframes

* docs

* review fixes

* lints

* Animations: subtargets format (ampproject#9655)

* Animations: subtargets format

* fix docs

* Fix extern for integration test (ampproject#9657)

* Remove whitelisted link for PR 9434 (ampproject#9656)

* Add callouts for CORS + cleanup (ampproject#9591)

* Add callouts for CORS + cleanup

* Update amp-access.md

* Doubleclick Fast Fetch: Send default safeframe version on ad request (ampproject#9613)

* Send default safeframe version on ad request

* fix test failure

* amp-ima-video: Fixes some undefined variables in compiled extension (ampproject#9617)

* Add experimental input-debounced to the actions and event doc (ampproject#9650)

* Copy and rename compiled script directly for deprecated version (ampproject#9587)

* Remove A4A Dependency on ads/_config.js (ampproject#9462)

* Added doubleclick specific config

* Modified other networks

* Update MockA4AImpl

* Remove unneeded config lines

* Add cid, remove renderStartImplemented

* Fix lint complaint

* Changed from mandatory config to overrrideable method

* Addressed feedback, modified getAdCid

* Addressed additional feedback

* Responded to feedback

* Removed unnneeded guard against null value

* Changed signature of function

* Fixed lint issues

* addressed comments

* Addressed comments, fixed broken test

* Change signature / update tests

* Fix wrong contents and white spaces in amp-imgur.md (ampproject#9658)

* Performance: separate ini-load from first visible ini-load (ampproject#9665)

* amp-bind: Fix more integration tests (ampproject#9598)

* more bind test fixes

* include object.assign polyfill

* move polyfills to separate file

* replace use of map() from bind-validator with ownProperty()

* fix types and test

* Stop using IOS elastic scroll when slidescroll takes over animation (Only for the NON SNAP-POINTS flow) (ampproject#9668)

* removing overflow touch on no-scroll for ios

* Stop using IOS elastic scroll when slidescroll takes over animation (Only for the NON SNAP-POINTS flow)

* make sure css is compiled before entry points (ampproject#9643)

* update npm package version (ampproject#9671)

* A4A: expose visibilityCsi (ampproject#9667)

* A4A: expose visibilityCsi

* tests

* amp-bind: Fix embedding in FIF (ampproject#9541)

* move element service changes from ampproject#9447

* store element service ids in extension struct

* add unit test, fix other tests

* Fix null value binding (ampproject#9674)

* Add amp-form's submit action to the docs (ampproject#9675)

* Write cookie for ad-cid. (ampproject#9594)

* Write cookie for ad-cid.

* fix tests

* amp-access-laterpay fixes (ampproject#9633)

* Add support for LaterPay subscriptions

* Fix canonical link on laterpay example

* Tweak default styling

* Add an example locale message for the header

* Fix indentation of JsDoc (ampproject#9677)

It should be indented at the same level as the method it's documenting.

* Add example to amp iframe docs (ampproject#9660)

* Change image to static link hosted on ampproject.org

* Update amp-iframe doc w example + gen cleanup

* Adds SFG experiment branches to doubleclick-a4a-config.js. (ampproject#9662)

* Added experiment branches corresponding to exp=a4a:(5|6).

* Removed test file.

* Removed changes to yarn.lock + minor fixes.

* Fixed adsense-a4a-config.js.

* Fixed return statement.

* Removed reference to style tag for opacity (ampproject#9634)

As style tag for `opacity` was replaced with `amp-boilerplate (back in this [commit](ampproject@0a056ca), updated the text to reflect those changes.

* De-flake amp-bind integration tests (ampproject#9683)

* split bind fixtures into one file per extension

* fix width/height binding bug, avoid race condition w/ dynamic containers

* actually, just skip the tests affected by race condition for now

* Remove timer calls to prevent future flakiness (ampproject#9478)

* Remove timer calls to prevent future flakiness

* Revert the line order change

* Poll instead of using stub callback constructor

* Add jsdoc and rename variables

* amp-animation: polyfill partial keyframes from CSS (ampproject#9689)

* Animations: whitelist offset distance and regroup condition types (ampproject#9688)

* Use Docker containers in Travis (ampproject#9666)

* clean up web-worker experiment (ampproject#9706)

* Load examiner.js when #development=2 (ampproject#9680)

* Load examiner.js when #development=1

* address comments

* Use development=2

* Fix presubmit

* Implementation of amp-ad-exit (ampproject#9390)

* Implementation of the amp-ad-exit component for managing ad navigation (ampproject#8515).

Changes from the I2I:
- Only RANDOM, CLICK_X, and CLICK_Y variables can be used, alongside custom vars.
- It doesn't work well with <a> tags - the tap action doesn't trigger on middle clicks. Recommendation is to use other elements.
- ClickLocationFilter for border click protection is not yet implemented. It will be added in a future change.

The component does some lightweight validation of the JSON config when it's
built. This may be overkill.

Tested:
gulp test --files 'extensions/amp-ad-exit/0.1/test/*'
gulp check-types

* Update JSDoc for some functions. Fix lint issue (ignore unused parameters in an interface method).

* sinon.spy -> sandbox.spy

* Add amp-ad-exit to the list of allowed extensions in A4A pages and fix validation rules.

Address PR: add experiment flag; use camelCase for config properties

address some comments

Instantiate a new Filter for each spec.

* Instantiate a new Filter for each spec.

* remove duplicate doctype tag in example

* Make ActionEventDef public for type annotations.

* Add filter factory.

* fix event var

* update type annotation for Filter.filter

* address PR comments

* Move filter config validation to the ctor. Remove Filter.buildCallback().

* camelCase in documentation examples

* Introducing "it.yield", a convenient way to test promise code (ampproject#9601)

* Introduce yield.

* Address comments.

* fix done state error handling

* Document amp-access as a special target (ampproject#9568)

`amp-access` is a special target that's not mentioned in this document.
It's special because you can't give an arbitrary ID to it, i.e., you
can't just change the id of the `amp-access` script tag to `amp-access2`
and expect `on="tap:amp-access2.login"` to work. Given that the
"actions" for `amp-access` is a bit dynamic depending on the structure
of the `amp-access` JSON, instead of listing out the actions in a
tabular form, a reference to the `amp-access` documentation is added.

* Run presubmit tests soon after building (ampproject#9716)

* Revert "Use Docker containers in Travis (ampproject#9666)" (ampproject#9717)

This reverts commit 98ecb9b.

It turns out that while using Docker containers instead of VMs on Travis reduces VM startup time, it has significantly increased build and test time, increasing the overall PR check round trip from < 20 mins to > 30 mins.

Compare https://travis-ci.org/ampproject/amphtml/jobs/238464404 (Docker) against https://travis-ci.org/ampproject/amphtml/jobs/238462066 (VM).

Fixes ampproject#9651

* Validator Rollup (ampproject#9719)

* Add validator tests for amp-imgur.

* Disallow amp-embed as child of amp-app-banner.

* Minor cleanup, Make sure our set numbers are consistent in javascript.

* amp-state: Allow both `src` and script child (ampproject#9721)

* adapt code from ampproject#8897

* also_requires_attr should be in trigger

* Remove experiment for input-debounced. Update docs. (ampproject#9724)

* Validator Rollup (ampproject#9727)

* Minor cleanup of amp-ad-exit

* Disallow amp-ad/amp-embed as children of amp ad containers when data-multi-size is present.

* Revision bump

* Enabling dynamic queryparam addition to anchour links  (ampproject#9684)

* Enabling queryparam addition to anchour links

* Code review changes

* Additional cache urls (ampproject#9733)

* Significantly speed up gulp build --css-only (ampproject#9726)

Prior to this PR, gulp build --css-only would take ~ 1 minute to run, and end up building a bunch of js files in addition to compiling css.

After this PR, gulp build --css-only takes ~ 0.5 seconds to run (a 100x speed up), and the only output files in build/ after running it are .css, and .css.js files.

Resulting speeds on my workstation:
gulp build --css-only: ~500ms
gulp build: ~1m 45s
gulp dist --fortesting: ~3m 30s

Fixes ampproject#9640

* add amp-analytics Facebook Pixel support (ampproject#9449)

* add amp-analytics Facebook Pixel support

* add amp-analytics Facebook Pixel support

* fix test on extensions/amp-analytics/0.1/test/vendor-requests.json

* add Facebook Pixel standard events https://developers.facebook.com/docs/ads-for-websites/pixel-events/v2.9#events

* Update vendors.js

fix conflicts

* Update vendors.js

fix conflicts

* Update vendor-requests.json

fix conflicts

* Update vendor-requests.json

fix conflicts

* Update vendor-requests.json

* Update analytics.amp.html

* Update analytics-vendors.amp.html

* Update vendors.js

* Update analytics-vendors.amp.html

* fix lint error

* update

* facebook pixel

* Update vendor-requests.json

* Update analytics.amp.html

* fix lint error

* fix lint error

*  fix alphabetical order

* Update yarn.lock

* AMP-ad supports Seznam Imedia ad network (ampproject#8893)

* AMP-ad supports Seznam Imedia ad network

* Fixed requested changes

* Fixed requested changes

* Render API implemented

* Improved placing ads

* Fixed requested changes

* Render API fixed context
* Renamed IM.cz to Imedia
* Described JSON parameters
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

8 participants