From 293fe8f2caae8e36e5a736b957741c06f105692d Mon Sep 17 00:00:00 2001 From: Khalil Estell Date: Sat, 30 Nov 2024 03:16:30 +0100 Subject: [PATCH] :truck: Fix atomic spin lock & merge mock with util (#41) * :bug: Fix linking error with atomic_spin_lock * :truck: Migrate libhal-mock to libhal-util Deprecate libhal-mock and merge with libhal-util. Rationale: Maintaining additional package repos requires effort and work. I don't see the value in keeping mock as its own repo when we could provide it via util. I don't think mocks are going to change that often to be destructive to libhal-util. I don't see what value they have as their own repos. But I see valuing in util having mocking capabilities built in. --- CMakeLists.txt | 2 + include/libhal-util/mock/adc.hpp | 57 +++++++ include/libhal-util/mock/can.hpp | 100 +++++++++++++ include/libhal-util/mock/dac.hpp | 47 ++++++ include/libhal-util/mock/input_pin.hpp | 80 ++++++++++ include/libhal-util/mock/interrupt_pin.hpp | 55 +++++++ include/libhal-util/mock/motor.hpp | 47 ++++++ include/libhal-util/mock/output_pin.hpp | 60 ++++++++ include/libhal-util/mock/pwm.hpp | 55 +++++++ include/libhal-util/mock/servo.hpp | 47 ++++++ include/libhal-util/mock/spi.hpp | 59 ++++++++ include/libhal-util/mock/steady_clock.hpp | 70 +++++++++ include/libhal-util/mock/testing.hpp | 155 ++++++++++++++++++++ include/libhal-util/mock/timer.hpp | 70 +++++++++ src/atomic_spin_lock.cpp | 78 ++++++++++ tests/atomic_spin_lock.test.cpp | 163 +++++++++++++++++++++ 16 files changed, 1145 insertions(+) create mode 100644 include/libhal-util/mock/adc.hpp create mode 100644 include/libhal-util/mock/can.hpp create mode 100644 include/libhal-util/mock/dac.hpp create mode 100644 include/libhal-util/mock/input_pin.hpp create mode 100644 include/libhal-util/mock/interrupt_pin.hpp create mode 100644 include/libhal-util/mock/motor.hpp create mode 100644 include/libhal-util/mock/output_pin.hpp create mode 100644 include/libhal-util/mock/pwm.hpp create mode 100644 include/libhal-util/mock/servo.hpp create mode 100644 include/libhal-util/mock/spi.hpp create mode 100644 include/libhal-util/mock/steady_clock.hpp create mode 100644 include/libhal-util/mock/testing.hpp create mode 100644 include/libhal-util/mock/timer.hpp create mode 100644 src/atomic_spin_lock.cpp create mode 100644 tests/atomic_spin_lock.test.cpp diff --git a/CMakeLists.txt b/CMakeLists.txt index 41dee05..ed44c2c 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,7 @@ set(TEST_SOURCES_LIST tests/inert_drivers/inert_temperature_sensor.test.cpp tests/inert_drivers/inert_timer.test.cpp tests/as_bytes.test.cpp + tests/atomic_spin_lock.test.cpp tests/can.test.cpp tests/bit.test.cpp tests/enum.test.cpp @@ -58,6 +59,7 @@ set(TEST_SOURCES_LIST set(SOURCES_LIST src/steady_clock.cpp src/streams.cpp + src/atomic_spin_lock.cpp ) if(NOT ${CMAKE_CROSSCOMPILING}) diff --git a/include/libhal-util/mock/adc.hpp b/include/libhal-util/mock/adc.hpp new file mode 100644 index 0000000..0b1c2aa --- /dev/null +++ b/include/libhal-util/mock/adc.hpp @@ -0,0 +1,57 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include +#include + +namespace hal { +/** + * @brief Mock adc implementation for use in unit tests and simulations. + */ +struct mock_adc : public hal::adc +{ + /** + * @brief Queues the floats to be returned for read() + * + * @param p_adc_values - queue of floats + */ + void set(std::queue& p_adc_values) + { + m_adc_values = p_adc_values; + } + +private: + /** + * @brief mock implementation of driver_read() + * + * @return float - adc value from queue + * @throws throw hal::operation_not_permitted - if the adc queue runs out + */ + float driver_read() override + { + if (m_adc_values.size() == 0) { + throw hal::operation_not_permitted(this); + } + auto m_current_value = m_adc_values.front(); + m_adc_values.pop(); + return m_current_value; + } + + std::queue m_adc_values{}; +}; +} // namespace hal diff --git a/include/libhal-util/mock/can.hpp b/include/libhal-util/mock/can.hpp new file mode 100644 index 0000000..175ff4e --- /dev/null +++ b/include/libhal-util/mock/can.hpp @@ -0,0 +1,100 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "testing.hpp" + +namespace hal { +/** + * @brief Mock can implementation for use in unit tests and simulations + * + */ +struct mock_can : public hal::can +{ + /** + * @brief Reset spy information for functions + * + */ + void reset() + { + spy_configure.reset(); + spy_send.reset(); + spy_on_receive.reset(); + spy_bus_on.reset(); + } + + /// Spy handler for hal::can::configure() + spy_handler spy_configure; + /// Spy handler for hal::can::send() + spy_handler spy_send; + /// Spy handler for hal::can::bus_on() will always have content of "true" + spy_handler spy_bus_on; + /// Spy handler for hal::can::on_receive() + spy_handler> spy_on_receive; + +private: + void driver_configure(settings const& p_settings) override + { + spy_configure.record(p_settings); + } + + void driver_bus_on() override + { + spy_bus_on.record(true); + } + + void driver_send(message_t const& p_message) override + { + spy_send.record(p_message); + } + + void driver_on_receive(hal::callback p_handler) override + { + spy_on_receive.record(p_handler); + } +}; +} // namespace hal + +/** + * @brief print can::message_t type using ostreams + * + * Meant for unit testing, testing and simulation purposes + * C++ streams, in general, should not be used for any embedded project that + * will ever have to be used on an MCU due to its memory cost. + * + * @tparam CharT - character type + * @tparam Traits - ostream traits type + * @param p_ostream - the ostream + * @param p_message - object to convert to a string + * @return std::basic_ostream& - reference to the ostream + */ +template +std::basic_ostream& operator<<( + std::basic_ostream& p_ostream, + hal::can::message_t const& p_message) +{ + p_ostream << "{ id: " << std::hex << "0x" << p_message.id; + p_ostream << ", length: " << std::dec << unsigned{ p_message.length }; + p_ostream << ", is_remote_request: " << p_message.is_remote_request; + p_ostream << ", payload = ["; + for (auto const& element : p_message.payload) { + p_ostream << std::hex << "0x" << unsigned{ element } << ", "; + } + p_ostream << "] }"; + return p_ostream; +} diff --git a/include/libhal-util/mock/dac.hpp b/include/libhal-util/mock/dac.hpp new file mode 100644 index 0000000..2abca41 --- /dev/null +++ b/include/libhal-util/mock/dac.hpp @@ -0,0 +1,47 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "testing.hpp" + +namespace hal { +/** + * @brief Mock dac implementation for use in unit tests and simulations with a + * spy function for write() + * + */ +struct dac_mock : public hal::dac +{ + /** + * @brief Reset spy information for write() + * + */ + void reset() + { + spy_write.reset(); + } + + /// Spy handler for hal::dac::write() + spy_handler spy_write; + +private: + void driver_write(float p_value) override + { + spy_write.record(p_value); + }; +}; +} // namespace hal diff --git a/include/libhal-util/mock/input_pin.hpp b/include/libhal-util/mock/input_pin.hpp new file mode 100644 index 0000000..1cd44b7 --- /dev/null +++ b/include/libhal-util/mock/input_pin.hpp @@ -0,0 +1,80 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include + +#include "testing.hpp" + +namespace hal { +/** + * @brief mock input_pin implementation for use in unit tests and simulations. + * + */ +struct mock_input_pin : public hal::input_pin +{ + /** + * @brief Reset spy information for configure() + * + */ + void reset() + { + spy_configure.reset(); + } + + /// Spy handler for embed:input_pin::configure() + spy_handler spy_configure; + + /** + * @brief Queues the active levels to be returned for levels() + * + * @param p_levels - queue of actives levels + */ + void set(std::queue& p_levels) + { + m_levels = p_levels; + } + +private: + void driver_configure(settings const& p_settings) override + { + spy_configure.record(p_settings); + } + /** + * @brief Mock implementation of input_pin::driver_level + * + * @return true - high voltage + * @return false - low voltage + * @throws throw hal::operation_not_permitted - if the input pin value queue + * runs out of elements + */ + bool driver_level() override + { + // This comparison performs bounds checking because front() and pop() do + // not bounds check and results in undefined behavior if the queue is empty. + if (m_levels.size() == 0) { + throw hal::operation_not_permitted(this); + } + auto m_current_value = m_levels.front(); + m_levels.pop(); + return m_current_value; + } + + std::queue m_levels{}; +}; +} // namespace hal diff --git a/include/libhal-util/mock/interrupt_pin.hpp b/include/libhal-util/mock/interrupt_pin.hpp new file mode 100644 index 0000000..1c95011 --- /dev/null +++ b/include/libhal-util/mock/interrupt_pin.hpp @@ -0,0 +1,55 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "testing.hpp" + +namespace hal { +/** + * @brief mock interrupt_pin implementation for use in unit tests and + * simulations. + * + */ +struct mock_interrupt_pin : public hal::interrupt_pin +{ + /** + * @brief Reset spy information for configure(), on_trigger(), and + * disable() + * + */ + void reset() + { + spy_configure.reset(); + spy_on_trigger.reset(); + } + + /// Spy handler for hal::interrupt_pin::configure() + spy_handler spy_configure; + /// Spy handler for hal::interrupt_pin::on_trigger() + spy_handler> spy_on_trigger; + +private: + void driver_configure(settings const& p_settings) override + { + spy_configure.record(p_settings); + } + void driver_on_trigger(hal::callback p_callback) override + { + spy_on_trigger.record(p_callback); + } +}; +} // namespace hal diff --git a/include/libhal-util/mock/motor.hpp b/include/libhal-util/mock/motor.hpp new file mode 100644 index 0000000..ca3a409 --- /dev/null +++ b/include/libhal-util/mock/motor.hpp @@ -0,0 +1,47 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "testing.hpp" + +namespace hal { +/** + * @brief Mock motor implementation for use in unit tests and simulations with a + * spy function for power() + * + */ +struct mock_motor : public hal::motor +{ + /** + * @brief Reset spy information for power() + * + */ + void reset() + { + spy_power.reset(); + } + + /// Spy handler for hal::motor::write() + spy_handler spy_power; + +private: + void driver_power(float p_power) override + { + spy_power.record(p_power); + }; +}; +} // namespace hal diff --git a/include/libhal-util/mock/output_pin.hpp b/include/libhal-util/mock/output_pin.hpp new file mode 100644 index 0000000..009d2cf --- /dev/null +++ b/include/libhal-util/mock/output_pin.hpp @@ -0,0 +1,60 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "testing.hpp" + +namespace hal { +/** + * @brief mock output pin for use in unit tests and simulations + * + */ +struct mock_output_pin : public hal::output_pin +{ + /** + * @brief Reset spy information for configure() and level() + * + */ + void reset() + { + spy_configure.reset(); + spy_level.reset(); + } + + /// Spy handler for hal::output_pin::configure() + spy_handler spy_configure; + /// Spy handler for hal::output_pin::level() + spy_handler spy_level; + +private: + void driver_configure(settings const& p_settings) override + { + spy_configure.record(p_settings); + } + void driver_level(bool p_high) override + { + m_current_level = p_high; + spy_level.record(m_current_level); + } + bool driver_level() override + { + return m_current_level; + } + + bool m_current_level{ false }; +}; +} // namespace hal diff --git a/include/libhal-util/mock/pwm.hpp b/include/libhal-util/mock/pwm.hpp new file mode 100644 index 0000000..181af8b --- /dev/null +++ b/include/libhal-util/mock/pwm.hpp @@ -0,0 +1,55 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "testing.hpp" + +namespace hal { +/** + * @brief Mock pwm implementation for use in unit tests and simulations with spy + * functions for frequency() and duty_cycle(). + * + */ +struct mock_pwm : public hal::pwm +{ + /** + * @brief Reset spy information for both frequency() and duty_cycle() + * + */ + void reset() + { + spy_frequency.reset(); + spy_duty_cycle.reset(); + } + + /// Spy handler for hal::pwm::frequency() + spy_handler spy_frequency; + /// Spy handler for hal::pwm::duty_cycle() + spy_handler spy_duty_cycle; + +private: + void driver_frequency(hertz p_settings) override + { + spy_frequency.record(p_settings); + } + + void driver_duty_cycle(float p_duty_cycle) override + { + spy_duty_cycle.record(p_duty_cycle); + } +}; +} // namespace hal diff --git a/include/libhal-util/mock/servo.hpp b/include/libhal-util/mock/servo.hpp new file mode 100644 index 0000000..9cedc8e --- /dev/null +++ b/include/libhal-util/mock/servo.hpp @@ -0,0 +1,47 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include "testing.hpp" + +namespace hal { +/** + * @brief Mock servo implementation for use in unit tests and simulations with a + * spy function for position() + * + */ +struct mock_servo : public hal::servo +{ + /** + * @brief Reset spy information for position() + * + */ + void reset() + { + spy_position.reset(); + } + + /// Spy handler for hal::servo::position() + spy_handler spy_position; + +private: + void driver_position(float p_position) override + { + spy_position.record(p_position); + }; +}; +} // namespace hal diff --git a/include/libhal-util/mock/spi.hpp b/include/libhal-util/mock/spi.hpp new file mode 100644 index 0000000..0c92402 --- /dev/null +++ b/include/libhal-util/mock/spi.hpp @@ -0,0 +1,59 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "testing.hpp" + +namespace hal { +/** + * @brief Mock spi implementation for use in unit tests and simulations with a + * spy functions for configure() and a record for the transfer() out data. The + * record ignores the in buffer and just stores the data being sent so it can be + * inspected later. + */ +struct mock_write_only_spi : public hal::spi +{ + /** + * @brief Reset spy information for both configure() and transfer() + * + */ + void reset() + { + spy_configure.reset(); + write_record.clear(); + } + + /// Spy handler for hal::spi::configure() + spy_handler spy_configure; + /// Record of the out data from hal::spi::transfer() + std::vector> write_record; + +private: + void driver_configure(settings const& p_settings) override + { + spy_configure.record(p_settings); + }; + + void driver_transfer(std::span p_data_out, + [[maybe_unused]] std::span p_data_in, + [[maybe_unused]] hal::byte p_filler) override + { + write_record.push_back({ p_data_out.begin(), p_data_out.end() }); + }; +}; +} // namespace hal diff --git a/include/libhal-util/mock/steady_clock.hpp b/include/libhal-util/mock/steady_clock.hpp new file mode 100644 index 0000000..c9c29b8 --- /dev/null +++ b/include/libhal-util/mock/steady_clock.hpp @@ -0,0 +1,70 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include + +#include + +namespace hal { +/** + * @brief mock steady_clock implementation for use in unit tests and + * simulations. + * + */ +struct mock_steady_clock : public hal::steady_clock +{ + /** + * @brief Set the frequency to be returned from frequency() + * + * @param p_frequency - Frequency to return + */ + void set_frequency(hal::hertz p_frequency) + { + m_frequency = p_frequency; + } + + /** + * @brief Queues the uptimes to be returned from uptimes() + * + * @param p_uptime_values - Queue of uptimes + */ + void set_uptimes(std::queue& p_uptime_values) + { + m_uptime_values = p_uptime_values; + } + +private: + hal::hertz driver_frequency() + { + return m_frequency; + } + + std::uint64_t driver_uptime() + { + if (m_uptime_values.size() == 0) { + return m_last_uptime; + } + + m_last_uptime = m_uptime_values.front(); + m_uptime_values.pop(); + return m_last_uptime; + } + + hal::hertz m_frequency{ 1.0_Hz }; + std::queue m_uptime_values{}; + std::uint64_t m_last_uptime{}; +}; +} // namespace hal diff --git a/include/libhal-util/mock/testing.hpp b/include/libhal-util/mock/testing.hpp new file mode 100644 index 0000000..9d7878e --- /dev/null +++ b/include/libhal-util/mock/testing.hpp @@ -0,0 +1,155 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace hal { +/** + * @brief Helper utility for making mocks for class functions that return + * status. + * + * This class stores records of a functions call history in order to be + * recovered later for inspection in tests and simulations. + * + * See pwm_mock.hpp and tests/pwm_mock.test.cpp as an example of how this is + * done in practice. + * + * @tparam args_t - the arguments of the class function + */ +template +class spy_handler +{ +public: + /** + * @brief Set the record function to return an error after a specified number + * of recordings. + * + * @param p_call_count_before_trigger - the number of calls before an error + * is thrown. + * @param p_exception_callback - a callable function that throws an exception + * when p_call_count_before_trigger reaches 1. + * @throws std::range_error - if p_call_count_before_trigger is below 0. + */ + template + void trigger_error_on_call(int p_call_count_before_trigger, + F&& p_exception_callback) + { + if (p_call_count_before_trigger < 0) { + throw std::range_error("trigger_error_on_call() must be 0 or above"); + } + m_error_trigger = p_call_count_before_trigger; + m_exception_callback = p_exception_callback; + } + + /** + * @brief Record the arguments of a function being spied on. + * + * @param p_args - arguments to record + * @throws ? - once the error trigger count reaches 1. The error depends on + * what is thrown from `p_exception_callback` in the `trigger_error_on_call`. + */ + void record(args_t... p_args) + { + m_call_history.push_back(std::make_tuple(p_args...)); + + if (m_error_trigger > 1) { + m_error_trigger--; + } else if (m_error_trigger == 1) { + m_error_trigger--; + if (m_exception_callback) { + m_exception_callback(); + } + } + } + + /** + * @brief Return the call history of the save function + * + * @return const auto& - reference to the call history vector + */ + [[nodiscard]] auto const& call_history() const + { + return m_call_history; + } + + /** + * @brief Return argument from one of call history parameters + * + * @param p_call - history call from 0 to N + * @return const auto& - reference to the call history vector + * @throws std::out_of_range - if p_call is beyond the size of call_history + */ + template + [[nodiscard]] auto const& history(size_t p_call) const + { + return std::get(m_call_history.at(p_call)); + } + + /** + * @brief Reset call recordings and turns off error trigger + * + */ + void reset() + { + m_call_history.clear(); + m_error_trigger = 0; + } + +private: + std::vector> m_call_history{}; + std::function m_exception_callback{}; + int m_error_trigger = 0; +}; +} // namespace hal + +template +inline std::ostream& operator<<( + std::ostream& p_os, + std::chrono::duration const& p_duration) +{ + return p_os << p_duration.count() << " * (" << Period::num << "/" + << Period::den << ")s"; +} + +template +inline std::ostream& operator<<(std::ostream& p_os, + std::array const& p_array) +{ + p_os << "{"; + for (auto const& element : p_array) { + p_os << element << ", "; + } + return p_os << "}\n"; +} + +template +inline std::ostream& operator<<(std::ostream& p_os, std::span const& p_array) +{ + p_os << "{"; + for (auto const& element : p_array) { + p_os << element << ", "; + } + return p_os << "}\n"; +} diff --git a/include/libhal-util/mock/timer.hpp b/include/libhal-util/mock/timer.hpp new file mode 100644 index 0000000..499b205 --- /dev/null +++ b/include/libhal-util/mock/timer.hpp @@ -0,0 +1,70 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#pragma once + +#include +#include + +#include "testing.hpp" + +namespace hal { +/** + * @brief Mock timer implementation for use in unit tests and simulations with + * spy functions for schedule(), clear(), and is_running() + * + */ +struct mock_timer : public hal::timer +{ + /** + * @brief Reset spy information for schedule(), clear(), and is_running() + * + */ + void reset() + { + spy_schedule.reset(); + spy_cancel.reset(); + spy_is_running.reset(); + } + + /// Spy handler for hal::timer::schedule() + spy_handler, std::chrono::nanoseconds> spy_schedule; + /// Spy handler for hal::timer::is_running() + spy_handler spy_is_running; + /// Spy handler for hal::timer::clear() + spy_handler spy_cancel; + +private: + void driver_schedule(hal::callback p_callback, + std::chrono::nanoseconds p_delay) override + { + m_is_running = true; + spy_schedule.record(p_callback, p_delay); + } + + bool driver_is_running() override + { + spy_is_running.record(true); + return m_is_running; + } + + void driver_cancel() override + { + m_is_running = false; + spy_cancel.record(true); + } + + bool m_is_running = false; +}; +} // namespace hal diff --git a/src/atomic_spin_lock.cpp b/src/atomic_spin_lock.cpp new file mode 100644 index 0000000..940ec55 --- /dev/null +++ b/src/atomic_spin_lock.cpp @@ -0,0 +1,78 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include + +namespace hal { +void atomic_spin_lock::os_lock() +{ + // In order to acquire a lock, the lock must first be available. In the case + // of atomic_flag, the lock is available, or unlocked, if it is set to false. + // The flag is considered locked if its value is set to true. + // So if the current thread calls `test_and_set()`, then it sets the flag to + // true. If the value returned from this was false, it means that the flag was + // originally available and this thread was the one that locked it by setting + // the value to true. If the return value was true, then that indicates that + // the lock was not available and the thread must wait. + while (m_flag.test_and_set(std::memory_order_acquire)) { + continue; // spin lock + } +} + +void atomic_spin_lock::os_unlock() +{ + m_flag.clear(); +} + +bool atomic_spin_lock::os_try_lock() +{ + // We invert this because we actually acquire the lock when previous state of + // the lock was false. Seeing the flag as false meant that it was available. + // if we attempted to take the lock and it was originally true, then the lock + // was not available and the code needs to keep polling it until it receives a + // false back. + return not m_flag.test_and_set(std::memory_order_acquire); +} + +void timed_atomic_spin_lock::os_lock() +{ + m_atomic_spin_lock.lock(); +} + +void timed_atomic_spin_lock::os_unlock() +{ + m_atomic_spin_lock.unlock(); +} + +bool timed_atomic_spin_lock::os_try_lock() +{ + return m_atomic_spin_lock.try_lock(); +} + +bool timed_atomic_spin_lock::os_try_lock_for(hal::time_duration p_poll_time) +{ + auto future_deadline = hal::future_deadline(*m_steady_clock, p_poll_time); + + while (m_steady_clock->uptime() < future_deadline) { + auto acquired_lock = m_atomic_spin_lock.try_lock(); + if (acquired_lock) { + return true; + } + } + + return false; +} +} // namespace hal diff --git a/tests/atomic_spin_lock.test.cpp b/tests/atomic_spin_lock.test.cpp new file mode 100644 index 0000000..79c8486 --- /dev/null +++ b/tests/atomic_spin_lock.test.cpp @@ -0,0 +1,163 @@ +// Copyright 2024 Khalil Estell +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#include + +#include +#include + +#include + +#include + +namespace hal { +boost::ut::suite atomic_spin_lock_test = []() { + using namespace boost::ut; + using namespace std::chrono_literals; + + "hal::atomic_spin_lock"_test = []() { + "::lock & ::unlock"_test = []() { + // Setup + hal::atomic_spin_lock test_subject; + // Setup: Take the lock pre-before the thread can take it. + test_subject.lock(); + bool thread_started = false; + bool thread_ended = false; + + std::thread lock_taker_thread([&] { + thread_started = true; + test_subject.lock(); + thread_ended = true; + }); + + while (not thread_started) { + std::this_thread::sleep_for(1ms); + } + + std::this_thread::sleep_for(1ms); + + // Exercise + expect(that % not thread_ended); + test_subject.unlock(); + std::this_thread::sleep_for(1ms); + lock_taker_thread.join(); + + // Verify + expect(that % thread_ended); + }; + + "::try_lock"_test = []() { + // Setup + hal::atomic_spin_lock test_subject; + test_subject.lock(); + + // Exercise + Verify + expect(not test_subject.try_lock()); + expect(not test_subject.try_lock()); + expect(not test_subject.try_lock()); + expect(not test_subject.try_lock()); + + test_subject.unlock(); + + expect(test_subject.try_lock()); + expect(not test_subject.try_lock()); + }; + }; + + "hal::timed_atomic_spin_lock"_test = []() { + "::lock"_test = []() { + // Setup + using namespace std::chrono_literals; + mock_steady_clock mock_steady_clock; + hal::timed_atomic_spin_lock test_subject(mock_steady_clock); + + // Setup: Take the lock pre-before the thread can take it. + test_subject.lock(); + bool thread_started = false; + bool thread_ended = false; + + std::thread lock_taker_thread([&] { + thread_started = true; + test_subject.lock(); + thread_ended = true; + }); + + while (not thread_started) { + std::this_thread::sleep_for(1ms); + } + + std::this_thread::sleep_for(1ms); + + // Exercise + expect(that % not thread_ended); + test_subject.unlock(); + std::this_thread::sleep_for(1ms); + lock_taker_thread.join(); + + // Verify + expect(that % thread_ended); + }; + + "::try_lock"_test = []() { + // Setup + mock_steady_clock mock_steady_clock; + hal::timed_atomic_spin_lock test_subject(mock_steady_clock); + test_subject.lock(); + + // Exercise + Verify + expect(not test_subject.try_lock()); + expect(not test_subject.try_lock()); + expect(not test_subject.try_lock()); + expect(not test_subject.try_lock()); + + test_subject.unlock(); + + expect(test_subject.try_lock()); + expect(not test_subject.try_lock()); + }; + "::try_lock_for"_test = []() { + // Setup + mock_steady_clock mock_steady_clock; + // Setup: Use 1kHz to make each uptime tick 1ms. + mock_steady_clock.set_frequency(1.0_kHz); + std::queue uptime_queue; + // Setup: Add enough uptime ticks to fork for the time below. + for (int i = 0; i < 4; i++) { + uptime_queue.push(i); + } + mock_steady_clock.set_uptimes(uptime_queue); + hal::timed_atomic_spin_lock test_subject(mock_steady_clock); + bool dead_locked = true; + std::thread dead_lock_checker([&dead_locked] { + std::this_thread::sleep_for(10ms); + if (dead_locked) { + throw std::runtime_error("Test dead locked!"); + } + }); + + // Exercise + test_subject.lock(); + auto lock_acquired_0 = test_subject.try_lock_for(2ms); + test_subject.unlock(); + auto lock_acquired_1 = test_subject.try_lock_for(2ms); + dead_locked = false; + dead_lock_checker.join(); + + // Verify + expect(that % not lock_acquired_0); + expect(that % lock_acquired_1); + }; + }; +}; +} // namespace hal