| /* |
| * libjingle |
| * Copyright 2004--2011, Google Inc. |
| * |
| * Redistribution and use in source and binary forms, with or without |
| * modification, are permitted provided that the following conditions are met: |
| * |
| * 1. Redistributions of source code must retain the above copyright notice, |
| * this list of conditions and the following disclaimer. |
| * 2. Redistributions in binary form must reproduce the above copyright notice, |
| * this list of conditions and the following disclaimer in the documentation |
| * and/or other materials provided with the distribution. |
| * 3. The name of the author may not be used to endorse or promote products |
| * derived from this software without specific prior written permission. |
| * |
| * THIS SOFTWARE IS PROVIDED BY THE AUTHOR ``AS IS'' AND ANY EXPRESS OR IMPLIED |
| * WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF |
| * MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO |
| * EVENT SHALL THE AUTHOR BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, |
| * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, |
| * PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; |
| * OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, |
| * WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR |
| * OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF |
| * ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. |
| */ |
| |
| #ifdef POSIX |
| #include <sys/time.h> |
| #endif // POSIX |
| |
| // TODO: Remove this once the cause of sporadic failures in these |
| // tests is tracked down. |
| #include <iostream> |
| |
| #ifdef WIN32 |
| #include "talk/base/win32.h" |
| #endif // WIN32 |
| |
| #include "talk/base/common.h" |
| #include "talk/base/gunit.h" |
| #include "talk/base/logging.h" |
| #include "talk/base/task.h" |
| #include "talk/base/taskrunner.h" |
| #include "talk/base/thread.h" |
| #include "talk/base/timeutils.h" |
| |
| namespace talk_base { |
| |
| 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 talk_base |