aurelia/aurelia

View on GitHub
packages/__tests__/src/2-runtime/scheduler.spec.ts

Summary

Maintainability
F
1 wk
Test Coverage
import { TestContext, assert } from '@aurelia/testing';
import { QueueTaskOptions, ITask, TaskQueue } from '@aurelia/platform';

function createExposedPromise() {
  let resolve: () => void;
  let reject: (err: any) => void;

  const promise = new Promise<void>(function ($resolve, $reject) {
    resolve = $resolve;
    reject = $reject;
  });

  return {
    resolve,
    reject,
    promise,
  };
}

function round(num: number) {
  return ((num * 10 + .5) | 0) / 10;
}

function reportTask(task: any) {
  const id = task.id;
  const created = round(task.createdTime);
  const queue = round(task.queueTime);
  const preempt = task.preempt;
  const reusable = task.reusable;
  const persistent = task.persistent;
  const status = task._status;

  return `id=${id} createdTime=${created} queueTime=${queue} preempt=${preempt} reusable=${reusable} persistent=${persistent} status=${status}`;
}

describe('2-runtime/scheduler.spec.ts', function () {
  // There is only ever one global platform, so we might as well store it here instead of initializing all extra boilerplate each test
  const platform = TestContext.create().platform;

  function queueRecursive(sut: TaskQueue, opts: QueueTaskOptions, count: number, cb: () => void) {
    function $queue() {
      cb();

      if (--count > 0) {
        sut.queueTask($queue, opts);
      }
    }

    sut.queueTask($queue, opts);
  }

  function queueRecursiveAsync(sut: TaskQueue, opts: QueueTaskOptions, count: number, cb: () => Promise<void>) {
    async function $queue() {
      await cb();

      if (--count > 0) {
        sut.queueTask($queue, opts);
      }
    }

    sut.queueTask($queue, opts);
  }

  function queueSequential(sut: TaskQueue, opts: QueueTaskOptions, count: number, cb: () => void) {
    while (count-- > 0) {
      sut.queueTask(cb, opts);
    }
  }

  function queueSequentialAsync(sut: TaskQueue, opts: QueueTaskOptions, count: number, cb: () => Promise<void>) {
    while (count-- > 0) {
      sut.queueTask(cb, opts);
    }
  }

  const prioritySpecs = [
    {
      sut: platform.domQueue,
      name: 'domWriteQueue',
    },
    {
      sut: platform.taskQueue,
      name: 'taskQueue',
    },
    {
      sut: platform.domReadQueue,
      name: 'domReadQueue',
    },
    {
      sut: platform.domQueue,
      name: 'domWriteQueue',
    },
    {
      sut: platform.taskQueue,
      name: 'taskQueue',
    },
  ];

  for (const reusable of [true, false]) {
    for (const { sut, name } of prioritySpecs) {
      describe(`can queue ${name}`, function () {
        it('x1, {preempt: false, delay: 0}', function (done) {
          sut.queueTask(
            function () {
              assert.areTaskQueuesEmpty();

              done();
            },
            {
              reusable,
              preempt: false,
              delay: 0,
            },
          );
        });

        it('x1, {preempt: true, delay: 0}', function (done) {
          sut.queueTask(
            function () {
              assert.areTaskQueuesEmpty();

              done();
            },
            {
              reusable,
              preempt: true,
              delay: 0,
            },
          );
        });

        it('x1, {delay: 5}', function (done) {
          sut.queueTask(
            function () {
              assert.areTaskQueuesEmpty();

              done();
            },
            {
              reusable,
              delay: 5,
            },
          );
        });

        it('x1, queue {delay: 5} -> {delay: 0}, invoke {delay: 0} -> {delay: 5}', function (done) {
          const calls: number[] = [];

          sut.queueTask(
            function () {
              calls.push(1);

              assert.deepStrictEqual(calls, [2, 1], 'calls');

              assert.areTaskQueuesEmpty();

              done();
            },
            {
              reusable,
              delay: 5,
            },
          );

          sut.queueTask(
            function () {
              calls.push(2);
            },
            {
              reusable,
              delay: 0,
            },
          );
        });

        it('x1, queue {preempt: false} -> {preempt: true}, invoke {preempt: true} -> {preempt: false}', function (done) {
          const calls: number[] = [];

          sut.queueTask(
            function () {
              calls.push(1);

              assert.deepStrictEqual(calls, [2, 1], 'calls');

              assert.areTaskQueuesEmpty();

              done();
            },
            {
              reusable,
              preempt: false,
            },
          );

          sut.queueTask(
            function () {
              calls.push(2);
            },
            {
              reusable,
              preempt: true,
            },
          );
        });

        it('x1, queue {delay: 5} -> {preempt: false} -> {preempt: true}, invoke {preempt: true} -> {preempt: false} -> {delay: 5}', function (done) {
          const calls: number[] = [];

          sut.queueTask(
            function () {
              calls.push(1);

              assert.deepStrictEqual(calls, [3, 2, 1], 'calls');

              assert.areTaskQueuesEmpty();

              done();
            },
            {
              reusable,
              delay: 5,
            },
          );

          sut.queueTask(
            function () {
              calls.push(2);
            },
            {
              reusable,
              preempt: false,
            },
          );

          sut.queueTask(
            function () {
              calls.push(3);
            },
            {
              reusable,
              preempt: true,
            },
          );
        });

        for (const delay of [1, 5]) {
          for (const expected of [2, 4]) {
            it(`${name} x${expected} sequential, {delay: ${delay}}`, function (done) {
              let actual = 0;
              function increment() {
                if (++actual === expected) {
                  assert.areTaskQueuesEmpty();

                  done();
                }
              }
              queueSequential(
                sut,
                {
                  reusable,
                  delay,
                },
                expected,
                increment,
              );
            });

            it(`${name} x${expected} recursive, {delay: ${delay}}`, function (done) {
              let actual = 0;
              function increment() {
                if (++actual === expected) {
                  assert.areTaskQueuesEmpty();

                  done();
                }
              }
              queueRecursive(
                sut,
                {
                  reusable,
                  delay,
                },
                expected,
                increment,
              );
            });
          }
        }
      });

      describe(`can await ${name}`, function () {
        it('x1, {preempt: false, delay: 0}', function () {
          const task = sut.queueTask(
            function () {
              /* */
            },
            {
              reusable,
              preempt: false,
              delay: 0,
            },
          );

          return task.result;
        });

        it('x1, {preempt: true, delay: 0}', function () {
          const task = sut.queueTask(
            function () {
              /* */
            },
            {
              reusable,
              preempt: true,
              delay: 0,
            },
          );

          return task.result;
        });

        it('x1, {delay: 5}', function () {
          const task = sut.queueTask(
            function () {
              /* */
            },
            {
              reusable,
              delay: 5,
            },
          );

          return task.result;
        });

        it('x1, queue {delay: 5} -> {delay: 0}, invoke {delay: 0} -> {delay: 5}', function () {
          const calls: number[] = [];

          const task = sut.queueTask(
            function () {
              calls.push(1);

              assert.deepStrictEqual(calls, [2, 1], 'calls');

              assert.areTaskQueuesEmpty();
            },
            {
              reusable,
              delay: 5,
            },
          );

          sut.queueTask(
            function () {
              calls.push(2);
            },
            {
              reusable,
              delay: 0,
            },
          );

          return task.result;
        });

        it('x1, queue {preempt: false} -> {preempt: true}, invoke {preempt: true} -> {preempt: false}', function () {
          const calls: number[] = [];

          const task = sut.queueTask(
            function () {
              calls.push(1);

              assert.deepStrictEqual(calls, [2, 1], 'calls');

              assert.areTaskQueuesEmpty();
            },
            {
              reusable,
              preempt: false,
            },
          );

          sut.queueTask(
            function () {
              calls.push(2);
            },
            {
              reusable,
              preempt: true,
            },
          );

          return task.result;
        });

        it('x1, queue {delay: 5} -> {preempt: false} -> {preempt: true}, invoke {preempt: true} -> {preempt: false} -> {delay: 5}', function () {
          const calls: number[] = [];

          const task = sut.queueTask(
            function () {
              calls.push(1);

              assert.deepStrictEqual(calls, [3, 2, 1], 'calls');

              assert.areTaskQueuesEmpty();
            },
            {
              reusable,
              delay: 5,
            },
          );

          sut.queueTask(
            function () {
              calls.push(2);
            },
            {
              reusable,
              preempt: false,
            },
          );

          sut.queueTask(
            function () {
              calls.push(3);
            },
            {
              reusable,
              preempt: true,
            },
          );

          return task.result;
        });

        for (const delay of [1, 5]) {
          for (const expected of [2, 4]) {
            it(`x${expected} sequential, {delay: ${delay}}`, async function () {
              let actual = 0;
              queueSequential(
                sut,
                {
                  reusable,
                  delay,
                },
                expected,
                function () {
                  ++actual;
                },
              );

              await sut.yield();

              assert.strictEqual(actual, expected, 'callCount');

              assert.areTaskQueuesEmpty();
            });

            it(`x${expected} recursive, {delay: ${delay}}`, async function () {
              let actual = 0;
              queueRecursive(
                sut,
                {
                  reusable,
                  delay,
                },
                expected,
                function () {
                  ++actual;
                },
              );

              await sut.yield();

              assert.strictEqual(actual, expected, 'callCount');

              assert.areTaskQueuesEmpty();
            });
          }
        }
      });

      describe(`can persist ${name}`, function () {
        for (const iterations of [1, 2, 3]) {
          describe(`runs until canceled after ${iterations} iterations`, function () {
            it(`from within the running task`, function (done) {
              let count = 0;

              const task = sut.queueTask(
                function () {
                  if (++count === iterations) {
                    assert.strictEqual(task.status, 'running', `task.status at count=${count} ${reportTask(task)}`);

                    task.cancel();

                    assert.areTaskQueuesEmpty();
                  }
                },
                {
                  persistent: true,
                  reusable,
                },
              );

              let thenCount = 0;
              function callback() {
                if (++thenCount === iterations) {
                  assert.strictEqual(task.status, 'canceled', `task.status at thenCount=${thenCount} ${reportTask(task)}`);

                  assert.areTaskQueuesEmpty();

                  done();
                } else {
                  assert.strictEqual(task.status, 'pending', `task.status at thenCount=${thenCount} ${reportTask(task)}`);

                  task.result.then(callback).catch((error) => { throw error; });
                }
              }

              task.result.then(callback).catch((error) => { throw error; });
            });

            it(`from within a followup task`, function (done) {
              let count = 0;

              const task = sut.queueTask(
                function () {
                  assert.strictEqual(nextTask.status, 'pending', `nextTask.status in task at count=${count} ${reportTask(nextTask)}`);
                  assert.strictEqual(task.status, 'running', `task.status in task at count=${count} ${reportTask(task)}`);

                  ++count;
                },
                {
                  persistent: true,
                  reusable,
                },
              );

              let nextTask: ITask;
              function createNextTask() {
                return sut.queueTask(
                  function () {
                    assert.strictEqual(nextTask.status, 'running', `nextTask.status in nextTask at count=${count} ${reportTask(nextTask)}`);
                    assert.strictEqual(task.status, 'pending', `task.status in nextTask at count=${count} ${reportTask(task)}`);

                    if (count === iterations) {
                      task.cancel();

                      assert.areTaskQueuesEmpty();
                    } else {
                      nextTask = createNextTask();
                    }
                  },
                  {
                    reusable,
                  },
                );
              }

              nextTask = createNextTask();

              let thenCount = 0;
              function callback() {
                if (++thenCount === iterations) {
                  assert.strictEqual(nextTask.status, 'completed', `nextTask.status at thenCount=${thenCount} ${reportTask(nextTask)}`);
                  assert.strictEqual(task.status, 'canceled', `task.status at thenCount=${thenCount} ${reportTask(task)}`);

                  assert.areTaskQueuesEmpty();

                  done();
                } else {
                  assert.strictEqual(nextTask.status, 'pending', `nextTask.status at thenCount=${thenCount} ${reportTask(nextTask)}`);
                  assert.strictEqual(task.status, 'pending', `task.status at thenCount=${thenCount} ${reportTask(task)}`);

                  nextTask.result.then(callback).catch((error) => { throw error; });
                }
              }

              nextTask.result.then(callback).catch((error) => { throw error; });
            });

            it(`yields after the first iteration with no other tasks`, function (done) {
              let count = 0;
              let yieldCount = 0;

              const task = sut.queueTask(
                function () {
                  assert.strictEqual(task.status, 'running', `task.status at count=${count} ${reportTask(task)}`);

                  assert.strictEqual(++count, yieldCount + 1, '++count === yieldCount + 1');
                },
                {
                  persistent: true,
                  reusable,
                },
              );

              function yieldAndVerify() {
                sut.yield().then(() => {
                  assert.strictEqual(count, ++yieldCount, 'count === ++yieldCount');

                  if (yieldCount < iterations) {
                    yieldAndVerify();
                  } else {
                    task.cancel();

                    assert.areTaskQueuesEmpty();

                    done();
                  }
                }).catch((error) => { throw error; });
              }

              yieldAndVerify();
            });

            it(`yields after the first iteration with several other tasks`, function (done) {
              let count = 0;

              const task = sut.queueTask(
                function () {
                  assert.strictEqual(task.status, 'running', `task.status at count=${count} ${reportTask(task)}`);

                  ++count;
                },
                {
                  persistent: true,
                  reusable,
                },
              );

              let otherCount = 0;
              sut.queueTask(
                function () {
                  ++otherCount;
                },
                {
                  preempt: true,
                  reusable,
                },
              );

              sut.queueTask(
                function () {
                  ++otherCount;
                },
                {
                  preempt: false,
                  reusable,
                },
              );

              sut.queueTask(
                function () {
                  ++otherCount;
                },
                {
                  preempt: true,
                  reusable,
                },
              );

              sut.queueTask(
                function () {
                  ++otherCount;
                },
                {
                  preempt: false,
                  reusable,
                },
              );

              sut.yield().then(() => {
                assert.strictEqual(count, 1, 'count');
                assert.strictEqual(otherCount, 4, 'otherCount');

                task.cancel();

                assert.areTaskQueuesEmpty();

                done();
              }).catch((error) => { throw error; });
            });
          });
        }
      });
    }
  }

  // TODO(fkleuver): we need async tests with suspend: false.
  // This is indirectly tested by various integration tests but we need at least a couple of thorough platform-specific tests as well.
  describe('async', function () {
    for (const reusable of [true, false]) {
      const $reusable = reusable ? 'reusable' : 'non-reusable';

      for (const { sut, name } of prioritySpecs) {
        describe(`can queue ${$reusable} ${name}`, function () {
          it('x1, {preempt: false, delay: 0}', async function () {
            const { promise, resolve } = createExposedPromise();

            sut.queueTask(
              async function () {
                assert.areTaskQueuesEmpty();

                resolve();
              },
              {
                reusable,
                preempt: false,
                delay: 0,
                suspend: true,
              },
            );

            await promise;
          });

          it('x1, {preempt: true, delay: 0}', async function () {
            const { promise, resolve } = createExposedPromise();

            sut.queueTask(
              async function () {
                assert.areTaskQueuesEmpty();

                resolve();
              },
              {
                reusable,
                preempt: true,
                delay: 0,
                suspend: true,
              },
            );

            await promise;
          });

          it('x1, {delay: 5}', async function () {
            const { promise, resolve } = createExposedPromise();

            sut.queueTask(
              async function () {
                assert.areTaskQueuesEmpty();

                resolve();
              },
              {
                reusable,
                delay: 5,
                suspend: true,
              },
            );

            await promise;
          });

          it('x1, queue {delay: 5} -> {delay: 0}, invoke {delay: 0} -> {delay: 5}', async function () {
            const { promise, resolve } = createExposedPromise();

            const calls: number[] = [];

            sut.queueTask(
              async function () {
                calls.push(1);

                assert.deepStrictEqual(calls, [2, 1], 'calls');

                assert.areTaskQueuesEmpty();

                resolve();
              },
              {
                reusable,
                delay: 5,
                suspend: true,
              },
            );

            sut.queueTask(
              async function () {
                calls.push(2);
              },
              {
                reusable,
                delay: 0,
                suspend: true,
              },
            );

            await promise;
          });

          it('x1, queue {preempt: false} -> {preempt: true}, invoke {preempt: true} -> {preempt: false}', async function () {
            const { promise, resolve } = createExposedPromise();

            const calls: number[] = [];

            sut.queueTask(
              async function () {
                calls.push(1);

                assert.deepStrictEqual(calls, [2, 1], 'calls');

                assert.areTaskQueuesEmpty();

                resolve();
              },
              {
                reusable,
                preempt: false,
                suspend: true,
              },
            );

            sut.queueTask(
              async function () {
                calls.push(2);
              },
              {
                reusable,
                preempt: true,
                suspend: true,
              },
            );

            await promise;
          });

          it('x1, queue {delay: 5} -> {preempt: false} -> {preempt: true}, invoke {preempt: true} -> {preempt: false} -> {delay: 5}', async function () {
            const { promise, resolve } = createExposedPromise();

            const calls: number[] = [];

            sut.queueTask(
              async function () {
                calls.push(1);

                assert.deepStrictEqual(calls, [3, 2, 1], 'calls');

                assert.areTaskQueuesEmpty();

                resolve();
              },
              {
                reusable,
                delay: 5,
                suspend: true,
              },
            );

            sut.queueTask(
              async function () {
                calls.push(2);
              },
              {
                reusable,
                preempt: false,
                suspend: true,
              },
            );

            sut.queueTask(
              async function () {
                calls.push(3);
              },
              {
                reusable,
                preempt: true,
                suspend: true,
              },
            );

            await promise;
          });

          for (const delay of [1, 5]) {
            for (const expected of [2, 4]) {
              it(`${name} x${expected} sequential, {delay: ${delay}}`, async function () {
                const { promise, resolve } = createExposedPromise();

                let actual = 0;
                async function increment() {
                  if (++actual === expected) {
                    assert.areTaskQueuesEmpty();

                    resolve();
                  }
                }
                queueSequentialAsync(
                  sut,
                  {
                    reusable,
                    delay,
                    suspend: true,
                  },
                  expected,
                  increment,
                );

                await promise;
              });

              it(`${name} x${expected} recursive, {delay: ${delay}}`, async function () {
                const { promise, resolve } = createExposedPromise();

                let actual = 0;
                async function increment() {
                  if (++actual === expected) {
                    assert.areTaskQueuesEmpty();

                    resolve();
                  }
                }
                queueRecursiveAsync(
                  sut,
                  {
                    reusable,
                    delay,
                    suspend: true,
                  },
                  expected,
                  increment,
                );

                await promise;
              });
            }
          }
        });

        describe(`can await ${$reusable} ${name}`, function () {
          it(`manual 2x recursive`, async function () {
            const opts = {
              reusable,
              preempt: false,
              delay: 0,
              suspend: true,
            };

            let count = 0;

            sut.queueTask(
              async function () {
                await Promise.resolve();
                ++count;

                sut.queueTask(
                  async function () {
                    ++count;
                  },
                  opts,
                );
              },
              opts,
            );

            await sut.yield();

            assert.strictEqual(count, 2);

            assert.areTaskQueuesEmpty();
          });

          it('x1, {preempt: false, delay: 0}', async function () {
            const task = sut.queueTask(
              async function () {
                /* */
              },
              {
                reusable,
                preempt: false,
                delay: 0,
                suspend: true,
              },
            );

            await task.result;
          });

          it('x1, {preempt: true, delay: 0}', async function () {
            const task = sut.queueTask(
              async function () {
                /* */
              },
              {
                reusable,
                preempt: true,
                delay: 0,
                suspend: true,
              },
            );

            await task.result;
          });

          it('x1, {delay: 5}', async function () {
            const task = sut.queueTask(
              async function () {
                /* */
              },
              {
                reusable,
                delay: 5,
                suspend: true,
              },
            );

            await task.result;
          });

          it('x1, queue {delay: 5} -> {delay: 0}, invoke {delay: 0} -> {delay: 5}', async function () {
            const calls: number[] = [];

            const task = sut.queueTask(
              async function () {
                calls.push(1);

                assert.deepStrictEqual(calls, [2, 1], 'calls');

                assert.areTaskQueuesEmpty();
              },
              {
                reusable,
                delay: 5,
                suspend: true,
              },
            );

            sut.queueTask(
              async function () {
                calls.push(2);
              },
              {
                reusable,
                delay: 0,
                suspend: true,
              },
            );

            await task.result;
          });

          it('x1, queue {preempt: false} -> {preempt: true}, invoke {preempt: true} -> {preempt: false}', async function () {
            const calls: number[] = [];

            const task = sut.queueTask(
              async function () {
                calls.push(1);

                assert.deepStrictEqual(calls, [2, 1], 'calls');

                assert.areTaskQueuesEmpty();
              },
              {
                reusable,
                preempt: false,
                suspend: true,
              },
            );

            sut.queueTask(
              async function () {
                calls.push(2);
              },
              {
                reusable,
                preempt: true,
                suspend: true,
              },
            );

            await task.result;
          });

          it('x1, queue {delay: 5} -> {preempt: false} -> {preempt: true}, invoke {preempt: true} -> {preempt: false} -> {delay: 5}', async function () {
            const calls: number[] = [];

            const task = sut.queueTask(
              async function () {
                calls.push(1);

                assert.deepStrictEqual(calls, [3, 2, 1], 'calls');

                assert.areTaskQueuesEmpty();
              },
              {
                reusable,
                delay: 5,
                suspend: true,
              },
            );

            sut.queueTask(
              async function () {
                calls.push(2);
              },
              {
                reusable,
                preempt: false,
                suspend: true,
              },
            );

            sut.queueTask(
              async function () {
                calls.push(3);
              },
              {
                reusable,
                preempt: true,
                suspend: true,
              },
            );

            await task.result;
          });

          for (const delay of [1, 5]) {
            for (const expected of [2, 4]) {
              it(`x${expected} sequential, {delay: ${delay}}`, async function () {
                let actual = 0;
                queueSequentialAsync(
                  sut,
                  {
                    reusable,
                    delay,
                    suspend: true,
                  },
                  expected,
                  async function () {
                    ++actual;
                  },
                );

                await sut.yield();

                assert.strictEqual(actual, expected, 'callCount');

                assert.areTaskQueuesEmpty();
              });

              it(`x${expected} recursive, {delay: ${delay}}`, async function () {
                let actual = 0;
                queueRecursiveAsync(
                  sut,
                  {
                    reusable,
                    delay,
                    suspend: true,
                  },
                  expected,
                  async function () {
                    ++actual;
                  },
                );

                await sut.yield();

                assert.strictEqual(actual, expected, 'callCount');

                assert.areTaskQueuesEmpty();
              });
            }
          }
        });

        describe(`can persist ${$reusable} ${name}`, function () {
          for (const iterations of [1, 2, 3]) {
            describe(`runs until canceled after ${iterations} iterations`, function () {
              it(`from within the running task`, async function () {
                const { promise, resolve } = createExposedPromise();

                let count = 0;

                const task = sut.queueTask(
                  async function () {
                    if (++count === iterations) {
                      assert.strictEqual(task.status, 'running', `task.status at count=${count} ${reportTask(task)}`);

                      task.cancel();

                      assert.areTaskQueuesEmpty();
                    }
                  },
                  {
                    persistent: true,
                    reusable,
                    suspend: true,
                  },
                );

                let thenCount = 0;
                async function callback() {
                  if (++thenCount === iterations) {
                    assert.strictEqual(task.status, 'canceled', `task.status at thenCount=${thenCount} ${reportTask(task)}`);

                    assert.areTaskQueuesEmpty();

                    resolve();
                  } else {
                    assert.strictEqual(task.status, 'pending', `task.status at thenCount=${thenCount} ${reportTask(task)}`);

                    await task.result;
                    await callback();
                  }
                }

                await task.result;
                await callback();
                await promise;
              });

              it(`from within a followup task`, async function () {
                const { promise, resolve } = createExposedPromise();

                let count = 0;

                const task = sut.queueTask(
                  async function () {
                    assert.strictEqual(nextTask.status, 'pending', `nextTask.status in task at count=${count} ${reportTask(nextTask)}`);
                    assert.strictEqual(task.status, 'running', `task.status in task at count=${count} ${reportTask(task)}`);

                    ++count;
                  },
                  {
                    persistent: true,
                    reusable,
                    suspend: true,
                  },
                );

                let nextTask: ITask;
                function createNextTask() {
                  return sut.queueTask(
                    async function () {
                      assert.strictEqual(nextTask.status, 'running', `nextTask.status in nextTask at count=${count} ${reportTask(nextTask)}`);
                      assert.strictEqual(task.status, 'pending', `task.status in nextTask at count=${count} ${reportTask(task)}`);

                      if (count === iterations) {
                        task.cancel();

                        assert.areTaskQueuesEmpty();
                      } else {
                        nextTask = createNextTask();
                      }
                    },
                    {
                      reusable,
                      suspend: true,
                    },
                  );
                }

                nextTask = createNextTask();

                let thenCount = 0;
                async function callback() {
                  if (++thenCount === iterations) {
                    assert.strictEqual(nextTask.status, 'completed', `nextTask.status at thenCount=${thenCount} ${reportTask(nextTask)}`);
                    assert.strictEqual(task.status, 'canceled', `task.status at thenCount=${thenCount} ${reportTask(task)}`);

                    assert.areTaskQueuesEmpty();

                    resolve();
                  } else {
                    assert.strictEqual(nextTask.status, 'pending', `nextTask.status at thenCount=${thenCount} ${reportTask(nextTask)}`);
                    assert.strictEqual(task.status, 'pending', `task.status at thenCount=${thenCount} ${reportTask(task)}`);

                    await nextTask.result;

                    // eslint-disable-next-line @typescript-eslint/no-floating-promises
                    callback();
                  }
                }

                await task.result;
                assert.strictEqual(nextTask.status, 'pending', `nextTask.status after awaiting task.result at thenCount=${thenCount} ${reportTask(nextTask)}`);
                assert.strictEqual(task.status, 'pending', `task.status after awaiting task.result at thenCount=${thenCount} ${reportTask(task)}`);

                await nextTask.result;

                await callback();

                await promise;
              });

              it(`yields after the first iteration with no other tasks`, async function () {
                const { promise, resolve } = createExposedPromise();

                let count = 0;
                let yieldCount = 0;

                const task = sut.queueTask(
                  async function () {
                    assert.strictEqual(task.status, 'running', `task.status at count=${count} ${reportTask(task)}`);
                    assert.strictEqual(++count, yieldCount + 1, '++count === yieldCount + 1');
                  },
                  {
                    persistent: true,
                    reusable,
                    suspend: true,
                  },
                );

                async function yieldAndVerify() {
                  await sut.yield();

                  assert.strictEqual(count, ++yieldCount, 'count === ++yieldCount');

                  if (yieldCount < iterations) {
                    // eslint-disable-next-line @typescript-eslint/no-floating-promises
                    yieldAndVerify();
                  } else {
                    task.cancel();

                    assert.areTaskQueuesEmpty();

                    resolve();
                  }
                }

                await yieldAndVerify();
                await promise;
              });

              it(`yields after the first iteration with several other tasks`, async function () {
                let count = 0;

                const task = sut.queueTask(
                  async function () {
                    assert.strictEqual(task.status, 'running', `task.status at count=${count} ${reportTask(task)}`);

                    ++count;
                  },
                  {
                    persistent: true,
                    reusable,
                    suspend: true,
                  },
                );

                let otherCount = 0;
                sut.queueTask(
                  async function () {
                    ++otherCount;
                  },
                  {
                    preempt: true,
                    reusable,
                    suspend: true,
                  },
                );

                sut.queueTask(
                  async function () {
                    ++otherCount;
                  },
                  {
                    preempt: false,
                    reusable,
                    suspend: true,
                  },
                );

                sut.queueTask(
                  async function () {
                    ++otherCount;
                  },
                  {
                    preempt: true,
                    reusable,
                    suspend: true,
                  },
                );

                sut.queueTask(
                  async function () {
                    ++otherCount;
                  },
                  {
                    preempt: false,
                    reusable,
                    suspend: true,
                  },
                );

                await sut.yield();

                assert.strictEqual(count, 1, 'count');
                assert.strictEqual(otherCount, 4, 'otherCount');

                task.cancel();

                assert.areTaskQueuesEmpty();
              });
            });
          }

          it(`yields after the first iteration with no other tasks, after finishing a persistent task that was canceled from within a followup task`, async function () {
            const primerTask = sut.queueTask(
              async function () {
                assert.strictEqual(primerTask.status, 'running', `primerTask.status in primerTask ${reportTask(primerTask)}`);
                assert.strictEqual(primerCancelTask.status, 'pending', `primerCancelTask.status in primerTask ${reportTask(primerCancelTask)}`);
              },
              {
                persistent: true,
                reusable,
                suspend: true,
              },
            );

            const primerCancelTask = sut.queueTask(
              async function () {
                assert.strictEqual(primerTask.status, 'pending', `primerTask.status in primerCancelTask ${reportTask(primerTask)}`);
                assert.strictEqual(primerCancelTask.status, 'running', `primerCancelTask.status in primerCancelTask ${reportTask(primerCancelTask)}`);

                primerTask.cancel();

                assert.areTaskQueuesEmpty();
              },
              {
                reusable,
                suspend: true,
              },
            );

            await primerTask.result;
            assert.strictEqual(primerTask.status, 'pending', `primerTask.status after awaiting primerTask.result ${reportTask(primerTask)}`);
            assert.strictEqual(primerCancelTask.status, 'pending', `primerCancelTask.status after awaiting primerTask.result ${reportTask(primerCancelTask)}`);

            await primerCancelTask.result;
            assert.strictEqual(primerTask.status, 'canceled', `primerTask.status after awaiting primerCancelTask.result ${reportTask(primerTask)}`);
            assert.strictEqual(primerCancelTask.status, 'completed', `primerCancelTask.status after awaiting primerCancelTask.result ${reportTask(primerCancelTask)}`);

            assert.areTaskQueuesEmpty();

            let count = 0;
            let yieldCount = 0;

            const persistentTask = sut.queueTask(
              async function () {
                assert.strictEqual(persistentTask.status, 'running', `persistentTask.status in persistentTask ${reportTask(persistentTask)}`);
                assert.strictEqual(++count, yieldCount + 1, `++count (${count}) === yieldCount + 1 (${yieldCount + 1}) in persistentTask`);
              },
              {
                persistent: true,
                reusable,
                suspend: true,
              },
            );

            await sut.yield();

            assert.strictEqual(count, ++yieldCount, `count (${count}) === ++yieldCount (${yieldCount}) after awaiting sut.yield()`);

            persistentTask.cancel();

            assert.areTaskQueuesEmpty();
          });
        });
      }
    }

    const enum TaskState {
      NotStarted = 0,
      Started = 1,
      Finished = 2,
    }
    it(`awaits the first task before starting the second`, async function () {
      const states = [
        TaskState.NotStarted,
        TaskState.NotStarted,
        TaskState.NotStarted,
      ];

      async function callback0() {
        states[0] = TaskState.Started;
        assert.deepStrictEqual(states, [TaskState.Started, 0, 0], `state at the start of callback0`);
        await new Promise(resolve => setTimeout(resolve, 50));
        states[0] = TaskState.Finished;
        assert.deepStrictEqual(states, [TaskState.Finished, 0, 0], `state at the end of callback0`);
      }

      async function callback1() {
        states[1] = TaskState.Started;
        assert.deepStrictEqual(states, [TaskState.Finished, TaskState.Started, 0], `state at the start of callback1`);
        await new Promise(resolve => setTimeout(resolve, 50));
        states[1] = TaskState.Finished;
        assert.deepStrictEqual(states, [TaskState.Finished, TaskState.Finished, 0], `state at the end of callback1`);
      }

      async function callback2() {
        states[2] = TaskState.Started;
        assert.deepStrictEqual(states, [TaskState.Finished, TaskState.Finished, TaskState.Started], `state at the start of callback2`);
        await new Promise(resolve => setTimeout(resolve, 50));
        states[2] = TaskState.Finished;
        assert.deepStrictEqual(states, [TaskState.Finished, TaskState.Finished, TaskState.Finished], `state at the end of callback2`);
      }

      const opts: QueueTaskOptions = {
        suspend: true,
      };

      const task0 = platform.taskQueue.queueTask(callback0, opts);
      const task1 = platform.taskQueue.queueTask(callback1, opts);
      const task2 = platform.taskQueue.queueTask(callback2, opts);

      assert.deepStrictEqual(states, [TaskState.NotStarted, TaskState.NotStarted, TaskState.NotStarted], `state after queueing 3 tasks`);

      await task0.result;

      // Note: the assertion pattern here is to verify that the next task is started on the next 'cycle' rather than immediately after the previous task finished.
      // If we were to verify the opposite, the expected state would be [Finished, Started, NotStarted] instead of [Finished, NotStarted, NotStarted].
      assert.deepStrictEqual(states, [TaskState.Finished, TaskState.NotStarted, TaskState.NotStarted], `state after awaiting task0`);

      await task1.result;

      assert.deepStrictEqual(states, [TaskState.Finished, TaskState.Finished, TaskState.NotStarted], `state after awaiting task1`);

      await task2.result;

      assert.deepStrictEqual(states, [TaskState.Finished, TaskState.Finished, TaskState.Finished], `state after awaiting task2`);

      assert.areTaskQueuesEmpty();
    });
  });
});