// Copyright 2014 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include "base/task/cancelable_task_tracker.h"

#include <cstddef>

#include "base/bind.h"
#include "base/bind_helpers.h"
#include "base/location.h"
#include "base/logging.h"
#include "base/memory/ref_counted.h"
#include "base/memory/weak_ptr.h"
#include "base/message_loop/message_loop.h"
#include "base/run_loop.h"
#include "base/single_thread_task_runner.h"
#include "base/test/gtest_util.h"
#include "base/test/test_simple_task_runner.h"
#include "base/threading/thread.h"
#include "testing/gtest/include/gtest/gtest.h"

namespace base {

namespace {

class CancelableTaskTrackerTest : public testing::Test {
 protected:
  ~CancelableTaskTrackerTest() override { RunCurrentLoopUntilIdle(); }

  void RunCurrentLoopUntilIdle() {
    RunLoop run_loop;
    run_loop.RunUntilIdle();
  }

  CancelableTaskTracker task_tracker_;

 private:
  // Needed by CancelableTaskTracker methods.
  MessageLoop message_loop_;
};

void AddFailureAt(const Location& location) {
  ADD_FAILURE_AT(location.file_name(), location.line_number());
}

// Returns a closure that fails if run.
Closure MakeExpectedNotRunClosure(const Location& location) {
  return Bind(&AddFailureAt, location);
}

// A helper class for MakeExpectedRunClosure() that fails if it is
// destroyed without Run() having been called.  This class may be used
// from multiple threads as long as Run() is called at most once
// before destruction.
class RunChecker {
 public:
  explicit RunChecker(const Location& location)
      : location_(location), called_(false) {}

  ~RunChecker() {
    if (!called_) {
      ADD_FAILURE_AT(location_.file_name(), location_.line_number());
    }
  }

  void Run() { called_ = true; }

