diff --git a/.clang-format b/.clang-format new file mode 100644 index 0000000..c039979 --- /dev/null +++ b/.clang-format @@ -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 diff --git a/.gitignore b/.gitignore index bc87d63..f20cad4 100644 --- a/.gitignore +++ b/.gitignore @@ -1,4 +1,6 @@ # ---> C++ +# build artifacts +build/ # Prerequisites *.d diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..8a55ac0 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,20 @@ +# 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) + +# Core library +add_library(core + src/core/SysfsRead.cxx +) + +target_include_directories(core PUBLIC ${CMAKE_CURRENT_SOURCE_DIR}/include) + +#tests +enable_testing() +add_subdirectory(tests) diff --git a/README.md b/README.md index d1a9c05..8c289fb 100644 --- a/README.md +++ b/README.md @@ -1,3 +1,23 @@ # azkoyen_technical_test -Azkoyen technical test implementation. Implemented (mostly) on standard c++ 17 framework, but with Qt wherever was necessary \ No newline at end of file +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. diff --git a/include/SysfsRead.hpp b/include/SysfsRead.hpp new file mode 100644 index 0000000..c8c9e34 --- /dev/null +++ b/include/SysfsRead.hpp @@ -0,0 +1,44 @@ +// SysfsRead.hpp +// +// SPDX-License-Identifier GPL-3.0-or-later +// Author: Unai Blazquez Gomez + +#pragma once + +#include + +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. +}; diff --git a/src/core/SysfsRead.cxx b/src/core/SysfsRead.cxx new file mode 100644 index 0000000..807bcbf --- /dev/null +++ b/src/core/SysfsRead.cxx @@ -0,0 +1,56 @@ +// SysfsRead.cxx + +// SPDX-License-Identifier: GPL-3.0-or-later +// Author: Unai Blazquez + +#include "SysfsRead.hpp" + +#include +#include + +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()); +} diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..ae62cc1 --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,15 @@ +# 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) + diff --git a/tests/test_sysfs_read.cxx b/tests/test_sysfs_read.cxx new file mode 100644 index 0000000..417c006 --- /dev/null +++ b/tests/test_sysfs_read.cxx @@ -0,0 +1,77 @@ +#include + +#include + +#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); +}