/* Any copyright is dedicated to the Public Domain.
   http://creativecommons.org/publicdomain/zero/1.0/ */

/**
 * This file tests the Task.jsm module.
 */

////////////////////////////////////////////////////////////////////////////////
/// Globals

ChromeUtils.defineModuleGetter(this, "Task",
                               "resource://testing-common/Task.jsm");

/**
 * Returns a promise that will be resolved with the given value, when an event
 * posted on the event loop of the main thread is processed.
 */
function promiseResolvedLater(aValue) {
  return new Promise(resolve => {
    Services.tm.dispatchToMainThread(() => resolve(aValue));
  });
}

////////////////////////////////////////////////////////////////////////////////
/// Tests

function run_test()
{
  run_next_test();
}

add_test(function test_spawn_primitive()
{
  function fibonacci(n) {
    return n < 2 ? n : fibonacci(n - 1) + fibonacci(n - 2);
  };

  // Polymorphism between task and non-task functions (see "test_recursion").
  Task.spawn(fibonacci(6)).then(function (result) {
    Assert.equal(8, result);
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_spawn_function()
{
  Task.spawn(function () {
    return "This is not a generator.";
  }).then(function (result) {
    Assert.equal("This is not a generator.", result);
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_spawn_function_this()
{
  Task.spawn(function () {
    return this;
  }).then(function (result) {
    // Since the task function wasn't defined in strict mode, its "this" object
    // should be the same as the "this" object in this function, i.e. the global
    // object.
    Assert.equal(result, this);
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_spawn_function_this_strict()
{
  "use strict";
  Task.spawn(function () {
    return this;
  }).then(function (result) {
    // Since the task function was defined in strict mode, its "this" object
    // should be undefined.
    Assert.equal(typeof(result), "undefined");
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_spawn_function_returning_promise()
{
  Task.spawn(function () {
    return promiseResolvedLater("Resolution value.");
  }).then(function (result) {
    Assert.equal("Resolution value.", result);
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_spawn_function_exceptions()
{
  Task.spawn(function () {
    throw new Error("Exception uncaught by task.");
  }).then(function (result) {
    do_throw("Unexpected success!");
  }, function (ex) {
    Assert.equal("Exception uncaught by task.", ex.message);
    run_next_test();
  });
});

add_test(function test_spawn_function_taskresult()
{
  Task.spawn(function () {
    throw new Task.Result("Task result");
  }).then(function (result) {
    Assert.equal("Task result", result);
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_yielded_undefined()
{
  Task.spawn(function* () {
    yield;
    return "We continued correctly.";
  }).then(function (result) {
    Assert.equal("We continued correctly.", result);
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_yielded_primitive()
{
  Task.spawn(function* () {
    return "Primitive " + (yield "value.");
  }).then(function (result) {
    Assert.equal("Primitive value.", result);
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_star_normal()
{
  Task.spawn(function* () {
    let result = yield Promise.resolve("Value");
    for (let i = 0; i < 3; i++) {
      result += yield promiseResolvedLater("!");
    }
    return "Task result: " + result;
  }).then(function (result) {
    Assert.equal("Task result: Value!!!", result);
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_star_exceptions()
{
  Task.spawn(function* () {
    try {
      yield Promise.reject("Rejection result by promise.");
      do_throw("Exception expected because the promise was rejected.");
    } catch (ex) {
      // We catch this exception now, we will throw a different one later.
      Assert.equal("Rejection result by promise.", ex);
    }
    throw new Error("Exception uncaught by task.");
  }).then(function (result) {
    do_throw("Unexpected success!");
  }, function (ex) {
    Assert.equal("Exception uncaught by task.", ex.message);
    run_next_test();
  });
});

add_test(function test_star_recursion()
{
  function* task_fibonacci(n) {
    return n < 2 ? n : (yield task_fibonacci(n - 1)) +
                       (yield task_fibonacci(n - 2));
  };

  Task.spawn(task_fibonacci(6)).then(function (result) {
    Assert.equal(8, result);
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_nested_star()
{
  Task.spawn(function* () {
    return yield (function* () {
      return yield 5;
    })();
  }).then(function (result) {
    Assert.equal(5, result);
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_async_function_from_generator()
{
  Task.spawn(function* () {
    let object = {
      asyncFunction: Task.async(function* (param) {
        Assert.equal(this, object);
        return param;
      })
    };

    // Ensure the async function returns a promise that resolves as expected.
    Assert.equal((yield object.asyncFunction(1)), 1);

    // Ensure a second call to the async function also returns such a promise.
    Assert.equal((yield object.asyncFunction(3)), 3);
  }).then(function () {
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_async_function_from_function()
{
  Task.spawn(function* () {
    return Task.spawn(function* () {
      let object = {
        asyncFunction: Task.async(function (param) {
          Assert.equal(this, object);
          return param;
        })
      };

      // Ensure the async function returns a promise that resolves as expected.
      Assert.equal((yield object.asyncFunction(5)), 5);

      // Ensure a second call to the async function also returns such a promise.
      Assert.equal((yield object.asyncFunction(7)), 7);
    });
  }).then(function () {
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_async_function_that_throws_rejects_promise()
{
  Task.spawn(function* () {
    let object = {
      asyncFunction: Task.async(function* () {
        throw "Rejected!";
      })
    };

    yield object.asyncFunction();
  }).then(function () {
    do_throw("unexpected success calling async function that throws error");
  }, function (ex) {
    Assert.equal(ex, "Rejected!");
    run_next_test();
  });
});

add_test(function test_async_return_function()
{
  Task.spawn(function* () {
    // Ensure an async function that returns a function resolves to the function
    // itself instead of calling the function and resolving to its return value.
    return Task.spawn(function* () {
      let returnValue = function () {
        return "These aren't the droids you're looking for.";
      };

      let asyncFunction = Task.async(function () {
        return returnValue;
      });

      Assert.equal((yield asyncFunction()), returnValue);
    });
  }).then(function () {
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_async_throw_argument_not_function()
{
  Task.spawn(function* () {
    // Ensure Task.async throws if its aTask argument is not a function.
    Assert.throws(() => Task.async("not a function"),
                  /aTask argument must be a function/);
  }).then(function () {
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});

add_test(function test_async_throw_on_function_in_place_of_promise()
{
  Task.spawn(function* () {
    // Ensure Task.spawn throws if passed an async function.
    Assert.throws(() => Task.spawn(Task.async(function* () {})),
                  /Cannot use an async function in place of a promise/);
  }).then(function () {
    run_next_test();
  }, function (ex) {
    do_throw("Unexpected error: " + ex);
  });
});


////////////////// Test rewriting of stack traces

// Backup Task.Debuggin.maintainStack.
// Will be restored by `exit_stack_tests`.
var maintainStack;
add_test(function enter_stack_tests() {
  maintainStack = Task.Debugging.maintainStack;
  Task.Debugging.maintainStack = true;
  run_next_test();
});


/**
 * Ensure that a list of frames appear in a stack, in the right order
 */
function do_check_rewritten_stack(frames, ex) {
  info("Checking that the expected frames appear in the right order");
  info(frames.join(", "));
  let stack = ex.stack;
  info(stack);

  let framesFound = 0;
  let lineNumber = 0;
  let reLine = /([^\r\n])+/g;
  let match;
  while (framesFound < frames.length && (match = reLine.exec(stack))) {
    let line = match[0];
    let frame = frames[framesFound];
    info("Searching for " + frame + " in line " + line);
    if (line.includes(frame)) {
      info("Found " + frame);
      ++framesFound;
    } else {
      info("Didn't find " + frame);
    }
  }

  if (framesFound >= frames.length) {
    return;
  }
  do_throw("Did not find: " + frames.slice(framesFound).join(", ") +
           " in " + stack.substr(reLine.lastIndex));

  info("Ensuring that we have removed Task.jsm, Promise.jsm");
  Assert.ok(!stack.includes("Task.jsm"));
  Assert.ok(!stack.includes("Promise.jsm"));
  Assert.ok(!stack.includes("Promise-backend.js"));
}


// Test that we get an acceptable rewritten stack when we launch
// an error in a Task.spawn.
add_test(function test_spawn_throw_stack() {
  Task.spawn(function* task_spawn_throw_stack() {
    for (let i = 0; i < 5; ++i) {
      yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
    }
    throw new Error("BOOM");
  }).then(do_throw, function(ex) {
    do_check_rewritten_stack(["task_spawn_throw_stack",
                              "test_spawn_throw_stack"],
                             ex);
    run_next_test();
  });
});

// Test that we get an acceptable rewritten stack when we yield
// a rejection in a Task.spawn.
add_test(function test_spawn_yield_reject_stack() {
  Task.spawn(function* task_spawn_yield_reject_stack() {
    for (let i = 0; i < 5; ++i) {
      yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
    }
    yield Promise.reject(new Error("BOOM"));
  }).then(do_throw, function(ex) {
    do_check_rewritten_stack(["task_spawn_yield_reject_stack",
                              "test_spawn_yield_reject_stack"],
                              ex);
    run_next_test();
  });
});

// Test that we get an acceptable rewritten stack when we launch
// an error in a Task.async function.
add_test(function test_async_function_throw_stack() {
  let task_async_function_throw_stack = Task.async(function*() {
    for (let i = 0; i < 5; ++i) {
      yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
    }
    throw new Error("BOOM");
  })().then(do_throw, function(ex) {
    do_check_rewritten_stack(["task_async_function_throw_stack",
                              "test_async_function_throw_stack"],
                             ex);
    run_next_test();
  });
});

// Test that we get an acceptable rewritten stack when we launch
// an error in a Task.async function.
add_test(function test_async_function_yield_reject_stack() {
  let task_async_function_yield_reject_stack = Task.async(function*() {
    for (let i = 0; i < 5; ++i) {
      yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
    }
    yield Promise.reject(new Error("BOOM"));
  })().then(do_throw, function(ex) {
    do_check_rewritten_stack(["task_async_function_yield_reject_stack",
                              "test_async_function_yield_reject_stack"],
                              ex);
    run_next_test();
  });
});

// Test that we get an acceptable rewritten stack when we launch
// an error in a Task.async function.
add_test(function test_async_method_throw_stack() {
  let object = {
   task_async_method_throw_stack: Task.async(function*() {
    for (let i = 0; i < 5; ++i) {
      yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
    }
    throw new Error("BOOM");
   })
  };
  object.task_async_method_throw_stack().then(do_throw, function(ex) {
    do_check_rewritten_stack(["task_async_method_throw_stack",
                              "test_async_method_throw_stack"],
                             ex);
    run_next_test();
  });
});

// Test that we get an acceptable rewritten stack when we launch
// an error in a Task.async function.
add_test(function test_async_method_yield_reject_stack() {
  let object = {
    task_async_method_yield_reject_stack: Task.async(function*() {
      for (let i = 0; i < 5; ++i) {
        yield Promise.resolve(); // Without stack rewrite, this would lose valuable information
      }
      yield Promise.reject(new Error("BOOM"));
    })
  };
  object.task_async_method_yield_reject_stack().then(do_throw, function(ex) {
    do_check_rewritten_stack(["task_async_method_yield_reject_stack",
                              "test_async_method_yield_reject_stack"],
                              ex);
    run_next_test();
  });
});

// Test that two tasks whose execution takes place interleaved do not capture each other's stack.
add_test(function test_throw_stack_do_not_capture_the_wrong_task() {
  for (let iter_a of [3, 4, 5]) { // Vary the interleaving
    for (let iter_b of [3, 4, 5]) {
      Task.spawn(function* task_a() {
        for (let i = 0; i < iter_a; ++i) {
          yield Promise.resolve();
        }
        throw new Error("BOOM");
      }).then(do_throw, function(ex) {
        do_check_rewritten_stack(["task_a",
                                  "test_throw_stack_do_not_capture_the_wrong_task"],
                                  ex);
        Assert.ok(!ex.stack.includes("task_b"));
        run_next_test();
      });
      Task.spawn(function* task_b() {
        for (let i = 0; i < iter_b; ++i) {
          yield Promise.resolve();
        }
      });
    }
  }
});

// Put things together
add_test(function test_throw_complex_stack()
{
  // Setup the following stack:
  //    inner_method()
  //    task_3()
  //    task_2()
  //    task_1()
  //    function_3()
  //    function_2()
  //    function_1()
  //    test_throw_complex_stack()
  (function function_1() {
    return (function function_2() {
      return (function function_3() {
        return Task.spawn(function* task_1() {
          yield Promise.resolve();
          try {
            yield Task.spawn(function* task_2() {
              yield Promise.resolve();
              yield Task.spawn(function* task_3() {
                yield Promise.resolve();
                  let inner_object = {
                    inner_method: Task.async(function*() {
                      throw new Error("BOOM");
                    })
                  };
                  yield Promise.resolve();
                  yield inner_object.inner_method();
                });
              });
            } catch (ex) {
              yield Promise.resolve();
              throw ex;
            }
          });
        })();
      })();
  })().then(
    () => do_throw("Shouldn't have succeeded"),
    (ex) => {
      let expect = ["inner_method",
        "task_3",
        "task_2",
        "task_1",
        "function_3",
        "function_2",
        "function_1",
        "test_throw_complex_stack"];
      do_check_rewritten_stack(expect, ex);

      run_next_test();
    });
});

add_test(function test_without_maintainStack() {
  info("Calling generateReadableStack without a Task");
  Task.Debugging.generateReadableStack(new Error("Not a real error"));

  Task.Debugging.maintainStack = false;

  info("Calling generateReadableStack with neither a Task nor maintainStack");
  Task.Debugging.generateReadableStack(new Error("Not a real error"));

  info("Calling generateReadableStack without maintainStack");
  Task.spawn(function*() {
    Task.Debugging.generateReadableStack(new Error("Not a real error"));
    run_next_test();
  });
});

add_test(function exit_stack_tests() {
  Task.Debugging.maintainStack = maintainStack;
  run_next_test();
});
