From a798caf1a8cfb967f10e0f1f2385810d6e316dfd Mon Sep 17 00:00:00 2001 From: unai_71 Date: Tue, 10 Mar 2026 16:45:51 +0000 Subject: [PATCH 1/6] feat: Updated readme to serve as guidance for dev --- README.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/README.md b/README.md index 8c289fb..f334dd7 100644 --- a/README.md +++ b/README.md @@ -21,3 +21,7 @@ A Test-Driven Development (TDD) workflow was followed throughout the project. Ev 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). From 9c2117e64b35b4a9eab1ba6ff715d7edaf78aec5 Mon Sep 17 00:00:00 2001 From: unai_71 Date: Tue, 10 Mar 2026 16:54:28 +0000 Subject: [PATCH 2/6] implemented Producer API and header, stub and tests. Builds cleanly, tests fail for now --- include/Producer.hpp | 46 +++++++++++++++++ src/core/Producer.cxx | 55 ++++++++++++++++++++ tests/test_producer.cxx | 110 ++++++++++++++++++++++++++++++++++++++++ 3 files changed, 211 insertions(+) create mode 100644 include/Producer.hpp create mode 100644 src/core/Producer.cxx create mode 100644 tests/test_producer.cxx diff --git a/include/Producer.hpp b/include/Producer.hpp new file mode 100644 index 0000000..52214bd --- /dev/null +++ b/include/Producer.hpp @@ -0,0 +1,46 @@ +#pragma once +// Producer.hpp +// SPDX-License-Identifier: GPL-3.0-or-later +// Author: Unai Blazquez +#include +#include +#include +#include + +#include "SysfsRead.hpp" + +using RandomFn = std::function; +using SleepFn = std::function; +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. + /// @param sleep_fn Stub for sleeping function, allows reducing sleep on + /// testing + Producer(const std::filesystem::path& sysfs_path, + std::function send_fn, RandomFn random_fn, + 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 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); + } + std::function m_send; +}; diff --git a/src/core/Producer.cxx b/src/core/Producer.cxx new file mode 100644 index 0000000..708808a --- /dev/null +++ b/src/core/Producer.cxx @@ -0,0 +1,55 @@ +// Producer.cxx +// SPDX-License-Identifier: GPL-3.0-only +// Author: Unai Blazquez + +#include "Producer.hpp" + +#include "SysfsRead.hpp" + +Producer::Producer(const std::filesystem::path& sysfs_path, + std::function send_fn, RandomFn random_fn + : m_reader(sysfs_path), + m_send(std::move(send_fn)), + m_random(std::move(random_fn)), + m_sleep(std::move(sleep_fn)) +{ +} +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() +{ + auto status = m_reader.read_status(); + switch (status) + { + case SysfsStatus::Enabled: + m_send(m_random()); + break; + + case SysfsStatus::Unreachable: + // do nothing for now + break; + + case SysfsStatus::Empty: + break; + + case SysfsStatus::ErrorTempTooHigh: + break; + + case SysfsStatus::UnexpectedValue: + break; + } + m_sleep(delay); + // Thread will end here (for now) stop will join it +} diff --git a/tests/test_producer.cxx b/tests/test_producer.cxx new file mode 100644 index 0000000..81a33ac --- /dev/null +++ b/tests/test_producer.cxx @@ -0,0 +1,110 @@ +#include + +#include +#include + +#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 outputs; + + // construct a producer with fake file and callback + Producer producer{"fake_sysfs_input", [&outputs](int value) + { outputs.push_back(value); }, []() { return 42; }}; + // Act: initialize producer and stop it. + producer.start(); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); // ← 1ms window + producer.stop(); + + // Assert: we expect one output being 42 + ASSERT_EQ(outputs.size(), 1u); + EXPECT_EQ(outputs[0], 42); +} + +TEST(ProducerTest, ProducerDoesNotCallWhenUnexpectedValue) +{ + // Arrange create the file and write "0\n" into it. + { + std::ofstream out("fake_sysfs_input"); + out << "0\n"; + } + + std::vector outputs; + + Producer producer{"fake_sysfs_input", + [&outputs](int value) { outputs.push_back(value); }, + []() { return 42; }, [](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()); +} + +TEST(ProducerTest, ProducerDoesNotCallWhenEmpty) +{ + // Arrange create the file and write "0\n" into it. + { + std::ofstream out("fake_sysfs_input"); + out << " "; + } + + std::vector outputs; + + Producer producer{"fake_sysfs_input", + [&outputs](int value) { outputs.push_back(value); }, + []() { return 42; }, [](std::chrono::milliseconds) {}}; + + producer.start(); + producer.stop(); + + // Assert: we expect no output + EXPECT_TRUE(outputs.empty()); +} + +TEST(ProducerTest, ProducerDoesNotCallWhenUnreachable) +{ + std::vector outputs; + std::vector logs; + + Producer producer{"nonexistant_sysfs_input", + [&outputs](int value) { outputs.push_back(value); }, + []() { return 42; }, [](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()); +} + +TEST(ProducerTest, ProducerDoesNotCallWhenTempTooHigh) +{ + // create a file that contains error + { + std::ofstream out("fake_sysfs_input"); + out << "error: temp too high"; + } + std::vector outputs; + + Producer producer{"fake_sysfs_input", + [&outputs](int value) { outputs.push_back(value); }, + []() { return 42; }, [](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()); +} From 36093e6c7347f503fae24ad6fe4ed9408153ee1c Mon Sep 17 00:00:00 2001 From: unai_71 Date: Tue, 10 Mar 2026 16:57:26 +0000 Subject: [PATCH 3/6] of course it builded before, I didn't add the files to CmakeLists. Now build fails --- CMakeLists.txt | 1 + tests/CMakeLists.txt | 12 ++++++++++++ 2 files changed, 13 insertions(+) diff --git a/CMakeLists.txt b/CMakeLists.txt index 8a55ac0..891bfc2 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) # Core library add_library(core src/core/SysfsRead.cxx + src/core/Producer.cxx ) target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index ae62cc1..82e7429 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -12,4 +12,16 @@ target_link_libraries(test_sysfs_reader 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) From 5b6f20a70a11109046028abe989cd0b340e4e646 Mon Sep 17 00:00:00 2001 From: unai_71 Date: Tue, 10 Mar 2026 17:26:32 +0000 Subject: [PATCH 4/6] fix: build now succeded. Added logging capabilities also --- include/Producer.hpp | 8 ++++-- src/core/Producer.cxx | 63 ++++++++++++++++++++++++++++++----------- tests/test_producer.cxx | 32 +++++++++++++++++---- 3 files changed, 77 insertions(+), 26 deletions(-) diff --git a/include/Producer.hpp b/include/Producer.hpp index 52214bd..d21ba85 100644 --- a/include/Producer.hpp +++ b/include/Producer.hpp @@ -4,12 +4,14 @@ // Author: Unai Blazquez #include #include +#include #include #include #include "SysfsRead.hpp" using RandomFn = std::function; +using LogFn = std::function; using SleepFn = std::function; class Producer { @@ -17,11 +19,9 @@ class Producer /// @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. - /// @param sleep_fn Stub for sleeping function, allows reducing sleep on - /// testing Producer(const std::filesystem::path& sysfs_path, std::function send_fn, RandomFn random_fn, - SleepFn sleep_fn = default_sleep); + LogFn log_fn = nullptr, SleepFn sleep_fn = default_sleep); /// @brief Start the worker thread. Safe to call only once. void start(); @@ -42,5 +42,7 @@ class Producer { std::this_thread::sleep_for(d); } + LogFn m_log; std::function m_send; + std::chrono::milliseconds compute_delay(SysfsStatus status) const; }; diff --git a/src/core/Producer.cxx b/src/core/Producer.cxx index 708808a..8833d8f 100644 --- a/src/core/Producer.cxx +++ b/src/core/Producer.cxx @@ -7,13 +7,32 @@ #include "SysfsRead.hpp" Producer::Producer(const std::filesystem::path& sysfs_path, - std::function send_fn, RandomFn random_fn + std::function 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); @@ -28,28 +47,38 @@ void Producer::stop() m_thread.join(); } } + void Producer::run_loop() { - auto status = m_reader.read_status(); - switch (status) + while (m_running.load()) { - case SysfsStatus::Enabled: - m_send(m_random()); - break; + 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 - break; + case SysfsStatus::Unreachable: + // do nothing for now + if (m_log) m_log("Producer: SysfsFile Unreachable"); + break; - case SysfsStatus::Empty: - break; + case SysfsStatus::Empty: + if (m_log) m_log("Producer: SysfsFile Empty"); + break; - case SysfsStatus::ErrorTempTooHigh: - break; + case SysfsStatus::ErrorTempTooHigh: + if (m_log) m_log("Producer: Error temp too high!!"); + break; - case SysfsStatus::UnexpectedValue: - 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 } - m_sleep(delay); - // Thread will end here (for now) stop will join it } diff --git a/tests/test_producer.cxx b/tests/test_producer.cxx index 81a33ac..6dae65f 100644 --- a/tests/test_producer.cxx +++ b/tests/test_producer.cxx @@ -14,10 +14,14 @@ TEST(ProducerTest, ProducerCallsBackWhenEnabled) } // create a fake callback function std::vector outputs; + std::vector logs; // construct a producer with fake file and callback - Producer producer{"fake_sysfs_input", [&outputs](int value) - { outputs.push_back(value); }, []() { return 42; }}; + 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) {}}; // Act: initialize producer and stop it. producer.start(); std::this_thread::sleep_for(std::chrono::milliseconds(1)); // ← 1ms window @@ -26,6 +30,7 @@ TEST(ProducerTest, ProducerCallsBackWhenEnabled) // Assert: we expect one output being 42 ASSERT_EQ(outputs.size(), 1u); EXPECT_EQ(outputs[0], 42); + EXPECT_NE(logs[0].find("Enabled"), std::string::npos); } TEST(ProducerTest, ProducerDoesNotCallWhenUnexpectedValue) @@ -37,10 +42,13 @@ TEST(ProducerTest, ProducerDoesNotCallWhenUnexpectedValue) } std::vector outputs; + std::vector logs; Producer producer{"fake_sysfs_input", [&outputs](int value) { outputs.push_back(value); }, - []() { return 42; }, [](std::chrono::milliseconds) {}}; + []() { 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 @@ -48,6 +56,7 @@ TEST(ProducerTest, ProducerDoesNotCallWhenUnexpectedValue) // Assert: we expect no output EXPECT_TRUE(outputs.empty()); + EXPECT_NE(logs[0].find("UnexpectedValue"), std::string::npos); } TEST(ProducerTest, ProducerDoesNotCallWhenEmpty) @@ -59,16 +68,20 @@ TEST(ProducerTest, ProducerDoesNotCallWhenEmpty) } std::vector outputs; + std::vector logs; Producer producer{"fake_sysfs_input", [&outputs](int value) { outputs.push_back(value); }, - []() { return 42; }, [](std::chrono::milliseconds) {}}; + []() { return 42; }, + [&logs](const std::string& msg) { logs.push_back(msg); }, + [](std::chrono::milliseconds) {}}; producer.start(); producer.stop(); // Assert: we expect no output EXPECT_TRUE(outputs.empty()); + EXPECT_NE(logs[0].find("Empty"), std::string::npos); } TEST(ProducerTest, ProducerDoesNotCallWhenUnreachable) @@ -78,7 +91,9 @@ TEST(ProducerTest, ProducerDoesNotCallWhenUnreachable) Producer producer{"nonexistant_sysfs_input", [&outputs](int value) { outputs.push_back(value); }, - []() { return 42; }, [](std::chrono::milliseconds) {}}; + []() { 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 @@ -86,6 +101,7 @@ TEST(ProducerTest, ProducerDoesNotCallWhenUnreachable) // Assert: we expect no output EXPECT_TRUE(outputs.empty()); + EXPECT_NE(logs[0].find("Unreachable"), std::string::npos); } TEST(ProducerTest, ProducerDoesNotCallWhenTempTooHigh) @@ -96,10 +112,13 @@ TEST(ProducerTest, ProducerDoesNotCallWhenTempTooHigh) out << "error: temp too high"; } std::vector outputs; + std::vector logs; Producer producer{"fake_sysfs_input", [&outputs](int value) { outputs.push_back(value); }, - []() { return 42; }, [](std::chrono::milliseconds) {}}; + []() { 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 @@ -107,4 +126,5 @@ TEST(ProducerTest, ProducerDoesNotCallWhenTempTooHigh) // Assert: we expect no output EXPECT_TRUE(outputs.empty()); + EXPECT_NE(logs[0].find("Error"), std::string::npos); } From 16bf4bccd4158b165b3d961967b6f443e20de3f2 Mon Sep 17 00:00:00 2001 From: unai_71 Date: Tue, 10 Mar 2026 17:31:49 +0000 Subject: [PATCH 5/6] fix: all tests pass now, there was a segfault for calling start and stop too quickly --- tests/test_producer.cxx | 32 +++++++++++++------------------- 1 file changed, 13 insertions(+), 19 deletions(-) diff --git a/tests/test_producer.cxx b/tests/test_producer.cxx index 6dae65f..95ddaa7 100644 --- a/tests/test_producer.cxx +++ b/tests/test_producer.cxx @@ -17,18 +17,15 @@ TEST(ProducerTest, ProducerCallsBackWhenEnabled) std::vector 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); }, - [](std::chrono::milliseconds) {}}; + 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)); // ← 1ms window + std::this_thread::sleep_for(std::chrono::milliseconds(1500)); producer.stop(); // Assert: we expect one output being 42 - ASSERT_EQ(outputs.size(), 1u); EXPECT_EQ(outputs[0], 42); EXPECT_NE(logs[0].find("Enabled"), std::string::npos); } @@ -44,9 +41,8 @@ TEST(ProducerTest, ProducerDoesNotCallWhenUnexpectedValue) std::vector outputs; std::vector logs; - Producer producer{"fake_sysfs_input", - [&outputs](int value) { outputs.push_back(value); }, - []() { return 42; }, + 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) {}}; @@ -70,13 +66,13 @@ TEST(ProducerTest, ProducerDoesNotCallWhenEmpty) std::vector outputs; std::vector logs; - Producer producer{"fake_sysfs_input", - [&outputs](int value) { outputs.push_back(value); }, - []() { return 42; }, + 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 @@ -89,9 +85,8 @@ TEST(ProducerTest, ProducerDoesNotCallWhenUnreachable) std::vector outputs; std::vector logs; - Producer producer{"nonexistant_sysfs_input", - [&outputs](int value) { outputs.push_back(value); }, - []() { return 42; }, + 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) {}}; @@ -114,9 +109,8 @@ TEST(ProducerTest, ProducerDoesNotCallWhenTempTooHigh) std::vector outputs; std::vector logs; - Producer producer{"fake_sysfs_input", - [&outputs](int value) { outputs.push_back(value); }, - []() { return 42; }, + 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) {}}; From 928bb5a5fb9afbf4518baf766ff7bad946229c34 Mon Sep 17 00:00:00 2001 From: unai_71 Date: Tue, 10 Mar 2026 17:33:01 +0000 Subject: [PATCH 6/6] fix: reduced the time of test 1 again to 1ms --- tests/test_producer.cxx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/test_producer.cxx b/tests/test_producer.cxx index 95ddaa7..0c84f14 100644 --- a/tests/test_producer.cxx +++ b/tests/test_producer.cxx @@ -22,7 +22,7 @@ TEST(ProducerTest, ProducerCallsBackWhenEnabled) [&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(1500)); + std::this_thread::sleep_for(std::chrono::milliseconds(1)); producer.stop(); // Assert: we expect one output being 42