Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions cores/ardulinux/linux/LinuxSerial.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -302,8 +302,18 @@ namespace arduino {
return -1;
}

/** No-op: stdout is unbuffered at this level. */
void SimSerial::flush(void) {}
/**
* Push any buffered stdout to the underlying fd.
*
* stdout is line-buffered (see the constructor), so a completed log line
* leaves on its own newline. A partial line -- Serial.print() with no
* trailing newline, e.g. a prompt or a progress counter -- sits in the
* buffer until the next newline or exit. Arduino's flush() contract is
* "wait for outgoing data to be transmitted", so honour it here.
*/
void SimSerial::flush(void) {
fflush(stdout);
}

/**
* Write one byte to stdout.
Expand Down
2 changes: 1 addition & 1 deletion cores/ardulinux/linux/LinuxSerial.h
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ namespace arduino {
virtual int peek(void);
/** Always returns -1. */
virtual int read(void);
/** No-op. */
/** Push buffered stdout out now (flushes partial, newline-less lines). */
virtual void flush(void);
/**
* Write one byte to stdout.
Expand Down
2 changes: 1 addition & 1 deletion platform.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "ardulinux",
"version": "0.2.2",
"version": "0.2.3",
"title": "ArduLinux",
"description": "ArduLinux - Arduino API for Linux",
"keywords": ["arduino", "linux", "native", "posix", "gpio", "spi", "i2c", "portduino"],
Expand Down
87 changes: 82 additions & 5 deletions tests/unit/test_sim_serial.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,10 @@

#include <catch2/catch_test_macros.hpp>
#include "linux/LinuxSerial.h"
#include <string>
#include <stdio.h>
#include <unistd.h>
#include <fcntl.h>

// ─── SimSerial API contract ───────────────────────────────────────────────────

Expand Down Expand Up @@ -56,11 +60,6 @@ TEST_CASE("SimSerial::end is a no-op and does not crash", "[serial][sim]") {
CHECK_NOTHROW(sim.end());
}

TEST_CASE("SimSerial::flush is a no-op and does not crash", "[serial][sim]") {
arduino::SimSerial sim;
CHECK_NOTHROW(sim.flush());
}

// ─── Global Serial instance ───────────────────────────────────────────────────

TEST_CASE("arduino::Serial (global SimSerial) is always true", "[serial][sim]") {
Expand All @@ -70,3 +69,81 @@ TEST_CASE("arduino::Serial (global SimSerial) is always true", "[serial][sim]")
TEST_CASE("arduino::Serial::available returns 0", "[serial][sim]") {
CHECK(arduino::Serial.available() == 0);
}

// ─── stdout buffering and flush ───────────────────────────────────────────────
//
// SimSerial's constructor line-buffers stdout once, at static init, so Serial
// output reaches a non-TTY sink (pipe, file, journald) on each newline instead
// of sitting in stdio's full-buffer until exit. flush() covers the remainder:
// a partial, newline-less line has nothing to trigger the line flush.
//
// Both are asserted behaviourally, by pointing fd 1 at a pipe and reading back
// what actually arrived.

namespace {

/**
* Run @p emit with fd 1 pointed at a pipe; return the bytes that reached the
* pipe by the time @p emit returned.
*
* The read is non-blocking and happens before any fflush() of our own: a
* courtesy flush here would push the data through even when stdout is fully
* buffered, and mask exactly the regression these tests exist to catch.
*
* @param emit Callable that writes via arduino::Serial.
* @return What landed in the pipe; empty if stdout held onto it.
*/
template <typename F>
std::string capture_stdout_fd(F emit) {
// Drain anything the harness left pending so the pipe sees only emit()'s bytes.
fflush(stdout);

int pipefd[2];
REQUIRE(pipe(pipefd) == 0);
// Nothing may ever be written; the read has to report that rather than block.
REQUIRE(fcntl(pipefd[0], F_SETFL, O_NONBLOCK) == 0);

int saved = dup(STDOUT_FILENO);
REQUIRE(saved != -1);
REQUIRE(dup2(pipefd[1], STDOUT_FILENO) != -1);

emit();

char buf[128] = {0};
ssize_t n = read(pipefd[0], buf, sizeof(buf) - 1);

// Restore fd 1 before the caller asserts, so failures still reach the console.
dup2(saved, STDOUT_FILENO);
close(saved);
close(pipefd[1]);
close(pipefd[0]);

return (n > 0) ? std::string(buf, static_cast<size_t>(n)) : std::string();
}

} // namespace

TEST_CASE("SimSerial constructor line-buffers stdout", "[serial][sim][buffering]") {
// The trailing newline is what triggers the flush under _IOLBF.
std::string out = capture_stdout_fd([] { arduino::Serial.println("LINEBUF"); });
CHECK(out.find("LINEBUF") != std::string::npos);
}

TEST_CASE("SimSerial holds a partial line until flushed", "[serial][sim][buffering]") {
// No newline, so line buffering has nothing to act on. This is the control
// for the flush test below: without it, that test would pass even if
// stdout were unbuffered and flush() still a no-op.
std::string out = capture_stdout_fd([] { arduino::Serial.print("HELD"); });
CHECK(out.find("HELD") == std::string::npos);

// Drop the held bytes on the real stdout rather than leaving them buffered.
arduino::Serial.flush();
}

TEST_CASE("SimSerial::flush pushes a partial line out", "[serial][sim][buffering]") {
std::string out = capture_stdout_fd([] {
arduino::Serial.print("PARTIAL");
arduino::Serial.flush();
});
CHECK(out.find("PARTIAL") != std::string::npos);
}