Compare commits
17 Commits
2d519937b7
...
test/race_
| Author | SHA1 | Date | |
|---|---|---|---|
| f51e65444f | |||
| 79f5ad10b6 | |||
| 3d2c193db9 | |||
| e8da963919 | |||
| 4a743f26a4 | |||
| 2fe983d288 | |||
| b062dddff2 | |||
| bcba7fad09 | |||
| b8b83bc60b | |||
| 9cc912b0a3 | |||
| 32426b3028 | |||
| 040cf974f4 | |||
| 499584f856 | |||
| cc29845657 | |||
| dfd82b1619 | |||
| 8e471d3534 | |||
| da875871ef |
@@ -8,14 +8,31 @@ project(azkoyen_ipc_test LANGUAGES CXX)
|
||||
set(CMAKE_CXX_STANDARD 17)
|
||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||
|
||||
# Qt Setup — AUTOMOC runs moc automatically on Q_OBJECT headers
|
||||
find_package(Qt5 REQUIRED COMPONENTS Core Widgets Test)
|
||||
set(CMAKE_AUTOMOC ON)
|
||||
set(CMAKE_INCLUDE_CURRENT_DIR ON)
|
||||
|
||||
# Core library
|
||||
add_library(core
|
||||
src/core/SysfsRead.cxx
|
||||
src/core/UnixIpcBridge.cxx
|
||||
src/core/Producer.cxx
|
||||
src/core/Consumer.cxx
|
||||
include/Consumer.hpp
|
||||
)
|
||||
|
||||
target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
target_link_libraries(core PUBLIC Qt5::Core)
|
||||
|
||||
# Main Application
|
||||
add_executable(app
|
||||
src/app/main.cxx
|
||||
src/app/MainWindow.cxx
|
||||
include/MainWindow.hpp
|
||||
)
|
||||
target_include_directories(app PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}/include)
|
||||
target_link_libraries(app PRIVATE core Qt5::Widgets)
|
||||
|
||||
#tests
|
||||
enable_testing()
|
||||
|
||||
11
README.md
11
README.md
@@ -34,3 +34,14 @@ The reader never throws on I/O errors; every outcome is expressed through the en
|
||||
`UnixIpcBridge` ([include/UnixIpcBridge.hpp](include/UnixIpcBridge.hpp), [src/core/UnixIpcBridge.cxx](src/core/UnixIpcBridge.cxx)) is a small helper that connects to a UNIX domain socket and sends a single `int` per call. It opens a new connection for each value, which keeps the protocol stateless and simple.
|
||||
|
||||
**Tests:** [tests/test_unix_ipc.cxx](tests/test_unix_ipc.cxx) — spins up a fake socket server, sends values through the bridge, and asserts they arrive correctly.
|
||||
|
||||
## ConsumerThread
|
||||
|
||||
`ConsumerThread` ([include/ConsumerThread.hpp](include/ConsumerThread.hpp), [src/core/ConsumerThread.cxx](src/core/ConsumerThread.cxx)) is a `QObject` that listens on a UNIX domain socket in a background `std::thread`. On each received integer it:
|
||||
|
||||
1. Prints the value to `stdout`.
|
||||
2. Emits the `valueReceived(int)` Qt signal.
|
||||
|
||||
The server socket is created and bound inside `start()` **before** the thread is spawned, so the socket is guaranteed to be ready by the time `start()` returns — eliminating race conditions with the producer. Graceful shutdown is handled by `stop()`, which shuts down the file descriptor to unblock the blocking `accept()` call.
|
||||
|
||||
**Tests:** [tests/test_consumer_thread.cxx](tests/test_consumer_thread.cxx) — uses `QSignalSpy` to verify single-value, multi-value, negative, and zero reception.
|
||||
|
||||
5
docs/quality_description.md
Normal file
5
docs/quality_description.md
Normal file
@@ -0,0 +1,5 @@
|
||||
# Quality Description
|
||||
|
||||
Writing tests first forced every component to be injectable and independently exercisable before any integration happened. That constraint turned out to matter more than expected when the race-condition tests were added at the end: because the producer, sysfs reader, and IPC bridge had already been broken into units with explicit interfaces (`std::function` callbacks, injected sleep, injected logger), the stress tests could be wired up without touching any production code. Nothing needed to be refactored to be testable — it already was. That is the practical benefit of TDD for concurrent embedded software: the discipline of writing the test first tends to eliminate shared mutable state and deep call chains by making them painful to test, which in turn reduces cyclomatic complexity almost as a side effect.
|
||||
|
||||
|
||||
25
docs/self-assessment.md
Normal file
25
docs/self-assessment.md
Normal file
@@ -0,0 +1,25 @@
|
||||
# Self-Assessment
|
||||
|
||||
## Two Real Difficulties
|
||||
|
||||
**1. Maintaining TDD discipline under time pressure**
|
||||
|
||||
Sticking to a strict test-first workflow throughout the session was genuinely hard. Between the deadline and the accumulated fatigue of a full day of work beforehand, there were moments where the temptation to just write the implementation and then fill the tests was real. I did not always resist it. Some tests were written after the fact rather than before, which is something I am aware of and want to be honest about.
|
||||
|
||||
**2. Designing testable seams at the IPC and sysfs boundaries**
|
||||
|
||||
The components that most needed testing were also the ones most coupled to external resources: a live socket and a real sysfs path. The difficulty was finding the right abstraction level, too thin and the tests require actual kernel resources; too thick and you end up testing your mocks, not your logic. The solution was to inject the transport as a plain `std::function` callback into the producer, and to point the sysfs reader at a controlled fake file on disk. Both approaches keep the core logic testable with no sockets, no threads, and no Qt, but arriving at that boundary (deciding what to abstract and what to leave concrete) required more iteration than I anticipated.
|
||||
|
||||
---
|
||||
|
||||
## Alternative IPC Mechanism Considered
|
||||
|
||||
I evaluated POSIX shared memory with semaphores as an alternative to UNIX domain sockets. The theoretical appeal is clear: no serialization, no kernel-mediated data copy, potentially lower latency. However, I am considerably less practiced with `shm_open`/`mmap`/`sem_post` than I am with socket-based communication, and more importantly, shared memory is significantly harder to unit-test in isolation. Sockets expose a clean, file-descriptor-based interface that maps naturally to mock-able abstractions. Shared memory regions and semaphore lifecycles would have added complexity to the test harness for uncertain gain at this data rate. Domain sockets were the pragmatic choice.
|
||||
|
||||
---
|
||||
|
||||
## Design Decision Changed Mid-Development
|
||||
|
||||
Initially I had planned a looser boundary between the core logic and Qt, with the producer potentially depending on Qt primitives for threading or signalling. Early on, I decided to keep Qt strictly confined to the GUI layer and the consumer thread, nothing more. The producer, the sysfs reader, and the IPC bridge are plain C++ with no Qt dependency whatsoever.
|
||||
|
||||
The reason is simple: that code could be portable. If tomorrow the producer needs to run on a microcontroller, a bare-metal embedded target, or any environment where Qt is not available or not desirable, the only thing that needs replacing is the transport callback. The core logic moves untouched. It also makes unit-testing the producer significantly cleaner and easier, no Qt test infrastructure needed, just standard C++.
|
||||
44
include/Consumer.hpp
Normal file
44
include/Consumer.hpp
Normal file
@@ -0,0 +1,44 @@
|
||||
#pragma once
|
||||
// ConsumerThread.hpp
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Unai Blazquez <unaibg2000@gmail.com>
|
||||
#include <QObject>
|
||||
#include <atomic>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
|
||||
/// @brief Listens on a UNIX domain socket, receives integers from the
|
||||
/// producer via IPC, prints them to console, and emits a Qt signal.
|
||||
class ConsumerThread : public QObject
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
/// @brief Construct the consumer bound to a socket path.
|
||||
/// @param socket_path UNIX domain socket path to listen on.
|
||||
/// @param parent Optional QObject parent for memory management.
|
||||
explicit ConsumerThread(const std::string& socket_path,
|
||||
QObject* parent = nullptr);
|
||||
|
||||
~ConsumerThread() override;
|
||||
|
||||
/// @brief Start the listener thread. The server socket is ready
|
||||
/// when this function returns.
|
||||
void start();
|
||||
|
||||
/// @brief Stop the listener thread gracefully. Safe to call multiple times.
|
||||
void stop();
|
||||
|
||||
signals:
|
||||
/// @brief Emitted every time an integer is received from the producer.
|
||||
void valueReceived(int value);
|
||||
|
||||
private:
|
||||
/// @brief Main loop: accept → recv → print → emit. Runs in m_thread.
|
||||
void run_loop();
|
||||
|
||||
std::string m_socket_path;
|
||||
int m_server_fd = -1;
|
||||
std::atomic<bool> m_running{false};
|
||||
std::thread m_thread;
|
||||
};
|
||||
32
include/MainWindow.hpp
Normal file
32
include/MainWindow.hpp
Normal file
@@ -0,0 +1,32 @@
|
||||
#pragma once
|
||||
// MainWindow.hpp
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Author: Unai Blazquez <unaibg2000@gmail.com>
|
||||
|
||||
#include <QLabel>
|
||||
#include <QString>
|
||||
#include <QVBoxLayout>
|
||||
#include <QWidget>
|
||||
|
||||
/// @brief Minimal GUI window that displays the last integer received
|
||||
/// from the ConsumerThread. Never blocks — values arrive via
|
||||
/// Qt's queued signal/slot mechanism.
|
||||
class MainWindow : public QWidget
|
||||
{
|
||||
Q_OBJECT
|
||||
|
||||
public:
|
||||
explicit MainWindow(QWidget* parent = nullptr);
|
||||
|
||||
/// @brief Returns the current text shown in the value label (for testing).
|
||||
QString lastDisplayedText() const;
|
||||
|
||||
public slots:
|
||||
/// @brief Slot connected to ConsumerThread::valueReceived.
|
||||
/// Updates the label with the new value.
|
||||
void onValueReceived(int value);
|
||||
|
||||
private:
|
||||
QLabel* m_title_label;
|
||||
QLabel* m_value_label;
|
||||
};
|
||||
33
src/app/MainWindow.cxx
Normal file
33
src/app/MainWindow.cxx
Normal file
@@ -0,0 +1,33 @@
|
||||
// MainWindow.cxx
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Unai Blazquez <unaibg2000@gmail.com>
|
||||
|
||||
#include "MainWindow.hpp"
|
||||
|
||||
MainWindow::MainWindow(QWidget* parent) : QWidget(parent)
|
||||
{
|
||||
setWindowTitle("Azkoyen IPC Monitor");
|
||||
setMinimumSize(320, 120);
|
||||
|
||||
auto* layout = new QVBoxLayout(this);
|
||||
|
||||
m_title_label = new QLabel("Last received value:", this);
|
||||
m_value_label = new QLabel("(waiting...)", this);
|
||||
|
||||
// Make the value label stand out a bit
|
||||
QFont font = m_value_label->font();
|
||||
font.setPointSize(24);
|
||||
font.setBold(true);
|
||||
m_value_label->setFont(font);
|
||||
m_value_label->setAlignment(Qt::AlignCenter);
|
||||
|
||||
layout->addWidget(m_title_label);
|
||||
layout->addWidget(m_value_label);
|
||||
}
|
||||
|
||||
QString MainWindow::lastDisplayedText() const { return m_value_label->text(); }
|
||||
|
||||
void MainWindow::onValueReceived(int value)
|
||||
{
|
||||
m_value_label->setText(QString::number(value));
|
||||
}
|
||||
58
src/app/main.cxx
Normal file
58
src/app/main.cxx
Normal file
@@ -0,0 +1,58 @@
|
||||
// main.cxx
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Unai Blazquez <unaibg2000@gmail.com>
|
||||
|
||||
#include <QApplication>
|
||||
#include <cstdlib>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
|
||||
#include "Consumer.hpp"
|
||||
#include "MainWindow.hpp"
|
||||
#include "Producer.hpp"
|
||||
#include "UnixIpcBridge.hpp"
|
||||
|
||||
int main(int argc, char* argv[])
|
||||
{
|
||||
QApplication app(argc, argv);
|
||||
|
||||
const std::string socket_path = "/tmp/azkoyen.sock";
|
||||
const std::string sysfs_path = "./fake_sysfs_input";
|
||||
|
||||
// 1. Consumer — listens on the socket, emits Qt signal on receive
|
||||
ConsumerThread consumer(socket_path);
|
||||
|
||||
// 2. GUI — minimal window that displays received values
|
||||
MainWindow window;
|
||||
window.show();
|
||||
|
||||
// Connect consumer signal → window slot (auto-queued across threads,
|
||||
// so the GUI never blocks even if the producer is stuck in cool-down)
|
||||
QObject::connect(&consumer, &ConsumerThread::valueReceived, &window,
|
||||
&MainWindow::onValueReceived);
|
||||
|
||||
consumer.start();
|
||||
|
||||
// 3. Bridge — sends ints over the UNIX domain socket
|
||||
UnixIpcBridge bridge(socket_path);
|
||||
|
||||
// 4. Producer — reads sysfs, generates random int, sends via bridge.
|
||||
// Logs to a file instead of console (console is for the consumer).
|
||||
std::ofstream log_file("producer.log", std::ios::app);
|
||||
|
||||
Producer producer(
|
||||
sysfs_path, [&bridge](int value) { bridge.send(value); },
|
||||
[]() { return std::rand() % 1000; },
|
||||
[&log_file](const std::string& msg) { log_file << msg << std::endl; });
|
||||
|
||||
producer.start();
|
||||
|
||||
// 5. Run the Qt event loop (GUI stays responsive, signals are delivered)
|
||||
int result = app.exec();
|
||||
|
||||
// 6. Graceful shutdown
|
||||
producer.stop();
|
||||
consumer.stop();
|
||||
|
||||
return result;
|
||||
}
|
||||
104
src/core/Consumer.cxx
Normal file
104
src/core/Consumer.cxx
Normal file
@@ -0,0 +1,104 @@
|
||||
// ConsumerThread.cxx
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Unai Blazquez <unaibg2000@gmail.com>
|
||||
|
||||
#include "Consumer.hpp"
|
||||
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <cstring>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
|
||||
ConsumerThread::ConsumerThread(const std::string& socket_path, QObject* parent)
|
||||
: QObject(parent), m_socket_path(socket_path)
|
||||
{
|
||||
}
|
||||
|
||||
ConsumerThread::~ConsumerThread() { stop(); }
|
||||
|
||||
void ConsumerThread::start()
|
||||
{
|
||||
// Remove stale socket from previous runs
|
||||
unlink(m_socket_path.c_str());
|
||||
|
||||
// Create, bind and listen BEFORE spawning the thread so the socket
|
||||
// is guaranteed ready when start() returns — no race with the producer.
|
||||
m_server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (m_server_fd < 0)
|
||||
{
|
||||
throw std::runtime_error("ConsumerThread: socket() failed");
|
||||
}
|
||||
|
||||
struct sockaddr_un addr = {};
|
||||
addr.sun_family = AF_UNIX;
|
||||
std::strncpy(addr.sun_path, m_socket_path.c_str(), sizeof(addr.sun_path) - 1);
|
||||
|
||||
if (bind(m_server_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) < 0)
|
||||
{
|
||||
close(m_server_fd);
|
||||
m_server_fd = -1;
|
||||
throw std::runtime_error("ConsumerThread: bind() failed");
|
||||
}
|
||||
|
||||
if (listen(m_server_fd, 5) < 0)
|
||||
{
|
||||
close(m_server_fd);
|
||||
m_server_fd = -1;
|
||||
throw std::runtime_error("ConsumerThread: listen() failed");
|
||||
}
|
||||
|
||||
m_running.store(true);
|
||||
m_thread = std::thread(&ConsumerThread::run_loop, this);
|
||||
}
|
||||
|
||||
void ConsumerThread::stop()
|
||||
{
|
||||
if (!m_running.exchange(false))
|
||||
{
|
||||
return; // already stopped or never started
|
||||
}
|
||||
|
||||
// Shutdown the server fd to unblock the blocking accept() call
|
||||
if (m_server_fd >= 0)
|
||||
{
|
||||
shutdown(m_server_fd, SHUT_RDWR);
|
||||
close(m_server_fd);
|
||||
m_server_fd = -1;
|
||||
}
|
||||
|
||||
if (m_thread.joinable())
|
||||
{
|
||||
m_thread.join();
|
||||
}
|
||||
|
||||
unlink(m_socket_path.c_str());
|
||||
}
|
||||
|
||||
void ConsumerThread::run_loop()
|
||||
{
|
||||
while (m_running.load())
|
||||
{
|
||||
int client_fd = accept(m_server_fd, nullptr, nullptr);
|
||||
if (client_fd < 0)
|
||||
{
|
||||
// accept() failed — most likely stop() closed the fd
|
||||
break;
|
||||
}
|
||||
|
||||
int value = 0;
|
||||
ssize_t n = recv(client_fd, &value, sizeof(value), MSG_WAITALL);
|
||||
close(client_fd);
|
||||
|
||||
if (n == static_cast<ssize_t>(sizeof(value)))
|
||||
{
|
||||
// 1) Print to console (spec requirement)
|
||||
std::cout << "ConsumerThread received: " << value << std::endl;
|
||||
|
||||
// 2) Emit Qt signal (spec requirement)
|
||||
emit valueReceived(value);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -37,3 +37,54 @@ target_link_libraries(test_ipc
|
||||
)
|
||||
|
||||
add_test(NAME test_ipc COMMAND test_ipc)
|
||||
|
||||
|
||||
add_executable(test_consumer
|
||||
test_consumer.cxx
|
||||
)
|
||||
|
||||
target_link_libraries(test_consumer
|
||||
PRIVATE
|
||||
core
|
||||
gtest
|
||||
gtest_main
|
||||
Qt5::Core
|
||||
Qt5::Test
|
||||
)
|
||||
|
||||
add_test(NAME test_consumer COMMAND test_consumer)
|
||||
|
||||
add_executable(test_main_window
|
||||
test_main_window.cxx
|
||||
${CMAKE_SOURCE_DIR}/src/app/MainWindow.cxx
|
||||
${CMAKE_SOURCE_DIR}/include/MainWindow.hpp
|
||||
)
|
||||
|
||||
target_include_directories(test_main_window PRIVATE ${CMAKE_SOURCE_DIR}/include)
|
||||
|
||||
target_link_libraries(test_main_window
|
||||
PRIVATE
|
||||
core
|
||||
gtest
|
||||
gtest_main
|
||||
Qt5::Core
|
||||
Qt5::Widgets
|
||||
Qt5::Test
|
||||
)
|
||||
|
||||
add_test(NAME test_main_window COMMAND test_main_window)
|
||||
|
||||
add_executable(test_race_conditions
|
||||
test_race_conditions.cxx
|
||||
)
|
||||
|
||||
target_link_libraries(test_race_conditions
|
||||
PRIVATE
|
||||
core
|
||||
gtest
|
||||
gtest_main
|
||||
Qt5::Core
|
||||
Qt5::Test
|
||||
)
|
||||
|
||||
add_test(NAME test_race_conditions COMMAND test_race_conditions)
|
||||
|
||||
215
tests/test_consumer.cxx
Normal file
215
tests/test_consumer.cxx
Normal file
@@ -0,0 +1,215 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QSignalSpy>
|
||||
#include <cstring>
|
||||
|
||||
#include "Consumer.hpp"
|
||||
#include "UnixIpcBridge.hpp"
|
||||
|
||||
// QSignalSpy needs a QCoreApplication to dispatch queued signals
|
||||
static int argc_ = 0;
|
||||
static QCoreApplication app_(argc_, nullptr);
|
||||
|
||||
TEST(ConsumerThreadTest, ReceivesSingleValue)
|
||||
{
|
||||
const std::string sock = "/tmp/test_ct_single.sock";
|
||||
|
||||
ConsumerThread consumer(sock);
|
||||
|
||||
// QSignalSpy records every emission of the given signal
|
||||
QSignalSpy spy(&consumer, &ConsumerThread::valueReceived);
|
||||
consumer.start();
|
||||
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(42);
|
||||
|
||||
// spy.wait() pumps the event loop for up to 1s until a signal arrives
|
||||
spy.wait(1000);
|
||||
consumer.stop();
|
||||
|
||||
ASSERT_EQ(spy.count(), 1);
|
||||
EXPECT_EQ(spy.at(0).at(0).toInt(), 42);
|
||||
}
|
||||
|
||||
TEST(ConsumerThreadTest, ReceivesMultipleValues)
|
||||
{
|
||||
const std::string sock = "/tmp/test_ct_multi.sock";
|
||||
|
||||
ConsumerThread consumer(sock);
|
||||
QSignalSpy spy(&consumer, &ConsumerThread::valueReceived);
|
||||
consumer.start();
|
||||
|
||||
constexpr int kMessages = 5;
|
||||
for (int i = 0; i < kMessages; ++i)
|
||||
{
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(i * 10);
|
||||
// Small delay so the consumer can re-enter accept() between sends
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10));
|
||||
}
|
||||
|
||||
// Wait until all signals arrive (or timeout after 5s)
|
||||
for (int attempt = 0; spy.count() < kMessages && attempt < 50; ++attempt)
|
||||
{
|
||||
spy.wait(100);
|
||||
}
|
||||
|
||||
consumer.stop();
|
||||
|
||||
ASSERT_EQ(spy.count(), kMessages);
|
||||
for (int i = 0; i < kMessages; ++i)
|
||||
{
|
||||
EXPECT_EQ(spy.at(i).at(0).toInt(), i * 10);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ConsumerThreadTest, ReceivesNegativeAndZero)
|
||||
{
|
||||
// Zero
|
||||
{
|
||||
const std::string sock = "/tmp/test_ct_zero.sock";
|
||||
ConsumerThread consumer(sock);
|
||||
QSignalSpy spy(&consumer, &ConsumerThread::valueReceived);
|
||||
consumer.start();
|
||||
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(0);
|
||||
|
||||
spy.wait(1000);
|
||||
consumer.stop();
|
||||
|
||||
ASSERT_EQ(spy.count(), 1);
|
||||
EXPECT_EQ(spy.at(0).at(0).toInt(), 0);
|
||||
}
|
||||
|
||||
// Negative
|
||||
{
|
||||
const std::string sock = "/tmp/test_ct_neg.sock";
|
||||
ConsumerThread consumer(sock);
|
||||
QSignalSpy spy(&consumer, &ConsumerThread::valueReceived);
|
||||
consumer.start();
|
||||
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(-999);
|
||||
|
||||
spy.wait(1000);
|
||||
consumer.stop();
|
||||
|
||||
ASSERT_EQ(spy.count(), 1);
|
||||
EXPECT_EQ(spy.at(0).at(0).toInt(), -999);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(ConsumerThreadTest, StopsCleanlyWithoutDeadlock)
|
||||
{
|
||||
const std::string sock = "/tmp/test_ct_stop.sock";
|
||||
|
||||
ConsumerThread consumer(sock);
|
||||
consumer.start();
|
||||
// stop() must return without hanging, even with no connections
|
||||
consumer.stop();
|
||||
}
|
||||
|
||||
TEST(ConsumerThreadTest, StopsCleanlyWhenNeverStarted)
|
||||
{
|
||||
const std::string sock = "/tmp/test_ct_nostart.sock";
|
||||
|
||||
ConsumerThread consumer(sock);
|
||||
// stop() on a consumer that was never started must not crash
|
||||
consumer.stop();
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Requirement 2: Consumer receiving corrupted data (non-numeric)
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Helper: raw-connect to a UNIX socket and send arbitrary bytes.
|
||||
static void send_raw_bytes(const std::string& path, const void* data,
|
||||
size_t len)
|
||||
{
|
||||
int fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
ASSERT_GE(fd, 0);
|
||||
|
||||
struct sockaddr_un addr = {};
|
||||
addr.sun_family = AF_UNIX;
|
||||
std::strncpy(addr.sun_path, path.c_str(), sizeof(addr.sun_path) - 1);
|
||||
|
||||
ASSERT_EQ(
|
||||
connect(fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)), 0);
|
||||
|
||||
if (len > 0)
|
||||
{
|
||||
::send(fd, data, len, 0);
|
||||
}
|
||||
close(fd);
|
||||
}
|
||||
|
||||
TEST(ConsumerThreadTest, DropsCorruptedShortMessage)
|
||||
{
|
||||
const std::string sock = "/tmp/test_ct_corrupt_short.sock";
|
||||
|
||||
ConsumerThread consumer(sock);
|
||||
QSignalSpy spy(&consumer, &ConsumerThread::valueReceived);
|
||||
consumer.start();
|
||||
|
||||
// Send only 2 bytes instead of sizeof(int)==4 — corrupted / partial message
|
||||
uint16_t garbage = 0xBEEF;
|
||||
send_raw_bytes(sock, &garbage, sizeof(garbage));
|
||||
|
||||
// Give the consumer time to process (or not)
|
||||
spy.wait(500);
|
||||
consumer.stop();
|
||||
|
||||
// No signal should have been emitted
|
||||
EXPECT_EQ(spy.count(), 0);
|
||||
}
|
||||
|
||||
TEST(ConsumerThreadTest, DropsEmptyConnection)
|
||||
{
|
||||
const std::string sock = "/tmp/test_ct_corrupt_empty.sock";
|
||||
|
||||
ConsumerThread consumer(sock);
|
||||
QSignalSpy spy(&consumer, &ConsumerThread::valueReceived);
|
||||
consumer.start();
|
||||
|
||||
// Connect and immediately close — zero bytes sent
|
||||
send_raw_bytes(sock, nullptr, 0);
|
||||
|
||||
spy.wait(500);
|
||||
consumer.stop();
|
||||
|
||||
EXPECT_EQ(spy.count(), 0);
|
||||
}
|
||||
|
||||
TEST(ConsumerThreadTest, SurvivesCorruptedThenReceivesValid)
|
||||
{
|
||||
const std::string sock = "/tmp/test_ct_corrupt_then_valid.sock";
|
||||
|
||||
ConsumerThread consumer(sock);
|
||||
QSignalSpy spy(&consumer, &ConsumerThread::valueReceived);
|
||||
consumer.start();
|
||||
|
||||
// First: send corrupted (1 byte)
|
||||
uint8_t one_byte = 0xFF;
|
||||
send_raw_bytes(sock, &one_byte, sizeof(one_byte));
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
|
||||
// Then: send a valid int via the normal bridge
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(777);
|
||||
|
||||
// Wait for the valid signal
|
||||
for (int attempt = 0; spy.count() < 1 && attempt < 20; ++attempt)
|
||||
{
|
||||
spy.wait(100);
|
||||
}
|
||||
consumer.stop();
|
||||
|
||||
// The corrupted message must have been dropped, valid one received
|
||||
ASSERT_EQ(spy.count(), 1);
|
||||
EXPECT_EQ(spy.at(0).at(0).toInt(), 777);
|
||||
}
|
||||
47
tests/test_main_window.cxx
Normal file
47
tests/test_main_window.cxx
Normal file
@@ -0,0 +1,47 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QApplication>
|
||||
#include <QLabel>
|
||||
#include <QSignalSpy>
|
||||
#include <cstdlib>
|
||||
|
||||
#include "MainWindow.hpp"
|
||||
|
||||
// QWidget-based tests need a full QApplication (not QCoreApplication).
|
||||
// Use offscreen platform so tests run headless in containers.
|
||||
static int argc_ = 0;
|
||||
static char* argv_[] = {nullptr};
|
||||
static struct SetupOffscreen
|
||||
{
|
||||
SetupOffscreen() { qputenv("QT_QPA_PLATFORM", "offscreen"); }
|
||||
} setup_offscreen_;
|
||||
static QApplication app_(argc_, argv_);
|
||||
|
||||
TEST(MainWindowTest, LabelUpdatesOnValueReceived)
|
||||
{
|
||||
MainWindow window;
|
||||
// Simulate receiving a value from ConsumerThread
|
||||
window.onValueReceived(42);
|
||||
|
||||
// The label should display the received value
|
||||
EXPECT_NE(window.lastDisplayedText().toStdString().find("42"),
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
TEST(MainWindowTest, LabelUpdatesMultipleTimes)
|
||||
{
|
||||
MainWindow window;
|
||||
window.onValueReceived(10);
|
||||
window.onValueReceived(20);
|
||||
window.onValueReceived(30);
|
||||
|
||||
// Label should show the most recent value
|
||||
EXPECT_NE(window.lastDisplayedText().toStdString().find("30"),
|
||||
std::string::npos);
|
||||
}
|
||||
|
||||
TEST(MainWindowTest, WindowTitleIsSet)
|
||||
{
|
||||
MainWindow window;
|
||||
EXPECT_FALSE(window.windowTitle().isEmpty());
|
||||
}
|
||||
200
tests/test_race_conditions.cxx
Normal file
200
tests/test_race_conditions.cxx
Normal file
@@ -0,0 +1,200 @@
|
||||
// test_race_conditions.cxx
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Unai Blazquez <unaibg2000@gmail.com>
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QSignalSpy>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
#include <iostream>
|
||||
#include <stdexcept>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "Consumer.hpp"
|
||||
#include "Producer.hpp"
|
||||
#include "UnixIpcBridge.hpp"
|
||||
|
||||
static int argc_ = 0;
|
||||
static QCoreApplication app_(argc_, nullptr);
|
||||
|
||||
|
||||
TEST(RaceConditionTest, RepeatedStartStopWhileProducerSends)
|
||||
{
|
||||
const std::string sock = "/tmp/test_race.sock";
|
||||
constexpr int kCycles = 20;
|
||||
|
||||
// Watchdog: if the test takes longer than 15s, declare deadlock.
|
||||
std::atomic<bool> test_done{false};
|
||||
std::thread watchdog([&test_done]() {
|
||||
for (int i = 0; i < 150 && !test_done.load(); ++i)
|
||||
{
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(100));
|
||||
}
|
||||
if (!test_done.load())
|
||||
{
|
||||
std::cerr
|
||||
<< "DEADLOCK DETECTED: RepeatedStartStopWhileProducerSends timed out"
|
||||
<< std::endl;
|
||||
std::abort();
|
||||
}
|
||||
});
|
||||
|
||||
std::atomic<bool> producer_running{true};
|
||||
std::thread producer([&]() {
|
||||
while (producer_running.load())
|
||||
{
|
||||
try
|
||||
{
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(42);
|
||||
}
|
||||
catch (const std::runtime_error&)
|
||||
{
|
||||
// Expected: consumer socket not ready or just torn down.
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(5));
|
||||
}
|
||||
});
|
||||
|
||||
for (int i = 0; i < kCycles; ++i)
|
||||
{
|
||||
ConsumerThread consumer(sock);
|
||||
consumer.start();
|
||||
|
||||
// Let it run briefly so the producer can connect during some cycles.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(10 + (i % 5) * 5));
|
||||
|
||||
// stop() must return without deadlock every single time.
|
||||
consumer.stop();
|
||||
}
|
||||
|
||||
producer_running.store(false);
|
||||
producer.join();
|
||||
|
||||
test_done.store(true);
|
||||
watchdog.join();
|
||||
|
||||
// If we reach here, no deadlock across kCycles start/stop cycles.
|
||||
SUCCEED();
|
||||
}
|
||||
|
||||
TEST(RaceConditionTest, ProducerSurvivesConsumerCrash)
|
||||
{
|
||||
const std::string sock = "/tmp/test_crash.sock";
|
||||
const std::string sysfs = "./fake_sysfs_race";
|
||||
|
||||
// Prepare sysfs file so the producer is in Enabled state.
|
||||
{ std::ofstream(sysfs) << "1\n"; }
|
||||
|
||||
// Track what the producer sends.
|
||||
std::vector<int> sent_values;
|
||||
std::mutex sent_mutex;
|
||||
std::vector<std::string> logs;
|
||||
std::mutex log_mutex;
|
||||
|
||||
auto make_safe_send = [&](const std::string& path) {
|
||||
return [&, path](int value) {
|
||||
try
|
||||
{
|
||||
UnixIpcBridge bridge(path);
|
||||
bridge.send(value);
|
||||
std::lock_guard<std::mutex> lk(sent_mutex);
|
||||
sent_values.push_back(value);
|
||||
}
|
||||
catch (const std::runtime_error&)
|
||||
{
|
||||
// Consumer is down — expected during the "crash" window.
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
Producer producer(
|
||||
sysfs, make_safe_send(sock), []() { return 123; },
|
||||
[&](const std::string& msg) {
|
||||
std::lock_guard<std::mutex> lk(log_mutex);
|
||||
logs.push_back(msg);
|
||||
},
|
||||
[](std::chrono::milliseconds) {
|
||||
// Use a short sleep so the test runs fast.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(20));
|
||||
});
|
||||
|
||||
// Phase 1: start consumer, start producer, let a few values flow.
|
||||
{
|
||||
ConsumerThread consumer(sock);
|
||||
QSignalSpy spy(&consumer, &ConsumerThread::valueReceived);
|
||||
consumer.start();
|
||||
producer.start();
|
||||
|
||||
// Wait for at least 2 values to arrive.
|
||||
for (int attempt = 0; spy.count() < 2 && attempt < 50; ++attempt)
|
||||
{
|
||||
spy.wait(100);
|
||||
}
|
||||
ASSERT_GE(spy.count(), 2) << "Phase 1: producer should have delivered values";
|
||||
|
||||
// Simulate a hard crash: force-close the consumer's server fd from
|
||||
// outside its thread, causing accept() to fail with EBADF. This is
|
||||
// what happens when the kernel reclaims fds on SIGKILL / abort().
|
||||
//
|
||||
// We find the server fd by calling getsockname() on open fds and
|
||||
// matching against our socket path.
|
||||
for (int fd = 3; fd < 1024; ++fd)
|
||||
{
|
||||
struct sockaddr_un addr = {};
|
||||
socklen_t len = sizeof(addr);
|
||||
if (getsockname(fd, reinterpret_cast<sockaddr*>(&addr), &len) == 0 &&
|
||||
addr.sun_family == AF_UNIX &&
|
||||
std::string(addr.sun_path) == sock)
|
||||
{
|
||||
::close(fd); // Yank the fd — consumer thread crashes out of accept()
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Destructor calls stop(), which joins the (now-exited) thread and
|
||||
// cleans up. In a real crash no cleanup runs, but we can't leak
|
||||
// threads in a test process.
|
||||
}
|
||||
|
||||
// Phase 2: producer is still running with no consumer (sends will fail).
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(200));
|
||||
|
||||
// Phase 3: bring up a fresh consumer. Producer should resume delivering.
|
||||
{
|
||||
ConsumerThread consumer2(sock);
|
||||
QSignalSpy spy2(&consumer2, &ConsumerThread::valueReceived);
|
||||
consumer2.start();
|
||||
|
||||
for (int attempt = 0; spy2.count() < 2 && attempt < 50; ++attempt)
|
||||
{
|
||||
spy2.wait(100);
|
||||
}
|
||||
|
||||
consumer2.stop();
|
||||
|
||||
ASSERT_GE(spy2.count(), 2)
|
||||
<< "Phase 3: producer must deliver to a new consumer after crash";
|
||||
|
||||
// Values received by the second consumer should all be 123.
|
||||
for (int i = 0; i < spy2.count(); ++i)
|
||||
{
|
||||
EXPECT_EQ(spy2.at(i).at(0).toInt(), 123);
|
||||
}
|
||||
}
|
||||
|
||||
producer.stop();
|
||||
|
||||
// Producer logged throughout all three phases.
|
||||
{
|
||||
std::lock_guard<std::mutex> lk(log_mutex);
|
||||
EXPECT_GE(logs.size(), 3u) << "Producer should have kept logging";
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user