blob: 30150c07b25cc9dd46bbdeb73cf2e87cf746783d [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_linux/notifier.h"
#include <sys/eventfd.h>
#include <chrono>
#include <thread>
#include "log_errno.h"
#include "pw_assert/check.h"
#include "pw_log/log.h"
#include "pw_sync/counting_semaphore.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;
constexpr auto kWaitForDataTimeout = 100ms;
class NotifierTest : public ::testing::Test {
protected:
void SetUp() override {
ASSERT_OK_AND_ASSIGN(notifier_, LinuxGpioNotifier::Create());
}
LinuxGpioNotifier& notifier() { return *notifier_; }
private:
std::shared_ptr<LinuxGpioNotifier> notifier_;
};
class FakeLine : public LinuxGpioNotifier::Handler {
public:
explicit FakeLine(LinuxGpioNotifier& notifier)
: notifier_(notifier), event_fd_(eventfd(0, EFD_SEMAPHORE)) {
PW_CHECK_INT_GE(event_fd_, 0, "Failed to create event fd: %d", errno);
}
~FakeLine() override {
// Unregister now, if still registered, and make errors fail the test.
EXPECT_OK(Unregister());
int result = close(event_fd_);
PW_CHECK_INT_EQ(result, 0, "Failed to close event fd, err %d", errno);
}
FakeLine(const FakeLine&) = delete;
FakeLine& operator=(const FakeLine&) = delete;
// Register the line with the notifier.
pw::Status Register() {
PW_CHECK(!registered_);
registered_ = true;
return notifier_.RegisterLine(event_fd_, *this);
}
// Unregister the line from the notifier.
pw::Status Unregister() {
if (!registered_) {
return OkStatus();
}
registered_ = false;
return notifier_.UnregisterLine(event_fd_);
}
// Atomically send one or more events.
void SendEvents(int count) {
PW_CHECK_INT_GE(count, 1, "Must send one or more events");
uint64_t data = count;
ssize_t result = write(event_fd_, &data, sizeof(data));
PW_CHECK_INT_EQ(result,
sizeof(data),
"Failed to write to event fd: " ERRNO_FORMAT_STRING,
ERRNO_FORMAT_ARGS(errno));
}
// Wait until the line has received an event or internal timeout expires.
bool TryWaitForData() {
return received_events_.try_acquire_for(kWaitForDataTimeout);
}
unsigned int total_received_events() const { return total_received_events_; }
protected:
// Implement LinuxGpioNotifier::Handler
void HandleEvents() override {
uint64_t val;
auto size_read = read(event_fd_, &val, sizeof(val));
PW_CHECK_INT_GE(size_read,
1,
"Failed to read an event: " ERRNO_FORMAT_STRING,
ERRNO_FORMAT_ARGS(errno));
++total_received_events_;
received_events_.release(1);
}
LinuxGpioNotifier& notifier() { return notifier_; }
int event_fd() const { return event_fd_; }
private:
LinuxGpioNotifier& notifier_;
int event_fd_;
unsigned int total_received_events_ = 0;
pw::sync::CountingSemaphore received_events_;
bool registered_ = false;
};
TEST_F(NotifierTest, TestNoEvent) {
FakeLine line(notifier());
ASSERT_OK(line.Register());
constexpr auto timeout = 0; // Don't block
auto result = notifier().WaitForEvents(timeout);
EXPECT_EQ(result.status(), pw::Status::DeadlineExceeded());
EXPECT_EQ(line.total_received_events(), 0u);
EXPECT_OK(line.Unregister());
}
TEST_F(NotifierTest, TestSendReceiveOneEventManual) {
FakeLine line(notifier());
ASSERT_OK(line.Register());
constexpr unsigned int num_events = 1;
line.SendEvents(num_events);
constexpr auto timeout = 0; // Don't block
ASSERT_OK_AND_ASSIGN(unsigned int count, notifier().WaitForEvents(timeout));
EXPECT_EQ(count, num_events);
EXPECT_EQ(line.total_received_events(), num_events);
EXPECT_OK(line.Unregister());
}
TEST_F(NotifierTest, TestSendReceiveMultipleEventsManual) {
FakeLine line(notifier());
ASSERT_OK(line.Register());
constexpr unsigned int num_events = 4;
line.SendEvents(num_events);
// WaitForEvents will only handle one event per line, per iteration.
// So call it in a loop until it expires.
unsigned int total_result = 0;
while (true) {
constexpr auto timeout = 0; // Don't block
auto result = notifier().WaitForEvents(timeout);
if (!result.ok()) {
EXPECT_EQ(result.status(), pw::Status::DeadlineExceeded());
break;
}
total_result += result.value();
}
EXPECT_EQ(total_result, num_events);
EXPECT_EQ(line.total_received_events(), num_events);
EXPECT_OK(line.Unregister());
}
TEST_F(NotifierTest, TestSendReceiveEventsThread) {
FakeLine line(notifier());
ASSERT_OK(line.Register());
pw::thread::Thread notif_thread(pw::thread::stl::Options(), notifier());
constexpr unsigned int num_events = 3;
line.SendEvents(num_events);
while (line.TryWaitForData()) {
}
EXPECT_EQ(line.total_received_events(), num_events);
EXPECT_OK(line.Unregister());
notifier().CancelWait();
notif_thread.join();
}
TEST_F(NotifierTest, TestRegisterLineMultipleLinesThread) {
// Make primary line
FakeLine line1(notifier());
ASSERT_OK(line1.Register());
pw::thread::Thread notif_thread(pw::thread::stl::Options(), notifier());
line1.SendEvents(1);
EXPECT_TRUE(line1.TryWaitForData());
{
// Make secondary line in smaller scope
FakeLine line2(notifier());
ASSERT_OK(line2.Register());
line1.SendEvents(1);
line2.SendEvents(1);
EXPECT_TRUE(line1.TryWaitForData());
EXPECT_TRUE(line2.TryWaitForData());
EXPECT_OK(line2.Unregister());
}
// Line 1 is once again the only line that is registered.
line1.SendEvents(1);
EXPECT_TRUE(line1.TryWaitForData());
EXPECT_OK(line1.Unregister());
notifier().CancelWait();
notif_thread.join();
}
} // namespace
} // namespace pw::digital_io