blob: b559b9d4f5c10983ae201aa6b9fdf6fda18ffc25 [file] [log] [blame]
// Copyright 2024 The Pigweed Authors
//
// Licensed under the Apache License, Version 2.0 (the "License"); you may not
// use this file except in compliance with the License. You may obtain a copy of
// the License at
//
// https://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
// WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
// License for the specific language governing permissions and limitations under
// the License.
#include "pw_digital_io/digital_io.h"
#include <linux/gpio.h>
#include <algorithm>
#include <functional>
#include <memory>
#include <mutex>
#include <queue>
#include <vector>
#include "mock_vfs.h"
#include "pw_digital_io_linux/digital_io.h"
#include "pw_log/log.h"
#include "pw_result/result.h"
#include "pw_sync/mutex.h"
#include "pw_sync/timed_thread_notification.h"
#include "pw_thread/thread.h"
#include "pw_thread_stl/options.h"
#include "pw_unit_test/framework.h"
#include "test_utils.h"
namespace pw::digital_io {
namespace {
using namespace std::chrono_literals;
class DigitalIoTest;
class LineHandleFile;
class LineEventFile;
// Represents a mocked in-kernel GPIO line object.
class Line {
public:
//
// Harness-side interface: Intended for use by DigitalIoTest and the
// MockFile subclasses.
//
explicit Line(uint32_t index) : index_(index) {}
// Get the logical value of the line, respecting active_low.
Result<bool> GetValue() const {
// Linux lets you read the value of an output.
if (requested_ == RequestedState::kNone) {
PW_LOG_ERROR("Cannot get value of unrequested line");
return Status::FailedPrecondition();
}
return physical_state_ ^ active_low_;
}
// Set the logical value of the line, respecting active_low.
// Returns OK on success; FAILED_PRECONDITION if not requested as output.
Status SetValue(bool value) {
if (requested_ != RequestedState::kOutput) {
PW_LOG_ERROR("Cannot set value of line not requested as output");
return Status::FailedPrecondition();
}
physical_state_ = value ^ active_low_;
PW_LOG_DEBUG("Set line %u to physical %u", index_, physical_state_);
return OkStatus();
}
Status RequestInput(LineHandleFile* handle, bool active_low) {
PW_TRY(DoRequest(RequestedState::kInput, active_low));
current_line_handle_ = handle;
return OkStatus();
}
Status RequestInputInterrupt(LineEventFile* handle, bool active_low) {
PW_TRY(DoRequest(RequestedState::kInputInterrupt, active_low));
current_event_handle_ = handle;
return OkStatus();
}
Status RequestOutput(LineHandleFile* handle, bool active_low) {
PW_TRY(DoRequest(RequestedState::kOutput, active_low));
current_line_handle_ = handle;
return OkStatus();
}
void ClearRequest() {
requested_ = RequestedState::kNone;
current_line_handle_ = nullptr;
current_event_handle_ = nullptr;
}
//
// Test-side interface: Intended for use by the tests themselves.
//
enum class RequestedState {
kNone, // Not requested by "userspace"
kInput, // Requested by "userspace" as an input
kInputInterrupt, // Requested by "userspace" as an interrupt (event)
kOutput, // Requested by "userspace" as an output
};
RequestedState requested() const { return requested_; }
LineHandleFile* current_line_handle() const { return current_line_handle_; }
LineEventFile* current_event_handle() const { return current_event_handle_; }
void ForcePhysicalState(bool state) { physical_state_ = state; }
bool physical_state() const { return physical_state_; }
private:
const uint32_t index_;
bool physical_state_ = false;
RequestedState requested_ = RequestedState::kNone;
bool active_low_ = false;
LineHandleFile* current_line_handle_ = nullptr;
LineEventFile* current_event_handle_ = nullptr;
Status DoRequest(RequestedState request, bool active_low) {
if (requested_ != RequestedState::kNone) {
PW_LOG_ERROR("Cannot request already-requested line");
return Status::FailedPrecondition();
}
requested_ = request;
active_low_ = active_low;
return OkStatus();
}
};
#define EXPECT_LINE_NOT_REQUESTED(line) \
EXPECT_EQ(line.requested(), Line::RequestedState::kNone)
#define EXPECT_LINE_REQUESTED_OUTPUT(line) \
EXPECT_EQ(line.requested(), Line::RequestedState::kOutput)
#define EXPECT_LINE_REQUESTED_INPUT(line) \
EXPECT_EQ(line.requested(), Line::RequestedState::kInput)
#define EXPECT_LINE_REQUESTED_INPUT_INTERRUPT(line) \
EXPECT_EQ(line.requested(), Line::RequestedState::kInputInterrupt)
// Represents a GPIO line handle, the result of issuing
// GPIO_GET_LINEHANDLE_IOCTL to an open chip file.
class LineHandleFile : public MockFile {
public:
LineHandleFile(MockVfs& vfs, int eventfd, const std::string& name, Line& line)
: MockFile(vfs, eventfd, name), line_(line) {}
private:
Line& line_;
//
// MockFile impl.
//
int DoClose() override {
line_.ClearRequest();
return 0;
}
int DoIoctl(unsigned long request, void* arg) override {
switch (request) {
case GPIOHANDLE_GET_LINE_VALUES_IOCTL:
return DoIoctlGetValues(static_cast<struct gpiohandle_data*>(arg));
case GPIOHANDLE_SET_LINE_VALUES_IOCTL:
return DoIoctlSetValues(static_cast<struct gpiohandle_data*>(arg));
default:
PW_LOG_ERROR("%s: Unhandled request=0x%lX", __FUNCTION__, request);
return -1;
}
}
// Handle GPIOHANDLE_GET_LINE_VALUES_IOCTL
int DoIoctlGetValues(struct gpiohandle_data* data) {
auto result = line_.GetValue();
if (!result.ok()) {
return -1;
}
data->values[0] = *result;
return 0;
}
// Handle GPIOHANDLE_SET_LINE_VALUES_IOCTL
int DoIoctlSetValues(struct gpiohandle_data* data) {
auto status = line_.SetValue(data->values[0]);
if (!status.ok()) {
return -1;
}
return 0;
}
};
// Represents a GPIO line event handle, the result of issuing
// GPIO_GET_LINEEVENT_IOCTL to an open chip file.
class LineEventFile final : public MockFile {
public:
LineEventFile(MockVfs& vfs,
int eventfd,
const std::string& name,
Line& line,
uint32_t event_flags)
: MockFile(vfs, eventfd, name), line_(line), event_flags_(event_flags) {}
void EnqueueEvent(const struct gpioevent_data& event) {
static_assert(GPIOEVENT_REQUEST_RISING_EDGE == GPIOEVENT_EVENT_RISING_EDGE);
static_assert(GPIOEVENT_REQUEST_FALLING_EDGE ==
GPIOEVENT_EVENT_FALLING_EDGE);
if ((event.id & event_flags_) == 0) {
return;
}
{
std::lock_guard lock(mutex_);
event_queue_.push(event);
}
// Make this file's fd readable (one token).
WriteEventfd();
}
private:
Line& line_;
uint32_t const event_flags_;
std::queue<struct gpioevent_data> event_queue_;
pw::sync::Mutex mutex_;
// Hide these
using MockFile::ReadEventfd;
using MockFile::WriteEventfd;
//
// MockFile impl.
//
int DoClose() override {
line_.ClearRequest();
return 0;
}
int DoIoctl(unsigned long request, void* arg) override {
switch (request) {
case GPIOHANDLE_GET_LINE_VALUES_IOCTL:
return DoIoctlGetValues(static_cast<struct gpiohandle_data*>(arg));
// Unlinke LineHandleFile, this only supports "get", as it is only for
// inputs.
default:
PW_LOG_ERROR("%s: Unhandled request=0x%lX", __FUNCTION__, request);
return -1;
}
}
int DoIoctlGetValues(struct gpiohandle_data* data) {
auto result = line_.GetValue();
if (!result.ok()) {
return -1;
}
data->values[0] = *result;
return 0;
}
ssize_t DoRead(void* buf, size_t count) override {
// Consume the readable state of the eventfd (one token).
PW_CHECK_INT_EQ(ReadEventfd(), 1); // EFD_SEMAPHORE
std::lock_guard lock(mutex_);
// Pop the event from the queue.
PW_CHECK(!event_queue_.empty());
struct gpioevent_data event = event_queue_.front();
if (count < sizeof(event)) {
return -1;
}
event_queue_.pop();
memcpy(buf, &event, sizeof(event));
return sizeof(event);
}
};
// Represents an open GPIO chip file, the result of opening /dev/gpiochip*.
class ChipFile : public MockFile {
public:
ChipFile(MockVfs& vfs,
int eventfd,
const std::string& name,
std::vector<Line>& lines)
: MockFile(vfs, eventfd, name), lines_(lines) {}
private:
std::vector<Line>& lines_;
//
// MockFile impl.
//
int DoIoctl(unsigned long request, void* arg) override {
switch (request) {
case GPIO_GET_LINEHANDLE_IOCTL:
return DoLinehandleIoctl(static_cast<struct gpiohandle_request*>(arg));
case GPIO_GET_LINEEVENT_IOCTL:
return DoLineeventIoctl(static_cast<struct gpioevent_request*>(arg));
default:
PW_LOG_ERROR("%s: Unhandled request=0x%lX", __FUNCTION__, request);
return -1;
}
}
// Handle GPIO_GET_LINEHANDLE_IOCTL
int DoLinehandleIoctl(struct gpiohandle_request* req) {
uint32_t const direction =
req->flags & (GPIOHANDLE_REQUEST_OUTPUT | GPIOHANDLE_REQUEST_INPUT);
// Validate flags.
if (direction == (GPIOHANDLE_REQUEST_OUTPUT | GPIOHANDLE_REQUEST_INPUT)) {
PW_LOG_ERROR("%s: OUTPUT and INPUT are mutually exclusive", __FUNCTION__);
return -1;
}
// Only support requesting one line at at time.
if (req->lines != 1) {
PW_LOG_ERROR("%s: Unsupported req->lines=%u", __FUNCTION__, req->lines);
return -1;
}
uint32_t const offset = req->lineoffsets[0];
uint8_t const default_value = req->default_values[0];
bool const active_low = req->flags & GPIOHANDLE_REQUEST_ACTIVE_LOW;
if (offset >= lines_.size()) {
PW_LOG_ERROR("%s: Invalid line offset: %u", __FUNCTION__, offset);
return -1;
}
Line& line = lines_[offset];
auto file = vfs().MakeFile<LineHandleFile>("line-handle", line);
// Ownership: The vfs owns this file, but the line borrows a reference to
// it. This is safe because the file's Close() method undoes that borrow.
Status status = OkStatus();
switch (direction) {
case GPIOHANDLE_REQUEST_OUTPUT:
status.Update(line.RequestOutput(file.get(), active_low));
status.Update(line.SetValue(default_value));
break;
case GPIOHANDLE_REQUEST_INPUT:
status.Update(line.RequestInput(file.get(), active_low));
break;
}
if (!status.ok()) {
return -1;
}
req->fd = vfs().InstallFile(std::move(file));
return 0;
}
int DoLineeventIoctl(struct gpioevent_request* req) {
uint32_t const direction = req->handleflags & (GPIOHANDLE_REQUEST_OUTPUT |
GPIOHANDLE_REQUEST_INPUT);
bool const active_low = req->handleflags & GPIOHANDLE_REQUEST_ACTIVE_LOW;
uint32_t const offset = req->lineoffset;
if (direction != GPIOHANDLE_REQUEST_INPUT) {
PW_LOG_ERROR("%s: Only input is supported by this ioctl", __FUNCTION__);
return -1;
}
if (offset >= lines_.size()) {
PW_LOG_ERROR("%s: Invalid line offset: %u", __FUNCTION__, offset);
return -1;
}
Line& line = lines_[offset];
auto file =
vfs().MakeFile<LineEventFile>("line-event", line, req->eventflags);
// Ownership: The vfs() owns this file, but the line borrows a reference to
// it. This is safe because the file's Close() method undoes that borrow.
Status status = line.RequestInputInterrupt(file.get(), active_low);
if (!status.ok()) {
return -1;
}
req->fd = vfs().InstallFile(std::move(file));
return 0;
}
};
// Test fixture for all digtal io tests.
class DigitalIoTest : public ::testing::Test {
protected:
void SetUp() override { GetMockVfs().Reset(); }
void TearDown() override { EXPECT_TRUE(GetMockVfs().AllFdsClosed()); }
LinuxDigitalIoChip OpenChip() {
int fd = GetMockVfs().InstallNewFile<ChipFile>("chip", lines_);
return LinuxDigitalIoChip(fd);
}
Line& line0() { return lines_[0]; }
Line& line1() { return lines_[1]; }
private:
std::vector<Line> lines_ = std::vector<Line>{
Line(0), // Input
Line(1), // Output
};
};
//
// Tests
//
TEST_F(DigitalIoTest, DoInput) {
LinuxDigitalIoChip chip = OpenChip();
auto& line = line0();
LinuxInputConfig config(
/* index= */ 0,
/* polarity= */ Polarity::kActiveHigh);
ASSERT_OK_AND_ASSIGN(auto input, chip.GetInputLine(config));
// Enable the input, and ensure it is requested.
EXPECT_LINE_NOT_REQUESTED(line);
ASSERT_OK(input.Enable());
EXPECT_LINE_REQUESTED_INPUT(line);
Result<State> state;
// Force the line high and assert it is seen as active (active high).
line.ForcePhysicalState(true);
state = input.GetState();
ASSERT_OK(state.status());
ASSERT_EQ(State::kActive, state.value());
// Force the line low and assert it is seen as inactive (active high).
line.ForcePhysicalState(false);
state = input.GetState();
ASSERT_OK(state.status());
ASSERT_EQ(State::kInactive, state.value());
// Disable the line and ensure it is no longer requested.
ASSERT_OK(input.Disable());
EXPECT_LINE_NOT_REQUESTED(line);
}
TEST_F(DigitalIoTest, DoInputInvert) {
LinuxDigitalIoChip chip = OpenChip();
auto& line = line0();
LinuxInputConfig config(
/* index= */ 0,
/* polarity= */ Polarity::kActiveLow);
ASSERT_OK_AND_ASSIGN(auto input, chip.GetInputLine(config));
// Enable the input, and ensure it is requested.
EXPECT_LINE_NOT_REQUESTED(line);
ASSERT_OK(input.Enable());
EXPECT_LINE_REQUESTED_INPUT(line);
Result<State> state;
// Force the line high and assert it is seen as inactive (active low).
line.ForcePhysicalState(true);
state = input.GetState();
ASSERT_OK(state.status());
ASSERT_EQ(State::kInactive, state.value());
// Force the line low and assert it is seen as active (active low).
line.ForcePhysicalState(false);
state = input.GetState();
ASSERT_OK(state.status());
ASSERT_EQ(State::kActive, state.value());
// Disable the line and ensure it is no longer requested.
ASSERT_OK(input.Disable());
EXPECT_LINE_NOT_REQUESTED(line);
}
TEST_F(DigitalIoTest, DoOutput) {
LinuxDigitalIoChip chip = OpenChip();
auto& line = line1();
LinuxOutputConfig config(
/* index= */ 1,
/* polarity= */ Polarity::kActiveHigh,
/* default_state= */ State::kActive);
ASSERT_OK_AND_ASSIGN(auto output, chip.GetOutputLine(config));
// Enable the output, and ensure it is requested.
EXPECT_LINE_NOT_REQUESTED(line);
ASSERT_OK(output.Enable());
EXPECT_LINE_REQUESTED_OUTPUT(line);
// Expect the line to go high, due to default_state=kActive (active high).
ASSERT_TRUE(line.physical_state());
// Set the output's state to inactive, and assert it goes low (active high).
ASSERT_OK(output.SetStateInactive());
ASSERT_FALSE(line.physical_state());
// Set the output's state to active, and assert it goes high (active high).
ASSERT_OK(output.SetStateActive());
ASSERT_TRUE(line.physical_state());
// Disable the line and ensure it is no longer requested.
ASSERT_OK(output.Disable());
EXPECT_LINE_NOT_REQUESTED(line);
// NOTE: We do not assert line.physical_state() here.
// See the warning on LinuxDigitalOut in docs.rst.
}
TEST_F(DigitalIoTest, DoOutputInvert) {
LinuxDigitalIoChip chip = OpenChip();
auto& line = line1();
LinuxOutputConfig config(
/* index= */ 1,
/* polarity= */ Polarity::kActiveLow,
/* default_state= */ State::kActive);
ASSERT_OK_AND_ASSIGN(auto output, chip.GetOutputLine(config));
// Enable the output, and ensure it is requested.
EXPECT_LINE_NOT_REQUESTED(line);
ASSERT_OK(output.Enable());
EXPECT_LINE_REQUESTED_OUTPUT(line);
// Expect the line to stay low, due to default_state=kActive (active low).
ASSERT_FALSE(line.physical_state());
// Set the output's state to inactive, and assert it goes high (active low).
ASSERT_OK(output.SetStateInactive());
ASSERT_TRUE(line.physical_state());
// Set the output's state to active, and assert it goes low (active low).
ASSERT_OK(output.SetStateActive());
ASSERT_FALSE(line.physical_state());
// Disable the line and ensure it is no longer requested.
ASSERT_OK(output.Disable());
EXPECT_LINE_NOT_REQUESTED(line);
// NOTE: We do not assert line.physical_state() here.
// See the warning on LinuxDigitalOut in docs.rst.
}
// Verify we can get the state of an output.
TEST_F(DigitalIoTest, OutputGetState) {
LinuxDigitalIoChip chip = OpenChip();
auto& line = line1();
LinuxOutputConfig config(
/* index= */ 1,
/* polarity= */ Polarity::kActiveHigh,
/* default_state= */ State::kInactive);
ASSERT_OK_AND_ASSIGN(auto output, chip.GetOutputLine(config));
ASSERT_OK(output.Enable());
// Expect the line to stay low, due to default_state=kInactive (active high).
ASSERT_FALSE(line.physical_state());
Result<State> state;
// Verify GetState() returns the expected state: inactive (default_state).
state = output.GetState();
ASSERT_OK(state.status());
ASSERT_EQ(State::kInactive, state.value());
// Set the output's state to active, then verify GetState() returns the
// new expected state.
ASSERT_OK(output.SetStateActive());
state = output.GetState();
ASSERT_OK(state.status());
ASSERT_EQ(State::kActive, state.value());
}
//
// Input interrupts
//
TEST_F(DigitalIoTest, DoInputInterruptsEnabledBefore) {
LinuxDigitalIoChip chip = OpenChip();
ASSERT_OK_AND_ASSIGN(auto notifier, LinuxGpioNotifier::Create());
auto& line = line0();
LinuxInputConfig config(
/* index= */ 0,
/* polarity= */ Polarity::kActiveHigh);
ASSERT_OK_AND_ASSIGN(auto input, chip.GetInterruptLine(config, notifier));
EXPECT_LINE_NOT_REQUESTED(line);
// Have to set a handler before we can enable interrupts.
ASSERT_OK(input.SetInterruptHandler(InterruptTrigger::kActivatingEdge,
[](State) {}));
// pw_digital_io says the line should be enabled before calling
// EnableInterruptHandler(), but we explicitly support it being called with
// the line disabled to avoid an unnecessary file close/reopen.
ASSERT_OK(input.EnableInterruptHandler());
ASSERT_OK(input.Enable());
// Interrupts requested; should be a line event handle.
EXPECT_LINE_REQUESTED_INPUT_INTERRUPT(line);
// Disable; nothing should be requested.
ASSERT_OK(input.Disable());
EXPECT_LINE_NOT_REQUESTED(line);
}
TEST_F(DigitalIoTest, DoInputInterruptsEnabledAfter) {
LinuxDigitalIoChip chip = OpenChip();
ASSERT_OK_AND_ASSIGN(auto notifier, LinuxGpioNotifier::Create());
auto& line = line0();
LinuxInputConfig config(
/* index= */ 0,
/* polarity= */ Polarity::kActiveHigh);
ASSERT_OK_AND_ASSIGN(auto input, chip.GetInterruptLine(config, notifier));
EXPECT_LINE_NOT_REQUESTED(line);
ASSERT_OK(input.Enable());
// No interrupts requested; should be a normal line handle.
EXPECT_LINE_REQUESTED_INPUT(line);
// Interrupts requested while enabled; should be a line event handle.
// Have to set a handler before we can enable interrupts.
ASSERT_OK(input.SetInterruptHandler(InterruptTrigger::kActivatingEdge,
[](State) {}));
ASSERT_OK(input.EnableInterruptHandler());
EXPECT_LINE_REQUESTED_INPUT_INTERRUPT(line);
// Interrupts disabled while enabled; should revert to a normal line handle.
ASSERT_OK(input.DisableInterruptHandler());
EXPECT_LINE_REQUESTED_INPUT(line);
// Disable; nothing should be requested.
ASSERT_OK(input.Disable());
EXPECT_LINE_NOT_REQUESTED(line);
}
TEST_F(DigitalIoTest, DoInputInterruptsReadOne) {
LinuxDigitalIoChip chip = OpenChip();
ASSERT_OK_AND_ASSIGN(auto notifier, LinuxGpioNotifier::Create());
auto& line = line0();
LinuxInputConfig config(
/* index= */ 0,
/* polarity= */ Polarity::kActiveHigh);
ASSERT_OK_AND_ASSIGN(auto input, chip.GetInterruptLine(config, notifier));
std::vector<State> interrupts;
auto handler = [&interrupts](State state) {
PW_LOG_DEBUG("Interrupt handler fired with state=%s",
state == State::kActive ? "active" : "inactive");
interrupts.push_back(state);
};
ASSERT_OK(
input.SetInterruptHandler(InterruptTrigger::kActivatingEdge, handler));
// pw_digital_io says the line should be enabled before calling
// EnableInterruptHandler(), but we explicitly support it being called with
// the line disabled to avoid an unnecessary file close/reopen.
ASSERT_OK(input.EnableInterruptHandler());
ASSERT_OK(input.Enable());
EXPECT_LINE_REQUESTED_INPUT_INTERRUPT(line);
LineEventFile* evt = line.current_event_handle();
ASSERT_NE(evt, nullptr);
evt->EnqueueEvent({
.timestamp = 1122334455667788,
.id = GPIOEVENT_EVENT_RISING_EDGE,
});
constexpr int timeout = 0; // Don't block
PW_LOG_DEBUG("WaitForEvents(%d)", timeout);
ASSERT_OK_AND_ASSIGN(unsigned int count, notifier->WaitForEvents(timeout));
EXPECT_EQ(count, 1u);
EXPECT_EQ(interrupts,
std::vector<State>({
State::kActive,
}));
}
TEST_F(DigitalIoTest, DoInputInterruptsThread) {
LinuxDigitalIoChip chip = OpenChip();
ASSERT_OK_AND_ASSIGN(auto notifier, LinuxGpioNotifier::Create());
auto& line = line0();
LinuxInputConfig config(
/* index= */ 0,
/* polarity= */ Polarity::kActiveHigh);
ASSERT_OK_AND_ASSIGN(auto input, chip.GetInterruptLine(config, notifier));
constexpr unsigned int kCount = 10;
struct {
sync::TimedThreadNotification done;
std::vector<State> interrupts;
void HandleInterrupt(State state) {
interrupts.push_back(state);
if (interrupts.size() == kCount) {
done.release();
}
}
} context;
auto handler = [&context](State state) {
PW_LOG_DEBUG("Interrupt handler fired with state=%s",
state == State::kActive ? "active" : "inactive");
context.HandleInterrupt(state);
};
ASSERT_OK(input.SetInterruptHandler(InterruptTrigger::kBothEdges, handler));
// pw_digital_io says the line should be enabled before calling
// EnableInterruptHandler(), but we explicitly support it being called with
// the line disabled to avoid an unnecessary file close/reopen.
ASSERT_OK(input.EnableInterruptHandler());
ASSERT_OK(input.Enable());
// Run a notifier thread.
pw::thread::Thread notif_thread(pw::thread::stl::Options(), *notifier);
EXPECT_LINE_REQUESTED_INPUT_INTERRUPT(line);
LineEventFile* evt = line.current_event_handle();
ASSERT_NE(evt, nullptr);
// Feed the line with events.
auto nth_event = [](unsigned int i) -> uint32_t {
return (i % 2) ? GPIOEVENT_EVENT_FALLING_EDGE : GPIOEVENT_EVENT_RISING_EDGE;
};
auto nth_state = [](unsigned int i) -> State {
return (i % 2) ? State::kInactive : State::kActive;
};
for (unsigned int i = 0; i < kCount; i++) {
evt->EnqueueEvent({
.timestamp = 1122334400000000u + i,
.id = nth_event(i),
});
}
// Wait for the notifier to pick them all up.
constexpr auto kWaitForDataTimeout = 1000ms;
ASSERT_TRUE(context.done.try_acquire_for(kWaitForDataTimeout));
// Stop the notifier thread.
notifier->CancelWait();
notif_thread.join();
// Verify we received all of the expected callbacks.
EXPECT_EQ(context.interrupts.size(), kCount);
for (unsigned int i = 0; i < kCount; i++) {
EXPECT_EQ(context.interrupts[i], nth_state(i));
}
}
} // namespace
} // namespace pw::digital_io