 private:
  Location location_;
  bool called_;
};

// Returns a closure that fails on destruction if it hasn't been run.
Closure MakeExpectedRunClosure(const Location& location) {
  return Bind(&RunChecker::Run, Owned(new RunChecker(location)));
}

}  // namespace

// With the task tracker, post a task, a task with a reply, and get a
// new task id without canceling any of them.  The tasks and the reply
// should run and the "is canceled" callback should return false.
TEST_F(CancelableTaskTrackerTest, NoCancel) {
  Thread worker_thread("worker thread");
  ASSERT_TRUE(worker_thread.Start());

  ignore_result(task_tracker_.PostTask(worker_thread.task_runner().get(),
                                       FROM_HERE,
                                       MakeExpectedRunClosure(FROM_HERE)));

  ignore_result(task_tracker_.PostTaskAndReply(
      worker_thread.task_runner().get(), FROM_HERE,
      MakeExpectedRunClosure(FROM_HERE), MakeExpectedRunClosure(FROM_HERE)));

  CancelableTaskTracker::IsCanceledCallback is_canceled;
  ignore_result(task_tracker_.NewTrackedTaskId(&is_canceled));

  worker_thread.Stop();

  RunCurrentLoopUntilIdle();

  EXPECT_FALSE(is_canceled.Run());
}

// Post a task with the task tracker but cancel it before running the
// task runner.  The task should not run.
TEST_F(CancelableTaskTrackerTest, CancelPostedTask) {
  scoped_refptr<TestSimpleTaskRunner> test_task_runner(
      new TestSimpleTaskRunner());

  CancelableTaskTracker::TaskId task_id = task_tracker_.PostTask(
      test_task_runner.get(), FROM_HERE, MakeExpectedNotRunClosure(FROM_HERE));
  EXPECT_NE(CancelableTaskTracker::kBadTaskId, task_id);

  EXPECT_EQ(1U, test_task_runner->NumPendingTasks());

  task_tracker_.TryCancel(task_id);

  test_task_runner->RunUntilIdle();
}

// Post a task with reply with the task tracker and cancel it before
// running the task runner.  Neither the task nor the reply should
// run.
TEST_F(CancelableTaskTrackerTest, CancelPostedTaskAndReply) {
  scoped_refptr<TestSimpleTaskRunner> test_task_runner(
      new TestSimpleTaskRunner());

  CancelableTaskTracker::TaskId task_id =
      task_tracker_.PostTaskAndReply(test_task_runner.get(),
                                     FROM_HERE,
                                     MakeExpectedNotRunClosure(FROM_HERE),
                                     MakeExpectedNotRunClosure(FROM_HERE));
  EXPECT_NE(CancelableTaskTracker::kBadTaskId, task_id);

  task_tracker_.TryCancel(task_id);

  test_task_runner->RunUntilIdle();
}

// Post a task with reply with the task tracker and cancel it after
// running the task runner but before running the current message
// loop.  The task should run but the reply should not.
TEST_F(CancelableTaskTrackerTest, CancelReply) {
  scoped_refptr<TestSimpleTaskRunner> test_task_runner(
      new TestSimpleTaskRunner());

  CancelableTaskTracker::TaskId task_id =
      task_tracker_.PostTaskAndReply(test_task_runner.get(),
                                     FROM_HERE,
                                     MakeExpectedRunClosure(FROM_HERE),
                                     MakeExpectedNotRunClosure(FROM_HERE));
  EXPECT_NE(CancelableTaskTracker::kBadTaskId, task_id);

  test_task_runner->RunUntilIdle();

  task_tracker_.TryCancel(task_id);
}

// Post a task with reply with the task tracker on a worker thread and
// cancel it before running the current message loop.  The task should
// run but the reply should not.
TEST_F(CancelableTaskTrackerTest, CancelReplyDifferentThread) {
  Thread worker_thread("worker thread");
  ASSERT_TRUE(worker_thread.Start());

  CancelableTaskTracker::TaskId task_id = task_tracker_.PostTaskAndReply(
      worker_thread.task_runner().get(), FROM_HERE, DoNothing(),
      MakeExpectedNotRunClosure(FROM_HERE));
  EXPECT_NE(CancelableTaskTracker::kBadTaskId, task_id);

  task_tracker_.TryCancel(task_id);

  worker_thread.Stop();
}

void ExpectIsCanceled(
    const CancelableTaskTracker::IsCanceledCallback& is_canceled,
    bool expected_is_canceled) {
  EXPECT_EQ(expected_is_canceled, is_canceled.Run());
}

// Create a new task ID and check its status on a separate thread
// before and after canceling.  The is-canceled callback should be
// thread-safe (i.e., nothing should blow up).
TEST_F(CancelableTaskTrackerTest, NewTrackedTaskIdDifferentThread) {
  CancelableTaskTracker::IsCanceledCallback is_canceled;
  CancelableTaskTracker::TaskId task_id =
      task_tracker_.NewTrackedTaskId(&is_canceled);

  EXPECT_FALSE(is_canceled.Run());

  Thread other_thread("other thread");
  ASSERT_TRUE(other_thread.Start());
  other_thread.task_runner()->PostTask(
      FROM_HERE, BindOnce(&ExpectIsCanceled, is_canceled, false));
  other_thread.Stop();

  task_tracker_.TryCancel(task_id);

  ASSERT_TRUE(other_thread.Start());
  other_thread.task_runner()->PostTask(
      FROM_HERE, BindOnce(&ExpectIsCanceled, is_canceled, true));
  other_thread.Stop();
}

// With the task tracker, post a task, a task with a reply, get a new
// task id, and then cancel all of them.  None of the tasks nor the
// reply should run and the "is canceled" callback should return
// true.
TEST_F(CancelableTaskTrackerTest, CancelAll) {
  scoped_refptr<TestSimpleTaskRunner> test_task_runner(
      new TestSimpleTaskRunner());

  ignore_result(task_tracker_.PostTask(
      test_task_runner.get(), FROM_HERE, MakeExpectedNotRunClosure(FROM_HERE)));

  ignore_result(
      task_tracker_.PostTaskAndReply(test_task_runner.get(),
                                     FROM_HERE,
                                     MakeExpectedNotRunClosure(FROM_HERE),
                                     MakeExpectedNotRunClosure(FROM_HERE)));

  CancelableTaskTracker::IsCanceledCallback is_canceled;
  ignore_result(task_tracker_.NewTrackedTaskId(&is_canceled));

  task_tracker_.TryCancelAll();

  test_task_runner->RunUntilIdle();

  RunCurrentLoopUntilIdle();

  EXPECT_TRUE(is_canceled.Run());
}

// With the task tracker, post a task, a task with a reply, get a new
// task id, and then cancel all of them.  None of the tasks nor the
// reply should run and the "is canceled" callback should return
// true.
TEST_F(CancelableTaskTrackerTest, DestructionCancelsAll) {
  scoped_refptr<TestSimpleTaskRunner> test_task_runner(
      new TestSimpleTaskRunner());

  CancelableTaskTracker::IsCanceledCallback is_canceled;

  {
    // Create another task tracker with a smaller scope.
    CancelableTaskTracker task_tracker;

    ignore_result(task_tracker.PostTask(test_task_runner.get(),
                                        FROM_HERE,
                                        MakeExpectedNotRunClosure(FROM_HERE)));

    ignore_result(
        task_tracker.PostTaskAndReply(test_task_runner.get(),
                                      FROM_HERE,
                                      MakeExpectedNotRunClosure(FROM_HERE),
                                      MakeExpectedNotRunClosure(FROM_HERE)));

    ignore_result(task_tracker_.NewTrackedTaskId(&is_canceled));
  }

  test_task_runner->RunUntilIdle();

  RunCurrentLoopUntilIdle();

  EXPECT_FALSE(is_canceled.Run());
}

// Post a task and cancel it. HasTrackedTasks() should return false as soon as
// TryCancelAll() is called.
TEST_F(CancelableTaskTrackerTest, HasTrackedTasksPost) {
  scoped_refptr<TestSimpleTaskRunner> test_task_runner(
      new TestSimpleTaskRunner());

  EXPECT_FALSE(task_tracker_.HasTrackedTasks());

  ignore_result(task_tracker_.PostTask(
      test_task_runner.get(), FROM_HERE, MakeExpectedNotRunClosure(FROM_HERE)));

  task_tracker_.TryCancelAll();

  EXPECT_FALSE(task_tracker_.HasTrackedTasks());

  test_task_runner->RunUntilIdle();
  RunCurrentLoopUntilIdle();
}

// Post a task with a reply and cancel it. HasTrackedTasks() should return false
// as soon as TryCancelAll() is called.
TEST_F(CancelableTaskTrackerTest, HasTrackedTasksPostWithReply) {
  scoped_refptr<TestSimpleTaskRunner> test_task_runner(
      new TestSimpleTaskRunner());

  EXPECT_FALSE(task_tracker_.HasTrackedTasks());

  ignore_result(
      task_tracker_.PostTaskAndReply(test_task_runner.get(),
                                     FROM_HERE,
                                     MakeExpectedNotRunClosure(FROM_HERE),
                                     MakeExpectedNotRunClosure(FROM_HERE)));

  task_tracker_.TryCancelAll();

  EXPECT_FALSE(task_tracker_.HasTrackedTasks());

  test_task_runner->RunUntilIdle();
  RunCurrentLoopUntilIdle();
}

// Create a new tracked task ID. HasTrackedTasks() should return false as soon
// as TryCancelAll() is called.
TEST_F(CancelableTaskTrackerTest, HasTrackedTasksIsCancelled) {
  EXPECT_FALSE(task_tracker_.HasTrackedTasks());

  CancelableTaskTracker::IsCanceledCallback is_canceled;
  ignore_result(task_tracker_.NewTrackedTaskId(&is_canceled));

  task_tracker_.TryCancelAll();

  EXPECT_FALSE(task_tracker_.HasTrackedTasks());
}

// The death tests below make sure that calling task tracker member
// functions from a thread different from its owner thread DCHECKs in
// debug mode.

class CancelableTaskTrackerDeathTest : public CancelableTaskTrackerTest {
 protected:
  CancelableTaskTrackerDeathTest() {
    // The default style "fast" does not support multi-threaded tests.
    ::testing::FLAGS_gtest_death_test_style = "threadsafe";
  }
};

// Runs |fn| with |task_tracker|, expecting it to crash in debug mode.
void MaybeRunDeadlyTaskTrackerMemberFunction(
    CancelableTaskTracker* task_tracker,
    const Callback<void(CancelableTaskTracker*)>& fn) {
  EXPECT_DCHECK_DEATH(fn.Run(task_tracker));
}

void PostDoNothingTask(CancelableTaskTracker* task_tracker) {
  ignore_result(task_tracker->PostTask(
      scoped_refptr<TestSimpleTaskRunner>(new TestSimpleTaskRunner()).get(),
      FROM_HERE, DoNothing()));
}

TEST_F(CancelableTaskTrackerDeathTest, PostFromDifferentThread) {
  Thread bad_thread("bad thread");
  ASSERT_TRUE(bad_thread.Start());

  bad_thread.task_runner()->PostTask(
      FROM_HERE,
      BindOnce(&MaybeRunDeadlyTaskTrackerMemberFunction,
               Unretained(&task_tracker_), Bind(&PostDoNothingTask)));
}

void TryCancel(CancelableTaskTracker::TaskId task_id,
               CancelableTaskTracker* task_tracker) {
  task_tracker->TryCancel(task_id);
}

TEST_F(CancelableTaskTrackerDeathTest, CancelOnDifferentThread) {
  scoped_refptr<TestSimpleTaskRunner> test_task_runner(
      new TestSimpleTaskRunner());

  Thread bad_thread("bad thread");
  ASSERT_TRUE(bad_thread.Start());

  CancelableTaskTracker::TaskId task_id =
      task_tracker_.PostTask(test_task_runner.get(), FROM_HERE, DoNothing());
  EXPECT_NE(CancelableTaskTracker::kBadTaskId, task_id);

  bad_thread.task_runner()->PostTask(
      FROM_HERE,
      BindOnce(&MaybeRunDeadlyTaskTrackerMemberFunction,
               Unretained(&task_tracker_), Bind(&TryCancel, task_id)));

  test_task_runner->RunUntilIdle();
}

TEST_F(CancelableTaskTrackerDeathTest, CancelAllOnDifferentThread) {
  scoped_refptr<TestSimpleTaskRunner> test_task_runner(
      new TestSimpleTaskRunner());

  Thread bad_thread("bad thread");
  ASSERT_TRUE(bad_thread.Start());

  CancelableTaskTracker::TaskId task_id =
      task_tracker_.PostTask(test_task_runner.get(), FROM_HERE, DoNothing());
  EXPECT_NE(CancelableTaskTracker::kBadTaskId, task_id);

  bad_thread.task_runner()->PostTask(
      FROM_HERE, BindOnce(&MaybeRunDeadlyTaskTrackerMemberFunction,
                          Unretained(&task_tracker_),
                          Bind(&CancelableTaskTracker::TryCancelAll)));

  test_task_runner->RunUntilIdle();
}

}  // namespace base
