diff --git a/include/libserial/serial.hpp b/include/libserial/serial.hpp index 4c3ebcd..5483122 100644 --- a/include/libserial/serial.hpp +++ b/include/libserial/serial.hpp @@ -8,13 +8,16 @@ #include #include #include +#include #include #include #include +#include #include #include #include +#include #include #include #include @@ -227,7 +230,7 @@ void setDataLength(DataLength nbits); * @param parity The desired parity setting (ENABLE or DISABLE) * @throws SerialException if parity cannot be set */ -void setParity([[maybe_unused]] Parity parity); +void setParity(Parity parity); /** * @brief Sets the stop bits configuration @@ -237,7 +240,7 @@ void setParity([[maybe_unused]] Parity parity); * @param stop_bits The desired stop bits setting (ONE, ONE_AND_HALF, or TWO) * @throws SerialException if stop bits cannot be set */ -void setStopBits([[maybe_unused]] StopBits stop_bits); +void setStopBits(StopBits stop_bits); /** * @brief Sets the flow control configuration @@ -315,9 +318,41 @@ int getBaudRate() const; void setFdForTest(int fd) { fd_serial_port_ = fd; } + +// For testing - allow injection of mock functions +void setSystemCallFunctions( + std::function poll_func, + std::function read_func) { + poll_ = [poll_func](struct pollfd* f, nfds_t n, int t) { + return poll_func(f, n, t); + }; + read_ = [read_func](int fd, void* buf, size_t sz) { + return read_func(fd, buf, sz); + }; +} #endif private: +/** + * @brief Poll system call function wrapper + * + * Allows injection of custom poll function for testing. + */ +std::function poll_ = + [](struct pollfd* f, nfds_t n, int t) { + return ::poll(f, n, t); + }; + +/** + * @brief Read system call function wrapper + * + * Allows injection of custom read function for testing. + */ +std::function read_ = + [](int fd, void* buf, size_t sz) { + return ::read(fd, buf, sz); + }; + /** * @brief Applies terminal settings to the port * diff --git a/src/serial.cpp b/src/serial.cpp index 29f92c8..ff70861 100644 --- a/src/serial.cpp +++ b/src/serial.cpp @@ -73,19 +73,17 @@ size_t Serial::read(std::shared_ptr buffer) { // 0 => no wait (immediate return), -1 => block forever, positive => wait specified milliseconds int timeout_ms = static_cast(read_timeout_ms_.count()); - int pr = poll(&fd_poll, 1, timeout_ms); + int pr = poll_(&fd_poll, 1, timeout_ms); if (pr < 0) { - if (errno == EINTR) { - throw IOException("Interrupted while polling"); - } throw IOException(std::string("Error in poll(): ") + strerror(errno)); } if (pr == 0) { - throw IOException("Read operation timed out after " + std::to_string(timeout_ms) + "ms"); + throw IOException("Read operation timed out after " + std::to_string(timeout_ms) + + " milliseconds"); } // Data available: do the read - ssize_t bytes_read = ::read(fd_serial_port_, const_cast(buffer->data()), kMaxSafeReadSize); + ssize_t bytes_read = read_(fd_serial_port_, const_cast(buffer->data()), kMaxSafeReadSize); if (bytes_read < 0) { throw IOException(std::string("Error reading from serial port: ") + strerror(errno)); } @@ -104,7 +102,7 @@ size_t Serial::readBytes(std::shared_ptr buffer, size_t num_bytes) } if (num_bytes == 0) { - throw IOException("Invalid number of bytes requested"); + throw IOException("Number of bytes requested must be greater than zero"); } buffer->clear(); @@ -157,7 +155,7 @@ size_t Serial::readUntil(std::shared_ptr buffer, char terminator) { int64_t remaining_timeout = read_timeout_ms_.count() - elapsed; int timeout_ms = static_cast(remaining_timeout); - int poll_result = poll(&pfd, 1, timeout_ms); + int poll_result = poll_(&pfd, 1, timeout_ms); if (poll_result < 0) { throw IOException("Error in poll(): " + std::string(strerror(errno))); } @@ -167,7 +165,7 @@ size_t Serial::readUntil(std::shared_ptr buffer, char terminator) { } // Data is available, perform the read - ssize_t bytes_read = ::read(fd_serial_port_, &temp_char, 1); + ssize_t bytes_read = read_(fd_serial_port_, &temp_char, 1); if (bytes_read < 0) { if (errno == EAGAIN || errno == EWOULDBLOCK) { @@ -175,11 +173,11 @@ size_t Serial::readUntil(std::shared_ptr buffer, char terminator) { std::this_thread::sleep_for(std::chrono::milliseconds(1)); continue; } - throw SerialException("Error reading from serial port: " + std::string(strerror(errno))); + throw IOException("Error reading from serial port: " + std::string(strerror(errno))); } else if (bytes_read == 0) { // End of file or connection closed - throw SerialException("Connection closed while reading"); + throw IOException("Connection closed while reading: no terminator found"); } // Add the character to buffer (including terminator) @@ -246,35 +244,30 @@ void Serial::setDataLength(DataLength nbits) { this->setTermios2(); } -void Serial::setParity([[maybe_unused]] Parity parity) { -// this->getTermios2(); -// switch (parity) { -// case Parity::DISABLE: -// options_.c_cflag &= ~PARENB; -// break; -// case Parity::ENABLE: -// options_.c_cflag |= PARENB; -// break; -// default: -// options_.c_cflag &= ~PARENB; -// break; -// } -// this->setTermios2(); +void Serial::setParity(Parity parity) { + this->getTermios2(); + switch (parity) { + case Parity::DISABLE: + options_.c_cflag &= ~PARENB; + break; + case Parity::ENABLE: + options_.c_cflag |= PARENB; + break; + } + this->setTermios2(); } -void Serial::setStopBits([[maybe_unused]] StopBits stop_bits) { -// this->getTermios2(); -// switch (stop_bits) { -// case StopBits::DISABLE: -// options_.c_cflag &= ~CSTOP; -// break; -// case StopBits::ENABLE: -// options_.c_cflag |= CSTOP; -// default: -// options_.c_cflag &= ~CSTOP; -// break; -// } -// this->setTermios2(); +void Serial::setStopBits(StopBits stop_bits) { + this->getTermios2(); + switch (stop_bits) { + case StopBits::ONE: + options_.c_cflag &= ~CSTOP; + break; + case StopBits::TWO: + options_.c_cflag |= CSTOP; + break; + } + this->setTermios2(); } void Serial::setFlowControl([[maybe_unused]] FlowControl flow_control) { diff --git a/test/test_ports.cpp b/test/test_ports.cpp index 8f9a165..c12f2ed 100644 --- a/test/test_ports.cpp +++ b/test/test_ports.cpp @@ -57,7 +57,6 @@ TEST_F(PortsTest, ScanPortsThrowsWhenPathMissing) { FAIL() << "Expected libserial::SerialException to be thrown"; } catch (const libserial::SerialException& e) { - GTEST_LOG_(INFO) << "Exception: " << e.what(); // Optionally assert something about the message: EXPECT_NE(std::string(e.what()).find("Error while reading"), std::string::npos); } diff --git a/test/test_serial_pty.cpp b/test/test_serial_pty.cpp index 003afa4..89047db 100644 --- a/test/test_serial_pty.cpp +++ b/test/test_serial_pty.cpp @@ -57,12 +57,35 @@ void SetUp() override { FAIL() << "Failed to open slave pseudo-terminal"; return; } + + errors_poll_ = { + {EAGAIN, "Resource temporarily unavailable"}, + {ENOMEM, "Cannot allocate memory"}, + {EINVAL, "Invalid argument"}, + {EPERM, "Operation not permitted"}, + {EBADF, "Bad file descriptor"}, + {EEXIST, "File exists"}, + {ENOENT, "No such file or directory"}, + {EINTR, "Interrupted system call"} + }; + + errors_read_ = { + {EBADF, "Bad file descriptor"}, + {EIO, "Input/output error"}, + {EINTR, "Interrupted system call"}, + {EAGAIN, "Resource temporarily unavailable"}, + {EWOULDBLOCK, "Resource temporarily unavailable"}, + {EISDIR, "Is a directory"} + }; } void TearDown() override { if (master_fd_ != -1) close(master_fd_); if (slave_fd_ != -1) close(slave_fd_); } + +std::vector > errors_poll_; +std::vector > errors_read_; }; TEST_F(PseudoTerminalTest, OpenClosePort) { @@ -102,73 +125,57 @@ TEST_F(PseudoTerminalTest, SetAndGetBaudRate) { serial_port.close(); } -TEST_F(PseudoTerminalTest, GetAvailableData) { +TEST_F(PseudoTerminalTest, SetParity) { libserial::Serial serial_port; serial_port.open(slave_port_); - serial_port.setBaudRate(9600); - - // First check that no data is available initially - int initial_available{0}; - EXPECT_NO_THROW({ initial_available = serial_port.getAvailableData(); }); - EXPECT_EQ(initial_available, 0); - const std::string test_message = "Hello World!\n"; - ssize_t bytes_written = write(master_fd_, test_message.c_str(), test_message.length()); - ASSERT_GT(bytes_written, 0) << "Failed to write to master end"; + // Set parity to ENABLE + EXPECT_NO_THROW({ serial_port.setParity(libserial::Parity::ENABLE); }); - // Force flush and give more time for data to propagate - fsync(master_fd_); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + // Set parity to ODD + EXPECT_NO_THROW({ serial_port.setParity(libserial::Parity::DISABLE); }); - // Now check available data again - should match what we wrote - int available{0}; - EXPECT_NO_THROW({ available = serial_port.getAvailableData(); }); - EXPECT_EQ(available, bytes_written); + serial_port.close(); } -TEST_F(PseudoTerminalTest, ReadUntil) { +TEST_F(PseudoTerminalTest, SetStopBits) { libserial::Serial serial_port; serial_port.open(slave_port_); - serial_port.setBaudRate(9600); - - const std::string test_message = "Read Until! Test!\n"; - ssize_t bytes_written = write(master_fd_, test_message.c_str(), test_message.length()); - ASSERT_GT(bytes_written, 0) << "Failed to write to master end"; - - // Give time for data to propagate - fsync(master_fd_); - std::this_thread::sleep_for(std::chrono::milliseconds(100)); + // Set stop bits to 1 + EXPECT_NO_THROW({ serial_port.setStopBits(libserial::StopBits::ONE); }); - // Test reading with shared pointer - only read what's available - auto read_buffer = std::make_shared(); + // Set stop bits to 2 + EXPECT_NO_THROW({ serial_port.setStopBits(libserial::StopBits::TWO); }); - EXPECT_NO_THROW({serial_port.readUntil(read_buffer, '!'); }); - - EXPECT_EQ(*read_buffer, "Read Until!"); + serial_port.close(); } -TEST_F(PseudoTerminalTest, ReadUntilTimeout) { +TEST_F(PseudoTerminalTest, GetAvailableData) { libserial::Serial serial_port; serial_port.open(slave_port_); serial_port.setBaudRate(9600); - const std::string test_message = "Read Until Test"; + // First check that no data is available initially + int initial_available{0}; + EXPECT_NO_THROW({ initial_available = serial_port.getAvailableData(); }); + EXPECT_EQ(initial_available, 0); + const std::string test_message = "Hello World!\n"; ssize_t bytes_written = write(master_fd_, test_message.c_str(), test_message.length()); ASSERT_GT(bytes_written, 0) << "Failed to write to master end"; - // Give time for data to propagate + // Force flush and give more time for data to propagate fsync(master_fd_); std::this_thread::sleep_for(std::chrono::milliseconds(100)); - // Test reading with shared pointer - only read what's available - auto read_buffer = std::make_shared(); - - EXPECT_THROW({serial_port.readUntil(read_buffer, '!'); }, libserial::IOException); + // Now check available data again - should match what we wrote + int available{0}; + EXPECT_NO_THROW({ available = serial_port.getAvailableData(); }); + EXPECT_EQ(available, bytes_written); } TEST_F(PseudoTerminalTest, WriteTest) { @@ -278,21 +285,81 @@ TEST_F(PseudoTerminalTest, ReadTimeout) { serial_port.setBaudRate(9600); // Set a short read timeout - serial_port.setReadTimeout(std::chrono::milliseconds(100)); + int time_out_ms = 100; + serial_port.setReadTimeout(std::chrono::milliseconds(time_out_ms)); auto read_buffer = std::make_shared(); + auto expected_what = "Read operation timed out after " + std::to_string(time_out_ms) + + " milliseconds"; + EXPECT_THROW({ try { serial_port.read(read_buffer); } catch (const libserial::IOException& e) { - GTEST_LOG_(INFO) << "Exception: " << e.what(); + EXPECT_STREQ(expected_what.c_str(), e.what()); throw; } }, libserial::IOException); } +TEST_F(PseudoTerminalTest, ReadWithReadFail) { + libserial::Serial serial_port; + auto read_buffer = std::make_shared(); + + for (const auto& [error_num, error_msg] : errors_read_) { + serial_port.setSystemCallFunctions( + [](struct pollfd*, nfds_t, int) -> int { + return 1; + }, + [error_num](int, void*, size_t) -> ssize_t { + errno = error_num; + return -1; + }); + + auto expected_what = "Error reading from serial port: " + error_msg; + + EXPECT_THROW({ + try { + serial_port.read(read_buffer); + } + catch (const libserial::IOException& e) { + EXPECT_STREQ(expected_what.c_str(), e.what()); + throw; + } + }, libserial::IOException); + } +} + +TEST_F(PseudoTerminalTest, ReadWithPollFail) { + libserial::Serial serial_port; + auto read_buffer = std::make_shared(); + + for (const auto& [error_num, error_msg] : errors_poll_) { + serial_port.setSystemCallFunctions( + [error_num](struct pollfd*, nfds_t, int) -> int { + errno = error_num; + return -1; + }, + [](int, void*, size_t) -> ssize_t { + return 1; + }); + + auto expected_what = "Error in poll(): " + error_msg; + + EXPECT_THROW({ + try { + serial_port.read(read_buffer); + } + catch (const libserial::IOException& e) { + EXPECT_STREQ(expected_what.c_str(), e.what()); + throw; + } + }, libserial::IOException); + } +} + TEST_F(PseudoTerminalTest, ReadBytesNonCanonicalMode) { libserial::Serial serial_port; @@ -333,7 +400,7 @@ TEST_F(PseudoTerminalTest, ReadBytesWithNullBuffer) { serial_port.readBytes(null_buffer, 10); } catch (const libserial::IOException& e) { - GTEST_LOG_(INFO) << "Exception: " << e.what(); + EXPECT_STREQ("Null pointer passed to readBytes function", e.what()); throw; } }, libserial::IOException); @@ -353,7 +420,7 @@ TEST_F(PseudoTerminalTest, ReadBytesWithInvalidNumBytes) { serial_port.readBytes(read_buffer, 0); } catch (const libserial::IOException& e) { - GTEST_LOG_(INFO) << "Exception: " << e.what(); + EXPECT_STREQ("Number of bytes requested must be greater than zero", e.what()); throw; } }, libserial::IOException); @@ -374,8 +441,114 @@ TEST_F(PseudoTerminalTest, ReadBytesCanonicalMode) { ADD_FAILURE() << "Expected SerialException but no exception was thrown"; } catch (const libserial::IOException& e) { - GTEST_LOG_(INFO) << "Exception: " << e.what(); + EXPECT_STREQ( + "readBytes() is not supported in canonical mode; use read() or readUntil() instead", + e.what()); throw; } }, libserial::IOException); } + +TEST_F(PseudoTerminalTest, ReadUntil) { + libserial::Serial serial_port; + + serial_port.open(slave_port_); + serial_port.setBaudRate(9600); + + const std::string test_message = "Read Until! Test!\n"; + + ssize_t bytes_written = write(master_fd_, test_message.c_str(), test_message.length()); + ASSERT_GT(bytes_written, 0) << "Failed to write to master end"; + + // Give time for data to propagate + fsync(master_fd_); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Test reading with shared pointer - only read what's available + auto read_buffer = std::make_shared(); + + EXPECT_NO_THROW({serial_port.readUntil(read_buffer, '!'); }); + + EXPECT_EQ(*read_buffer, "Read Until!"); +} + +TEST_F(PseudoTerminalTest, ReadUntilTimeout) { + libserial::Serial serial_port; + + serial_port.open(slave_port_); + serial_port.setBaudRate(9600); + + const std::string test_message = "Read Until Test"; + + ssize_t bytes_written = write(master_fd_, test_message.c_str(), test_message.length()); + ASSERT_GT(bytes_written, 0) << "Failed to write to master end"; + + // Give time for data to propagate + fsync(master_fd_); + std::this_thread::sleep_for(std::chrono::milliseconds(100)); + + // Test reading with shared pointer - only read what's available + auto read_buffer = std::make_shared(); + + EXPECT_THROW({serial_port.readUntil(read_buffer, '!'); }, libserial::IOException); +} + +TEST_F(PseudoTerminalTest, ReadUntilWithReadFail) { + libserial::Serial serial_port; + auto read_buffer = std::make_shared(); + + for (const auto& [error_num, error_msg] : errors_read_) { + if (error_num == EAGAIN || error_num == EWOULDBLOCK) { + // Skip these as they are handled differently in readUntil + continue; + } + serial_port.setSystemCallFunctions( + [](struct pollfd*, nfds_t, int) -> int { + return 1; + }, + [error_num](int, void*, size_t) -> ssize_t { + errno = error_num; + return -1; + }); + + auto expected_what = "Error reading from serial port: " + error_msg; + + EXPECT_THROW({ + try { + serial_port.readUntil(read_buffer, '!'); + } + catch (const libserial::IOException& e) { + EXPECT_STREQ(expected_what.c_str(), e.what()); + throw; + } + }, libserial::IOException); + } +} + +TEST_F(PseudoTerminalTest, ReadUntilWithPollFail) { + libserial::Serial serial_port; + auto read_buffer = std::make_shared(); + + for (const auto& [error_num, error_msg] : errors_poll_) { + serial_port.setSystemCallFunctions( + [error_num](struct pollfd*, nfds_t, int) -> int { + errno = error_num; + return -1; + }, + [](int, void*, size_t) -> ssize_t { + return 1; + }); + + auto expected_what = "Error in poll(): " + error_msg; + + EXPECT_THROW({ + try { + serial_port.readUntil(read_buffer, '!'); + } + catch (const libserial::IOException& e) { + EXPECT_STREQ(expected_what.c_str(), e.what()); + throw; + } + }, libserial::IOException); + } +}