diff --git a/include/parallelzone/hardware/cpu/cpu.hpp b/include/parallelzone/hardware/cpu/cpu.hpp new file mode 100644 index 00000000..0adb2382 --- /dev/null +++ b/include/parallelzone/hardware/cpu/cpu.hpp @@ -0,0 +1,95 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 parallelzone::hardware { + +/** @brief Aggregates known profiling information. + * + * The class is primarily intended to ensure that `profile_it` can maintain a + * consistent API as new profiling information is added, i.e., + * `ProfileInformation` is intended to be an opaque type whose content can + * grow over time. + */ +struct ProfileInformation { + /// Type used to measure time durations + using duration = std::chrono::high_resolution_clock::duration; + + /// Long the function ran for + duration wall_time; +}; + +/** @brief Class representing a central processing unit (CPU). + * + * This class is intended to be a runtime interface for interacting with the + * CPU. Right now it can be used to profile a task run on a CPU. + */ +class CPU { +private: + /// Type used internally for wrapping tasks + using task_type = task::TaskWrapper; + + /// Type used internally for returning type-erased results + using result_type = typename task_type::type_erased_return_type; + +public: + /// Type containing profiling information + using profile_information = ProfileInformation; + + /** @brief Profiles a function. + * + * @tparam FxnType The type of the callable to profile. + * @tparam Args The types of the arguments to forward to the callable. + * + * @param[in] fxn The callable to run. + * @param[in] args The arguments to forward to the callable. + * + * @return A pair whose first element is the return of your callable and + * the second element is the profiling information obtained from + * running the callable. If your function does not have a return + * only the profiling info is returned. + * + * @throw ??? Throws if the callable throws. Same throw guarantee. + */ + template + auto profile_it(FxnType&& fxn, Args&&... args) const { + auto&& [t, unwrapper] = task::make_task(std::forward(fxn), + std::forward(args)...); + auto result = profile_it_(std::move(t)); + + using result_type = decltype(unwrapper(std::move(result.first))); + + if constexpr(std::is_same_v) { + return std::move(result.second); + } else { + auto unwrapped_result = unwrapper(std::move(result.first)); + return std::make_pair(std::move(unwrapped_result), + std::move(result.second)); + } + } + +private: + /// How the type-erased profile_it_ method returns its results + using profile_return_type = std::pair; + + /// Returns the result of the task and the profiling information obtained + profile_return_type profile_it_(task_type&& task) const; +}; + +} // namespace parallelzone::hardware \ No newline at end of file diff --git a/include/parallelzone/hardware/hardware.hpp b/include/parallelzone/hardware/hardware.hpp new file mode 100644 index 00000000..c7dc0fa7 --- /dev/null +++ b/include/parallelzone/hardware/hardware.hpp @@ -0,0 +1,19 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 diff --git a/include/parallelzone/logging/logging.hpp b/include/parallelzone/logging/logging.hpp new file mode 100644 index 00000000..ca0b97c8 --- /dev/null +++ b/include/parallelzone/logging/logging.hpp @@ -0,0 +1,19 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 \ No newline at end of file diff --git a/include/parallelzone/parallelzone.hpp b/include/parallelzone/parallelzone.hpp index c6666633..3860a633 100644 --- a/include/parallelzone/parallelzone.hpp +++ b/include/parallelzone/parallelzone.hpp @@ -23,7 +23,10 @@ * library. */ -#include "parallelzone/serialization.hpp" +#include +#include #include +#include +#include namespace parallelzone {} // namespace parallelzone diff --git a/include/parallelzone/task/argument_traits.hpp b/include/parallelzone/task/argument_traits.hpp new file mode 100644 index 00000000..616849e6 --- /dev/null +++ b/include/parallelzone/task/argument_traits.hpp @@ -0,0 +1,69 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 + +namespace parallelzone::task { + +/** @brief Structure that works out cv-qualifiers and reference-ness of @p T. + * + * @tparam The type being inspected. + * + * This structure is predominantly intended for use in template contexts where + * the properties of a type are not known. In those contexts, it can be + * useful to know if a type @p T is a reference, a const reference etc. + */ +template +struct ArgumentTraits { + /// Type of @p T w/o reference and cv-qualifiers + using value_type = std::decay_t; + + /// Type of a mutable reference to an object of type @p value_type + using reference = value_type&; + + /// Type of a read-only reference to an object of type @p value_type + using const_reference = const value_type&; + + /// Type of an rvalue reference to an object of type @p value_type + using rvalue_reference = value_type&&; + + /// Type of a pointer to an object of type @p value_type + using pointer = value_type*; + + /// Type of a pointer to a read-only object of type @p value_type + using const_pointer = const value_type*; + + /// Is @p T consistent with being passed by value? + static constexpr bool is_value_v = std::is_same_v; + + /// Is @p T consistent with being passed as a const value? + static constexpr bool is_const_value_v = + std::is_same_v; + + /// Is @p T consistent with being passed by reference? + static constexpr bool is_reference_v = std::is_same_v; + + /// Is @p T consistent with being passed by const reference? + static constexpr bool is_const_reference_v = + std::is_same_v; + + /// Is @p T consistent with being passed by rvalue reference? + static constexpr bool is_rvalue_reference_v = + std::is_same_v; +}; + +} // namespace parallelzone::task \ No newline at end of file diff --git a/include/parallelzone/task/argument_wrapper.hpp b/include/parallelzone/task/argument_wrapper.hpp new file mode 100644 index 00000000..9f932d31 --- /dev/null +++ b/include/parallelzone/task/argument_wrapper.hpp @@ -0,0 +1,127 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 + +namespace parallelzone::task { + +/** @brief Class to help forward arguments for generic functions. + * + * @tparam The type of the object *this will wrap. + * + * Depending on how the user passes an argument to a function, the function + * will own the value or alias it. ParallelZone is designed to generically + * interact with functions and in turn will need to capture arguments. When + * PZ captures an argument it will need to prolong the lifetime if the user + * passed the argument by value or rvalue reference, and maintain the aliasing + * if the user passed the argument by reference. + * + * The ArgumentWrapper class handles the logic of potentially prolonging the + * lifetime of the argument, while providing a consistent API regardless of + * whether or not it is owning or aliasing the argument. + */ +template +class ArgumentWrapper { +private: + /// Is @p U the same type as *this? + template + static constexpr bool is_arg_wrapper_v = + std::is_same_v, U>; + + /// Disables a template function if @p U is the same type as *this + template + using disable_if_arg_wrapper = std::enable_if_t>; + +public: + /// Type traits of @p T needed for TMP + using traits_t = ArgumentTraits; + + /// Unqualified type of @p T + using value_type = typename traits_t::value_type; + + /// Type of a mutable reference to an object of type @p value_type + using reference = typename traits_t::reference; + + /// Type of a read-only reference to an object of type @p value_type + using const_reference = typename traits_t::const_reference; + + /** @brief Creates a new object which wraps @p object. + * + * @tparam U The type of object must be implicitly convertible to @p T. + * @tparam Used to disable this ctor via SFINAE if @p U is + * an object of type ArgumentWrapper. + * + * The value ctor wraps the provided argument, automatically prolonging + * its lifetime if needed (needed when @p T is a value or rvalue + * reference). If an extended lifetime is not needed, @p object will be + * aliased and the user is responsible for ensuring @p object is alive + * for the duration of *this. + * + * @param object The value *this is wrapping. + * + * @throw None No throw guarantee. + */ + template>> + explicit ArgumentWrapper(U&& object) : m_value_(std::forward(object)) {} + + /** @brief Provides potentially mutable access to the value. + * + * This method is used to access the value. The result will be mutable if + * @p T is not const-qualified and read-only if @p T is const-qualified. + * + * @return A reference to the wrapped value. + * + * @throw None No throw guarantee. + */ + auto& value() { return m_value_; } + + /** @brief Provides read-only access to the value. + * + * This method is identical to the non-const version except that the result + * is guarantee to be read-only. + * + * @return A read-only reference to the wrapped value. + * + * @throw None No throw guarantee. + */ + const auto& value() const { return m_value_; } + +private: + /// Hold by value? + static constexpr bool hold_by_value = + traits_t::is_value_v || traits_t::is_rvalue_reference_v; + + /// Hold by reference? + static constexpr bool hold_by_reference = traits_t::is_reference_v; + + /// Hold by const reference? + static constexpr bool hold_by_const_reference = + traits_t::is_const_reference_v; + + /// Type, if being held by reference + using reference_holder = + std::conditional_t; + + /// Works out the type of the holder + using holder_type = + std::conditional_t; + + /// The actual value being held. + holder_type m_value_; +}; + +} // namespace parallelzone::task \ No newline at end of file diff --git a/include/parallelzone/task/detail_/task_wrapper_.hpp b/include/parallelzone/task/detail_/task_wrapper_.hpp new file mode 100644 index 00000000..7f5c6953 --- /dev/null +++ b/include/parallelzone/task/detail_/task_wrapper_.hpp @@ -0,0 +1,137 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 parallelzone::task::detail_ { + +/** @brief Creates a tuple that will forward arguments. + * + * @tparams Args The types of @p args + * + * In order to store arguments we need to put them in ArgumentWrapper objects + * (which will manage the lifetime). This function forwards a parameter pack + * into a tuple of ArgumentWrapper objects. + * + * @param[in] args The arguments to wrap into a tuple. + * + * @throw None No throw guarantee. + */ +template +auto wrap_args_(Args&&... args) { + return std::tuple{ArgumentWrapper(std::forward(args))...}; +} + +/// Implements apply_ by expanding tuple. See other overload +template +auto apply_(FxnType&& fxn, std::tuple...> args, + std::index_sequence) { + return fxn(std::forward(std::get(args).value())...); +} + +/** @brief Calls a callable with the provided arguments. + * + * @tparam FxnType The type of the callable. + * @tparam Args The types of the arguments as the user wants them to be passed. + * + * Most functions expect positional arguments and not a tuple of arguments, + * but tuples are more convenient for meta-programming. This function will + * take a tuple and unpack it into the positional arguments of the provided + * function, and then call the function. + * + * @note This function reimplements std::apply because we need to additionally + * call `.value()` on each element of the tuple. + * + * @param[in] fxn The callable to call. + * @param[in] args A tuple containing the arguments to forward to @p fxn. + * + * @return Returns if @p fxn returns. + * + * @throws Throws if @p fxn throws. Same throw guarantee. + */ +template +auto apply_(FxnType&& fxn, std::tuple...> args) { + return apply_(std::forward(fxn), std::move(args), + std::make_index_sequence()); +} + +/** @brief Creates a lambda that wil forward @p args to @p fxn. + * + * @tparam FxnType A callable type. + * @tparam Args The types of args + * + * This function creates a lambda that can be called with no arguments. When + * the lambda is called, @p args will be forwarded to @p fxn and the result + * returned from the lambda, i.e., this function creates a lambda that can be + * used to run @p fxn lazily. + * + * @param[in] fxn The callable to run. + * @param[in] args The arguments to forward to @p fxn. + * + * @return A lambda that wraps the process of forwarding @p args to @p fxn. + * + * @throw None No throw guarantee. + */ +template +auto make_inner_lambda_(FxnType&& fxn, Args&&... args) { + auto wrapped_args = wrap_args_(std::forward(args)...); + auto l = [fxn = std::forward(fxn), + args = std::move(wrapped_args)]() mutable { + return apply_(fxn, std::move(args)); + }; + return l; +} + +/** @brief Wraps the process of type-erasing a lazy function call. + * + * @tparam FxnType The type of the callable. + * @tparam Args The types of the arguments to forward to the function. + * + * This is the top-level function implementing the type-erasure of lazily + * executing a call. It relies on `make_inner_lambda_` to create the lambda + * capable of lazily running the call. The lambda from make_inner_lambda_ still + * retains the return type, which is what the lambda from this function + * erases. The lambda resulting from this function has the signature + * `std::any lambda();`. + * + * @param[in] fxn The callable to wrap. + * @param[in] args The arguments to pass to the callable when it is executed. + * + * @return A type-erased callable, that when run will execute @p fxn. + * + * @throw None No throw guarantee. + */ +template +static auto make_outer_lambda_(FxnType&& fxn, Args&&... args) { + auto inner_lambda = make_inner_lambda_(std::forward(fxn), + std::forward(args)...); + + using result_type = decltype(inner_lambda()); + if constexpr(std::is_same_v) { + return [inner_lambda = std::move(inner_lambda)]() mutable { + inner_lambda(); + return std::any{}; + }; + } else { + return [inner_lambda = std::move(inner_lambda)]() mutable { + return std::make_any(inner_lambda()); + }; + } +} + +} // namespace parallelzone::task::detail_ \ No newline at end of file diff --git a/include/parallelzone/task/task.hpp b/include/parallelzone/task/task.hpp new file mode 100644 index 00000000..3ae5d8a6 --- /dev/null +++ b/include/parallelzone/task/task.hpp @@ -0,0 +1,19 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 \ No newline at end of file diff --git a/include/parallelzone/task/task_wrapper.hpp b/include/parallelzone/task/task_wrapper.hpp new file mode 100644 index 00000000..0a8f2f89 --- /dev/null +++ b/include/parallelzone/task/task_wrapper.hpp @@ -0,0 +1,212 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 parallelzone::task { + +/** @brief Infrastructure for type-erasing a task. + * + * A lot of ParallelZone revolves around scheduling, running, and measuring + * "tasks". To PZ, a task is nothing more than executing a callable. The + * callable may or may not return something and it may have side-effects (e.g., + * logging). The TaskWrapper class provides a generic type-erased interface for + * interacting with the task. + * + * @code + * // Say you want to call function foo with arguments arg0, arg1. Then the + * // usage is: + * auto [task, unwrapper] = make_task(foo, arg0, arg1); + * + * // Run the task + * auto result = task(); + * + * // Get the result + * auto unwrapped_result = unwrapper(result); + * @endcode + * + * It should be noted that wrapping functions with zero arguments is also + * possible, just pass the name of the function to make_task. You can + * + */ +class TaskWrapper { +private: + /// Is type @p U the same as TaskWrapper? + template + static constexpr bool is_task_wrapper_v = std::is_same_v; + + /// Disables a method via SFINAE if @p U is TaskWrapper + template + using disable_if_task_wrapper_t = std::enable_if_t>; + +public: + /// Type used to return the results of the task + using type_erased_return_type = std::any; + + /** @brief Value ctor. Creates a TaskWrapper given a callable and arguments. + * + * @tparam FxnType The type of the callable *this will wrap. + * @tparam Args The types of the arguments to forward to the wrapped + * callable. + * @tparam Used to disable this method via SFINAE if + * @p FxnType is TaskWrapper. + * + * Users are encouraged to rely on the `make_task` free function for + * creating TaskWrapper objects. That freed function relies on this ctor. + * This ctor will create a TensorWrapper object which wraps the provided + * callable, @p fxn, and the arguments to pass to @p fxn, @p args. When + * *this is executed via `operator()()` @p args will be forwarded to + * @p fxn and the result returned as an object of type + * @p type_erased_return_type. + * + * @param[in] fxn The callable object this will wrap. + * @param[in] args The arguments to forward to @p fxn when *this is + * executed. + * + * @throw ??? Function may throw if capturing @p fxn or @p args throws. + * This should be very unlikely though as captures are typically + * by reference. Strong throw guarantee. + */ + template>> + explicit TaskWrapper(FxnType&& fxn, Args&&... args) : + m_erased_function_(detail_::make_outer_lambda_( + std::forward(fxn), std::forward(args)...)) {} + + /** @brief Takes ownership of another task. + * + * This method will transfer the task wrapped by a different TaskWrapper + * object into *this. The TaskWrapper that *this takes the object from + * will no longer own the task and is in a valid, but otherwise unspecified + * state. + * + * @param[in,out] other The object to take the task from. After this + * operation @p other is in a valid, but otherwise + * unspecified state. + * + * @throw None No throw guarantee. + */ + TaskWrapper(TaskWrapper&& other) noexcept = default; + + /** @brief Overrides the task in *this with that in @p rhs. + * + * This method will release the task currently wrapped by *this and + * replace it with the task wrapped by @p rhs. The task is taken from + * @p rhs so that *this owns it and @p rhs no longer does. After this + * operation @p rhs is in a valid, but otherwise unspecified state. + * + * @param[in,out] rhs The object to take the task from. After this + * operation @p rhs is in a valid, but otherwise + * unspecified state. + * + * @return *this after overriding the task. + * + * @throw None no throw guarantee. + */ + TaskWrapper& operator=(TaskWrapper&& rhs) noexcept = default; + + /** @brief Runs the task held by *this. + * + * TaskWrapper objects allow lazy execution the wrapped task. Calling this + * method will execute the task. Generally speaking, tasks are single use, + * so this function should only be called once. The TaskWrapper class does + * NOT enforce this because without knowledge of the wrapped function's + * implementation it can not know for sure whether this is the case or + * not. + * + * @return A type erased return. If the wrapped function does not return, + * the resulting object should NOT be unwrapped. + * + * @throw ??? Throws if the wrapped function throws. Same throw guarantee. + */ + type_erased_return_type operator()() { return m_erased_function_(); } + + /** @brief Creates a function which can unwrap a type-erased result. + * + * @tparam ReturnType The type to unwrap the object as. + * + * This function creates a callable that takes an rvalue reference to a + * @p type_erased_return_type object and returns the object that was + * inside the object. Note that the return from the callable is the object, + * not a copy or reference. In turn the object passed to the callable is + * consumed and should NOT be used again. + * + * @return A callable capable of unwrapping a @p type_erased_return_type + * object into a @p ReturnType object. + * + * @throw None No throw guarantee. + */ + template + static auto unwrapper() { + return [](type_erased_return_type&& returned_object) { + if constexpr(std::is_same_v) { + return; + } else { + return std::any_cast(std::move(returned_object)); + } + }; + } + +private: + /// Deleted because there can only be one instance of a task. + TaskWrapper(const TaskWrapper&) = delete; + TaskWrapper& operator=(const TaskWrapper&) = delete; + + /// Type *this will use to hold the task + using erased_function_type = std::function; + + /// Type type-erased task + erased_function_type m_erased_function_; +}; + +/** @brief Wraps a task in a TaskWrapper object. + * + * @related TaskWrapper + * + * @tparam FxnType The type of the callable that *this will wrap. + * @tparam Args The types of the arguments that will be forwarded to the + * callable. + * + * To interact generically with a task, ParallelZone requires the task to be + * wrapped in a TaskWrapper object. When a TaskWrapper object executes its + * task the results of the task, if any, are type-erased too. To restore + * their types TaskWrapper objects can generate unwrapping callables. While + * TaskWrapper objects and unwrapping callables can be made directly through + * the TaskWrapper API, the resulting calls are template-heavy. This free + * function relies on a little bit of under-the-hood template meta-programming + * to take care of the templating for the caller, thus providing a nicer API. + * + * @param[in] fxn The callable to wrap. + * @param[in] args The arguments to forward to @p fxn. + * + * @return The task and a callable for unwrapping the result of the task. + * + * @throw ??? This function may throw if capturing the arguments throws. This + * should be a very unlikely occurrence. Strong throw guarantee. + */ +template +auto make_task(FxnType&& fxn, Args&&... args) { + using return_type = + decltype(std::declval()(std::declval()...)); + auto unwrapper = TaskWrapper::unwrapper(); + TaskWrapper t(std::forward(fxn), std::forward(args)...); + return std::make_pair(std::move(t), std::move(unwrapper)); +} + +} // namespace parallelzone::task \ No newline at end of file diff --git a/src/parallelzone/hardware/cpu/cpu.cpp b/src/parallelzone/hardware/cpu/cpu.cpp new file mode 100644 index 00000000..7adb8c2c --- /dev/null +++ b/src/parallelzone/hardware/cpu/cpu.cpp @@ -0,0 +1,30 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 + +namespace parallelzone::hardware { + +typename CPU::profile_return_type CPU::profile_it_(task_type&& task) const { + profile_information i; + const auto t1 = std::chrono::high_resolution_clock::now(); + auto result = task(); + const auto t2 = std::chrono::high_resolution_clock::now(); + i.wall_time = (t2 - t1); + return std::make_pair(std::move(result), std::move(i)); +} + +} // namespace parallelzone::hardware \ No newline at end of file diff --git a/tests/cxx/unit_tests/parallelzone/hardware/cpu/cpu.cpp b/tests/cxx/unit_tests/parallelzone/hardware/cpu/cpu.cpp new file mode 100644 index 00000000..c5a86e23 --- /dev/null +++ b/tests/cxx/unit_tests/parallelzone/hardware/cpu/cpu.cpp @@ -0,0 +1,50 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 "../../catch.hpp" +#include + +using namespace parallelzone; + +TEST_CASE("CPU") { + hardware::CPU defaulted; + + SECTION("profile_it") { + using vector_type = std::vector; + vector_type a_vector{1, 2, 3}; + auto pa_vector = a_vector.data(); + + SECTION("No return") { + auto l = [pa_vector](vector_type v) { + REQUIRE(v.data() == pa_vector); + }; + + auto info = defaulted.profile_it(l, std::move(a_vector)); + REQUIRE(info.wall_time.count() > 0); // Should have taken time... + } + + SECTION("Has return") { + auto l = [pa_vector](vector_type v) { + REQUIRE(v.data() == pa_vector); + return v; + }; + + auto&& [rv, info] = defaulted.profile_it(l, std::move(a_vector)); + REQUIRE(rv.data() == pa_vector); // Test there's no hidden copies + REQUIRE(info.wall_time.count() > 0); // Should have taken time... + } + } +} \ No newline at end of file diff --git a/tests/cxx/unit_tests/parallelzone/task/argument_traits.cpp b/tests/cxx/unit_tests/parallelzone/task/argument_traits.cpp new file mode 100644 index 00000000..3ca2c391 --- /dev/null +++ b/tests/cxx/unit_tests/parallelzone/task/argument_traits.cpp @@ -0,0 +1,124 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 "../catch.hpp" +#include +#include +#include + +using namespace parallelzone::task; + +TEST_CASE("ArgumentTraits") { + SECTION("value") { + using type = int; + using traits_t = ArgumentTraits; + + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE(traits_t::is_value_v); + STATIC_REQUIRE_FALSE(traits_t::is_const_value_v); + STATIC_REQUIRE_FALSE(traits_t::is_reference_v); + STATIC_REQUIRE_FALSE(traits_t::is_const_reference_v); + STATIC_REQUIRE_FALSE(traits_t::is_rvalue_reference_v); + } + + SECTION("const value") { + using type = int; + using traits_t = ArgumentTraits; + + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE_FALSE(traits_t::is_value_v); + STATIC_REQUIRE(traits_t::is_const_value_v); + STATIC_REQUIRE_FALSE(traits_t::is_reference_v); + STATIC_REQUIRE_FALSE(traits_t::is_const_reference_v); + STATIC_REQUIRE_FALSE(traits_t::is_rvalue_reference_v); + } + + SECTION("reference") { + using type = int; + using traits_t = ArgumentTraits; + + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE_FALSE(traits_t::is_value_v); + STATIC_REQUIRE_FALSE(traits_t::is_const_value_v); + STATIC_REQUIRE(traits_t::is_reference_v); + STATIC_REQUIRE_FALSE(traits_t::is_const_reference_v); + STATIC_REQUIRE_FALSE(traits_t::is_rvalue_reference_v); + } + + SECTION("const reference") { + using type = int; + using traits_t = ArgumentTraits; + + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE_FALSE(traits_t::is_value_v); + STATIC_REQUIRE_FALSE(traits_t::is_const_value_v); + STATIC_REQUIRE_FALSE(traits_t::is_reference_v); + STATIC_REQUIRE(traits_t::is_const_reference_v); + STATIC_REQUIRE_FALSE(traits_t::is_rvalue_reference_v); + } + + SECTION("rvalue reference") { + using type = int; + using traits_t = ArgumentTraits; + + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE(std::is_same_v); + STATIC_REQUIRE( + std::is_same_v); + STATIC_REQUIRE_FALSE(traits_t::is_value_v); + STATIC_REQUIRE_FALSE(traits_t::is_const_value_v); + STATIC_REQUIRE_FALSE(traits_t::is_reference_v); + STATIC_REQUIRE_FALSE(traits_t::is_const_reference_v); + STATIC_REQUIRE(traits_t::is_rvalue_reference_v); + } +} \ No newline at end of file diff --git a/tests/cxx/unit_tests/parallelzone/task/argument_wrapper.cpp b/tests/cxx/unit_tests/parallelzone/task/argument_wrapper.cpp new file mode 100644 index 00000000..98838b0b --- /dev/null +++ b/tests/cxx/unit_tests/parallelzone/task/argument_wrapper.cpp @@ -0,0 +1,122 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 "../catch.hpp" +#include +#include +#include + +using namespace parallelzone::task; + +TEST_CASE("ArgumentWrapper") { + SECTION("Ctors") { + using T = std::vector; + T value{1, 2, 3}; + + ArgumentWrapper arg_wrapper(value); + auto pdata = arg_wrapper.value().data(); + REQUIRE(arg_wrapper.value() == value); + REQUIRE(pdata != value.data()); + + SECTION("Copy ctor") { + ArgumentWrapper copies(arg_wrapper); + REQUIRE(copies.value() == value); + REQUIRE(copies.value().data() != pdata); + } + + SECTION("Move ctor") { + ArgumentWrapper moves(std::move(arg_wrapper)); + REQUIRE(moves.value() == value); + REQUIRE(moves.value().data() == pdata); + } + + SECTION("Copy assignment") { + ArgumentWrapper copies(T{7, 8, 9}); + auto pcopies = &(copies = arg_wrapper); + REQUIRE(copies.value() == value); + REQUIRE(copies.value().data() != pdata); + REQUIRE(pcopies == &copies); + } + + SECTION("Move assignment") { + ArgumentWrapper moves(T{7, 8, 9}); + auto pmoves = &(moves = std::move(arg_wrapper)); + REQUIRE(moves.value() == value); + REQUIRE(moves.value().data() == pdata); + REQUIRE(pmoves == &moves); + } + } + + SECTION("T == int") { + using T = int; + SECTION("By value") { + ArgumentWrapper by_value(1); + REQUIRE(by_value.value() == 1); + REQUIRE(std::as_const(by_value).value() == 1); + } + + SECTION("By reference") { + int value = 2; + ArgumentWrapper by_ref(value); + REQUIRE(&by_ref.value() == &value); + REQUIRE(&std::as_const(by_ref).value() == &value); + } + + SECTION("By const reference") { + int value = 2; + ArgumentWrapper by_cref(value); + REQUIRE(&by_cref.value() == &value); + REQUIRE(&std::as_const(by_cref).value() == &value); + } + + SECTION("By rvalue reference") { + int value = 2; + ArgumentWrapper by_rref(std::move(value)); + REQUIRE(by_rref.value() == value); + REQUIRE(std::as_const(by_rref).value() == value); + } + } + + SECTION("T == std::vector") { + using T = std::vector; + T value{1, 2, 3}; + + SECTION("By value") { + ArgumentWrapper by_value(T{1, 2, 3}); + REQUIRE(by_value.value() == value); + REQUIRE(std::as_const(by_value).value() == value); + } + + SECTION("By reference") { + ArgumentWrapper by_ref(value); + REQUIRE(&by_ref.value() == &value); + REQUIRE(&std::as_const(by_ref).value() == &value); + } + + SECTION("By const reference") { + ArgumentWrapper by_cref(value); + REQUIRE(&by_cref.value() == &value); + REQUIRE(&std::as_const(by_cref).value() == &value); + } + + SECTION("By rvalue reference") { + auto pvalue = value.data(); + ArgumentWrapper by_rref(std::move(value)); + REQUIRE(by_rref.value().data() == pvalue); + REQUIRE(std::as_const(by_rref).value().data() == pvalue); + } + } +} \ No newline at end of file diff --git a/tests/cxx/unit_tests/parallelzone/task/detail_/task_wrapper_.cpp b/tests/cxx/unit_tests/parallelzone/task/detail_/task_wrapper_.cpp new file mode 100644 index 00000000..b4a99d60 --- /dev/null +++ b/tests/cxx/unit_tests/parallelzone/task/detail_/task_wrapper_.cpp @@ -0,0 +1,257 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 "../../catch.hpp" +#include +#include +#include + +using namespace parallelzone::task; + +namespace { + +using vector_type = std::vector; + +} // namespace + +TEST_CASE("wrap_args_") { + int an_int = 1; + vector_type a_vector{1, 2, 3}; + auto pa_vector = a_vector.data(); + + SECTION("No args") { + auto rv = detail_::wrap_args_(); + REQUIRE(rv == std::make_tuple()); + } + + SECTION("by value") { + auto int_rv = detail_::wrap_args_(1); + REQUIRE(std::get<0>(int_rv).value() == 1); + + auto vector_rv = detail_::wrap_args_(vector_type{1, 2, 3}); + REQUIRE(std::get<0>(vector_rv).value() == a_vector); + REQUIRE(std::get<0>(vector_rv).value().data() != pa_vector); + } + + SECTION("by reference") { + auto int_rv = detail_::wrap_args_(an_int); + REQUIRE(std::get<0>(int_rv).value() == 1); + REQUIRE(&std::get<0>(int_rv).value() == &an_int); + + auto vector_rv = detail_::wrap_args_(a_vector); + REQUIRE(std::get<0>(vector_rv).value() == a_vector); + REQUIRE(&std::get<0>(vector_rv).value() == &a_vector); + } + + SECTION("by const reference") { + auto int_rv = detail_::wrap_args_(std::as_const(an_int)); + REQUIRE(std::get<0>(int_rv).value() == 1); + REQUIRE(&std::get<0>(int_rv).value() == &an_int); + + auto vector_rv = detail_::wrap_args_(std::as_const(a_vector)); + REQUIRE(std::get<0>(vector_rv).value() == a_vector); + REQUIRE(&std::get<0>(vector_rv).value() == &a_vector); + } + + SECTION("by rvalue reference") { + auto vector_rv = detail_::wrap_args_(std::move(a_vector)); + REQUIRE(std::get<0>(vector_rv).value().data() == pa_vector); + } +} + +TEST_CASE("apply_") { + vector_type a_vector{1, 2, 3}; + auto pa_vector = a_vector.data(); + + SECTION("Function takes no arguments") { + auto inputs = detail_::wrap_args_(); + auto lambda = []() {}; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + SECTION("Function takes by value and passed by value") { + auto inputs = detail_::wrap_args_(vector_type{1, 2, 3}); + auto lambda = [&a_vector](vector_type x) { REQUIRE(x == a_vector); }; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + SECTION("Function takes by value and passed by reference") { + auto inputs = detail_::wrap_args_(a_vector); + auto lambda = [&a_vector](vector_type x) { + REQUIRE(x == a_vector); + REQUIRE(x.data() != a_vector.data()); + }; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + SECTION("Function takes by value and passed by const reference") { + auto inputs = detail_::wrap_args_(std::as_const(a_vector)); + auto lambda = [&a_vector](vector_type x) { + REQUIRE(x == a_vector); + REQUIRE(x.data() != a_vector.data()); + }; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + SECTION("Function takes by value and passed by rvalue") { + auto inputs = detail_::wrap_args_(std::move(a_vector)); + auto lambda = [pa_vector](vector_type x) { + REQUIRE(x.data() == pa_vector); + }; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + // Should raise a compiler error + // SECTION("Function takes by reference and passed by value") { + // auto inputs = detail_::wrap_args_(vector_type{1, 2, 3}); + // auto lambda = [&a_vector](vector_type& x) { }; + // REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + //} + + SECTION("Function takes by reference and passed by reference") { + auto inputs = detail_::wrap_args_(a_vector); + auto lambda = [&a_vector](vector_type& x) { REQUIRE(&x == &a_vector); }; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + // Should raise a compiler error + // SECTION("Function takes by reference and passed by const reference") { + // auto inputs = detail_::wrap_args_(std::as_const(a_vector)); + // auto lambda = [&a_vector](vector_type& x) { }; + // REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + // } + + // Should raise a compiler error + // SECTION("Function takes by reference and passed by rvalue reference") { + // auto inputs = detail_::wrap_args_(std::move(a_vector)); + // auto lambda = [pa_vector](vector_type& x) { }; + // REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + // } + + SECTION("Function takes by const reference and passed by value") { + auto inputs = detail_::wrap_args_(vector_type{1, 2, 3}); + auto lambda = [&a_vector](const vector_type& x) { + REQUIRE(x == a_vector); + }; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + SECTION("Function takes by const reference and passed by reference") { + auto inputs = detail_::wrap_args_(a_vector); + auto lambda = [&a_vector](const vector_type& x) { + REQUIRE(&x == &a_vector); + }; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + SECTION("Function takes by const reference and passed by const reference") { + auto inputs = detail_::wrap_args_(std::as_const(a_vector)); + auto lambda = [&a_vector](const vector_type& x) { + REQUIRE(&x == &a_vector); + }; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + SECTION( + "Function takes by const reference and passed by rvalue reference") { + auto inputs = detail_::wrap_args_(std::move(a_vector)); + auto lambda = [pa_vector](const vector_type& x) { + REQUIRE(x.data() == pa_vector); + }; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + SECTION("Function takes by rvalue reference and passed by value") { + auto inputs = detail_::wrap_args_(vector_type{1, 2, 3}); + auto lambda = [&a_vector](vector_type&& x) { REQUIRE(x == a_vector); }; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + // Should raise a compiler error + // SECTION("Function takes by rvalue reference and passed by reference") { + // auto inputs = detail_::wrap_args_(a_vector); + // auto lambda = [&a_vector](vector_type&& x) {}; + // REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + // } + + // Should raise a compiler error + // SECTION( + // "Function takes by rvalue reference and passed by const reference") { + // auto inputs = detail_::wrap_args_(std::as_const(a_vector)); + // auto lambda = [&a_vector](vector_type&& x) {}; + // REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + // } + + SECTION( + "Function takes by rvalue reference and passed by rvalue reference") { + auto inputs = detail_::wrap_args_(std::move(a_vector)); + auto lambda = [pa_vector](vector_type&& x) { + REQUIRE(x.data() == pa_vector); + }; + REQUIRE_NOTHROW(detail_::apply_(lambda, std::move(inputs))); + } + + SECTION("Function returns by value") { + auto inputs = detail_::wrap_args_(std::move(a_vector)); + auto lambda = [pa_vector](vector_type x) { + REQUIRE(x.data() == pa_vector); + return x; + }; + auto rv = detail_::apply_(lambda, std::move(inputs)); + REQUIRE(rv.data() == pa_vector); + } +} + +TEST_CASE("make_inner_lambda_") { + // This function is largely implemented by wrap_args_ and apply_. As long + // as those two functions work this function should work. Point being we + // just spot check a round-trip of the input + + vector_type a_vector{1, 2, 3}; + auto pa_vector = a_vector.data(); + + auto lambda = [pa_vector](vector_type x) { + REQUIRE(x.data() == pa_vector); + return x; + }; + auto lambda2 = detail_::make_inner_lambda_(lambda, std::move(a_vector)); + REQUIRE(lambda2().data() == pa_vector); +} + +TEST_CASE("make_outer_lambda_") { + // This function is largely implemented by make_inner_lambda_. Here we just + // check it wraps the return correctly + + vector_type a_vector{1, 2, 3}; + auto pa_vector = a_vector.data(); + + SECTION("Doesn't return") { + auto lambda = [pa_vector](vector_type x) { + REQUIRE(x.data() == pa_vector); + }; + auto lambda2 = detail_::make_outer_lambda_(lambda, std::move(a_vector)); + REQUIRE_FALSE(lambda2().has_value()); + } + + SECTION("Returns") { + auto lambda = [pa_vector](vector_type x) { + REQUIRE(x.data() == pa_vector); + return x; + }; + auto lambda2 = detail_::make_outer_lambda_(lambda, std::move(a_vector)); + REQUIRE(std::any_cast(lambda2()).data() == pa_vector); + } +} \ No newline at end of file diff --git a/tests/cxx/unit_tests/parallelzone/task/task_wrapper.cpp b/tests/cxx/unit_tests/parallelzone/task/task_wrapper.cpp new file mode 100644 index 00000000..8fd599e8 --- /dev/null +++ b/tests/cxx/unit_tests/parallelzone/task/task_wrapper.cpp @@ -0,0 +1,150 @@ +/* + * Copyright 2025 NWChemEx-Project + * + * 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 "../catch.hpp" +#include +#include +#include + +using namespace parallelzone::task; + +namespace { + +using vector_type = std::vector; + +// Free functions with no returns and various inputs +void no_return_free0() {} +void no_return_free1(int) {} +void no_return_free2(int* pv, vector_type v) { REQUIRE(v.data() == pv); } + +vector_type return_free2(int* pv, vector_type v) { + REQUIRE(v.data() == pv); + return v; +} + +struct TestFunctor { + TestFunctor(vector_type v) : m_v(std::move(v)) {} + + vector_type operator()() { return std::move(m_v); } + + vector_type m_v; +}; + +} // namespace + +using type_erased_return_type = TaskWrapper::type_erased_return_type; + +TEST_CASE("TaskWrapper") { + vector_type a_vector{1, 2, 3}; + auto pa_vector = a_vector.data(); + + SECTION("Ctor") { + SECTION("value") { + // This is heavily tested throughout the rest of the test case. + // So just spot check we can pass different function types + + SECTION("lambda") { + auto l = [pa_vector](vector_type v) { + REQUIRE(v.data() == pa_vector); + }; + TaskWrapper t(l, std::move(a_vector)); + t(); + } + SECTION("free function") { + TaskWrapper t(no_return_free2, pa_vector, std::move(a_vector)); + t(); + } + SECTION("Functor") { + TestFunctor fxn(std::move(a_vector)); + TaskWrapper t(std::move(fxn)); + t(); + } + } + + SECTION("move") { + TaskWrapper t(no_return_free2, pa_vector, std::move(a_vector)); + TaskWrapper t_moved(std::move(t)); + t_moved(); + } + + SECTION("move assignment") { + TaskWrapper t(return_free2, pa_vector, std::move(a_vector)); + TaskWrapper t_moved(no_return_free0); + auto pt_moved = &(t_moved = std::move(t)); + auto rv = t_moved(); + REQUIRE(std::any_cast(rv).data() == pa_vector); + REQUIRE(pt_moved == &t_moved); + } + } + + SECTION("operator()") { + // This method just runs the wrapped function and returns the + // result. As long as make_outer_lambda_ works, this method should + // work and we just spot check + TaskWrapper t(return_free2, pa_vector, std::move(a_vector)); + auto rv = t(); + REQUIRE(std::any_cast(rv).data() == pa_vector); + } + + SECTION("unwrapper") { + SECTION("no result, i.e., void") { + auto u = TaskWrapper::unwrapper(); + std::any empty; + REQUIRE_NOTHROW(u(std::move(empty))); + } + + SECTION("int") { + auto u = TaskWrapper::unwrapper(); + auto da_any = std::make_any(1); + REQUIRE(u(std::move(da_any)) == 1); + } + + SECTION("vector") { + auto u = TaskWrapper::unwrapper(); + auto da_any = std::make_any(std::move(a_vector)); + REQUIRE(std::any_cast(da_any).data() == pa_vector); + REQUIRE(u(std::move(da_any)).data() == pa_vector); + } + } + + SECTION("No return") { + auto tester = [](auto&& task, auto&& unwrapper) { + type_erased_return_type result; + REQUIRE_NOTHROW(result = task()); + REQUIRE_FALSE(result.has_value()); + REQUIRE_NOTHROW(unwrapper(std::move(result))); + }; + + std::apply(tester, make_task(no_return_free0)); + + std::apply(tester, make_task(no_return_free1, 1)); + + auto task = make_task(no_return_free2, pa_vector, std::move(a_vector)); + std::apply(tester, std::move(task)); + } + + SECTION("return") { + auto tester = [pa_vector](auto&& task, auto&& unwrapper) { + type_erased_return_type result; + REQUIRE_NOTHROW(result = task()); + REQUIRE(result.has_value()); + REQUIRE(unwrapper(std::move(result)).data() == pa_vector); + }; + + auto task = make_task(return_free2, pa_vector, std::move(a_vector)); + std::apply(tester, std::move(task)); + } +} \ No newline at end of file