blob: bd1ef089c9bf5b0effafa2ad56b5d4ab7c81be8c [file] [log] [blame]
// Copyright 2023 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
#include "src/graphics/display/drivers/amlogic-display/hot-plug-detection.h"
#include <fidl/fuchsia.hardware.gpio/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/driver/testing/cpp/driver_runtime.h>
#include <lib/driver/testing/cpp/scoped_global_logger.h>
#include <lib/zx/clock.h>
#include <lib/zx/interrupt.h>
#include <lib/zx/result.h>
#include <zircon/assert.h>
#include <zircon/compiler.h>
#include <zircon/types.h>
#include <atomic>
#include <cstddef>
#include <memory>
#include <vector>
#include <fbl/auto_lock.h>
#include <fbl/mutex.h>
#include <gmock/gmock.h>
#include <gtest/gtest.h>
#include "src/devices/gpio/testing/fake-gpio/fake-gpio.h"
#include "src/lib/testing/predicates/status.h"
namespace amlogic_display {
namespace {
struct GpioResources {
fidl::ClientEnd<fuchsia_hardware_gpio::Gpio> client;
zx::interrupt interrupt;
};
class HotPlugDetectionTest : public ::testing::Test {
public:
void SetUp() override {
fake_gpio_loop_.StartThread("fake-gpio-loop");
pin_gpio_.SetDefaultReadResponse(zx::ok(uint8_t{0u}));
}
void TearDown() override { fake_gpio_loop_.Shutdown(); }
fidl::ClientEnd<fuchsia_hardware_gpio::Gpio> GetPinGpioClient() {
auto [gpio_client, gpio_server] = fidl::Endpoints<fuchsia_hardware_gpio::Gpio>::Create();
fidl::BindServer(fake_gpio_loop_.dispatcher(), std::move(gpio_server), &pin_gpio_);
return std::move(gpio_client);
}
GpioResources GetPinGpioResources() {
fidl::ClientEnd<fuchsia_hardware_gpio::Gpio> pin_gpio_client = GetPinGpioClient();
zx::interrupt pin_gpio_interrupt = runtime_.PerformBlockingWork([&]() -> zx::interrupt {
fidl::WireResult result = fidl::WireCall(pin_gpio_client)->GetInterrupt({});
ZX_ASSERT_MSG(result.ok(), "FIDL connection failed: %s", result.status_string());
fidl::WireResultUnwrapType<fuchsia_hardware_gpio::Gpio::GetInterrupt>& interrupt_value =
result.value();
ZX_ASSERT_MSG(interrupt_value.is_ok(), "GPIO GetInterrupt failed: %s",
zx_status_get_string(interrupt_value.error_value()));
return std::move(interrupt_value.value()->interrupt);
});
return {
.client = std::move(pin_gpio_client),
.interrupt = std::move(pin_gpio_interrupt),
};
}
std::unique_ptr<HotPlugDetection> CreateAndInitHotPlugDetection() {
// The existing pin GPIO interrupt may be invalid or have been destroyed
// and cannot be used; we need to create a new virtual interrupt for each
// new HotPlugDetection created.
ResetPinGpioInterrupt();
GpioResources pin_gpio_resources = GetPinGpioResources();
zx::result<fdf::SynchronizedDispatcher> create_dispatcher_result =
fdf::SynchronizedDispatcher::Create(fdf::SynchronizedDispatcher::Options::kAllowSyncCalls,
"hot-plug-detection-thread",
/*shutdown_handler=*/[](fdf_dispatcher_t*) {},
/*scheduler_role=*/{});
ZX_ASSERT(create_dispatcher_result.status_value() == ZX_OK);
fdf::SynchronizedDispatcher dispatcher = std::move(create_dispatcher_result).value();
auto hpd = std::make_unique<HotPlugDetection>(
std::move(pin_gpio_resources.client), std::move(pin_gpio_resources.interrupt),
[this](HotPlugDetectionState state) { RecordHotPlugDetectionState(state); },
std::move(dispatcher));
// HotPlugDetection::Init() sets up the GPIO using synchronous FIDL calls.
// The fake GPIO FIDL server can only be bound on the test thread's default
// dispatcher, so Init() must be called on another thread.
zx::result<> init_result = runtime_.PerformBlockingWork([&] { return hpd->Init(); });
EXPECT_OK(init_result);
return hpd;
}
void DestroyHotPlugDetection(std::unique_ptr<HotPlugDetection>& hpd) {
// The driver framework shuts down all dispatchers before destroying the
// HotPlugDetection instance.
runtime_.ShutdownAllDispatchers(/*dut_initial_dispatcher=*/nullptr);
hpd.reset();
}
void ResetPinGpioInterrupt() {
zx_status_t status =
zx::interrupt::create(zx::resource(), 0u, ZX_INTERRUPT_VIRTUAL, &pin_gpio_interrupt_);
ASSERT_OK(status);
zx::interrupt gpio_interrupt;
status = pin_gpio_interrupt_.duplicate(ZX_RIGHT_SAME_RIGHTS, &gpio_interrupt);
ASSERT_OK(status);
pin_gpio_.SetInterrupt(zx::ok(std::move(gpio_interrupt)));
}
void RecordHotPlugDetectionState(HotPlugDetectionState state) {
fbl::AutoLock lock(&mutex_);
recorded_detection_states_.push_back(state);
}
std::vector<HotPlugDetectionState> GetHotPlugDetectionStates() const {
fbl::AutoLock lock(&mutex_);
return recorded_detection_states_;
}
protected:
fdf_testing::ScopedGlobalLogger logger_;
fdf_testing::DriverRuntime runtime_;
async::Loop fake_gpio_loop_{&kAsyncLoopConfigNeverAttachToThread};
fake_gpio::FakeGpio pin_gpio_;
zx::interrupt pin_gpio_interrupt_;
mutable fbl::Mutex mutex_;
std::vector<HotPlugDetectionState> recorded_detection_states_ __TA_GUARDED(&mutex_);
};
TEST_F(HotPlugDetectionTest, NoHotplugEvents) {
std::unique_ptr<HotPlugDetection> hpd = CreateAndInitHotPlugDetection();
DestroyHotPlugDetection(hpd);
}
TEST_F(HotPlugDetectionTest, DisplayPlug) {
std::unique_ptr<HotPlugDetection> hpd = CreateAndInitHotPlugDetection();
pin_gpio_interrupt_.trigger(0u, zx::clock::get_boot());
pin_gpio_.SetDefaultReadResponse(zx::ok(uint8_t{1u}));
runtime_.RunUntil([&] { return GetHotPlugDetectionStates().size() >= 1; });
EXPECT_THAT(GetHotPlugDetectionStates(), testing::ElementsAre(HotPlugDetectionState::kDetected));
EXPECT_EQ(pin_gpio_.GetInterruptMode(), fuchsia_hardware_gpio::InterruptMode::kLevelLow);
DestroyHotPlugDetection(hpd);
}
TEST_F(HotPlugDetectionTest, DisplayPlugUnplug) {
std::unique_ptr<HotPlugDetection> hpd = CreateAndInitHotPlugDetection();
// Simulate plugging the display.
pin_gpio_interrupt_.trigger(0u, zx::clock::get_boot());
pin_gpio_.SetDefaultReadResponse(zx::ok(uint8_t{1u}));
runtime_.RunUntil([&] { return GetHotPlugDetectionStates().size() >= 1; });
EXPECT_THAT(GetHotPlugDetectionStates(), testing::ElementsAre(HotPlugDetectionState::kDetected));
// Simulate unplugging the display.
pin_gpio_interrupt_.trigger(0u, zx::clock::get_boot());
pin_gpio_.SetDefaultReadResponse(zx::ok(uint8_t{0u}));
runtime_.RunUntil([&] { return GetHotPlugDetectionStates().size() >= 2; });
EXPECT_THAT(
GetHotPlugDetectionStates(),
testing::ElementsAre(HotPlugDetectionState::kDetected, HotPlugDetectionState::kNotDetected));
EXPECT_EQ(pin_gpio_.GetInterruptMode(), fuchsia_hardware_gpio::InterruptMode::kLevelHigh);
DestroyHotPlugDetection(hpd);
}
TEST_F(HotPlugDetectionTest, SpuriousPlugInterrupt) {
std::unique_ptr<HotPlugDetection> hpd = CreateAndInitHotPlugDetection();
const size_t num_state_changes_before_hotplug_gpio_read = pin_gpio_.GetStateLog().size();
std::atomic<bool> hotplug_gpio_read = false;
pin_gpio_interrupt_.trigger(0u, zx::clock::get_boot());
pin_gpio_.PushReadCallback([&](fake_gpio::FakeGpio& gpio) {
hotplug_gpio_read.store(true, std::memory_order_relaxed);
return zx::ok(uint8_t{0});
});
runtime_.RunUntil([&] { return hotplug_gpio_read.load(std::memory_order_relaxed); });
const size_t num_state_changes_after_hotplug_gpio_read = pin_gpio_.GetStateLog().size();
// The GPIO state (polarity, input / output config) should not change if the
// GPIO reading doesn't change.
EXPECT_EQ(num_state_changes_after_hotplug_gpio_read, num_state_changes_before_hotplug_gpio_read);
EXPECT_THAT(GetHotPlugDetectionStates(), testing::IsEmpty());
DestroyHotPlugDetection(hpd);
}
TEST_F(HotPlugDetectionTest, SpuriousUnplugInterrupt) {
std::unique_ptr<HotPlugDetection> hpd = CreateAndInitHotPlugDetection();
std::atomic<bool> first_hotplug_gpio_read = false;
pin_gpio_interrupt_.trigger(0u, zx::clock::get_boot());
pin_gpio_.PushReadCallback([&](fake_gpio::FakeGpio& gpio) {
first_hotplug_gpio_read.store(true, std::memory_order_relaxed);
return zx::ok(uint8_t{1});
});
runtime_.RunUntil([&] {
return first_hotplug_gpio_read.load(std::memory_order_relaxed) &&
GetHotPlugDetectionStates().size() >= 1;
});
EXPECT_THAT(GetHotPlugDetectionStates(), testing::ElementsAre(HotPlugDetectionState::kDetected));
const size_t num_state_changes_before_second_hotplug_gpio_read = pin_gpio_.GetStateLog().size();
std::atomic<bool> second_hotplug_gpio_read = false;
pin_gpio_interrupt_.trigger(0u, zx::clock::get_boot());
pin_gpio_.PushReadCallback([&](fake_gpio::FakeGpio& gpio) {
second_hotplug_gpio_read.store(true, std::memory_order_relaxed);
return zx::ok(uint8_t{1});
});
runtime_.RunUntil([&] { return second_hotplug_gpio_read.load(std::memory_order_relaxed); });
const size_t num_state_changes_after_second_hotplug_gpio_read = pin_gpio_.GetStateLog().size();
EXPECT_THAT(GetHotPlugDetectionStates(), testing::ElementsAre(HotPlugDetectionState::kDetected));
// The GPIO state (polarity, input / output config) should not change if the
// GPIO reading doesn't change.
EXPECT_EQ(num_state_changes_after_second_hotplug_gpio_read,
num_state_changes_before_second_hotplug_gpio_read);
DestroyHotPlugDetection(hpd);
}
} // namespace
} // namespace amlogic_display