blob: 0c4a7a20e84662c35aa10885a9a5f5106d4310d6 [file] [log] [blame]
/*
* 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