Skip to content

Commit 24ea099

Browse files
committed
🔀 Merge branch 'main' into update-docs
2 parents 668e84d + cbd2668 commit 24ea099

File tree

6 files changed

+194
-14
lines changed

6 files changed

+194
-14
lines changed

‎CMakeLists.txt‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -140,6 +140,7 @@ if(BUILD_TESTING)
140140
add_executable(serial_tests
141141
test/test_serial_simple.cpp
142142
test/test_serial_pty.cpp
143+
test/test_ports.cpp
143144
)
144145

145146
target_include_directories(serial_tests PRIVATE

‎docs/api_reference.rst‎

Lines changed: 18 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -12,14 +12,28 @@ Classes
1212
.. doxygenclass:: libserial::Ports
1313
:members:
1414

15+
.. doxygenclass:: libserial::Device
16+
:members:
17+
1518
Exceptions
1619
----------
1720

1821
.. doxygenclass:: libserial::SerialException
1922
:members:
2023

21-
Data Structures
22-
---------------
24+
Enumerations
25+
------------
26+
27+
.. doxygenenum:: libserial::Parity
28+
29+
.. doxygenenum:: libserial::StopBits
30+
31+
.. doxygenenum:: libserial::FlowControl
32+
33+
.. doxygenenum:: libserial::CanonicalMode
34+
35+
.. doxygenenum:: libserial::Terminator
36+
37+
.. doxygenenum:: libserial::BaudRate
2338

24-
.. doxygenstruct:: libserial::DeviceStruct
25-
:members:
39+
.. doxygenenum:: libserial::DataLength

‎include/libserial/ports.hpp‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,15 @@ class Ports {
4040
*/
4141
Ports() = default;
4242

43+
/**
44+
* @brief Testable constructor allowing a custom system path
45+
*
46+
* Primarily intended for testing to inject a non-existent or
47+
* non-readable directory to validate error handling paths.
48+
*/
49+
explicit Ports(const char* sys_path) : sys_path_(sys_path) {
50+
}
51+
4352
/**
4453
* @brief Destroyer of the Ports class
4554
*
@@ -50,9 +59,7 @@ Ports() = default;
5059
* @brief Scans the system for available serial ports
5160
*
5261
* @return uint16_t The number of serial ports found
53-
* @throws SerialException if scanning fails
5462
* @throws SerialException if no ports are found
55-
* @throws PermissionDeniedException if insufficient permissions
5663
*/
5764
uint16_t scanPorts();
5865

@@ -98,6 +105,11 @@ std::optional<std::string> findName(uint16_t id) const;
98105
*/
99106
static constexpr const char* kSysSerialByIdPath = "/dev/serial/by-id/";
100107

108+
/**
109+
* @brief Configurable path used by scanPorts; defaults to kSysSerialByIdPath
110+
*/
111+
const char* sys_path_ { kSysSerialByIdPath };
112+
101113
/**
102114
* @brief Internal list of detected serial devices
103115
*/

‎include/libserial/serial_exception.hpp‎

Lines changed: 62 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -20,16 +20,73 @@ namespace libserial {
2020
*/
2121
class SerialException : public std::exception {
2222
public:
23-
explicit SerialException(std::string message) : message_(std::move(message)) {
23+
explicit SerialException(std::string message)
24+
: message_(std::move(message)) {
2425
}
25-
2626
const char* what() const noexcept override {
2727
return message_.c_str();
2828
}
29-
3029
private:
31-
std::string message_; // NOLINT(runtime/string)
32-
};
30+
std::string message_;
31+
}; // class SerialException
32+
33+
/**
34+
* @class PortNotFoundException
35+
* @brief Exception class for port not found errors
36+
*
37+
* The PortNotFoundException class is derived from SerialException
38+
* and is used to indicate that a specified serial port could not be found.
39+
*/
40+
class PortNotFoundException : public SerialException {
41+
public:
42+
explicit PortNotFoundException(std::string message)
43+
: SerialException(std::move(message)) {
44+
}
45+
}; // class PortNotFoundException
46+
47+
/**
48+
* @class PermissionDeniedException
49+
* @brief Exception class for permission denied errors
50+
*
51+
* The PermissionDeniedException class is derived from SerialException
52+
* and is used to indicate that permission to access a specified serial port was denied.
53+
*/
54+
class PermissionDeniedException : public SerialException {
55+
public:
56+
explicit PermissionDeniedException(std::string message)
57+
: SerialException(std::move(message)) {
58+
}
59+
}; // class PermissionDeniedException
60+
61+
/**
62+
* @class TimeoutException
63+
* @brief Exception class for timeout errors
64+
*
65+
* The TimeoutException class is derived from SerialException
66+
* and is used to indicate that a serial port operation has timed out.
67+
*/
68+
class TimeoutException : public SerialException {
69+
public:
70+
explicit TimeoutException(std::string message)
71+
: SerialException(std::move(message)) {
72+
}
73+
}; // class TimeoutException
74+
75+
/**
76+
* @class IOException
77+
* @brief Exception class for I/O errors
78+
*
79+
* The IOException class is derived from SerialException
80+
* and is used to indicate that an I/O error has occurred during
81+
* serial port operations.
82+
*/
83+
class IOException : public SerialException {
84+
public:
85+
explicit IOException(std::string message)
86+
: SerialException(std::move(message)) {
87+
}
88+
}; // class IOException
89+
3390
} // namespace libserial
3491

3592
#endif // INCLUDE_LIBSERIAL_SERIAL_EXCEPTION_HPP_

‎src/ports.cpp‎

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ uint16_t Ports::scanPorts() {
2323

2424
// Directory where udev creates symlinks for serial devices by ID
2525
// this directory may not exist if no serial devices are connected
26-
const char* by_id_dir = kSysSerialByIdPath;
26+
const char* by_id_dir = sys_path_;
2727
DIR* dir = opendir(by_id_dir);
2828
if (!dir) {
29-
std::cout << "No serial devices directory: " << by_id_dir << "\n";
30-
return -1;
29+
throw PortNotFoundException("Error while reading " + std::string(by_id_dir) + ": " +
30+
strerror(errno));
3131
}
3232

3333
// The POSIX directory-entry structure used by readdir() to describe files

‎test/test_ports.cpp‎

Lines changed: 96 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,96 @@
1+
// Copyright 2020-2025 Nestor Neto
2+
3+
#include <gtest/gtest.h>
4+
#include <cstdlib>
5+
#include <cstring>
6+
#include <dirent.h>
7+
#include <unistd.h>
8+
#include <memory>
9+
#include <string>
10+
#include <iostream>
11+
12+
#include "libserial/ports.hpp"
13+
#include "libserial/serial_exception.hpp"
14+
15+
// Simple unit tests that don't require actual hardware
16+
class PortsTest : public ::testing::Test {
17+
protected:
18+
void SetUp() override {
19+
char temp_template[] = "/tmp/fake_serial_XXXXXX";
20+
ASSERT_NE(mkdtemp(temp_template), nullptr) << "Failed to create temp directory";
21+
temp_dir_ = temp_template;
22+
}
23+
24+
void TearDown() override {
25+
// Clean up all symlinks in temp_dir_
26+
DIR* dir = opendir(temp_dir_.c_str());
27+
if (dir) {
28+
struct dirent* entry;
29+
while ((entry = readdir(dir)) != nullptr) {
30+
if (entry->d_name[0] != '.') {
31+
std::string path = temp_dir_ + "/" + entry->d_name;
32+
unlink(path.c_str());
33+
}
34+
}
35+
closedir(dir);
36+
}
37+
rmdir(temp_dir_.c_str());
38+
}
39+
40+
std::string temp_dir_;
41+
};
42+
43+
TEST_F(PortsTest, DefaultConstructor) {
44+
EXPECT_NO_THROW({
45+
libserial::Ports ports;
46+
});
47+
}
48+
49+
TEST_F(PortsTest, ScanPortsThrowsWhenPathMissing) {
50+
// Use an unlikely path to exist to trigger the exception path
51+
const char* missing_path = "/this/path/should/not/exist/serial/by-id";
52+
libserial::Ports ports(missing_path);
53+
54+
55+
try {
56+
(void)ports.scanPorts();
57+
FAIL() << "Expected libserial::SerialException to be thrown";
58+
}
59+
catch (const libserial::SerialException& e) {
60+
std::cout << "Caught SerialException: " << e.what() << std::endl;
61+
// Optionally assert something about the message:
62+
EXPECT_NE(std::string(e.what()).find("Error while reading"), std::string::npos);
63+
}
64+
catch (...) {
65+
FAIL() << "Expected libserial::SerialException, but got a different exception";
66+
}
67+
}
68+
69+
TEST_F(PortsTest, ScanPortsWithFakeDevices) {
70+
// Create fake device symlinks
71+
std::string fake_device1 = std::string(temp_dir_) + "/usb-FTDI_FT232R_USB_UART_A1B2C3D4";
72+
std::string fake_device2 = std::string(temp_dir_) + "/usb-Arduino_Uno_12345678";
73+
74+
// Create symlinks pointing to fake /dev/ttyUSB* paths
75+
// The actual target doesn't need to exist for scanPorts to process it
76+
ASSERT_EQ(symlink("../../ttyUSB0", fake_device1.c_str()), 0);
77+
ASSERT_EQ(symlink("../../ttyUSB1", fake_device2.c_str()), 0);
78+
79+
// Test scanPorts with the fake directory
80+
libserial::Ports ports(temp_dir_.c_str());
81+
uint16_t count = 0;
82+
EXPECT_NO_THROW({
83+
count = ports.scanPorts();
84+
});
85+
86+
EXPECT_EQ(count, 2) << "Should find 2 fake devices";
87+
88+
EXPECT_EQ(ports.findName(1).value(), "usb-FTDI_FT232R_USB_UART_A1B2C3D4");
89+
EXPECT_EQ(ports.findName(0).value(), "usb-Arduino_Uno_12345678");
90+
EXPECT_EQ(ports.findPortPath(1).value(), "/dev/ttyUSB0");
91+
EXPECT_EQ(ports.findPortPath(0).value(), "/dev/ttyUSB1");
92+
EXPECT_EQ(ports.findBusPath(1).value(), "/dev/ttyUSB0");
93+
EXPECT_EQ(ports.findBusPath(0).value(), "/dev/ttyUSB1");
94+
}
95+
96+

0 commit comments

Comments
 (0)