blob: b49f27ce4ac75e05b4ac6eb607521abb59e59f2e [file] [log] [blame]
#include "aos/condition.h"
#include <unistd.h>
#include <sys/types.h>
#include <sys/wait.h>
#include <atomic>
#include <chrono>
#include <thread>
#include "gtest/gtest.h"
#include "aos/time/time.h"
#include "aos/mutex/mutex.h"
#include "aos/testing/test_shm.h"
#include "aos/type_traits/type_traits.h"
#include "aos/ipc_lib/core_lib.h"
#include "aos/logging/logging.h"
#include "aos/macros.h"
#include "aos/ipc_lib/aos_sync.h"
#include "aos/die.h"
#include "aos/testing/prevent_exit.h"
namespace aos {
namespace testing {
namespace chrono = ::std::chrono;
class ConditionTestCommon : public ::testing::Test {
public:
ConditionTestCommon() {}
void Settle() { ::std::this_thread::sleep_for(chrono::milliseconds(8)); }
private:
DISALLOW_COPY_AND_ASSIGN(ConditionTestCommon);
};
// Some simple tests that don't rely on a TestSharedMemory to help with
// debugging problems that cause tests using that to just completely lock up.
class SimpleConditionTest : public ConditionTestCommon {
public:
SimpleConditionTest() : condition_(&mutex_) {}
Mutex mutex_;
Condition condition_;
private:
DISALLOW_COPY_AND_ASSIGN(SimpleConditionTest);
};
// Makes sure that nothing crashes or anything with a basic Wait() and then
// Signal().
// This test is written to hopefully fail instead of deadlocking on failure, but
// it's tricky because there's no way to kill the child in the middle.
TEST_F(SimpleConditionTest, Basic) {
::std::atomic_bool child_finished(false);
Condition child_ready(&mutex_);
ASSERT_FALSE(mutex_.Lock());
std::thread child([this, &child_finished, &child_ready] {
ASSERT_FALSE(mutex_.Lock());
child_ready.Broadcast();
ASSERT_FALSE(condition_.Wait());
child_finished.store(true);
mutex_.Unlock();
});
ASSERT_FALSE(child_ready.Wait());
EXPECT_FALSE(child_finished.load());
condition_.Signal();
mutex_.Unlock();
child.join();
EXPECT_TRUE(child_finished.load());
}
// Tests that contention on the associated mutex doesn't break anything.
// This seems likely to cause issues with AddressSanitizer in particular.
TEST_F(SimpleConditionTest, MutexContention) {
for (int i = 0; i < 1000; ++i) {
ASSERT_FALSE(mutex_.Lock());
::std::thread thread([this]() {
ASSERT_FALSE(mutex_.Lock());
condition_.Signal();
mutex_.Unlock();
});
ASSERT_FALSE(condition_.Wait());
mutex_.Unlock();
thread.join();
}
}
class ConditionTest : public ConditionTestCommon {
public:
struct Shared {
Shared() : condition(&mutex) {}
Mutex mutex;
Condition condition;
};
static_assert(shm_ok<Shared>::value,
"it's going to get shared between forked processes");
ConditionTest() : shared_(static_cast<Shared *>(shm_malloc(sizeof(Shared)))) {
new (shared_) Shared();
}
~ConditionTest() {
shared_->~Shared();
}
::aos::testing::TestSharedMemory my_shm_;
Shared *const shared_;
protected:
void SetUp() override {
SetDieTestMode(true);
}
private:
DISALLOW_COPY_AND_ASSIGN(ConditionTest);
};
class ConditionTestProcess {
public:
enum class Action {
kWaitLockStart, // lock, delay, wait, unlock
kWait, // delay, lock, wait, unlock
kWaitNoUnlock, // delay, lock, wait
};
// This amount gets added to any passed in delay to make the test repeatable.
static constexpr chrono::milliseconds kMinimumDelay =
chrono::milliseconds(150);
static constexpr chrono::milliseconds kDefaultTimeout =
chrono::milliseconds(150);
// delay is how long to wait before doing action to condition.
// timeout is how long to wait after delay before deciding that it's hung.
ConditionTestProcess(chrono::milliseconds delay, Action action,
Condition *condition,
chrono::milliseconds timeout = kDefaultTimeout)
: delay_(kMinimumDelay + delay),
action_(action),
condition_(condition),
timeout_(delay_ + timeout),
child_(-1),
shared_(static_cast<Shared *>(shm_malloc(sizeof(Shared)))) {
new (shared_) Shared();
}
~ConditionTestProcess() {
AOS_CHECK_EQ(child_, -1);
}
void Start() {
ASSERT_FALSE(shared_->started);
child_ = fork();
if (child_ == 0) { // in child
::aos::testing::PreventExit();
Run();
exit(EXIT_SUCCESS);
} else { // in parent
AOS_CHECK_NE(child_, -1);
ASSERT_EQ(0, futex_wait(&shared_->ready));
shared_->started = true;
}
}
bool IsFinished() {
return shared_->finished;
}
::testing::AssertionResult Hung() {
if (!shared_->started) {
ADD_FAILURE();
return ::testing::AssertionFailure() << "not started yet";
}
if (shared_->finished) {
Join();
return ::testing::AssertionFailure() << "already returned";
}
if (shared_->delayed) {
if (shared_->start_time > monotonic_clock::now() + timeout_) {
Kill();
return ::testing::AssertionSuccess() << "already been too long";
}
} else {
AOS_CHECK_EQ(0, futex_wait(&shared_->done_delaying));
}
::std::this_thread::sleep_for(chrono::milliseconds(10));
if (!shared_->finished) {
::std::this_thread::sleep_until(shared_->start_time + timeout_);
}
if (shared_->finished) {
Join();
return ::testing::AssertionFailure() << "completed within timeout";
} else {
Kill();
return ::testing::AssertionSuccess() << "took too long";
}
}
::testing::AssertionResult Test() {
Start();
return Hung();
}
private:
struct Shared {
Shared()
: started(false),
delayed(false),
done_delaying(0),
start_time(monotonic_clock::epoch()),
finished(false),
ready(0) {}
volatile bool started;
volatile bool delayed;
aos_futex done_delaying;
monotonic_clock::time_point start_time;
volatile bool finished;
aos_futex ready;
};
static_assert(shm_ok<Shared>::value,
"it's going to get shared between forked processes");
void Run() {
if (action_ == Action::kWaitLockStart) {
ASSERT_EQ(1, futex_set(&shared_->ready));
ASSERT_FALSE(condition_->m()->Lock());
}
::std::this_thread::sleep_for(delay_);
shared_->start_time = monotonic_clock::now();
shared_->delayed = true;
ASSERT_NE(-1, futex_set(&shared_->done_delaying));
if (action_ != Action::kWaitLockStart) {
ASSERT_EQ(1, futex_set(&shared_->ready));
ASSERT_FALSE(condition_->m()->Lock());
}
// TODO(brians): Test this returning true (aka the owner dying).
ASSERT_FALSE(condition_->Wait());
shared_->finished = true;
if (action_ != Action::kWaitNoUnlock) {
condition_->m()->Unlock();
}
}
void Join() {
AOS_CHECK_NE(child_, -1);
int status;
do {
AOS_CHECK_EQ(waitpid(child_, &status, 0), child_);
} while (!(WIFEXITED(status) || WIFSIGNALED(status)));
child_ = -1;
}
void Kill() {
AOS_CHECK_NE(child_, -1);
AOS_PCHECK(kill(child_, SIGTERM));
Join();
}
const chrono::milliseconds delay_;
const Action action_;
Condition *const condition_;
const chrono::milliseconds timeout_;
pid_t child_;
Shared *const shared_;
DISALLOW_COPY_AND_ASSIGN(ConditionTestProcess);
};
constexpr chrono::milliseconds ConditionTestProcess::kMinimumDelay;
constexpr chrono::milliseconds ConditionTestProcess::kDefaultTimeout;
// Makes sure that the testing framework and everything work for a really simple
// Wait() and then Signal().
TEST_F(ConditionTest, Basic) {
ConditionTestProcess child(chrono::milliseconds(0),
ConditionTestProcess::Action::kWait,
&shared_->condition);
child.Start();
Settle();
EXPECT_FALSE(child.IsFinished());
shared_->condition.Signal();
EXPECT_FALSE(child.Hung());
}
// Makes sure that the worker child locks before it tries to Wait() etc.
TEST_F(ConditionTest, Locking) {
ConditionTestProcess child(chrono::milliseconds(0),
ConditionTestProcess::Action::kWait,
&shared_->condition);
ASSERT_FALSE(shared_->mutex.Lock());
child.Start();
Settle();
// This Signal() shouldn't do anything because the child should still be
// waiting to lock the mutex.
shared_->condition.Signal();
Settle();
shared_->mutex.Unlock();
EXPECT_TRUE(child.Hung());
}
// Tests that the work child only catches a Signal() after the mutex gets
// unlocked.
TEST_F(ConditionTest, LockFirst) {
ConditionTestProcess child(chrono::milliseconds(0),
ConditionTestProcess::Action::kWait,
&shared_->condition);
ASSERT_FALSE(shared_->mutex.Lock());
child.Start();
Settle();
shared_->condition.Signal();
Settle();
EXPECT_FALSE(child.IsFinished());
shared_->mutex.Unlock();
Settle();
EXPECT_FALSE(child.IsFinished());
shared_->condition.Signal();
EXPECT_FALSE(child.Hung());
}
// Tests that the mutex gets relocked after Wait() returns.
TEST_F(ConditionTest, Relocking) {
ConditionTestProcess child(chrono::milliseconds(0),
ConditionTestProcess::Action::kWaitNoUnlock,
&shared_->condition);
child.Start();
Settle();
shared_->condition.Signal();
EXPECT_FALSE(child.Hung());
EXPECT_EQ(Mutex::State::kOwnerDied, shared_->mutex.TryLock());
shared_->mutex.Unlock();
}
// Tests that Signal() stops exactly 1 Wait()er.
TEST_F(ConditionTest, SignalOne) {
ConditionTestProcess child1(chrono::milliseconds(0),
ConditionTestProcess::Action::kWait,
&shared_->condition);
ConditionTestProcess child2(chrono::milliseconds(0),
ConditionTestProcess::Action::kWait,
&shared_->condition);
ConditionTestProcess child3(chrono::milliseconds(0),
ConditionTestProcess::Action::kWait,
&shared_->condition);
auto number_finished = [&]() { return (child1.IsFinished() ? 1 : 0) +
(child2.IsFinished() ? 1 : 0) + (child3.IsFinished() ? 1 : 0); };
child1.Start();
child2.Start();
child3.Start();
Settle();
EXPECT_EQ(0, number_finished());
shared_->condition.Signal();
Settle();
EXPECT_EQ(1, number_finished());
shared_->condition.Signal();
Settle();
EXPECT_EQ(2, number_finished());
shared_->condition.Signal();
Settle();
EXPECT_EQ(3, number_finished());
EXPECT_FALSE(child1.Hung());
EXPECT_FALSE(child2.Hung());
EXPECT_FALSE(child3.Hung());
}
// Tests that Brodcast() wakes multiple Wait()ers.
TEST_F(ConditionTest, Broadcast) {
ConditionTestProcess child1(chrono::milliseconds(0),
ConditionTestProcess::Action::kWait,
&shared_->condition);
ConditionTestProcess child2(chrono::milliseconds(0),
ConditionTestProcess::Action::kWait,
&shared_->condition);
ConditionTestProcess child3(chrono::milliseconds(0),
ConditionTestProcess::Action::kWait,
&shared_->condition);
child1.Start();
child2.Start();
child3.Start();
Settle();
shared_->condition.Broadcast();
EXPECT_FALSE(child1.Hung());
EXPECT_FALSE(child2.Hung());
EXPECT_FALSE(child3.Hung());
}
} // namespace testing
} // namespace aos