Compare commits
39 Commits
2a28e6e678
...
docs/flow-
| Author | SHA1 | Date | |
|---|---|---|---|
| e1fc753c3d | |||
| b062dddff2 | |||
| bcba7fad09 | |||
| b8b83bc60b | |||
| 9cc912b0a3 | |||
| 32426b3028 | |||
| 040cf974f4 | |||
| 499584f856 | |||
| cc29845657 | |||
| dfd82b1619 | |||
| 8e471d3534 | |||
| da875871ef | |||
| 2d519937b7 | |||
| 228bb998f6 | |||
| 572905a879 | |||
| 544d50ee6a | |||
| f63d40cffb | |||
| e1360ccbb4 | |||
| 149c3a22b7 | |||
| 928bb5a5fb | |||
| 16bf4bccd4 | |||
| 5b6f20a70a | |||
| 36093e6c73 | |||
| 9c2117e64b | |||
| a798caf1a8 | |||
| d7cfa3f76c | |||
| f07bec25f8 | |||
| 6e1cdec81f | |||
| f04de7ea34 | |||
| 17133281d2 | |||
| 80dc5d62eb | |||
| cb8cfd6da8 | |||
| 159d55c2a2 | |||
| fabcf5a146 | |||
| 409ee7aa35 | |||
| d992255125 | |||
| 946f93eaca | |||
| aea0cbc5e2 | |||
| 8511404afa |
11
.clang-format
Normal file
11
.clang-format
Normal file
@@ -0,0 +1,11 @@
|
||||
# Google style C++ Code Style settings
|
||||
# https://clang.llvm.org/docs/ClangFormatStyleOptions.html
|
||||
|
||||
Language: Cpp
|
||||
BasedOnStyle: Google
|
||||
AccessModifierOffset: -1
|
||||
AlignAfterOpenBracket: Align
|
||||
AlignOperands: Align
|
||||
AllowAllArgumentsOnNextLine: true
|
||||
ColumnLimit: 80
|
||||
BreakBeforeBraces: Allman
|
||||
33
.devcontainer/Dockerfile
Normal file
33
.devcontainer/Dockerfile
Normal file
@@ -0,0 +1,33 @@
|
||||
FROM debian:trixie
|
||||
RUN apt-get update && \
|
||||
apt-get install -y \
|
||||
build-essential \
|
||||
cmake \
|
||||
ninja-build \
|
||||
qtbase5-dev \
|
||||
qt5-qmake \
|
||||
qtbase5-dev-tools \
|
||||
googletest \
|
||||
git \
|
||||
curl \
|
||||
clangd \
|
||||
clang-format \
|
||||
nodejs \
|
||||
unzip \
|
||||
python3-venv \
|
||||
zsh \
|
||||
ripgrep \
|
||||
fd-find \
|
||||
npm \
|
||||
libgtest-dev \
|
||||
pkg-config && \
|
||||
rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Google test is downloaded as source code normally
|
||||
WORKDIR /usr/src/googletest/googletest
|
||||
RUN cmake . && make
|
||||
|
||||
# This is only needed for zsh, small quality-of-life upgrade
|
||||
RUN groupadd -g 1000 dev && useradd -m -u 1000 -g 1000 -s /bin/zsh dev
|
||||
USER dev
|
||||
RUN sh -c "$(curl -fsSL https://raw.githubusercontent.com/ohmyzsh/ohmyzsh/master/tools/install.sh)" "" --unattended
|
||||
15
.devcontainer/devcontainer.json
Normal file
15
.devcontainer/devcontainer.json
Normal file
@@ -0,0 +1,15 @@
|
||||
{
|
||||
"name": "C++ DEV",
|
||||
"build": {
|
||||
"dockerfile": "Dockerfile"
|
||||
},
|
||||
"remoteUser": "dev",
|
||||
"features": {
|
||||
"ghcr.io/duduribeiro/devcontainer-features/neovim:1": {
|
||||
"version": "stable"
|
||||
}
|
||||
},
|
||||
"mounts": [
|
||||
"source=${env:HOME}/.config/nvim,target=/home/dev/.config/nvim,type=bind"
|
||||
]
|
||||
}
|
||||
2
.gitignore
vendored
2
.gitignore
vendored
@@ -1,4 +1,6 @@
|
||||
# ---> C++
|
||||
# build artifacts
|
||||
build/
|
||||
# Prerequisites
|
||||
*.d
|
||||
|
||||
|
||||
39
CMakeLists.txt
Normal file
39
CMakeLists.txt
Normal file
@@ -0,0 +1,39 @@
|
||||
# Root cmake file sketch (might change it later)
|
||||
# Author: Unai Blazquez
|
||||
# License: GPL-3-or-later
|
||||
|
||||
cmake_minimum_required(VERSION 3.16)
|
||||
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()
|
||||
add_subdirectory(tests)
|
||||
46
README.md
46
README.md
@@ -1,3 +1,47 @@
|
||||
# azkoyen_technical_test
|
||||
|
||||
Azkoyen technical test implementation. Implemented (mostly) on standard c++ 17 framework, but with Qt wherever was necessary
|
||||
Azkoyen technical test implementation. Implemented (mostly) on standard c++ 17 framework, but with Qt wherever was necessary.
|
||||
|
||||
## Development approach
|
||||
|
||||
A Test-Driven Development (TDD) workflow was followed throughout the project. Every component — from the lowest-level file reader to the GUI window — has a corresponding Google Test suite that was written before (or alongside) the production code. This ensures each module behaves correctly in isolation and makes regressions immediately visible.
|
||||
|
||||
## SysfsRead class
|
||||
|
||||
`SysfsReader` ([include/SysfsRead.hpp](include/SysfsRead.hpp), [src/core/SysfsRead.cxx](src/core/SysfsRead.cxx)) is the lowest-level component. It opens a sysfs-like file and translates its raw text content into a `SysfsStatus` enum:
|
||||
|
||||
| File content | Status |
|
||||
|--------------------------|---------------------|
|
||||
| `"1"` | `Enabled` |
|
||||
| `"error: temp too high"` | `ErrorTempTooHigh` |
|
||||
| empty / whitespace-only | `Empty` |
|
||||
| file missing | `Unreachable` |
|
||||
| anything else | `UnexpectedValue` |
|
||||
|
||||
The reader never throws on I/O errors; every outcome is expressed through the enum so callers can react without exception handling. A helper `trim_in_place` strips trailing whitespace and newlines before comparison.
|
||||
|
||||
**Tests:** [tests/test_sysfs_read.cxx](tests/test_sysfs_read.cxx) — covers all five status branches by writing controlled content to a temporary file.
|
||||
|
||||
## Producer class / thread
|
||||
|
||||
`Producer` ([include/Producer.hpp](include/Producer.hpp), [src/core/Producer.cxx](src/core/Producer.cxx)) runs a worker `std::thread` that periodically polls the `SysfsReader` and, when the status is `Enabled`, generates a random integer and forwards it through an injected `send_fn` callback. The polling interval is 1 second under normal conditions and 7 seconds when the sysfs file reports `ErrorTempTooHigh` (cool-down).
|
||||
|
||||
## UnixIpcBridge
|
||||
|
||||
>[!note]
|
||||
>why unix domain sockets? Because I have more experience with them under linux than with posix shared memmory and semaphore, and I find them easier to unit-test.
|
||||
|
||||
`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.
|
||||
|
||||
BIN
docs/logic-flow-chart.png
Normal file
BIN
docs/logic-flow-chart.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 405 KiB |
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;
|
||||
};
|
||||
48
include/Producer.hpp
Normal file
48
include/Producer.hpp
Normal file
@@ -0,0 +1,48 @@
|
||||
#pragma once
|
||||
// Producer.hpp
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Unai Blazquez <unaibg2000@gmail.com>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <filesystem>
|
||||
#include <functional>
|
||||
#include <thread>
|
||||
|
||||
#include "SysfsRead.hpp"
|
||||
|
||||
using RandomFn = std::function<int()>;
|
||||
using LogFn = std::function<void(const std::string&)>;
|
||||
using SleepFn = std::function<void(std::chrono::milliseconds)>;
|
||||
class Producer
|
||||
{
|
||||
public:
|
||||
/// @brief Construct a Producer bound to a sysfs-like control file.
|
||||
/// @param sysfs_path Path to the control file (e.g. "./fake_sysfs_input").
|
||||
/// @param send_fn Function called whenever a new integer should be sent.
|
||||
Producer(const std::filesystem::path& sysfs_path,
|
||||
std::function<void(int)> send_fn, RandomFn random_fn,
|
||||
LogFn log_fn = nullptr, SleepFn sleep_fn = default_sleep);
|
||||
|
||||
/// @brief Start the worker thread. Safe to call only once.
|
||||
void start();
|
||||
|
||||
/// @brief Request the worker thread to stop and wait for it to finish.
|
||||
void stop();
|
||||
|
||||
private:
|
||||
/// @brief Main loop executed by the worker thread.
|
||||
void run_loop();
|
||||
|
||||
std::thread m_thread;
|
||||
std::atomic<bool> m_running;
|
||||
SysfsReader m_reader;
|
||||
RandomFn m_random;
|
||||
SleepFn m_sleep;
|
||||
static void default_sleep(std::chrono::milliseconds d)
|
||||
{
|
||||
std::this_thread::sleep_for(d);
|
||||
}
|
||||
LogFn m_log;
|
||||
std::function<void(int)> m_send;
|
||||
std::chrono::milliseconds compute_delay(SysfsStatus status) const;
|
||||
};
|
||||
44
include/SysfsRead.hpp
Normal file
44
include/SysfsRead.hpp
Normal file
@@ -0,0 +1,44 @@
|
||||
// SysfsRead.hpp
|
||||
//
|
||||
// SPDX-License-Identifier GPL-3.0-or-later
|
||||
// Author: Unai Blazquez Gomez <unaibg2000@gmail.com>
|
||||
|
||||
#pragma once
|
||||
|
||||
#include <filesystem>
|
||||
|
||||
enum class SysfsStatus
|
||||
{
|
||||
/// @brief File cannot be opened or does not exist.
|
||||
Unreachable,
|
||||
/// @brief File exists but is just empty.
|
||||
Empty,
|
||||
/// @brief File content indicates taht production is enabled (e.g. "1")
|
||||
Enabled,
|
||||
/// @brief File requests a cooldown ("error: temp too high")
|
||||
ErrorTempTooHigh,
|
||||
/// @brief File contains an UnexpectedValue; producer must not send.
|
||||
UnexpectedValue
|
||||
};
|
||||
|
||||
class SysfsReader
|
||||
{
|
||||
public:
|
||||
/// @brief Construct a SysfsReader bound to a specific input file path.
|
||||
/// @param input_path Path to the sysfs-like input file.
|
||||
explicit SysfsReader(const std::filesystem::path& input_path);
|
||||
|
||||
/// @brief Read and interpret the current status of the input file.
|
||||
///
|
||||
/// This function never throws on common I/O errors; instead it reports them
|
||||
/// via the SysfsStatus enum.
|
||||
/// @return Interpreted status of the input file
|
||||
SysfsStatus read_status() const;
|
||||
|
||||
private:
|
||||
/// @brief Helper method for trimming trailing whitespaces and
|
||||
/// newline indicators
|
||||
/// @param String from the m_path file
|
||||
static void trim_in_place(std::string& string);
|
||||
std::filesystem::path m_path; // Path to the input file.
|
||||
};
|
||||
31
include/UnixIpcBridge.hpp
Normal file
31
include/UnixIpcBridge.hpp
Normal file
@@ -0,0 +1,31 @@
|
||||
// UnixIpcBridge.hpp
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Author: Unai Blazquez <unaibg2000@gmail.com>
|
||||
|
||||
#pragma once
|
||||
#include <fcntl.h> // non‑blocking
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h> // close()
|
||||
|
||||
#include <string>
|
||||
|
||||
///@brief Small bridge to allow the producer class to send data over UNIX domain
|
||||
/// sockets
|
||||
class UnixIpcBridge
|
||||
{
|
||||
public:
|
||||
///@brief constructor
|
||||
///@param socket path pointing the socket
|
||||
explicit UnixIpcBridge(const std::string& socket_path);
|
||||
|
||||
///@brief sending function, this goes into the producer
|
||||
///@param integer to send over the socket
|
||||
void send(int value);
|
||||
|
||||
private:
|
||||
std::string m_socket_path;
|
||||
int m_socket_fd = -1;
|
||||
|
||||
void connect_to_consumer();
|
||||
};
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
84
src/core/Producer.cxx
Normal file
84
src/core/Producer.cxx
Normal file
@@ -0,0 +1,84 @@
|
||||
// Producer.cxx
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Author: Unai Blazquez <unaibg2000@gmail.com>
|
||||
|
||||
#include "Producer.hpp"
|
||||
|
||||
#include "SysfsRead.hpp"
|
||||
|
||||
Producer::Producer(const std::filesystem::path& sysfs_path,
|
||||
std::function<void(int)> send_fn, RandomFn random_fn,
|
||||
LogFn log_fn, SleepFn sleep_fn)
|
||||
: m_reader(sysfs_path),
|
||||
m_send(std::move(send_fn)),
|
||||
m_random(std::move(random_fn)),
|
||||
m_log(std::move(log_fn)),
|
||||
m_sleep(std::move(sleep_fn))
|
||||
{
|
||||
}
|
||||
std::chrono::milliseconds Producer::compute_delay(SysfsStatus status) const
|
||||
{
|
||||
using namespace std::chrono_literals;
|
||||
|
||||
auto standard = 1000ms; // example: 1s, must be < 7s
|
||||
auto hot = 7000ms; // exactly 7s
|
||||
|
||||
if (status == SysfsStatus::ErrorTempTooHigh)
|
||||
{ // when error = temp too high
|
||||
return hot;
|
||||
}
|
||||
else
|
||||
{
|
||||
return standard;
|
||||
}
|
||||
}
|
||||
|
||||
void Producer::start()
|
||||
{
|
||||
m_running.store(true);
|
||||
m_thread = std::thread(&Producer::run_loop, this);
|
||||
}
|
||||
|
||||
void Producer::stop()
|
||||
{
|
||||
m_running.store(false);
|
||||
if (m_thread.joinable())
|
||||
{
|
||||
m_thread.join();
|
||||
}
|
||||
}
|
||||
|
||||
void Producer::run_loop()
|
||||
{
|
||||
while (m_running.load())
|
||||
{
|
||||
auto status = m_reader.read_status();
|
||||
switch (status)
|
||||
{
|
||||
case SysfsStatus::Enabled:
|
||||
m_send(m_random());
|
||||
if (m_log) m_log("Producer: Enabled");
|
||||
break;
|
||||
|
||||
case SysfsStatus::Unreachable:
|
||||
// do nothing for now
|
||||
if (m_log) m_log("Producer: SysfsFile Unreachable");
|
||||
break;
|
||||
|
||||
case SysfsStatus::Empty:
|
||||
if (m_log) m_log("Producer: SysfsFile Empty");
|
||||
break;
|
||||
|
||||
case SysfsStatus::ErrorTempTooHigh:
|
||||
if (m_log) m_log("Producer: Error temp too high!!");
|
||||
break;
|
||||
|
||||
case SysfsStatus::UnexpectedValue:
|
||||
if (m_log) m_log("Producer: UnexpectedValue");
|
||||
break;
|
||||
}
|
||||
auto delay = compute_delay(status);
|
||||
m_sleep(delay);
|
||||
// Thread will end here (for now) stop will join it
|
||||
}
|
||||
}
|
||||
56
src/core/SysfsRead.cxx
Normal file
56
src/core/SysfsRead.cxx
Normal file
@@ -0,0 +1,56 @@
|
||||
// SysfsRead.cxx
|
||||
|
||||
// SPDX-License-Identifier: GPL-3.0-or-later
|
||||
// Author: Unai Blazquez <unaibg2000@gmail.com>
|
||||
|
||||
#include "SysfsRead.hpp"
|
||||
|
||||
#include <algorithm>
|
||||
#include <fstream>
|
||||
|
||||
SysfsReader::SysfsReader(const std::filesystem::path& input_path)
|
||||
: m_path(input_path)
|
||||
{
|
||||
}
|
||||
|
||||
SysfsStatus SysfsReader::read_status() const
|
||||
{
|
||||
std::ifstream input_file_stream(m_path);
|
||||
if (!input_file_stream.is_open())
|
||||
{
|
||||
return SysfsStatus::Unreachable;
|
||||
}
|
||||
std::stringstream buffer;
|
||||
buffer << input_file_stream.rdbuf(); // read entire stream into buffer
|
||||
std::string contents = buffer.str();
|
||||
|
||||
trim_in_place(contents); // clean input string
|
||||
// compare
|
||||
if (contents.empty())
|
||||
{
|
||||
return SysfsStatus::Empty;
|
||||
}
|
||||
if (contents == "1")
|
||||
{
|
||||
return SysfsStatus::Enabled;
|
||||
}
|
||||
if (contents == "error: temp too high")
|
||||
{
|
||||
return SysfsStatus::ErrorTempTooHigh;
|
||||
}
|
||||
return SysfsStatus::UnexpectedValue;
|
||||
}
|
||||
|
||||
void SysfsReader::trim_in_place(std::string& string)
|
||||
{
|
||||
// left trim
|
||||
string.erase(string.begin(),
|
||||
std::find_if(string.begin(), string.end(), [](unsigned char ch)
|
||||
{ return !std::isspace(ch); }));
|
||||
|
||||
// right trim
|
||||
string.erase(std::find_if(string.rbegin(), string.rend(),
|
||||
[](unsigned char ch) { return !std::isspace(ch); })
|
||||
.base(),
|
||||
string.end());
|
||||
}
|
||||
49
src/core/UnixIpcBridge.cxx
Normal file
49
src/core/UnixIpcBridge.cxx
Normal file
@@ -0,0 +1,49 @@
|
||||
// UnixIpcBridge.cxx
|
||||
// SPDX-License-Identifier: GPL-3.0-only
|
||||
// Author: Unai Blazquez <unaibg2000@gmail.com>
|
||||
#include "UnixIpcBridge.hpp"
|
||||
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
|
||||
UnixIpcBridge::UnixIpcBridge(const std::string& socket_path)
|
||||
: m_socket_path(socket_path)
|
||||
{
|
||||
}
|
||||
|
||||
void UnixIpcBridge::send(int value)
|
||||
{
|
||||
connect_to_consumer();
|
||||
|
||||
ssize_t n = ::send(m_socket_fd, &value, sizeof(value), 0);
|
||||
if (n != sizeof(value))
|
||||
{
|
||||
close(m_socket_fd);
|
||||
m_socket_fd = -1;
|
||||
throw std::runtime_error("UnixIpcBridge::send: failed to write value");
|
||||
}
|
||||
|
||||
close(m_socket_fd);
|
||||
m_socket_fd = -1;
|
||||
}
|
||||
|
||||
void UnixIpcBridge::connect_to_consumer()
|
||||
{
|
||||
m_socket_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
if (m_socket_fd < 0)
|
||||
{
|
||||
throw std::runtime_error("UnixIpcBridge: 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 (connect(m_socket_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)) <
|
||||
0)
|
||||
{
|
||||
close(m_socket_fd);
|
||||
m_socket_fd = -1;
|
||||
throw std::runtime_error("UnixIpcBridge: connect() failed");
|
||||
}
|
||||
}
|
||||
75
tests/CMakeLists.txt
Normal file
75
tests/CMakeLists.txt
Normal file
@@ -0,0 +1,75 @@
|
||||
# Author: Unai Blazquez
|
||||
# License: GPL-3-only
|
||||
|
||||
add_executable(test_sysfs_reader
|
||||
test_sysfs_read.cxx
|
||||
)
|
||||
|
||||
target_link_libraries(test_sysfs_reader
|
||||
PRIVATE
|
||||
core
|
||||
gtest
|
||||
gtest_main
|
||||
)
|
||||
add_test(NAME test_sysfs_reader COMMAND test_sysfs_reader)
|
||||
add_executable(test_producer
|
||||
test_producer.cxx
|
||||
)
|
||||
|
||||
target_link_libraries(test_producer
|
||||
PRIVATE
|
||||
core
|
||||
gtest
|
||||
gtest_main
|
||||
)
|
||||
|
||||
add_test(NAME test_producer COMMAND test_producer)
|
||||
|
||||
add_executable(test_ipc
|
||||
test_unix_ipc.cxx
|
||||
)
|
||||
|
||||
target_link_libraries(test_ipc
|
||||
PRIVATE
|
||||
core
|
||||
gtest
|
||||
gtest_main
|
||||
)
|
||||
|
||||
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)
|
||||
120
tests/test_consumer.cxx
Normal file
120
tests/test_consumer.cxx
Normal file
@@ -0,0 +1,120 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <QCoreApplication>
|
||||
#include <QSignalSpy>
|
||||
|
||||
#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();
|
||||
}
|
||||
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());
|
||||
}
|
||||
124
tests/test_producer.cxx
Normal file
124
tests/test_producer.cxx
Normal file
@@ -0,0 +1,124 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <chrono>
|
||||
#include <fstream>
|
||||
|
||||
#include "Producer.hpp"
|
||||
|
||||
TEST(ProducerTest, ProducerCallsBackWhenEnabled)
|
||||
{
|
||||
// Arrange create the file and write "1\n" into it.
|
||||
{
|
||||
std::ofstream out("fake_sysfs_input");
|
||||
out << "1\n";
|
||||
}
|
||||
// create a fake callback function
|
||||
std::vector<int> outputs;
|
||||
std::vector<std::string> logs;
|
||||
|
||||
// construct a producer with fake file and callback
|
||||
Producer producer{"fake_sysfs_input", [&outputs](int value)
|
||||
{ outputs.push_back(value); }, []() { return 42; },
|
||||
[&logs](const std::string& msg) { logs.push_back(msg); }};
|
||||
// Act: initialize producer and stop it.
|
||||
producer.start();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
producer.stop();
|
||||
|
||||
// Assert: we expect one output being 42
|
||||
EXPECT_EQ(outputs[0], 42);
|
||||
EXPECT_NE(logs[0].find("Enabled"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(ProducerTest, ProducerDoesNotCallWhenUnexpectedValue)
|
||||
{
|
||||
// Arrange create the file and write "0\n" into it.
|
||||
{
|
||||
std::ofstream out("fake_sysfs_input");
|
||||
out << "0\n";
|
||||
}
|
||||
|
||||
std::vector<int> outputs;
|
||||
std::vector<std::string> logs;
|
||||
|
||||
Producer producer{"fake_sysfs_input", [&outputs](int value)
|
||||
{ outputs.push_back(value); }, []() { return 42; },
|
||||
[&logs](const std::string& msg) { logs.push_back(msg); },
|
||||
[](std::chrono::milliseconds) {}};
|
||||
|
||||
producer.start();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // ← 1ms window
|
||||
producer.stop();
|
||||
|
||||
// Assert: we expect no output
|
||||
EXPECT_TRUE(outputs.empty());
|
||||
EXPECT_NE(logs[0].find("UnexpectedValue"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(ProducerTest, ProducerDoesNotCallWhenEmpty)
|
||||
{
|
||||
// Arrange create the file and write "0\n" into it.
|
||||
{
|
||||
std::ofstream out("fake_sysfs_input");
|
||||
out << " ";
|
||||
}
|
||||
|
||||
std::vector<int> outputs;
|
||||
std::vector<std::string> logs;
|
||||
|
||||
Producer producer{"fake_sysfs_input", [&outputs](int value)
|
||||
{ outputs.push_back(value); }, []() { return 42; },
|
||||
[&logs](const std::string& msg) { logs.push_back(msg); },
|
||||
[](std::chrono::milliseconds) {}};
|
||||
|
||||
producer.start();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // ← 1ms window
|
||||
producer.stop();
|
||||
|
||||
// Assert: we expect no output
|
||||
EXPECT_TRUE(outputs.empty());
|
||||
EXPECT_NE(logs[0].find("Empty"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(ProducerTest, ProducerDoesNotCallWhenUnreachable)
|
||||
{
|
||||
std::vector<int> outputs;
|
||||
std::vector<std::string> logs;
|
||||
|
||||
Producer producer{"nonexistant_sysfs_input", [&outputs](int value)
|
||||
{ outputs.push_back(value); }, []() { return 42; },
|
||||
[&logs](const std::string& msg) { logs.push_back(msg); },
|
||||
[](std::chrono::milliseconds) {}};
|
||||
|
||||
producer.start();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // ← 1ms window
|
||||
producer.stop();
|
||||
|
||||
// Assert: we expect no output
|
||||
EXPECT_TRUE(outputs.empty());
|
||||
EXPECT_NE(logs[0].find("Unreachable"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(ProducerTest, ProducerDoesNotCallWhenTempTooHigh)
|
||||
{
|
||||
// create a file that contains error
|
||||
{
|
||||
std::ofstream out("fake_sysfs_input");
|
||||
out << "error: temp too high";
|
||||
}
|
||||
std::vector<int> outputs;
|
||||
std::vector<std::string> logs;
|
||||
|
||||
Producer producer{"fake_sysfs_input", [&outputs](int value)
|
||||
{ outputs.push_back(value); }, []() { return 42; },
|
||||
[&logs](const std::string& msg) { logs.push_back(msg); },
|
||||
[](std::chrono::milliseconds) {}};
|
||||
|
||||
producer.start();
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1)); // ← 1ms window
|
||||
producer.stop();
|
||||
|
||||
// Assert: we expect no output
|
||||
EXPECT_TRUE(outputs.empty());
|
||||
EXPECT_NE(logs[0].find("Error"), std::string::npos);
|
||||
}
|
||||
77
tests/test_sysfs_read.cxx
Normal file
77
tests/test_sysfs_read.cxx
Normal file
@@ -0,0 +1,77 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <fstream>
|
||||
|
||||
#include "SysfsRead.hpp"
|
||||
|
||||
TEST(SysfsReaderTest, ReturnsEnabledWhenFileContainsOne)
|
||||
{
|
||||
// Arrange create the file and write "1\n" into it.
|
||||
{
|
||||
std::ofstream out("fake_sysfs_input");
|
||||
out << "1\n";
|
||||
// out is closed automatically at the end of this scope
|
||||
}
|
||||
|
||||
// 2) Act: construct the reader and read the status.
|
||||
SysfsReader reader{"fake_sysfs_input"};
|
||||
SysfsStatus status = reader.read_status();
|
||||
// 3) Assert: we expect Enabled.
|
||||
EXPECT_EQ(status, SysfsStatus::Enabled);
|
||||
}
|
||||
|
||||
TEST(SysfsReaderTest, ReturnsEmptyWhenFileIsEmpty)
|
||||
{
|
||||
// Arrange: create the file and don't write anything
|
||||
{
|
||||
std::ofstream out("fake_sysfs_input");
|
||||
out << "";
|
||||
}
|
||||
SysfsReader reader{"fake_sysfs_input"};
|
||||
SysfsStatus status1 = reader.read_status();
|
||||
|
||||
{
|
||||
std::ofstream out("fake_sysfs_input");
|
||||
out << " ";
|
||||
}
|
||||
SysfsReader reader_2{"fake_sysfs_input"};
|
||||
SysfsStatus status2 = reader_2.read_status();
|
||||
|
||||
// Assert
|
||||
EXPECT_EQ(status1, SysfsStatus::Empty);
|
||||
EXPECT_EQ(status2, SysfsStatus::Empty);
|
||||
}
|
||||
|
||||
TEST(SysfsReaderTest, ReturnsUnexpectedValue)
|
||||
{
|
||||
{
|
||||
std::ofstream out("fake_sysfs_input");
|
||||
out << "tdd development";
|
||||
}
|
||||
SysfsReader reader{"fake_sysfs_input"};
|
||||
SysfsStatus status = reader.read_status();
|
||||
|
||||
// Assert
|
||||
EXPECT_EQ(status, SysfsStatus::UnexpectedValue);
|
||||
}
|
||||
|
||||
TEST(SysfsReaderTest, ReturnsErrorTempTooHigh)
|
||||
{
|
||||
{
|
||||
std::ofstream out("fake_sysfs_input");
|
||||
out << "error: temp too high";
|
||||
}
|
||||
SysfsReader reader{"fake_sysfs_input"};
|
||||
SysfsStatus status = reader.read_status();
|
||||
|
||||
// Assert
|
||||
EXPECT_EQ(status, SysfsStatus::ErrorTempTooHigh);
|
||||
}
|
||||
|
||||
TEST(SysfsReaderTest, ReturnsUnreachableWhenDoesntExist)
|
||||
{
|
||||
SysfsReader reader{"nonexistent_sysfs_input"};
|
||||
SysfsStatus status = reader.read_status();
|
||||
// Assert
|
||||
EXPECT_EQ(status, SysfsStatus::Unreachable);
|
||||
}
|
||||
220
tests/test_unix_ipc.cxx
Normal file
220
tests/test_unix_ipc.cxx
Normal file
@@ -0,0 +1,220 @@
|
||||
#include <gtest/gtest.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <cstring>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <vector>
|
||||
|
||||
#include "UnixIpcBridge.hpp"
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Helper: a minimal UNIX-domain socket server that accepts one connection,
|
||||
// reads `count` ints, then tears down cleanly. Uses a ready-flag so the
|
||||
// client never races against bind/listen.
|
||||
// ---------------------------------------------------------------------------
|
||||
class FakeConsumer
|
||||
{
|
||||
public:
|
||||
explicit FakeConsumer(const std::string& path, int count = 1)
|
||||
: m_path(path), m_count(count)
|
||||
{
|
||||
// Remove stale socket from any previous failed run
|
||||
unlink(m_path.c_str());
|
||||
}
|
||||
|
||||
/// Start the server on a background thread.
|
||||
void start()
|
||||
{
|
||||
m_thread = std::thread([this] { run(); });
|
||||
|
||||
// Spin until the server signals it is listening (bounded wait).
|
||||
auto deadline = std::chrono::steady_clock::now() + std::chrono::seconds(5);
|
||||
while (!m_ready.load(std::memory_order_acquire))
|
||||
{
|
||||
if (std::chrono::steady_clock::now() > deadline)
|
||||
{
|
||||
throw std::runtime_error("FakeConsumer: server failed to start");
|
||||
}
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
}
|
||||
}
|
||||
|
||||
/// Block until the server thread finishes.
|
||||
void join() { m_thread.join(); }
|
||||
|
||||
/// Values received in order.
|
||||
const std::vector<int>& received() const { return m_received; }
|
||||
|
||||
~FakeConsumer() { unlink(m_path.c_str()); }
|
||||
|
||||
private:
|
||||
void run()
|
||||
{
|
||||
m_server_fd = socket(AF_UNIX, SOCK_STREAM, 0);
|
||||
ASSERT_GE(m_server_fd, 0) << "socket() failed: " << strerror(errno);
|
||||
|
||||
struct sockaddr_un addr = {};
|
||||
addr.sun_family = AF_UNIX;
|
||||
std::strncpy(addr.sun_path, m_path.c_str(), sizeof(addr.sun_path) - 1);
|
||||
|
||||
ASSERT_EQ(
|
||||
bind(m_server_fd, reinterpret_cast<sockaddr*>(&addr), sizeof(addr)), 0)
|
||||
<< "bind() failed: " << strerror(errno);
|
||||
ASSERT_EQ(listen(m_server_fd, 1), 0)
|
||||
<< "listen() failed: " << strerror(errno);
|
||||
|
||||
// Signal that we are ready to accept connections.
|
||||
m_ready.store(true, std::memory_order_release);
|
||||
|
||||
int client_fd = accept(m_server_fd, nullptr, nullptr);
|
||||
ASSERT_GE(client_fd, 0) << "accept() failed: " << strerror(errno);
|
||||
|
||||
for (int i = 0; i < m_count; ++i)
|
||||
{
|
||||
int value = 0;
|
||||
ssize_t n = recv(client_fd, &value, sizeof(value), MSG_WAITALL);
|
||||
ASSERT_EQ(n, static_cast<ssize_t>(sizeof(value)))
|
||||
<< "recv() short read on message " << i;
|
||||
m_received.push_back(value);
|
||||
}
|
||||
|
||||
close(client_fd);
|
||||
close(m_server_fd);
|
||||
}
|
||||
|
||||
std::string m_path;
|
||||
int m_count;
|
||||
int m_server_fd = -1;
|
||||
std::atomic<bool> m_ready{false};
|
||||
std::thread m_thread;
|
||||
std::vector<int> m_received;
|
||||
};
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Tests
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/// Sends a single integer value and verifies the consumer receives it.
|
||||
TEST(UnixIpcBridgeTest, SendsSingleInt)
|
||||
{
|
||||
const std::string sock = "/tmp/test_ipc_single.sock";
|
||||
|
||||
FakeConsumer consumer(sock, /*count=*/1);
|
||||
consumer.start();
|
||||
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(42);
|
||||
|
||||
consumer.join();
|
||||
|
||||
ASSERT_EQ(consumer.received().size(), 1u);
|
||||
EXPECT_EQ(consumer.received()[0], 42);
|
||||
}
|
||||
|
||||
/// Sends zero and a negative value — makes sure sign bits survive.
|
||||
TEST(UnixIpcBridgeTest, SendsZeroAndNegativeValues)
|
||||
{
|
||||
// Zero
|
||||
{
|
||||
const std::string sock = "/tmp/test_ipc_zero.sock";
|
||||
FakeConsumer consumer(sock, 1);
|
||||
consumer.start();
|
||||
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(0);
|
||||
|
||||
consumer.join();
|
||||
ASSERT_EQ(consumer.received().size(), 1u);
|
||||
EXPECT_EQ(consumer.received()[0], 0);
|
||||
}
|
||||
|
||||
// Negative
|
||||
{
|
||||
const std::string sock = "/tmp/test_ipc_neg.sock";
|
||||
FakeConsumer consumer(sock, 1);
|
||||
consumer.start();
|
||||
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(-1);
|
||||
|
||||
consumer.join();
|
||||
ASSERT_EQ(consumer.received().size(), 1u);
|
||||
EXPECT_EQ(consumer.received()[0], -1);
|
||||
}
|
||||
}
|
||||
|
||||
/// Sends INT_MAX / INT_MIN to check for truncation or overflow.
|
||||
TEST(UnixIpcBridgeTest, SendsExtremeBoundaryValues)
|
||||
{
|
||||
{
|
||||
const std::string sock = "/tmp/test_ipc_max.sock";
|
||||
FakeConsumer consumer(sock, 1);
|
||||
consumer.start();
|
||||
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(std::numeric_limits<int>::max());
|
||||
|
||||
consumer.join();
|
||||
ASSERT_EQ(consumer.received().size(), 1u);
|
||||
EXPECT_EQ(consumer.received()[0], std::numeric_limits<int>::max());
|
||||
}
|
||||
|
||||
{
|
||||
const std::string sock = "/tmp/test_ipc_min.sock";
|
||||
FakeConsumer consumer(sock, 1);
|
||||
consumer.start();
|
||||
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(std::numeric_limits<int>::min());
|
||||
|
||||
consumer.join();
|
||||
ASSERT_EQ(consumer.received().size(), 1u);
|
||||
EXPECT_EQ(consumer.received()[0], std::numeric_limits<int>::min());
|
||||
}
|
||||
}
|
||||
|
||||
/// Connecting to a non-existent socket must throw, not silently fail.
|
||||
TEST(UnixIpcBridgeTest, ThrowsWhenNoConsumerListening)
|
||||
{
|
||||
const std::string sock = "/tmp/test_ipc_noserver.sock";
|
||||
unlink(sock.c_str()); // make sure nothing is there
|
||||
|
||||
UnixIpcBridge bridge(sock);
|
||||
EXPECT_THROW(bridge.send(99), std::runtime_error);
|
||||
}
|
||||
|
||||
/// Multiple sequential sends (each reopens the connection).
|
||||
TEST(UnixIpcBridgeTest, MultipleSendsSequentially)
|
||||
{
|
||||
const std::string sock = "/tmp/test_ipc_multi.sock";
|
||||
constexpr int kMessages = 5;
|
||||
|
||||
// Server expects exactly kMessages ints from kMessages connections.
|
||||
// Because the bridge reconnects every send(), we run kMessages
|
||||
// single-message consumers sequentially.
|
||||
std::vector<int> all_received;
|
||||
for (int i = 0; i < kMessages; ++i)
|
||||
{
|
||||
FakeConsumer consumer(sock, 1);
|
||||
consumer.start();
|
||||
|
||||
UnixIpcBridge bridge(sock);
|
||||
bridge.send(i * 10);
|
||||
|
||||
consumer.join();
|
||||
ASSERT_EQ(consumer.received().size(), 1u);
|
||||
all_received.push_back(consumer.received()[0]);
|
||||
}
|
||||
|
||||
ASSERT_EQ(all_received.size(), static_cast<size_t>(kMessages));
|
||||
for (int i = 0; i < kMessages; ++i)
|
||||
{
|
||||
EXPECT_EQ(all_received[i], i * 10);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user