Compare commits

..

10 Commits

7 changed files with 62 additions and 14 deletions

View File

@ -1,10 +1,12 @@
# azkoyen_technical_test # azkoyen_technical_test
Azkoyen technical test implementation. Implemented (mostly) on standard c++ 17 framework, but with Qt wherever was necessary. [![License: GPL v3](https://img.shields.io/badge/License-GPLv3-blue.svg)](LICENSE)
Azkoyen technical test implementation. Implemented (mostly) on standard C++17, but with Qt wherever it was strictly necessary.
## Development approach ## 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. 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 written before or alongside the production code. This keeps each module verifiable in isolation and makes regressions immediately visible.
## SysfsRead class ## SysfsRead class
@ -24,24 +26,60 @@ The reader never throws on I/O errors; every outcome is expressed through the en
## Producer class / thread ## 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). `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). All dependencies (send function, random generator, logger, sleep) are injected, so the producer has no Qt dependency and no knowledge of sockets.
**Tests:** [tests/test_producer.cxx](tests/test_producer.cxx) — verifies that the send callback is called when `Enabled`, and is not called for `Unreachable`, `Empty`, `ErrorTempTooHigh`, and `UnexpectedValue`. Uses an injected no-op sleep to run at full speed.
## UnixIpcBridge ## UnixIpcBridge
>[!note] >[!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. >Why UNIX domain sockets? More experience with them under Linux than with POSIX shared memory and semaphores, and they map cleanly to mockable abstractions for unit testing.
`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. `UnixIpcBridge` ([include/UnixIpcBridge.hpp](include/UnixIpcBridge.hpp), [src/core/UnixIpcBridge.cxx](src/core/UnixIpcBridge.cxx)) connects to a UNIX domain socket and sends a single `int` per call. It opens a new connection for each value, keeping 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. **Tests:** [tests/test_unix_ipc.cxx](tests/test_unix_ipc.cxx) — spins up a `FakeConsumer` server, sends values through the bridge, and asserts they arrive correctly. Covers single value, zero, negative, `INT_MAX`/`INT_MIN`, multiple sequential sends, and throws-when-no-server.
## ConsumerThread ## 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: `ConsumerThread` ([include/Consumer.hpp](include/Consumer.hpp), [src/core/Consumer.cxx](src/core/Consumer.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`. 1. Prints the value to `stdout`.
2. Emits the `valueReceived(int)` Qt signal. 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. The server socket is created and bound inside `start()` **before** the thread is spawned, so the socket is guaranteed ready by the time `start()` returns — no race with the producer. Graceful shutdown is handled by `stop()`, which closes 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. **Tests:** [tests/test_consumer.cxx](tests/test_consumer.cxx) — uses `QSignalSpy` to verify single-value, multi-value, negative, and zero reception; clean stop without deadlock; stop when never started; and three corrupted-data cases (short message, empty connection, corrupted then valid).
## MainWindow
`MainWindow` ([include/MainWindow.hpp](include/MainWindow.hpp), [src/app/MainWindow.cxx](src/app/MainWindow.cxx)) is a minimal `QWidget` that displays the last integer received from `ConsumerThread`. It has no logic beyond updating a label via a slot connected to `valueReceived(int)` through Qt's queued connection — the GUI never blocks.
**Tests:** [tests/test_main_window.cxx](tests/test_main_window.cxx) — verifies label updates on single and repeated values, and that the window title is set.
## Race conditions and crash resilience
**Tests:** [tests/test_race_conditions.cxx](tests/test_race_conditions.cxx)
- **`RepeatedStartStopWhileProducerSends`** — starts and stops `ConsumerThread` 20 times while a producer thread continuously attempts sends. A watchdog thread aborts the process if any `stop()` call deadlocks within 15 seconds.
- **`ProducerSurvivesConsumerCrash`** — simulates a hard consumer crash by force-closing the server fd from outside its thread (equivalent to the kernel reclaiming fds on SIGKILL). Verifies that the producer keeps running and successfully delivers values to a fresh consumer started afterwards.
## Project structure
```
include/ Public headers for all core components
src/
app/ main.cxx and MainWindow.cxx — Qt application entry point
core/ Platform-independent logic: Producer, Consumer, SysfsReader, UnixIpcBridge
tests/ Google Test suites, one file per module + race conditions
docs/ Supporting documentation (see below)
build/ CMake out-of-source build directory
fake_sysfs_input Simulated sysfs control file used at runtime and in tests
```
## Docs
| File | Contents |
|------|----------|
| [docs/self-assessment.md](docs/self-assessment.md) | Honest breakdown of difficulties encountered, the IPC mechanism trade-off, and the main design decision that changed mid-development |
| [docs/quality_description.md](docs/quality_description.md) | One-paragraph explanation of how TDD keeps concurrent embedded software robust and reduces cyclomatic complexity by design |
| [docs/logic-flow-chart.png](docs/logic-flow-chart.png) | Architecture diagram covering thread layout, IPC flow, error-handling paths, and GUI data flow |

BIN
docs/logic-flow-chart.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 405 KiB

View File

@ -1,6 +1,6 @@
#pragma once #pragma once
// MainWindow.hpp // MainWindow.hpp
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-or-later
// Author: Unai Blazquez <unaibg2000@gmail.com> // Author: Unai Blazquez <unaibg2000@gmail.com>
#include <QLabel> #include <QLabel>

View File

@ -1,5 +1,5 @@
// UnixIpcBridge.hpp // UnixIpcBridge.hpp
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-or-later
// Author: Unai Blazquez <unaibg2000@gmail.com> // Author: Unai Blazquez <unaibg2000@gmail.com>
#pragma once #pragma once

View File

@ -1,9 +1,11 @@
// Producer.cxx // Producer.cxx
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-or-later
// Author: Unai Blazquez <unaibg2000@gmail.com> // Author: Unai Blazquez <unaibg2000@gmail.com>
#include "Producer.hpp" #include "Producer.hpp"
#include <random>
#include "SysfsRead.hpp" #include "SysfsRead.hpp"
Producer::Producer(const std::filesystem::path& sysfs_path, Producer::Producer(const std::filesystem::path& sysfs_path,
@ -27,6 +29,14 @@ std::chrono::milliseconds Producer::compute_delay(SysfsStatus status) const
{ // when error = temp too high { // when error = temp too high
return hot; return hot;
} }
if (status == SysfsStatus::Enabled)
{
static std::random_device rd;
static std::mt19937 gen(rd());
std::uniform_int_distribution<int> dist(1000,5000); //jittered around 3s with +-2s
return std::chrono::milliseconds(dist(gen));
}
else else
{ {
return standard; return standard;

View File

@ -1,5 +1,5 @@
// UnixIpcBridge.cxx // UnixIpcBridge.cxx
// SPDX-License-Identifier: GPL-3.0-only // SPDX-License-Identifier: GPL-3.0-or-later
// Author: Unai Blazquez <unaibg2000@gmail.com> // Author: Unai Blazquez <unaibg2000@gmail.com>
#include "UnixIpcBridge.hpp" #include "UnixIpcBridge.hpp"

View File

@ -1,5 +1,5 @@
# Author: Unai Blazquez # Author: Unai Blazquez
# License: GPL-3-only # License: GPL-3-or-later
add_executable(test_sysfs_reader add_executable(test_sysfs_reader
test_sysfs_read.cxx test_sysfs_read.cxx