|  | /* | 
|  | *  Copyright 2004 The WebRTC Project Authors. All rights reserved. | 
|  | * | 
|  | *  Use of this source code is governed by a BSD-style license | 
|  | *  that can be found in the LICENSE file in the root of the source | 
|  | *  tree. An additional intellectual property rights grant can be found | 
|  | *  in the file PATENTS.  All contributing project authors may | 
|  | *  be found in the AUTHORS file in the root of the source tree. | 
|  | */ | 
|  |  | 
|  | #if defined(WEBRTC_POSIX) | 
|  | #include <sys/time.h> | 
|  | #endif  // WEBRTC_POSIX | 
|  |  | 
|  | // TODO: Remove this once the cause of sporadic failures in these | 
|  | // tests is tracked down. | 
|  | #include <iostream> | 
|  |  | 
|  | #if defined(WEBRTC_WIN) | 
|  | #include "webrtc/base/win32.h" | 
|  | #endif  // WEBRTC_WIN | 
|  |  | 
|  | #include "webrtc/base/common.h" | 
|  | #include "webrtc/base/gunit.h" | 
|  | #include "webrtc/base/logging.h" | 
|  | #include "webrtc/base/task.h" | 
|  | #include "webrtc/base/taskrunner.h" | 
|  | #include "webrtc/base/thread.h" | 
|  | #include "webrtc/base/timeutils.h" | 
|  |  | 
|  | namespace rtc { | 
|  |  | 
|  | static int64 GetCurrentTime() { | 
|  | return static_cast<int64>(Time()) * 10000; | 
|  | } | 
|  |  | 
|  | // feel free to change these numbers.  Note that '0' won't work, though | 
|  | #define STUCK_TASK_COUNT 5 | 
|  | #define HAPPY_TASK_COUNT 20 | 
|  |  | 
|  | // this is a generic timeout task which, when it signals timeout, will | 
|  | // include the unique ID of the task in the signal (we don't use this | 
|  | // in production code because we haven't yet had occasion to generate | 
|  | // an array of the same types of task) | 
|  |  | 
|  | class IdTimeoutTask : public Task, public sigslot::has_slots<> { | 
|  | public: | 
|  | explicit IdTimeoutTask(TaskParent *parent) : Task(parent) { | 
|  | SignalTimeout.connect(this, &IdTimeoutTask::OnLocalTimeout); | 
|  | } | 
|  |  | 
|  | sigslot::signal1<const int> SignalTimeoutId; | 
|  | sigslot::signal1<const int> SignalDoneId; | 
|  |  | 
|  | virtual int ProcessStart() { | 
|  | return STATE_RESPONSE; | 
|  | } | 
|  |  | 
|  | void OnLocalTimeout() { | 
|  | SignalTimeoutId(unique_id()); | 
|  | } | 
|  |  | 
|  | protected: | 
|  | virtual void Stop() { | 
|  | SignalDoneId(unique_id()); | 
|  | Task::Stop(); | 
|  | } | 
|  | }; | 
|  |  | 
|  | class StuckTask : public IdTimeoutTask { | 
|  | public: | 
|  | explicit StuckTask(TaskParent *parent) : IdTimeoutTask(parent) {} | 
|  | virtual int ProcessStart() { | 
|  | return STATE_BLOCKED; | 
|  | } | 
|  | }; | 
|  |  | 
|  | class HappyTask : public IdTimeoutTask { | 
|  | public: | 
|  | explicit HappyTask(TaskParent *parent) : IdTimeoutTask(parent) { | 
|  | time_to_perform_ = rand() % (STUCK_TASK_COUNT / 2); | 
|  | } | 
|  | virtual int ProcessStart() { | 
|  | if (ElapsedTime() > (time_to_perform_ * 1000 * 10000)) | 
|  | return STATE_RESPONSE; | 
|  | else | 
|  | return STATE_BLOCKED; | 
|  | } | 
|  |  | 
|  | private: | 
|  | int time_to_perform_; | 
|  | }; | 
|  |  | 
|  | // simple implementation of a task runner which uses Windows' | 
|  | // GetSystemTimeAsFileTime() to get the current clock ticks | 
|  |  | 
|  | class MyTaskRunner : public TaskRunner { | 
|  | public: | 
|  | virtual void WakeTasks() { RunTasks(); } | 
|  | virtual int64 CurrentTime() { | 
|  | return GetCurrentTime(); | 
|  | } | 
|  |  | 
|  | bool timeout_change() const { | 
|  | return timeout_change_; | 
|  | } | 
|  |  | 
|  | void clear_timeout_change() { | 
|  | timeout_change_ = false; | 
|  | } | 
|  | protected: | 
|  | virtual void OnTimeoutChange() { | 
|  | timeout_change_ = true; | 
|  | } | 
|  | bool timeout_change_; | 
|  | }; | 
|  |  | 
|  | // | 
|  | // this unit test is primarily concerned (for now) with the timeout | 
|  | // functionality in tasks.  It works as follows: | 
|  | // | 
|  | //   * Create a bunch of tasks, some "stuck" (ie., guaranteed to timeout) | 
|  | //     and some "happy" (will immediately finish). | 
|  | //   * Set the timeout on the "stuck" tasks to some number of seconds between | 
|  | //     1 and the number of stuck tasks | 
|  | //   * Start all the stuck & happy tasks in random order | 
|  | //   * Wait "number of stuck tasks" seconds and make sure everything timed out | 
|  |  | 
|  | class TaskTest : public sigslot::has_slots<> { | 
|  | public: | 
|  | TaskTest() {} | 
|  |  | 
|  | // no need to delete any tasks; the task runner owns them | 
|  | ~TaskTest() {} | 
|  |  | 
|  | void Start() { | 
|  | // create and configure tasks | 
|  | for (int i = 0; i < STUCK_TASK_COUNT; ++i) { | 
|  | stuck_[i].task_ = new StuckTask(&task_runner_); | 
|  | stuck_[i].task_->SignalTimeoutId.connect(this, | 
|  | &TaskTest::OnTimeoutStuck); | 
|  | stuck_[i].timed_out_ = false; | 
|  | stuck_[i].xlat_ = stuck_[i].task_->unique_id(); | 
|  | stuck_[i].task_->set_timeout_seconds(i + 1); | 
|  | LOG(LS_INFO) << "Task " << stuck_[i].xlat_ << " created with timeout " | 
|  | << stuck_[i].task_->timeout_seconds(); | 
|  | } | 
|  |  | 
|  | for (int i = 0; i < HAPPY_TASK_COUNT; ++i) { | 
|  | happy_[i].task_ = new HappyTask(&task_runner_); | 
|  | happy_[i].task_->SignalTimeoutId.connect(this, | 
|  | &TaskTest::OnTimeoutHappy); | 
|  | happy_[i].task_->SignalDoneId.connect(this, | 
|  | &TaskTest::OnDoneHappy); | 
|  | happy_[i].timed_out_ = false; | 
|  | happy_[i].xlat_ = happy_[i].task_->unique_id(); | 
|  | } | 
|  |  | 
|  | // start all the tasks in random order | 
|  | int stuck_index = 0; | 
|  | int happy_index = 0; | 
|  | for (int i = 0; i < STUCK_TASK_COUNT + HAPPY_TASK_COUNT; ++i) { | 
|  | if ((stuck_index < STUCK_TASK_COUNT) && | 
|  | (happy_index < HAPPY_TASK_COUNT)) { | 
|  | if (rand() % 2 == 1) { | 
|  | stuck_[stuck_index++].task_->Start(); | 
|  | } else { | 
|  | happy_[happy_index++].task_->Start(); | 
|  | } | 
|  | } else if (stuck_index < STUCK_TASK_COUNT) { | 
|  | stuck_[stuck_index++].task_->Start(); | 
|  | } else { | 
|  | happy_[happy_index++].task_->Start(); | 
|  | } | 
|  | } | 
|  |  | 
|  | for (int i = 0; i < STUCK_TASK_COUNT; ++i) { | 
|  | std::cout << "Stuck task #" << i << " timeout is " << | 
|  | stuck_[i].task_->timeout_seconds() << " at " << | 
|  | stuck_[i].task_->timeout_time() << std::endl; | 
|  | } | 
|  |  | 
|  | // just a little self-check to make sure we started all the tasks | 
|  | ASSERT_EQ(STUCK_TASK_COUNT, stuck_index); | 
|  | ASSERT_EQ(HAPPY_TASK_COUNT, happy_index); | 
|  |  | 
|  | // run the unblocked tasks | 
|  | LOG(LS_INFO) << "Running tasks"; | 
|  | task_runner_.RunTasks(); | 
|  |  | 
|  | std::cout << "Start time is " << GetCurrentTime() << std::endl; | 
|  |  | 
|  | // give all the stuck tasks time to timeout | 
|  | for (int i = 0; !task_runner_.AllChildrenDone() && i < STUCK_TASK_COUNT; | 
|  | ++i) { | 
|  | Thread::Current()->ProcessMessages(1000); | 
|  | for (int j = 0; j < HAPPY_TASK_COUNT; ++j) { | 
|  | if (happy_[j].task_) { | 
|  | happy_[j].task_->Wake(); | 
|  | } | 
|  | } | 
|  | LOG(LS_INFO) << "Polling tasks"; | 
|  | task_runner_.PollTasks(); | 
|  | } | 
|  |  | 
|  | // We see occasional test failures here due to the stuck tasks not having | 
|  | // timed-out yet, which seems like it should be impossible. To help track | 
|  | // this down we have added logging of the timing information, which we send | 
|  | // directly to stdout so that we get it in opt builds too. | 
|  | std::cout << "End time is " << GetCurrentTime() << std::endl; | 
|  | } | 
|  |  | 
|  | void OnTimeoutStuck(const int id) { | 
|  | LOG(LS_INFO) << "Timed out task " << id; | 
|  |  | 
|  | int i; | 
|  | for (i = 0; i < STUCK_TASK_COUNT; ++i) { | 
|  | if (stuck_[i].xlat_ == id) { | 
|  | stuck_[i].timed_out_ = true; | 
|  | stuck_[i].task_ = NULL; | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | // getting a bad ID here is a failure, but let's continue | 
|  | // running to see what else might go wrong | 
|  | EXPECT_LT(i, STUCK_TASK_COUNT); | 
|  | } | 
|  |  | 
|  | void OnTimeoutHappy(const int id) { | 
|  | int i; | 
|  | for (i = 0; i < HAPPY_TASK_COUNT; ++i) { | 
|  | if (happy_[i].xlat_ == id) { | 
|  | happy_[i].timed_out_ = true; | 
|  | happy_[i].task_ = NULL; | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | // getting a bad ID here is a failure, but let's continue | 
|  | // running to see what else might go wrong | 
|  | EXPECT_LT(i, HAPPY_TASK_COUNT); | 
|  | } | 
|  |  | 
|  | void OnDoneHappy(const int id) { | 
|  | int i; | 
|  | for (i = 0; i < HAPPY_TASK_COUNT; ++i) { | 
|  | if (happy_[i].xlat_ == id) { | 
|  | happy_[i].task_ = NULL; | 
|  | break; | 
|  | } | 
|  | } | 
|  |  | 
|  | // getting a bad ID here is a failure, but let's continue | 
|  | // running to see what else might go wrong | 
|  | EXPECT_LT(i, HAPPY_TASK_COUNT); | 
|  | } | 
|  |  | 
|  | void check_passed() { | 
|  | EXPECT_TRUE(task_runner_.AllChildrenDone()); | 
|  |  | 
|  | // make sure none of our happy tasks timed out | 
|  | for (int i = 0; i < HAPPY_TASK_COUNT; ++i) { | 
|  | EXPECT_FALSE(happy_[i].timed_out_); | 
|  | } | 
|  |  | 
|  | // make sure all of our stuck tasks timed out | 
|  | for (int i = 0; i < STUCK_TASK_COUNT; ++i) { | 
|  | EXPECT_TRUE(stuck_[i].timed_out_); | 
|  | if (!stuck_[i].timed_out_) { | 
|  | std::cout << "Stuck task #" << i << " timeout is at " | 
|  | << stuck_[i].task_->timeout_time() << std::endl; | 
|  | } | 
|  | } | 
|  |  | 
|  | std::cout.flush(); | 
|  | } | 
|  |  | 
|  | private: | 
|  | struct TaskInfo { | 
|  | IdTimeoutTask *task_; | 
|  | bool timed_out_; | 
|  | int xlat_; | 
|  | }; | 
|  |  | 
|  | MyTaskRunner task_runner_; | 
|  | TaskInfo stuck_[STUCK_TASK_COUNT]; | 
|  | TaskInfo happy_[HAPPY_TASK_COUNT]; | 
|  | }; | 
|  |  | 
|  | TEST(start_task_test, Timeout) { | 
|  | TaskTest task_test; | 
|  | task_test.Start(); | 
|  | task_test.check_passed(); | 
|  | } | 
|  |  | 
|  | // Test for aborting the task while it is running | 
|  |  | 
|  | class AbortTask : public Task { | 
|  | public: | 
|  | explicit AbortTask(TaskParent *parent) : Task(parent) { | 
|  | set_timeout_seconds(1); | 
|  | } | 
|  |  | 
|  | virtual int ProcessStart() { | 
|  | Abort(); | 
|  | return STATE_NEXT; | 
|  | } | 
|  | private: | 
|  | DISALLOW_EVIL_CONSTRUCTORS(AbortTask); | 
|  | }; | 
|  |  | 
|  | class TaskAbortTest : public sigslot::has_slots<> { | 
|  | public: | 
|  | TaskAbortTest() {} | 
|  |  | 
|  | // no need to delete any tasks; the task runner owns them | 
|  | ~TaskAbortTest() {} | 
|  |  | 
|  | void Start() { | 
|  | Task *abort_task = new AbortTask(&task_runner_); | 
|  | abort_task->SignalTimeout.connect(this, &TaskAbortTest::OnTimeout); | 
|  | abort_task->Start(); | 
|  |  | 
|  | // run the task | 
|  | task_runner_.RunTasks(); | 
|  | } | 
|  |  | 
|  | private: | 
|  | void OnTimeout() { | 
|  | FAIL() << "Task timed out instead of aborting."; | 
|  | } | 
|  |  | 
|  | MyTaskRunner task_runner_; | 
|  | DISALLOW_EVIL_CONSTRUCTORS(TaskAbortTest); | 
|  | }; | 
|  |  | 
|  | TEST(start_task_test, Abort) { | 
|  | TaskAbortTest abort_test; | 
|  | abort_test.Start(); | 
|  | } | 
|  |  | 
|  | // Test for aborting a task to verify that it does the Wake operation | 
|  | // which gets it deleted. | 
|  |  | 
|  | class SetBoolOnDeleteTask : public Task { | 
|  | public: | 
|  | SetBoolOnDeleteTask(TaskParent *parent, bool *set_when_deleted) | 
|  | : Task(parent), | 
|  | set_when_deleted_(set_when_deleted) { | 
|  | EXPECT_TRUE(NULL != set_when_deleted); | 
|  | EXPECT_FALSE(*set_when_deleted); | 
|  | } | 
|  |  | 
|  | virtual ~SetBoolOnDeleteTask() { | 
|  | *set_when_deleted_ = true; | 
|  | } | 
|  |  | 
|  | virtual int ProcessStart() { | 
|  | return STATE_BLOCKED; | 
|  | } | 
|  |  | 
|  | private: | 
|  | bool* set_when_deleted_; | 
|  | DISALLOW_EVIL_CONSTRUCTORS(SetBoolOnDeleteTask); | 
|  | }; | 
|  |  | 
|  | class AbortShouldWakeTest : public sigslot::has_slots<> { | 
|  | public: | 
|  | AbortShouldWakeTest() {} | 
|  |  | 
|  | // no need to delete any tasks; the task runner owns them | 
|  | ~AbortShouldWakeTest() {} | 
|  |  | 
|  | void Start() { | 
|  | bool task_deleted = false; | 
|  | Task *task_to_abort = new SetBoolOnDeleteTask(&task_runner_, &task_deleted); | 
|  | task_to_abort->Start(); | 
|  |  | 
|  | // Task::Abort() should call TaskRunner::WakeTasks(). WakeTasks calls | 
|  | // TaskRunner::RunTasks() immediately which should delete the task. | 
|  | task_to_abort->Abort(); | 
|  | EXPECT_TRUE(task_deleted); | 
|  |  | 
|  | if (!task_deleted) { | 
|  | // avoid a crash (due to referencing a local variable) | 
|  | // if the test fails. | 
|  | task_runner_.RunTasks(); | 
|  | } | 
|  | } | 
|  |  | 
|  | private: | 
|  | void OnTimeout() { | 
|  | FAIL() << "Task timed out instead of aborting."; | 
|  | } | 
|  |  | 
|  | MyTaskRunner task_runner_; | 
|  | DISALLOW_EVIL_CONSTRUCTORS(AbortShouldWakeTest); | 
|  | }; | 
|  |  | 
|  | TEST(start_task_test, AbortShouldWake) { | 
|  | AbortShouldWakeTest abort_should_wake_test; | 
|  | abort_should_wake_test.Start(); | 
|  | } | 
|  |  | 
|  | // Validate that TaskRunner's OnTimeoutChange gets called appropriately | 
|  | //  * When a task calls UpdateTaskTimeout | 
|  | //  * When the next timeout task time, times out | 
|  | class TimeoutChangeTest : public sigslot::has_slots<> { | 
|  | public: | 
|  | TimeoutChangeTest() | 
|  | : task_count_(ARRAY_SIZE(stuck_tasks_)) {} | 
|  |  | 
|  | // no need to delete any tasks; the task runner owns them | 
|  | ~TimeoutChangeTest() {} | 
|  |  | 
|  | void Start() { | 
|  | for (int i = 0; i < task_count_; ++i) { | 
|  | stuck_tasks_[i] = new StuckTask(&task_runner_); | 
|  | stuck_tasks_[i]->set_timeout_seconds(i + 2); | 
|  | stuck_tasks_[i]->SignalTimeoutId.connect(this, | 
|  | &TimeoutChangeTest::OnTimeoutId); | 
|  | } | 
|  |  | 
|  | for (int i = task_count_ - 1; i >= 0; --i) { | 
|  | stuck_tasks_[i]->Start(); | 
|  | } | 
|  | task_runner_.clear_timeout_change(); | 
|  |  | 
|  | // At this point, our timeouts are set as follows | 
|  | // task[0] is 2 seconds, task[1] at 3 seconds, etc. | 
|  |  | 
|  | stuck_tasks_[0]->set_timeout_seconds(2); | 
|  | // Now, task[0] is 2 seconds, task[1] at 3 seconds... | 
|  | // so timeout change shouldn't be called. | 
|  | EXPECT_FALSE(task_runner_.timeout_change()); | 
|  | task_runner_.clear_timeout_change(); | 
|  |  | 
|  | stuck_tasks_[0]->set_timeout_seconds(1); | 
|  | // task[0] is 1 seconds, task[1] at 3 seconds... | 
|  | // The smallest timeout got smaller so timeout change be called. | 
|  | EXPECT_TRUE(task_runner_.timeout_change()); | 
|  | task_runner_.clear_timeout_change(); | 
|  |  | 
|  | stuck_tasks_[1]->set_timeout_seconds(2); | 
|  | // task[0] is 1 seconds, task[1] at 2 seconds... | 
|  | // The smallest timeout is still 1 second so no timeout change. | 
|  | EXPECT_FALSE(task_runner_.timeout_change()); | 
|  | task_runner_.clear_timeout_change(); | 
|  |  | 
|  | while (task_count_ > 0) { | 
|  | int previous_count = task_count_; | 
|  | task_runner_.PollTasks(); | 
|  | if (previous_count != task_count_) { | 
|  | // We only get here when a task times out.  When that | 
|  | // happens, the timeout change should get called because | 
|  | // the smallest timeout is now in the past. | 
|  | EXPECT_TRUE(task_runner_.timeout_change()); | 
|  | task_runner_.clear_timeout_change(); | 
|  | } | 
|  | Thread::Current()->socketserver()->Wait(500, false); | 
|  | } | 
|  | } | 
|  |  | 
|  | private: | 
|  | void OnTimeoutId(const int id) { | 
|  | for (int i = 0; i < ARRAY_SIZE(stuck_tasks_); ++i) { | 
|  | if (stuck_tasks_[i] && stuck_tasks_[i]->unique_id() == id) { | 
|  | task_count_--; | 
|  | stuck_tasks_[i] = NULL; | 
|  | break; | 
|  | } | 
|  | } | 
|  | } | 
|  |  | 
|  | MyTaskRunner task_runner_; | 
|  | StuckTask* (stuck_tasks_[3]); | 
|  | int task_count_; | 
|  | DISALLOW_EVIL_CONSTRUCTORS(TimeoutChangeTest); | 
|  | }; | 
|  |  | 
|  | TEST(start_task_test, TimeoutChange) { | 
|  | TimeoutChangeTest timeout_change_test; | 
|  | timeout_change_test.Start(); | 
|  | } | 
|  |  | 
|  | class DeleteTestTaskRunner : public TaskRunner { | 
|  | public: | 
|  | DeleteTestTaskRunner() { | 
|  | } | 
|  | virtual void WakeTasks() { } | 
|  | virtual int64 CurrentTime() { | 
|  | return GetCurrentTime(); | 
|  | } | 
|  | private: | 
|  | DISALLOW_EVIL_CONSTRUCTORS(DeleteTestTaskRunner); | 
|  | }; | 
|  |  | 
|  | TEST(unstarted_task_test, DeleteTask) { | 
|  | // This test ensures that we don't | 
|  | // crash if a task is deleted without running it. | 
|  | DeleteTestTaskRunner task_runner; | 
|  | HappyTask* happy_task = new HappyTask(&task_runner); | 
|  | happy_task->Start(); | 
|  |  | 
|  | // try deleting the task directly | 
|  | HappyTask* child_happy_task = new HappyTask(happy_task); | 
|  | delete child_happy_task; | 
|  |  | 
|  | // run the unblocked tasks | 
|  | task_runner.RunTasks(); | 
|  | } | 
|  |  | 
|  | TEST(unstarted_task_test, DoNotDeleteTask1) { | 
|  | // This test ensures that we don't | 
|  | // crash if a task runner is deleted without | 
|  | // running a certain task. | 
|  | DeleteTestTaskRunner task_runner; | 
|  | HappyTask* happy_task = new HappyTask(&task_runner); | 
|  | happy_task->Start(); | 
|  |  | 
|  | HappyTask* child_happy_task = new HappyTask(happy_task); | 
|  | child_happy_task->Start(); | 
|  |  | 
|  | // Never run the tasks | 
|  | } | 
|  |  | 
|  | TEST(unstarted_task_test, DoNotDeleteTask2) { | 
|  | // This test ensures that we don't | 
|  | // crash if a taskrunner is delete with a | 
|  | // task that has never been started. | 
|  | DeleteTestTaskRunner task_runner; | 
|  | HappyTask* happy_task = new HappyTask(&task_runner); | 
|  | happy_task->Start(); | 
|  |  | 
|  | // Do not start the task. | 
|  | // Note: this leaks memory, so don't do this. | 
|  | // Instead, always run your tasks or delete them. | 
|  | new HappyTask(happy_task); | 
|  |  | 
|  | // run the unblocked tasks | 
|  | task_runner.RunTasks(); | 
|  | } | 
|  |  | 
|  | }  // namespace rtc |