blob: ce4f1e9fe0cbeb93f067bab263490dc9797e30d0 [file]
// Copyright 2026 The Fuchsia Authors
//
// Use of this source code is governed by a MIT-style
// license that can be found in the LICENSE file or at
// https://opensource.org/licenses/MIT
#include <lib/fit/defer.h>
#include <lib/uart-gate.h>
#include <lib/unittest/unittest.h>
#include <zircon/time.h>
#include <zircon/types.h>
#include <arch/arch_ops.h>
#include <kernel/thread.h>
#include <ktl/array.h>
#include <ktl/atomic.h>
// Provides tests with access to UartGate internals.
struct UartGateTestAccess {
// Returns true if the gate's drained event was signaled, consuming the
// signal. Does not block.
static bool ConsumeDrainedSignal(UartGate& gate) {
return gate.drained_.Wait(Deadline::infinite_past()) == ZX_OK;
}
};
namespace {
bool InitialStateTest() {
BEGIN_TEST;
UartGate gate;
EXPECT_EQ(UartGate::Phase::kRunning, gate.phase());
ASSERT_TRUE(gate.TryEnter());
EXPECT_EQ(UartGate::Phase::kRunning, gate.phase());
gate.Exit();
ASSERT_TRUE(gate.TryEnterUnlessSuspended());
EXPECT_EQ(UartGate::Phase::kRunning, gate.phase());
gate.Exit();
END_TEST;
}
bool DrainWithNoActiveUsersTest() {
BEGIN_TEST;
UartGate gate;
gate.BeginDrain();
EXPECT_EQ(UartGate::Phase::kDraining, gate.phase());
// Normal operations are refused while draining.
EXPECT_FALSE(gate.TryEnter());
// With no active users, waiting for there to be none returns immediately.
EXPECT_OK(gate.WaitForNoActiveUsers());
// An operation may enter and exit while draining (e.g. the IRQ handler).
// Its exit leaves a pending wakeup, which a subsequent wait consumes.
ASSERT_TRUE(gate.TryEnterUnlessSuspended());
EXPECT_FALSE(gate.TryFinishDrain());
gate.Exit();
EXPECT_OK(gate.WaitForNoActiveUsers());
// With no active users, finishing the drain succeeds immediately.
ASSERT_TRUE(gate.TryFinishDrain());
EXPECT_EQ(UartGate::Phase::kSuspended, gate.phase());
// Once suspended, all operations are refused.
EXPECT_FALSE(gate.TryEnter());
EXPECT_FALSE(gate.TryEnterUnlessSuspended());
EXPECT_FALSE(gate.TryFinishDrain());
// Resuming reopens the gate.
gate.Resume();
EXPECT_EQ(UartGate::Phase::kRunning, gate.phase());
ASSERT_TRUE(gate.TryEnter());
gate.Exit();
END_TEST;
}
bool DrainWaitsForActiveUsersTest() {
BEGIN_TEST;
UartGate gate;
// Enter multiple times before draining starts.
ASSERT_TRUE(gate.TryEnter());
ASSERT_TRUE(gate.TryEnter());
ASSERT_TRUE(gate.TryEnterUnlessSuspended());
gate.BeginDrain();
EXPECT_EQ(UartGate::Phase::kDraining, gate.phase());
// Normal entry is refused while draining, and the drain cannot finish while
// active users remain.
EXPECT_FALSE(gate.TryEnter());
EXPECT_FALSE(gate.TryFinishDrain());
// Operations required to complete in-flight work may still enter while
// draining.
ASSERT_TRUE(gate.TryEnterUnlessSuspended());
EXPECT_FALSE(gate.TryFinishDrain());
// Exit three of the four active users; one remains.
gate.Exit();
EXPECT_FALSE(gate.TryFinishDrain());
gate.Exit();
EXPECT_FALSE(gate.TryFinishDrain());
gate.Exit();
EXPECT_FALSE(gate.TryFinishDrain());
// Exiting the final active user allows the drain to finish.
gate.Exit();
ASSERT_TRUE(gate.TryFinishDrain());
EXPECT_EQ(UartGate::Phase::kSuspended, gate.phase());
EXPECT_FALSE(gate.TryEnter());
EXPECT_FALSE(gate.TryEnterUnlessSuspended());
// Verify that the gate can be resumed and suspended again.
gate.Resume();
EXPECT_EQ(UartGate::Phase::kRunning, gate.phase());
gate.BeginDrain();
ASSERT_TRUE(gate.TryFinishDrain());
gate.Resume();
EXPECT_EQ(UartGate::Phase::kRunning, gate.phase());
END_TEST;
}
bool CancelDrainTest() {
BEGIN_TEST;
UartGate gate;
// Cancel with an active user. The user is unaffected, and normal entry is
// allowed again.
ASSERT_TRUE(gate.TryEnter());
EXPECT_TRUE(gate.HasActiveUsers());
gate.BeginDrain();
EXPECT_FALSE(gate.TryEnter());
gate.CancelDrain();
EXPECT_EQ(UartGate::Phase::kRunning, gate.phase());
EXPECT_TRUE(gate.HasActiveUsers());
ASSERT_TRUE(gate.TryEnter());
gate.Exit();
gate.Exit();
EXPECT_FALSE(gate.HasActiveUsers());
// Cancel with no active users.
gate.BeginDrain();
EXPECT_FALSE(gate.HasActiveUsers());
gate.CancelDrain();
EXPECT_EQ(UartGate::Phase::kRunning, gate.phase());
// The gate can still be suspended and resumed afterwards.
gate.BeginDrain();
ASSERT_TRUE(gate.TryFinishDrain());
EXPECT_EQ(UartGate::Phase::kSuspended, gate.phase());
gate.Resume();
EXPECT_EQ(UartGate::Phase::kRunning, gate.phase());
END_TEST;
}
// Note that because WaitForNoActiveUsers also polls, this test would pass even
// if the last exit did not wake the waiter. See LastExitSignalsTest, which
// verifies the signal directly.
bool WaitIsWokenByLastExitTest() {
BEGIN_TEST;
struct TestState {
UartGate gate;
ktl::atomic<bool> done{false};
zx_status_t status{ZX_ERR_INTERNAL};
};
TestState state;
// Two users enter before draining starts.
ASSERT_TRUE(state.gate.TryEnter());
ASSERT_TRUE(state.gate.TryEnter());
state.gate.BeginDrain();
thread_start_routine waiter = [](void* arg) -> int {
auto* s = static_cast<TestState*>(arg);
s->status = s->gate.WaitForNoActiveUsers();
s->done.store(true, ktl::memory_order_release);
return 0;
};
Thread* t = Thread::Create("uart_gate_test_waiter", waiter, &state, DEFAULT_PRIORITY);
ASSERT_NONNULL(t);
auto cleanup = fit::defer([&]() {
// Make sure the waiter can finish, even if we fail part way through.
while (state.gate.HasActiveUsers()) {
state.gate.Exit();
}
t->Join(nullptr, ZX_TIME_INFINITE);
});
t->Resume();
// Give the waiter a chance to block. It must not finish while users remain,
// including after a non-final exit.
Thread::Current::SleepRelative(ZX_MSEC(1));
EXPECT_FALSE(state.done.load(ktl::memory_order_acquire));
state.gate.Exit();
Thread::Current::SleepRelative(ZX_MSEC(1));
EXPECT_FALSE(state.done.load(ktl::memory_order_acquire));
// The final exit wakes the waiter.
state.gate.Exit();
cleanup.cancel();
ASSERT_OK(t->Join(nullptr, ZX_TIME_INFINITE));
EXPECT_TRUE(state.done.load(ktl::memory_order_acquire));
EXPECT_OK(state.status);
ASSERT_TRUE(state.gate.TryFinishDrain());
state.gate.Resume();
END_TEST;
}
// Verifies, without relying on timing, that only the last exit while draining
// (via Exit, not ExitWithoutWaking) signals the suspending thread.
bool LastExitSignalsTest() {
BEGIN_TEST;
UartGate gate;
// Exiting while running does not signal.
ASSERT_TRUE(gate.TryEnter());
gate.Exit();
EXPECT_FALSE(UartGateTestAccess::ConsumeDrainedSignal(gate));
// Only the last exit while draining signals.
ASSERT_TRUE(gate.TryEnter());
ASSERT_TRUE(gate.TryEnter());
gate.BeginDrain();
gate.Exit();
EXPECT_FALSE(UartGateTestAccess::ConsumeDrainedSignal(gate));
gate.Exit();
EXPECT_TRUE(UartGateTestAccess::ConsumeDrainedSignal(gate));
EXPECT_FALSE(UartGateTestAccess::ConsumeDrainedSignal(gate));
gate.CancelDrain();
// ExitWithoutWaking never signals, even as the last exit while draining.
ASSERT_TRUE(gate.TryEnter());
gate.BeginDrain();
gate.ExitWithoutWaking();
EXPECT_FALSE(UartGateTestAccess::ConsumeDrainedSignal(gate));
ASSERT_TRUE(gate.TryFinishDrain());
gate.Resume();
END_TEST;
}
// If the last user exits via ExitWithoutWaking, the waiter must still notice (after at most about
// one poll interval) rather than blocking indefinitely.
bool WaitNoticesLastExitWithoutWakingTest() {
BEGIN_TEST;
struct TestState {
UartGate gate;
zx_status_t status{ZX_ERR_INTERNAL};
};
TestState state;
ASSERT_TRUE(state.gate.TryEnter());
state.gate.BeginDrain();
thread_start_routine waiter = [](void* arg) -> int {
auto* s = static_cast<TestState*>(arg);
s->status = s->gate.WaitForNoActiveUsers();
return 0;
};
Thread* t = Thread::Create("uart_gate_test_waiter", waiter, &state, DEFAULT_PRIORITY);
ASSERT_NONNULL(t);
t->Resume();
// Give the waiter a chance to block, then exit without waking it.
Thread::Current::SleepRelative(ZX_MSEC(1));
state.gate.ExitWithoutWaking();
ASSERT_OK(t->Join(nullptr, ZX_TIME_INFINITE));
EXPECT_OK(state.status);
ASSERT_TRUE(state.gate.TryFinishDrain());
state.gate.Resume();
END_TEST;
}
bool ConcurrentEnterExitAndSuspendResumeTest() {
BEGIN_TEST;
struct TestState {
UartGate gate;
ktl::atomic<uint32_t> active_count{0};
ktl::atomic<bool> suspended{false};
ktl::atomic<bool> stop{false};
ktl::atomic<bool> saw_violation{false};
};
TestState state;
static constexpr auto worker_fn = [](void* arg, bool unless_suspended) -> int {
auto* s = static_cast<TestState*>(arg);
while (!s->stop.load(ktl::memory_order_relaxed)) {
const bool entered =
unless_suspended ? s->gate.TryEnterUnlessSuspended() : s->gate.TryEnter();
if (entered) {
s->active_count.fetch_add(1, ktl::memory_order_relaxed);
if (s->suspended.load(ktl::memory_order_relaxed) ||
s->gate.phase() == UartGate::Phase::kSuspended) {
s->saw_violation.store(true, ktl::memory_order_relaxed);
}
arch::Yield();
if (s->suspended.load(ktl::memory_order_relaxed) ||
s->gate.phase() == UartGate::Phase::kSuspended) {
s->saw_violation.store(true, ktl::memory_order_relaxed);
}
s->active_count.fetch_sub(1, ktl::memory_order_release);
s->gate.Exit();
}
// Spend some time outside the gate so that the drain is not starved by
// workers which may enter while draining.
Thread::Current::SleepRelative(ZX_USEC(10));
}
return 0;
};
thread_start_routine normal_worker = [](void* arg) -> int { return worker_fn(arg, false); };
thread_start_routine drain_worker = [](void* arg) -> int { return worker_fn(arg, true); };
constexpr size_t kNumWorkers = 4;
ktl::array<Thread*, kNumWorkers> workers{};
auto cleanup = fit::defer([&]() {
state.stop.store(true, ktl::memory_order_relaxed);
for (Thread* t : workers) {
if (t != nullptr) {
t->Join(nullptr, ZX_TIME_INFINITE);
}
}
});
for (size_t i = 0; i < kNumWorkers; ++i) {
workers[i] =
Thread::Create("uart_gate_test_worker", (i % 2 == 0) ? normal_worker : drain_worker, &state,
DEFAULT_PRIORITY);
ASSERT_NONNULL(workers[i]);
workers[i]->Resume();
}
constexpr size_t kNumCycles = 20;
for (size_t cycle = 0; cycle < kNumCycles; ++cycle) {
Thread::Current::SleepRelative(ZX_USEC(100));
state.gate.BeginDrain();
// A lost wakeup would cause this to hang.
do {
ASSERT_OK(state.gate.WaitForNoActiveUsers());
} while (!state.gate.TryFinishDrain());
EXPECT_EQ(UartGate::Phase::kSuspended, state.gate.phase());
EXPECT_EQ(0u, state.active_count.load(ktl::memory_order_acquire));
state.suspended.store(true, ktl::memory_order_relaxed);
Thread::Current::SleepRelative(ZX_USEC(100));
EXPECT_EQ(0u, state.active_count.load(ktl::memory_order_relaxed));
state.suspended.store(false, ktl::memory_order_release);
state.gate.Resume();
}
state.stop.store(true, ktl::memory_order_relaxed);
for (Thread*& t : workers) {
ASSERT_OK(t->Join(nullptr, ZX_TIME_INFINITE));
t = nullptr;
}
EXPECT_FALSE(state.saw_violation.load(ktl::memory_order_relaxed));
EXPECT_EQ(0u, state.active_count.load(ktl::memory_order_relaxed));
END_TEST;
}
} // namespace
UNITTEST_START_TESTCASE(uart_gate_tests)
UNITTEST("initial_state", InitialStateTest)
UNITTEST("drain_with_no_active_users", DrainWithNoActiveUsersTest)
UNITTEST("drain_waits_for_active_users", DrainWaitsForActiveUsersTest)
UNITTEST("cancel_drain", CancelDrainTest)
UNITTEST("wait_is_woken_by_last_exit", WaitIsWokenByLastExitTest)
UNITTEST("last_exit_signals", LastExitSignalsTest)
UNITTEST("wait_notices_last_exit_without_waking", WaitNoticesLastExitWithoutWakingTest)
UNITTEST("concurrent_enter_exit_and_suspend_resume", ConcurrentEnterExitAndSuspendResumeTest)
UNITTEST_END_TESTCASE(uart_gate_tests, "uart-gate", "UartGate tests")