Problem
The current design tightly couples the Serial class to POSIX system calls such as read, write, poll, and ioctl.
This makes it difficult to properly unit test error handling and edge cases, such as:
- partial reads/writes
- interrupted system calls (EINTR)
- non-blocking behavior (EAGAIN, EWOULDBLOCK)
- system-level failures (EIO, EBADF, etc.)
Currently, testing these scenarios requires introducing test-only hooks guarded by conditional compilation (e.g., #ifdef BUILD_TESTING_ON). This approach has some drawbacks:
- it creates different class definitions between test and production builds
- it exposes internal state manipulation (e.g., injecting file descriptors)
- it reduces test realism by relying on non-public APIs
I want to inject custom implementations of system calls so that I can deterministically test error handling and edge cases without modifying the production API or relying on conditional compilation.
Proposed solution
Introduce an explicit dependency injection mechanism for system calls used by the Serial class.
One possible approach is to define a Syscalls structure:
struct Syscalls {
std::function<ssize_t(int, void*, size_t)> read = ::read;
std::function<ssize_t(int, const void*, size_t)> write = ::write;
std::function<int(struct pollfd*, nfds_t, int)> poll = ::poll;
std::function<int(int, unsigned long, void*)> ioctl = ::ioctl;
};
Then allow injection via constructor:
class Serial {
public:
explicit Serial(const Syscalls& sys = {});
private:
Syscalls sys_;
};
All internal calls to system functions would then use sys_.read, sys_.write, etc.
Key properties:
- Default behavior remains unchanged (uses real POSIX calls)
- Tests can inject custom behavior (e.g., simulate errors or partial operations)
- No need for conditional compilation or test-only APIs
- Maintains a single consistent class definition across builds
Alternatives considered
- Conditional compilation (#ifdef BUILD_TESTING_ON)
- Currently used to inject test hooks
- Drawbacks:
- creates different APIs between builds
- introduces test-only methods into the class
- harder to maintain and reason about
- Public setter methods for system functions
- Example: setReadSystemFunction(...), setWriteSystemFunction(...)
- Simpler to implement, but:
- allows mutation of behavior after construction
- can lead to inconsistent object state
- less explicit than constructor-based injection
- Mocking at a higher level (e.g., pseudo-terminals only)
- Works for integration tests
- Does not allow deterministic testing of error paths or rare conditions
Impact
- Public API changes? yes
adds an optional constructor parameter or overload
- Examples/docs updates needed? yes
to document the injection mechanism and its intended use for testing/mocking
Additional context
This change would improve:
- testability (especially for error handling and edge cases)
- maintainability (removes need for test-only compilation paths)
- extensibility (e.g., simulation, custom backends, or embedded environments)
The proposed approach is backward-compatible if default system functions are provided, so existing users would not need to change their code.
Problem
The current design tightly couples the Serial class to POSIX system calls such as read, write, poll, and ioctl.
This makes it difficult to properly unit test error handling and edge cases, such as:
Currently, testing these scenarios requires introducing test-only hooks guarded by conditional compilation (e.g., #ifdef BUILD_TESTING_ON). This approach has some drawbacks:
I want to inject custom implementations of system calls so that I can deterministically test error handling and edge cases without modifying the production API or relying on conditional compilation.
Proposed solution
Introduce an explicit dependency injection mechanism for system calls used by the Serial class.
One possible approach is to define a Syscalls structure:
Then allow injection via constructor:
All internal calls to system functions would then use sys_.read, sys_.write, etc.
Key properties:
Alternatives considered
Impact
adds an optional constructor parameter or overload
to document the injection mechanism and its intended use for testing/mocking
Additional context
This change would improve:
The proposed approach is backward-compatible if default system functions are provided, so existing users would not need to change their code.