From 4081bcc6a423af290b1537d409f45e63cfc0f4cf Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Wed, 18 Feb 2026 18:22:46 +0100 Subject: [PATCH 1/6] Start segment tests --- include/chains/CMakeLists.txt | 2 +- include/chains/chains.hpp | 11 ----------- include/chains/segment.hpp | 37 +++++++++++++---------------------- include/chains/tuple.hpp | 6 +++--- test/CMakeLists.txt | 6 +++++- test/main.cpp | 6 ++++++ test/segment_tests.cpp | 18 +++++++++++++++++ test/tuple_tests.cpp | 6 ++++++ 8 files changed, 53 insertions(+), 39 deletions(-) create mode 100644 test/segment_tests.cpp diff --git a/include/chains/CMakeLists.txt b/include/chains/CMakeLists.txt index d11ed2e..f67af00 100644 --- a/include/chains/CMakeLists.txt +++ b/include/chains/CMakeLists.txt @@ -7,7 +7,7 @@ target_sources( chains INTERFACE #chains.hpp config.hpp #on.hpp - #segment.hpp + segment.hpp #split.hpp #start.hpp #sync_wait.hpp diff --git a/include/chains/chains.hpp b/include/chains/chains.hpp index 06eddc4..bfdfc76 100644 --- a/include/chains/chains.hpp +++ b/include/chains/chains.hpp @@ -163,17 +163,6 @@ class chain { std::forward(args)...); } -#if 0 - template - [[deprecated]] auto operator()(Args&&... args) && { - using result_t = result_type; - auto [receiver, future] = - stlab::package(stlab::immediate_executor, std::identity{}); - invoke(std::move(receiver), std::forward(args)...); - return std::move(future); - } -#endif - template friend auto operator|(chain&& c, F&& f) { return std::move(c).append(std::forward(f)); diff --git a/include/chains/segment.hpp b/include/chains/segment.hpp index 761bb59..2f3e7a5 100644 --- a/include/chains/segment.hpp +++ b/include/chains/segment.hpp @@ -1,7 +1,7 @@ /* -Copyright 2026 Adobe - Distributed under the Boost Software License, Version 1.0. - (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + Copyright 2026 Adobe + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) */ #ifndef CHAINS_SEGMENT_HPP @@ -9,26 +9,15 @@ Copyright 2026 Adobe #include -#include #include #include namespace chains { inline namespace CHAINS_VERSION_NAMESPACE() { + template struct type {}; -template -class segment; - -#if 0 - template - inline auto make_segment(Applicator&& apply, Fs&&... fs) { - return segment, std::decay_t...>{ - std::forward(apply), std::forward(fs)...}; - } -#endif - template class segment { std::tuple _functions; @@ -45,11 +34,12 @@ class segment { */ template auto result_type_helper(Args&&... args) && { - return tuple_compose_greedy(std::move(_functions))(std::forward(args)...); + return interpret(std::move(_functions))(std::forward(args)...); } explicit segment(type, Applicator&& apply, std::tuple&& functions) : _functions{std::move(functions)}, _apply{std::move(apply)} {} + explicit segment(type, Applicator&& apply, Fs&&... functions) : _functions{std::move(functions)...}, _apply{std::move(apply)} {} @@ -69,12 +59,6 @@ class segment { std::tuple_cat(std::move(_functions), std::tuple{std::forward(f)})}; } -#if 0 - template - auto operator()(Args&&... args) && /* const/non-const version? - noexcept(...) */ { - return std::move(_apply)(compose_tuple(std::move(_functions)), std::forward(args)...); - } -#endif /* The apply function for a segment always returns void. @@ -88,7 +72,7 @@ class segment { // TODO: must handle this cancel prior to invoking the segment. // if (receiver.canceled()) return; return std::move(_apply)( - [_f = tuple_compose_greedy(std::move(_functions)), + [_f = interpret(std::move(_functions)), _receiver = std::forward(receiver)](T&&... args) mutable noexcept { if (_receiver->canceled()) return; try { @@ -100,6 +84,13 @@ class segment { std::forward(args)...); } }; + +template +inline auto make_segment(Applicator&& apply, Fs&&... fs) { + return segment, std::decay_t...>{ + std::forward(apply), std::forward(fs)...}; +} + } // namespace CHAINS_VERSION_NAMESPACE() } // namespace chains diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index 4c1e366..c10b329 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -1,7 +1,7 @@ /* -Copyright 2026 Adobe - Distributed under the Boost Software License, Version 1.0. - (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + Copyright 2026 Adobe + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) */ #ifndef CHAIN_TUPLE_HPP diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 9d5496a..3d606d9 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -21,7 +21,11 @@ endif() include(${Catch2_SOURCE_DIR}/extras/Catch.cmake) -add_executable(tests main.cpp tuple_tests.cpp) +add_executable(tests + main.cpp + segment_tests.cpp + tuple_tests.cpp) + target_link_libraries( tests PRIVATE chains::chains_warnings diff --git a/test/main.cpp b/test/main.cpp index f361677..a989d6f 100644 --- a/test/main.cpp +++ b/test/main.cpp @@ -1,3 +1,9 @@ +/* + Copyright 2026 Adobe + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +*/ + #define CATCH_CONFIG_RUNNER #include diff --git a/test/segment_tests.cpp b/test/segment_tests.cpp new file mode 100644 index 0000000..e698fec --- /dev/null +++ b/test/segment_tests.cpp @@ -0,0 +1,18 @@ +/* + Copyright 2026 Adobe + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +*/ + +#include + +#include +#include // moveonly + +TEST_CASE("Basic segment operations", "[segment]") { + SECTION("simple creation") { + auto sut = chains::make_segment>([](auto f) { f(); }, []() { return 42;}); + CHECK(42 == sut.invoke()); + } + +} \ No newline at end of file diff --git a/test/tuple_tests.cpp b/test/tuple_tests.cpp index ab98c70..3eea3de 100644 --- a/test/tuple_tests.cpp +++ b/test/tuple_tests.cpp @@ -1,3 +1,9 @@ +/* + Copyright 2026 Adobe + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) +*/ + #include #include From 1a8794ed90ec2d8f0a81ff092070af0a2a5018a1 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Fri, 20 Feb 2026 20:05:29 +0100 Subject: [PATCH 2/6] Add the next set of tuple tests and start segment test * fix issue in tuple_consume found by test * add chains::interpret test cases --- include/chains/segment.hpp | 7 +- include/chains/tuple.hpp | 4 +- test/segment_tests.cpp | 214 ++++++++++++++++++++++- test/tuple_tests.cpp | 350 +++++++++++++++++++++++++++++++++++++ 4 files changed, 564 insertions(+), 11 deletions(-) diff --git a/include/chains/segment.hpp b/include/chains/segment.hpp index 2f3e7a5..d369645 100644 --- a/include/chains/segment.hpp +++ b/include/chains/segment.hpp @@ -8,7 +8,9 @@ #define CHAINS_SEGMENT_HPP #include +#include +#include #include #include @@ -85,11 +87,6 @@ class segment { } }; -template -inline auto make_segment(Applicator&& apply, Fs&&... fs) { - return segment, std::decay_t...>{ - std::forward(apply), std::forward(fs)...}; -} } // namespace CHAINS_VERSION_NAMESPACE() } // namespace chains diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index c10b329..33760fa 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -148,10 +148,10 @@ constexpr auto tuple_consume(Tuple&& values) { if constexpr (consumed == 0) { // Remaining is original tuple (no elements consumed) - return std::tuple_cat(std::tuple{std::move(result)}, std::move(_values)); + return std::tuple_cat(std::make_tuple(std::move(result)), std::move(_values)); } else { auto remaining = move_tuple_tail_at(std::move(_values)); - return std::tuple_cat(std::tuple{std::move(result)}, std::move(remaining)); + return std::tuple_cat(std::make_tuple(std::move(result)), std::move(remaining)); } }; } diff --git a/test/segment_tests.cpp b/test/segment_tests.cpp index e698fec..a1d3e45 100644 --- a/test/segment_tests.cpp +++ b/test/segment_tests.cpp @@ -9,10 +9,216 @@ #include #include // moveonly +#include +#include +#include + +// Mock receiver for testing invoke +struct mock_receiver { + bool _canceled{false}; + std::exception_ptr _exception; + int _result{0}; + + auto canceled() const -> bool { return _canceled; } + auto set_exception(std::exception_ptr e) -> void { _exception = std::move(e); } + auto set_value(int value) -> void { _result = value; } +}; + TEST_CASE("Basic segment operations", "[segment]") { - SECTION("simple creation") { - auto sut = chains::make_segment>([](auto f) { f(); }, []() { return 42;}); - CHECK(42 == sut.invoke()); + SECTION("simple creation with variadic constructor") { + auto sut = + chains::segment{chains::type>{}, [](auto f) { f(); }, []() { return 42; }}; + } + + SECTION("simple creation with tuple constructor") { + auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, + std::make_tuple([]() { return 42; })}; + } + + SECTION("creation with multiple functions") { + auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, + [](int x) { return x + 1; }, [](int x) { return x * 2; }}; + } + + SECTION("creation with empty function tuple") { + auto sut = + chains::segment{chains::type>{}, [](auto f) { f(); }, std::tuple<>{}}; + } +} + +TEST_CASE("Segment copy and move semantics", "[segment]") { + SECTION("copy constructor") { + auto original = + chains::segment{chains::type>{}, [](auto f) { f(); }, []() { return 42; }}; + [[maybe_unused]] auto copy{original}; // Use direct initialization due to explicit constructor + // Both should be valid and independent + } + + SECTION("move constructor") { + auto original = + chains::segment{chains::type>{}, [](auto f) { f(); }, []() { return 42; }}; + [[maybe_unused]] auto moved = std::move(original); + // moved should be valid + } + + SECTION("segment with move-only types") { + auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, + [m = stlab::move_only(42)]() { return m.member(); }}; + auto moved = std::move(sut); + // moved should be valid + } +} + +TEST_CASE("Segment result_type_helper", "[segment]") { + SECTION("single function returning int") { + auto sut = + chains::segment{chains::type>{}, [](auto f) { f(); }, []() { return 42; }}; + auto result = std::move(sut).result_type_helper(); + CHECK(result == 42); + } + + SECTION("function chain with transformations") { + auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, + [](int x) { return x + 1; }, [](int x) { return x * 2; }}; + auto result = std::move(sut).result_type_helper(5); + CHECK(result == 12); // (5 + 1) * 2 = 12 + } + + SECTION("function chain returning string") { + auto sut = + chains::segment{chains::type>{}, [](auto f) { f(); }, + [](int x) { return x * 2; }, [](int x) { return std::to_string(x); }}; + auto result = std::move(sut).result_type_helper(21); + CHECK(result == "42"); + } + + SECTION("void returning function") { + auto hit = 0; + auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, + [&hit](int x) { hit = x; }}; + std::move(sut).result_type_helper(42); + CHECK(hit == 42); + } +} + +TEST_CASE("Segment invoke with receiver", "[segment]") { + SECTION("invoke with non-canceled receiver") { + auto receiver = std::make_shared(); + auto hit = 0; + auto sut = + chains::segment{chains::type>{}, [](auto f, auto... args) { f(args...); }, + [&hit](int x) { hit = x; }}; + + std::move(sut).invoke(receiver, 42); + CHECK(hit == 42); + CHECK(receiver->_exception == nullptr); + } + + SECTION("invoke with canceled receiver") { + auto receiver = std::make_shared(); + receiver->_canceled = true; + auto hit = 0; + auto sut = + chains::segment{chains::type>{}, [](auto f, auto... args) { f(args...); }, + [&hit](int x) { hit = x; }}; + + std::move(sut).invoke(receiver, 42); + CHECK(hit == 0); // Should not execute + } + + SECTION("invoke with exception in segment") { + auto receiver = std::make_shared(); + auto sut = + chains::segment{chains::type>{}, [](auto f, auto... args) { f(args...); }, + [](int x) { + if (x == 42) throw std::runtime_error("test error"); + return x; + }}; + + std::move(sut).invoke(receiver, 42); + CHECK(receiver->_exception != nullptr); + + bool caught_exception = false; + try { + std::rethrow_exception(receiver->_exception); + } catch (const std::runtime_error& e) { + caught_exception = true; + CHECK(std::string(e.what()) == "test error"); + } + CHECK(caught_exception); + } + + SECTION("invoke with applicator that modifies behavior") { + auto receiver = std::make_shared(); + auto hit = 0; + + // Custom applicator that doubles the argument + auto custom_apply = [](auto f, int x) { f(x * 2); }; + + auto sut = chains::segment{chains::type>{}, std::move(custom_apply), + [&hit](int x) { hit = x; }}; + + std::move(sut).invoke(receiver, 21); + CHECK(hit == 42); // 21 * 2 = 42 + } + + SECTION("invoke with chained functions") { + auto receiver = std::make_shared(); + auto result = 0; + auto sut = + chains::segment{chains::type>{}, [](auto f, auto... args) { f(args...); }, + [](int x) { return x + 1; }, [](int x) { return x * 2; }, + [&result](int x) { result = x; }}; + + std::move(sut).invoke(receiver, 5); + CHECK(result == 12); // (5 + 1) * 2 = 12 + CHECK(receiver->_exception == nullptr); + } +} + +TEST_CASE("Segment with injected types", "[segment]") { + SECTION("segment with int injection") { + auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, + []() { return 42; }}; + // Segment should be constructible with injection type + } + + SECTION("segment with multiple injection types") { + auto sut = chains::segment{chains::type>{}, + [](auto f) { f(); }, []() { return 42; }}; + // Segment should be constructible with multiple injection types + } +} + +TEST_CASE("Segment edge cases", "[segment]") { + SECTION("empty segment with no functions") { + auto sut = + chains::segment{chains::type>{}, [](auto f) { f(); }, std::tuple<>{}}; + // Should be constructible + } + + SECTION("segment with void function") { + auto hit = false; + auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, + [&hit]() { hit = true; }}; + std::move(sut).result_type_helper(); + CHECK(hit); + } + + SECTION("segment with multiple void functions") { + auto hit1 = false; + auto hit2 = false; + auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, + [&hit1]() { hit1 = true; }, [&hit2]() { hit2 = true; }}; + std::move(sut).result_type_helper(); + CHECK(hit1); + CHECK(hit2); + } + + SECTION("segment with variadic function") { + auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, + [](auto... args) { return (args + ...); }}; + auto result = std::move(sut).result_type_helper(1, 2, 3, 4); + CHECK(result == 10); } - } \ No newline at end of file diff --git a/test/tuple_tests.cpp b/test/tuple_tests.cpp index 3eea3de..ac35b1c 100644 --- a/test/tuple_tests.cpp +++ b/test/tuple_tests.cpp @@ -42,6 +42,14 @@ TEST_CASE("move_tuple_tail_at", "[tuple]") { auto result = chains::move_tuple_tail_at<0>(std::tuple{stlab::move_only(42), 42}); CHECK(result == std::make_tuple(stlab::move_only(42), 42)); } + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<0>(std::tuple{1, 2.5f, std::string("test")}); + CHECK(result == std::make_tuple(1, 2.5f, std::string("test"))); + } + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<0>(std::tuple{1, 2, 3, 4}); + CHECK(result == std::make_tuple(1, 2, 3, 4)); + } } GIVEN("a tail of index one is requested") { SECTION("tuple") { @@ -56,6 +64,64 @@ TEST_CASE("move_tuple_tail_at", "[tuple]") { auto result = chains::move_tuple_tail_at<1>(std::tuple{stlab::move_only(42), 42}); CHECK(result == std::make_tuple(42)); } + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<1>(std::tuple{1, 2.5f, std::string("test")}); + CHECK(result == std::make_tuple(2.5f, std::string("test"))); + } + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<1>( + std::tuple{std::string("first"), 42, stlab::move_only(100)}); + CHECK(result == std::make_tuple(42, stlab::move_only(100))); + } + } + GIVEN("a tail of index two is requested") { + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<2>(std::tuple{1, 2, 3}); + CHECK(result == std::make_tuple(3)); + } + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<2>(std::tuple{1, 2.5f, std::string("test")}); + CHECK(result == std::make_tuple(std::string("test"))); + } + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<2>( + std::tuple{42, std::string("mid"), stlab::move_only(100)}); + CHECK(result == std::make_tuple(stlab::move_only(100))); + } + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<2>(std::tuple{1, 2, 3, 4}); + CHECK(result == std::make_tuple(3, 4)); + } + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<2>( + std::tuple{stlab::move_only(1), stlab::move_only(2), stlab::move_only(3)}); + CHECK(result == std::make_tuple(stlab::move_only(3))); + } + } + GIVEN("a tail of index three is requested") { + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<3>(std::tuple{1, 2, 3, 4}); + CHECK(result == std::make_tuple(4)); + } + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<3>( + std::tuple{std::string("a"), 1.5f, 42, stlab::move_only(100)}); + CHECK(result == std::make_tuple(stlab::move_only(100))); + } + SECTION("tuple") { + auto result = chains::move_tuple_tail_at<3>(std::tuple{1, 2, 3, 4, 5}); + CHECK(result == std::make_tuple(4, 5)); + } + } + GIVEN("offset equals tuple size") { + SECTION("tuple with offset 2") { + auto result = chains::move_tuple_tail_at<2>(std::tuple{1, 2}); + CHECK(result == std::make_tuple()); + } + SECTION("tuple with offset 3") { + auto result = chains::move_tuple_tail_at<3>(std::tuple{1, std::string("test"), 2.5f}); + CHECK(result == std::make_tuple()); + } } } @@ -101,6 +167,7 @@ struct oneInt2Moveonly { auto operator()(int a) const -> stlab::move_only { return {a}; } }; struct variadic2Int { + auto operator()() const -> int { return 0; } auto operator()(auto... v) const -> int { return (v + ...); } }; @@ -156,6 +223,11 @@ TEST_CASE("Test tuple consume", "[tuple]") { CHECK(hit == true); CHECK(result == std::tuple(std::monostate{})); } + SECTION("tuple -> int(float)") { + std::tuple t{3.14f}; + auto result = chains::tuple_consume(t)([](float f) { return static_cast(f); }); + CHECK(result == std::make_tuple(3)); + } } GIVEN("A tuple with two values is passed to tuple_consume") { @@ -185,6 +257,26 @@ TEST_CASE("Test tuple consume", "[tuple]") { CHECK(hit == 3); CHECK(result == std::make_tuple(std::monostate{})); } + SECTION("tuple -> int(int, int)") { + std::tuple t{5, 7}; + auto result = chains::tuple_consume(t)(twoInt2Int{}); + CHECK(result == std::make_tuple(12)); + } + SECTION("tuple -> int(move_only)") { + std::tuple t{stlab::move_only(42), 100}; + auto result = chains::tuple_consume(std::move(t))(moveonly2Int{}); + CHECK(result == std::make_tuple(42, 100)); + } + SECTION("tuple -> int(int) leaves move_only") { + std::tuple t{42, stlab::move_only(100)}; + auto result = chains::tuple_consume(std::move(t))(oneInt2Int{}); + CHECK(result == std::make_tuple(42, stlab::move_only(100))); + } + SECTION("tuple -> int(string)") { + std::tuple t{std::string("hello"), std::string("world")}; + auto result = chains::tuple_consume(t)(string2Int{}); + CHECK(result == std::make_tuple(5, std::string("world"))); + } } GIVEN("A tuple with three values is passed to tuple_consume") { SECTION("tuple -> int(int, int) function") { @@ -204,6 +296,137 @@ TEST_CASE("Test tuple consume", "[tuple]") { auto result = chains::tuple_consume(t)(multi_callable{}); CHECK(result == std::make_tuple(2, std::string("Don't panic!"))); } + SECTION("tuple -> void(void) leaves all") { + std::tuple t{1, 2, 3}; + auto hit{false}; + auto result = chains::tuple_consume(t)(void2void{hit}); + CHECK(hit == true); + CHECK(result == std::make_tuple(std::monostate{}, 1, 2, 3)); + } + SECTION("tuple -> int(void) prepends result") { + std::tuple t{1, 2, 3}; + auto result = chains::tuple_consume(t)(void2Int{}); + CHECK(result == std::make_tuple(42, 1, 2, 3)); + } + SECTION("tuple -> int(int) consumes first") { + std::tuple t{10, 20, 30}; + auto result = chains::tuple_consume(t)(oneInt2Int{}); + CHECK(result == std::make_tuple(10, 20, 30)); + } + SECTION("tuple -> void(int, int, int)") { + std::tuple t{1, 2, 3}; + auto hit{0}; + auto result = + chains::tuple_consume(t)([&hit](int a, int b, int c) { hit = a + b + c; }); + CHECK(hit == 6); + CHECK(result == std::make_tuple(std::monostate{})); + } + SECTION("tuple -> int(move_only) leaves tail") { + std::tuple t{stlab::move_only(5), 10, 15}; + auto result = chains::tuple_consume(std::move(t))(moveonly2Int{}); + CHECK(result == std::make_tuple(5, 10, 15)); + } + SECTION("tuple with int(int)") { + std::tuple t{42, stlab::move_only(100), 200}; + auto result = chains::tuple_consume(std::move(t))(oneInt2Int{}); + CHECK(result == std::make_tuple(42, stlab::move_only(100), 200)); + } + SECTION("tuple -> int(string)") { + std::tuple t{std::string("test"), 42, 3.14f}; + auto result = chains::tuple_consume(t)(string2Int{}); + CHECK(result == std::make_tuple(4, 42, 3.14f)); + } + } + GIVEN("A tuple with four or more values is passed to tuple_consume") { + SECTION("tuple -> int(int, int)") { + std::tuple t{1, 2, 3, 4}; + auto result = chains::tuple_consume(t)(twoInt2Int{}); + CHECK(result == std::make_tuple(3, 3, 4)); + } + SECTION("tuple -> void(int, int)") { + std::tuple t{1, 2, 3, 4}; + auto hit{0}; + auto result = chains::tuple_consume(t)(twoInt2void{hit}); + CHECK(hit == 3); + CHECK(result == std::make_tuple(std::monostate{}, 3, 4)); + } + SECTION("tuple -> int(int)") { + std::tuple t{10, 20, 30, 40}; + auto result = chains::tuple_consume(t)(oneInt2Int{}); + CHECK(result == std::make_tuple(10, 20, 30, 40)); + } + SECTION("tuple -> void(void)") { + std::tuple t{1, 2, 3, 4}; + auto hit{false}; + auto result = chains::tuple_consume(t)(void2void{hit}); + CHECK(hit == true); + CHECK(result == std::make_tuple(std::monostate{}, 1, 2, 3, 4)); + } + SECTION("tuple -> int(int, int)") { + std::tuple t{1, 2, 3, 4, 5}; + auto result = chains::tuple_consume(t)(twoInt2Int{}); + CHECK(result == std::make_tuple(3, 3, 4, 5)); + } + SECTION("tuple -> int(string)") { + std::tuple t{std::string("hello"), 42, 3.14f, std::string("world")}; + auto result = chains::tuple_consume(t)(string2Int{}); + CHECK(result == std::make_tuple(5, 42, 3.14f, std::string("world"))); + } + } + GIVEN("Variadic function consumption") { + SECTION("tuple -> int(int...)") { + std::tuple t{10, 20}; + auto result = chains::tuple_consume(t)(variadic2Int{}); + CHECK(result == std::make_tuple(30)); + } + SECTION("tuple -> int(int...)") { + std::tuple t{1, 2, 3}; + auto result = chains::tuple_consume(t)(variadic2Int{}); + CHECK(result == std::make_tuple(6)); + } + SECTION("tuple -> int(int...)") { + std::tuple t{1, 2, 3, 4}; + auto result = chains::tuple_consume(t)(variadic2Int{}); + CHECK(result == std::make_tuple(10)); + } + SECTION("tuple<> -> int(int...)") { + std::tuple t{}; + auto result = chains::tuple_consume(t)(variadic2Int{}); + CHECK(result == std::make_tuple(0)); + } + } + GIVEN("Multiple move-only types") { + SECTION("tuple -> int(move_only)") { + std::tuple t{stlab::move_only(10), stlab::move_only(20)}; + auto result = chains::tuple_consume(std::move(t))(moveonly2Int{}); + CHECK(result == std::make_tuple(10, stlab::move_only(20))); + } + SECTION("tuple -> int(move_only)") { + std::tuple t{stlab::move_only(5), stlab::move_only(15), 25}; + auto result = chains::tuple_consume(std::move(t))(moveonly2Int{}); + CHECK(result == std::make_tuple(5, stlab::move_only(15), 25)); + } + } + SECTION("Complex return types") { + SECTION("tuple -> tuple(int)") { + std::tuple t{42}; + auto result = chains::tuple_consume(t)([](int x) { return std::make_tuple(x, x * 2); }); + // Result is tuple> - a 1-element tuple containing a 2-element tuple + auto nested = std::get<0>(result); + CHECK(nested == std::make_tuple(42, 84)); + constexpr auto size = std::tuple_size_v; + CHECK(size == 1); + } + SECTION("tuple -> pair(int, int)") { + std::tuple t{3, 4}; + auto result = + chains::tuple_consume(t)([](int a, int b) { return std::make_pair(a, b); }); + // Result is tuple> - a 1-element tuple containing a pair + auto nested = std::get<0>(result); + CHECK(nested == std::make_pair(3, 4)); + constexpr auto size = std::tuple_size_v; + CHECK(size == 1); + } } } @@ -249,5 +472,132 @@ TEST_CASE("interpret", "[tuple]") { CHECK(hit2); CHECK(result == std::monostate{}); } + SECTION("int(void), int(int)") { + auto result = + chains::interpret(std::make_tuple(void2Int{}, oneInt2Int{}))(/* no args */); + CHECK(result == 42); + } + + SECTION("int(int), int(int)") { + auto result = chains::interpret(std::make_tuple(oneInt2Int{}, oneInt2Int{}))(42); + CHECK(result == 42); + } + + SECTION("void(int), int(void)") { + auto hit{0}; + auto result = chains::interpret(std::make_tuple(oneInt2Void{hit}, void2Int{}))(5); + CHECK(hit == 5); + CHECK(result == 42); + } + + SECTION("int(int), void(int)") { + auto hit{0}; + auto result = chains::interpret(std::make_tuple(oneInt2Int{}, oneInt2Void{hit}))(42); + CHECK(hit == 42); + CHECK(result == std::monostate{}); + } + + SECTION("int(int, int), int(int) - chained computation") { + auto result = chains::interpret(std::make_tuple(twoInt2Int{}, oneInt2Int{}))(3, 4); + CHECK(result == 7); // 3+4=7, then identity + } + + SECTION("string(int), int(string)") { + auto result = chains::interpret(std::make_tuple(oneInt2String{}, string2Int{}))(42); + CHECK(result == 2); // "42" has length 2 + } + } + + SECTION("test cases with three functions as arguments") { + SECTION("int(int), int(int), int(int) - identity chain") { + auto result = + chains::interpret(std::make_tuple(oneInt2Int{}, oneInt2Int{}, oneInt2Int{}))(42); + CHECK(result == 42); + } + + SECTION("int(int, int), int(int), string(int)") { + auto result = chains::interpret( + std::make_tuple(twoInt2Int{}, oneInt2Int{}, oneInt2String{}))(3, 4); + CHECK(result == std::string("7")); + } + + SECTION("void(void), int(void), int(int)") { + auto hit{false}; + auto result = chains::interpret( + std::make_tuple(void2void{hit}, void2Int{}, oneInt2Int{}))(/* no args */); + CHECK(hit); + CHECK(result == 42); + } + + SECTION("int(int), void(int), void(void)") { + auto hit1{0}; + auto hit2{false}; + auto result = chains::interpret( + std::make_tuple(oneInt2Int{}, oneInt2Void{hit1}, void2void{hit2}))(42); + CHECK(hit1 == 42); + CHECK(hit2); + CHECK(result == std::monostate{}); + } + } + + SECTION("test cases with multiple input arguments and stack behavior") { + SECTION("int(int), int(int) with two initial arguments - processes first, leaves second") { + auto result = chains::interpret(std::make_tuple(oneInt2Int{}, oneInt2Int{}))(10, 20); + CHECK(result == 10); // First int processed through both functions, second ignored + } + + SECTION("int(int, int), int(int) with three initial arguments") { + auto result = chains::interpret(std::make_tuple(twoInt2Int{}, oneInt2Int{}))(1, 2, 3); + CHECK(result == 3); // (1+2=3), then identity(3), leaves 3 on stack + } + + SECTION("void(int, int) with three initial arguments - leaves one") { + auto hit{0}; + auto result = chains::interpret(std::make_tuple(twoInt2void{hit}))(5, 7, 100); + CHECK(hit == 12); + CHECK(result == std::monostate{}); // Last argument remains on stack + } + } + + SECTION("test cases with move-only types") { + SECTION("move_only(int), int(move_only)") { + auto result = chains::interpret(std::make_tuple(oneInt2Moveonly{}, moveonly2Int{}))(42); + CHECK(result == 42); + } + + SECTION("int(move_only), move_only(int)") { + auto result = chains::interpret(std::make_tuple(moveonly2Int{}, oneInt2Moveonly{}))( + stlab::move_only(42)); + CHECK(result == stlab::move_only(42)); + } + } + + SECTION("test cases with no functions") { + SECTION("empty function tuple with no arguments") { + auto result = chains::interpret(std::make_tuple())(/* no args */); + CHECK(result == std::monostate{}); + } + + SECTION("empty function tuple with one argument") { + auto result = chains::interpret(std::make_tuple())(42); + CHECK(result == 42); + } + + SECTION("empty function tuple with multiple arguments") { + auto result = chains::interpret(std::make_tuple())(1, 2); + CHECK(result == 1); // Returns first element + } + } + + SECTION("test cases with variadic functions") { + SECTION("int(int...) consuming all arguments") { + auto result = chains::interpret(std::make_tuple(variadic2Int{}))(1, 2, 3, 4); + CHECK(result == 10); + } + + SECTION("int(int...), int(int) - variadic then unary") { + auto result = chains::interpret(std::make_tuple(variadic2Int{}, oneInt2Int{}))(1, 2, 3); + CHECK(result == 6); + } } } From 7fae9ddb05bdaf295663bbe31c79800cb1bc876a Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Fri, 20 Feb 2026 21:18:32 +0100 Subject: [PATCH 3/6] A set of different corrections * fix github action * fix all clang format errors * add pre-commit hook for clang format --- .github/workflows/auto-clang-format.yml | 13 ++-- .pre-commit-config.yaml | 17 +++++ include/chains/CMakeLists.txt | 2 +- include/chains/chains.hpp | 6 +- include/chains/segment.hpp | 11 ++- include/chains/start.hpp | 55 ++++++++------- include/chains/sync_wait.hpp | 91 ++++++++++++------------- include/chains/then.hpp | 46 ++++++------- test/CMakeLists.txt | 3 +- test/chains_test.cpp | 11 +++ test/on_test.cpp | 22 +++--- test/segment_tests.cpp | 8 ++- 12 files changed, 152 insertions(+), 133 deletions(-) create mode 100644 .pre-commit-config.yaml create mode 100644 test/chains_test.cpp diff --git a/.github/workflows/auto-clang-format.yml b/.github/workflows/auto-clang-format.yml index 0da73ac..eec62d6 100644 --- a/.github/workflows/auto-clang-format.yml +++ b/.github/workflows/auto-clang-format.yml @@ -1,5 +1,7 @@ name: auto-clang-format -on: [pull_request] +on: + pull_request: + types: [opened, synchronize, reopened] jobs: build: @@ -13,11 +15,4 @@ jobs: exclude: './third_party ./external' extensions: 'h,cpp,hpp' clangFormatVersion: 19 - inplace: True - - uses: EndBug/add-and-commit@v9 - with: - author_name: Clang Robot - author_email: robot@example.com - message: ':art: Committing clang-format changes' - env: - GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} + inplace: False diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..de256fe --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,17 @@ +repos: +- repo: https://github.com/pre-commit/pre-commit-hooks + rev: v4.5.0 + hooks: + - id: check-yaml + - id: end-of-file-fixer + exclude: \.uxf$ + - id: trailing-whitespace + - id: mixed-line-ending + args: [--fix=auto] + + +- repo: https://github.com/pre-commit/mirrors-clang-format + rev: v17.0.4 + hooks: + - id: clang-format + types_or: [c++, c] diff --git a/include/chains/CMakeLists.txt b/include/chains/CMakeLists.txt index f67af00..1eb7238 100644 --- a/include/chains/CMakeLists.txt +++ b/include/chains/CMakeLists.txt @@ -4,7 +4,7 @@ target_sources( chains INTERFACE BASE_DIRS ../ FILES - #chains.hpp + chains.hpp config.hpp #on.hpp segment.hpp diff --git a/include/chains/chains.hpp b/include/chains/chains.hpp index bfdfc76..2cf1f1b 100644 --- a/include/chains/chains.hpp +++ b/include/chains/chains.hpp @@ -190,10 +190,7 @@ auto operator|(segment&& head, F&& f) { #include #include -namespace chains::inline v1 { - - -} // namespace chains::inline v1 +namespace chains::inline v1 {} // namespace chains::inline v1 //-------------------------------------------------------------------------------------------------- @@ -208,7 +205,6 @@ using namespace stlab; // Cancellation example - TEST_CASE("Cancellation injection", "[initial_draft]") { { cancellation_source src; diff --git a/include/chains/segment.hpp b/include/chains/segment.hpp index d369645..df7d6ff 100644 --- a/include/chains/segment.hpp +++ b/include/chains/segment.hpp @@ -14,8 +14,7 @@ #include #include -namespace chains { -inline namespace CHAINS_VERSION_NAMESPACE() { +namespace chains::inline CHAINS_VERSION_NAMESPACE() { template struct type {}; @@ -51,8 +50,8 @@ class segment { */ explicit segment(const segment&) = default; segment(segment&&) noexcept = default; - segment& operator=(const segment&) = default; - segment& operator=(segment&&) noexcept = default; + auto operator=(const segment&) -> segment& = default; + auto operator=(segment&&) noexcept -> segment& = default; template auto append(F&& f) && { @@ -87,8 +86,6 @@ class segment { } }; - -} // namespace CHAINS_VERSION_NAMESPACE() -} // namespace chains +} // namespace chains::inline CHAINS_VERSION_NAMESPACE() #endif diff --git a/include/chains/start.hpp b/include/chains/start.hpp index dece377..1526161 100644 --- a/include/chains/start.hpp +++ b/include/chains/start.hpp @@ -15,44 +15,43 @@ Copyright 2026 Adobe namespace chains { inline namespace CHAINS_VERSION_NAMESPACE() { - template - inline auto start(Chain&& chain, Args&&... args) { - using result_t = Chain::template result_type; - using package_task_t = stlab::packaged_task; +template +inline auto start(Chain&& chain, Args&&... args) { + using result_t = Chain::template result_type; + using package_task_t = stlab::packaged_task; - using invoke_t = decltype(std::forward(chain).invoke( - std::declval>>(), - std::forward(args)...)); + using invoke_t = decltype(std::forward(chain).invoke( + std::declval>>(), + std::forward(args)...)); - auto shared_receiver = std::shared_ptr(); + auto shared_receiver = std::shared_ptr(); - if constexpr (std::is_same_v) { - auto [receiver, future] = stlab::package( - stlab::immediate_executor, [](T&& val) { return std::forward(val); }); + if constexpr (std::is_same_v) { + auto [receiver, future] = stlab::package( + stlab::immediate_executor, [](T&& val) { return std::forward(val); }); - // Promote receiver to shared_ptr to extend lifetime beyond this scope and circumvent the - // move only capabilities of package_task. - shared_receiver = std::make_shared(std::move(receiver)); + // Promote receiver to shared_ptr to extend lifetime beyond this scope and circumvent the + // move only capabilities of package_task. + shared_receiver = std::make_shared(std::move(receiver)); - std::forward(chain).invoke(std::move(shared_receiver), std::forward(args)...); + std::forward(chain).invoke(std::move(shared_receiver), std::forward(args)...); - return std::move(future); - } else { - auto p = std::make_shared>(); - auto [receiver, future] = stlab::package( - stlab::immediate_executor, - [p](V&& value) { return std::forward(value); }); + return std::move(future); + } else { + auto p = std::make_shared>(); + auto [receiver, future] = stlab::package( + stlab::immediate_executor, + [p](V&& value) { return std::forward(value); }); - shared_receiver = std::make_shared(std::move(receiver)); + shared_receiver = std::make_shared(std::move(receiver)); - *p = std::forward(chain).invoke(std::move(shared_receiver), - std::forward(args)...); - return std::move(future); - } + *p = std::forward(chain).invoke(std::move(shared_receiver), + std::forward(args)...); + return std::move(future); } - - } + +} // namespace CHAINS_VERSION_NAMESPACE() } // namespace chains #endif \ No newline at end of file diff --git a/include/chains/sync_wait.hpp b/include/chains/sync_wait.hpp index 5adfd5a..8f7d4e5 100644 --- a/include/chains/sync_wait.hpp +++ b/include/chains/sync_wait.hpp @@ -12,65 +12,64 @@ Copyright 2026 Adobe namespace chains { inline namespace CHAINS_VERSION_NAMESPACE() { - template - auto sync_wait(Chain&& chain, Args&&... args) { - using result_t = typename Chain::template result_type; - - struct receiver_t { - std::optional result; - std::exception_ptr error{nullptr}; - std::mutex m; - std::condition_variable cv; - - void operator()(result_t&& value) { - { - std::lock_guard lock(m); - result = std::move(value); - } - cv.notify_one(); +template +auto sync_wait(Chain&& chain, Args&&... args) { + using result_t = typename Chain::template result_type; + + struct receiver_t { + std::optional result; + std::exception_ptr error{nullptr}; + std::mutex m; + std::condition_variable cv; + + void operator()(result_t&& value) { + { + std::lock_guard lock(m); + result = std::move(value); } + cv.notify_one(); + } - void set_exception(std::exception_ptr p) { - { - std::lock_guard lock(m); - error = p; - } - cv.notify_one(); + void set_exception(std::exception_ptr p) { + { + std::lock_guard lock(m); + error = p; } + cv.notify_one(); + } - bool canceled() const { return false; } - }; + bool canceled() const { return false; } + }; - /* - REVISIT: (sean-parent) - chain invoke doesn't work with std::ref(receiver). We should - fix that but for now create a receiver-ref. - */ + /* + REVISIT: (sean-parent) - chain invoke doesn't work with std::ref(receiver). We should + fix that but for now create a receiver-ref. + */ - auto receiver = std::make_shared(); + auto receiver = std::make_shared(); - std::forward(chain).invoke(receiver, std::forward(args)...); + std::forward(chain).invoke(receiver, std::forward(args)...); - std::unique_lock lock(receiver->m); - receiver->cv.wait(lock, [&] { return receiver->result.has_value() || receiver->error; }); + std::unique_lock lock(receiver->m); + receiver->cv.wait(lock, [&] { return receiver->result.has_value() || receiver->error; }); - if (receiver->error) { - std::rethrow_exception(receiver->error); - } - return *receiver->result; + if (receiver->error) { + std::rethrow_exception(receiver->error); } + return *receiver->result; +} - /* - TODO: The ergonomics of chains are painful with three arguments. We could reduce to a - single argument or move to a concept? Here I really want the forward reference to be an - rvalue ref. - - The implementation of sync_wait is complicated by the fact that the promise is currently - hard/ wired into the chain. sync_wait needs to be able to invoke the promise/receiver - - _then_ flag the condition that it is ready. - */ +/* + TODO: The ergonomics of chains are painful with three arguments. We could reduce to a + single argument or move to a concept? Here I really want the forward reference to be an + rvalue ref. + The implementation of sync_wait is complicated by the fact that the promise is currently + hard/ wired into the chain. sync_wait needs to be able to invoke the promise/receiver - + _then_ flag the condition that it is ready. +*/ -} +} // namespace CHAINS_VERSION_NAMESPACE() } // namespace chains #endif \ No newline at end of file diff --git a/include/chains/then.hpp b/include/chains/then.hpp index 5392b9f..b4ec399 100644 --- a/include/chains/then.hpp +++ b/include/chains/then.hpp @@ -7,8 +7,8 @@ Copyright 2026 Adobe #ifndef CHAINS_THEN_HPP #define CHAINS_THEN_HPP -#include #include +#include #include #include @@ -16,36 +16,34 @@ Copyright 2026 Adobe namespace chains { inline namespace CHAINS_VERSION_NAMESPACE() { +/* + The `then` algorithm takes a future and returns a segment (chain) that will schedule the + segment as a continuation of the future. - /* - The `then` algorithm takes a future and returns a segment (chain) that will schedule the - segment as a continuation of the future. - - The segment returns void so the future is (kind of) detached - but this should be done - without the overhead of a future::detach. - - How is cancellation handled here? Let's say we have this: + The segment returns void so the future is (kind of) detached - but this should be done + without the overhead of a future::detach. - `auto f = start(then(future));` + How is cancellation handled here? Let's say we have this: - And we destruct f. We need to _delete_ the (detached) future. Where is this held? f is only - holding the promise. - */ + `auto f = start(then(future));` - template - auto then(F&& future) { - return chain{std::tuple<>{}, - segment{type::result_type>{}, - [_future = std::forward(future)](C&& continuation) mutable { - return std::move(_future).then(std::forward(continuation)); - }}}; - } + And we destruct f. We need to _delete_ the (detached) future. Where is this held? f is only + holding the promise. +*/ - // TODO: (sean-parent) - should we make this pipeable? - // TODO: (sean-parent) - fix case where invoke_t is void. +template +auto then(F&& future) { + return chain{std::tuple<>{}, + segment{type::result_type>{}, + [_future = std::forward(future)](C&& continuation) mutable { + return std::move(_future).then(std::forward(continuation)); + }}}; +} +// TODO: (sean-parent) - should we make this pipeable? +// TODO: (sean-parent) - fix case where invoke_t is void. -} +} // namespace CHAINS_VERSION_NAMESPACE() } // namespace chains #endif \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3d606d9..3abb7bb 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -21,7 +21,8 @@ endif() include(${Catch2_SOURCE_DIR}/extras/Catch.cmake) -add_executable(tests +add_executable(tests + chains_test.cpp main.cpp segment_tests.cpp tuple_tests.cpp) diff --git a/test/chains_test.cpp b/test/chains_test.cpp new file mode 100644 index 0000000..8f1ae5b --- /dev/null +++ b/test/chains_test.cpp @@ -0,0 +1,11 @@ +#include "catch2/catch_test_macros.hpp" + +// #include + +#include + +TEST_CASE("Basic chain operations", "[chain]") { + SECTION("") { + SECTION("") {} + } +} diff --git a/test/on_test.cpp b/test/on_test.cpp index 0675f5f..efb7e69 100644 --- a/test/on_test.cpp +++ b/test/on_test.cpp @@ -97,8 +97,9 @@ TEST_CASE("Initial draft", "[initial_draft]") { } GIVEN("a sequence of callables in a chain of chains synchronous") { - auto a0 = on(immediate_executor) | [](int x) { return x * 2; } | on(immediate_executor) | [](int x) { return to_string(x); } | - on(immediate_executor) | [](const string& s) { return s + "!"; }; + auto a0 = on(immediate_executor) | [](int x) { return x * 2; } | on(immediate_executor) | + [](int x) { return to_string(x); } | on(immediate_executor) | + [](const string& s) { return s + "!"; }; auto f = start(std::move(a0), 42); auto val = f.get_ready(); @@ -106,23 +107,24 @@ TEST_CASE("Initial draft", "[initial_draft]") { } GIVEN("a sequence of callables in a chain of chains asynchronous") { - auto a0 = on(default_executor) | [](int x) { return x * 2; } | on(immediate_executor) | [](int x) { return to_string(x); } | - on(default_executor) | [](const string& s) { return s + "!"; }; + auto a0 = on(default_executor) | [](int x) { return x * 2; } | on(immediate_executor) | + [](int x) { return to_string(x); } | on(default_executor) | + [](const string& s) { return s + "!"; }; auto val = sync_wait(std::move(a0), 42); REQUIRE(val == string("84!")); } } - TEST_CASE("Cancellation of then()", "[initial_draft]") { annotate_counters cnt; GIVEN("that a ") { - auto fut = async(default_executor, [] { - std::this_thread::sleep_for(std::chrono::seconds{3}); - std::cout << "Future did run" << std::endl; - return std::string("42"); - }).then([_counter = annotate{cnt}](const auto& s) { std::cout << s << std::endl; }); + auto fut = + async(default_executor, [] { + std::this_thread::sleep_for(std::chrono::seconds{3}); + std::cout << "Future did run" << std::endl; + return std::string("42"); + }).then([_counter = annotate{cnt}](const auto& s) { std::cout << s << std::endl; }); auto result_f = start(then(fut)); } diff --git a/test/segment_tests.cpp b/test/segment_tests.cpp index a1d3e45..62a7ef8 100644 --- a/test/segment_tests.cpp +++ b/test/segment_tests.cpp @@ -11,7 +11,10 @@ #include #include +#include #include +#include +#include // Mock receiver for testing invoke struct mock_receiver { @@ -19,7 +22,7 @@ struct mock_receiver { std::exception_ptr _exception; int _result{0}; - auto canceled() const -> bool { return _canceled; } + [[nodiscard]] auto canceled() const -> bool { return _canceled; } auto set_exception(std::exception_ptr e) -> void { _exception = std::move(e); } auto set_value(int value) -> void { _result = value; } }; @@ -50,7 +53,8 @@ TEST_CASE("Segment copy and move semantics", "[segment]") { SECTION("copy constructor") { auto original = chains::segment{chains::type>{}, [](auto f) { f(); }, []() { return 42; }}; - [[maybe_unused]] auto copy{original}; // Use direct initialization due to explicit constructor + [[maybe_unused]] auto copy{ + original}; // Use direct initialization due to explicit constructor // Both should be valid and independent } From 8716385946c9ee05fca4439d3d7034706d7f7868 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Fri, 20 Feb 2026 22:18:12 +0100 Subject: [PATCH 4/6] Cleanup before test --- include/chains/chains.hpp | 177 +++----------------------------------- test/chains_test.cpp | 151 +++++++++++++++++++++++++++++++- 2 files changed, 161 insertions(+), 167 deletions(-) diff --git a/include/chains/chains.hpp b/include/chains/chains.hpp index 2cf1f1b..3ed2da6 100644 --- a/include/chains/chains.hpp +++ b/include/chains/chains.hpp @@ -1,7 +1,7 @@ /* -Copyright 2026 Adobe - Distributed under the Boost Software License, Version 1.0. - (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) + Copyright 2026 Adobe + Distributed under the Boost Software License, Version 1.0. + (See accompanying file LICENSE_1_0.txt or copy at http://www.boost.org/LICENSE_1_0.txt) */ #ifndef CHAINS_CHAINS_HPP @@ -24,8 +24,7 @@ set on the receiver. */ -namespace chains { -inline namespace CHAINS_VERSION_NAMESPACE() { +namespace chains::inline CHAINS_VERSION_NAMESPACE() { /* segment is invoked with a receiver - @@ -34,11 +33,11 @@ segment is invoked with a receiver - template struct receiver_ref { Receiver* _receiver; - void operator()(auto&&... args) { + auto operator()(auto&&... args) -> void { _receiver->operator()(std::forward(args)...); } - void set_exception(std::exception_ptr p) { _receiver->set_exception(p); } - bool canceled() const { return _receiver->canceled(); } + auto set_exception(std::exception_ptr p) -> void { _receiver->set_exception(p); } + [[nodiscard]] auto canceled() const -> bool { return _receiver->canceled(); } }; namespace detail { @@ -142,8 +141,8 @@ class chain { chain(const chain&) = default; chain(chain&&) noexcept = default; - chain& operator=(const chain&) = default; - chain& operator=(chain&&) noexcept = default; + auto operator=(const chain&) -> chain& = default; + auto operator=(chain&&) noexcept -> chain& = default; // append function to the last sequence template @@ -183,160 +182,6 @@ auto operator|(segment&& head, F&& f) { return chain{std::tuple<>{}, std::move(head).append(std::forward(f))}; } -} // namespace CHAINS_VERSION_NAMESPACE() +} // namespace chains::inline CHAINS_VERSION_NAMESPACE() -//-------------------------------------------------------------------------------------------------- - -#include -#include - -namespace chains::inline v1 {} // namespace chains::inline v1 - -//-------------------------------------------------------------------------------------------------- - -#include -#include -#include -#include - -using namespace std; -using namespace chains; -using namespace stlab; - -// Cancellation example - -TEST_CASE("Cancellation injection", "[initial_draft]") { - { - cancellation_source src; - - // Build a chain where the first function expects the token as first argument. - auto c = with_cancellation(src) | [](cancellation_token token, int x) { - if (token.canceled()) return 0; - return x * 2; - } | [](int y) { return y + 10; }; // token only needed by first step - - auto f = start(std::move(c), 5); - REQUIRE(f.get_ready() == 20); // (5*2)+10 - - // Demonstrate cancel before start - src.cancel(); - auto c2 = with_cancellation(src) | [](cancellation_token token, int x) { - if (token.canceled()) return 0; - return x * 3; - }; - auto f2 = start(std::move(c2), 7); - REQUIRE(f2.get_ready() == 0); - } - - //{ - // cancellation_source src; - - // // Build a chain where each function expects the token as first argument. - // // First function uses the token, returns an int. - // auto c = with_cancellation(src) | [](cancellation_token token, int x) { - // if (token.canceled()) return 0; - // return x * 2; - // } | [](int y) { return y + 10; }; // token only needed by first step - - // auto f = start(std::move(c), 5); - // REQUIRE(f.get_ready() == 20); // (5*2)+10 - //} -} - -// --- Example test demonstrating split --------------------------------------------------------- -TEST_CASE("Split fan-out", "[initial_draft]") { - auto base = on(immediate_executor) | [](int a) { return a; } | [](int x) { return x + 5; }; - auto splitter = split(std::move(base)); - auto left = splitter.fan([](int v) { return v * 2; }) | [](int x) { return x + 1; }; - auto right = splitter.fan([](int v) { return std::string("v=") + std::to_string(v); }); - - auto f_right = start(std::move(right), 10); - auto f_left = start(std::move(left), 5); - REQUIRE(f_right.get_ready() == std::string("v=15")); - REQUIRE(f_left.get_ready() == 31); -} - -TEST_CASE("Split fan-out bound", "[initial_draft]") { - auto base = on(immediate_executor) | [](int a) { return a; } | [](int x) { return x + 5; }; - - // Bind upstream start argument 10 once: - auto splitter = split_bind(std::move(base), 10); - - // Branches now start with no args; upstream result (15) is injected. - auto left = splitter.fan([](int v) { return v * 2; }) | [](int x) { return x + 1; }; - auto right = splitter.fan([](int v) { return std::string("v=") + std::to_string(v); }); - - auto f_right = start(std::move(right)); // no argument - auto f_left = start(std::move(left)); // no argument - - REQUIRE(f_right.get_ready() == std::string("v=15")); - REQUIRE(f_left.get_ready() == 31); -} - -TEST_CASE("Initial draft", "[initial_draft]") { - GIVEN("a sequence of callables with different arguments") { - auto oneInt2Int = [](int a) { return a * 2; }; - auto twoInt2Int = [](int a, int b) { return a + b; }; - auto void2Int = []() { return 42; }; - - auto a0 = on(stlab::immediate_executor) | oneInt2Int | void2Int | twoInt2Int; - - auto f = start(std::move(a0), 2); - REQUIRE(f.is_ready()); - auto val = f.get_ready(); - REQUIRE(46 == val); - } - - GIVEN("a sequence of callables that just work with move only value") { - auto oneInt2Int = [](move_only a) { return move_only(a.member() * 2); }; - auto twoInt2Int = [](move_only a, move_only b) { - return move_only(a.member() + b.member()); - }; - auto void2Int = []() { return move_only(42); }; - - auto a0 = on(stlab::immediate_executor) | oneInt2Int | void2Int | twoInt2Int; - - auto f = start(std::move(a0), move_only(2)); - REQUIRE(f.is_ready()); - auto val = std::move(f).get_ready(); - REQUIRE(46 == val.member()); - } - - GIVEN("a sequence of callables in a chain of chains synchronous") { - auto a0 = on(immediate_executor) | [](int x) { return x * 2; } | on(immediate_executor) | - [](int x) { return to_string(x); } | on(immediate_executor) | - [](const string& s) { return s + "!"; }; - - auto f = start(std::move(a0), 42); - auto val = f.get_ready(); - REQUIRE(val == string("84!")); - } - - GIVEN("a sequence of callables in a chain of chains asynchronous") { - auto a0 = on(default_executor) | [](int x) { return x * 2; } | on(immediate_executor) | - [](int x) { return to_string(x); } | on(default_executor) | - [](const string& s) { return s + "!"; }; - - auto val = sync_wait(std::move(a0), 42); - REQUIRE(val == string("84!")); - } -} - -TEST_CASE("Cancellation of then()", "[initial_draft]") { - annotate_counters cnt; - GIVEN("that a ") { - auto fut = - async(default_executor, [] { - std::this_thread::sleep_for(std::chrono::seconds{3}); - std::cout << "Future did run" << std::endl; - return std::string("42"); - }).then([_counter = annotate{cnt}](const auto& s) { std::cout << s << std::endl; }); - - auto result_f = start(then(fut)); - } - std::this_thread::sleep_for(std::chrono::seconds{5}); - std::cout << cnt << std::endl; -} -} // namespace chains - -#endif \ No newline at end of file +#endif diff --git a/test/chains_test.cpp b/test/chains_test.cpp index 8f1ae5b..8d4c789 100644 --- a/test/chains_test.cpp +++ b/test/chains_test.cpp @@ -1,6 +1,6 @@ #include "catch2/catch_test_macros.hpp" -// #include +#include #include @@ -9,3 +9,152 @@ TEST_CASE("Basic chain operations", "[chain]") { SECTION("") {} } } + +#if 0 + +#include +#include +#include +#include + +using namespace std; +using namespace chains; +using namespace stlab; + +// Cancellation example + +TEST_CASE("Cancellation injection", "[initial_draft]") { + { + cancellation_source src; + + // Build a chain where the first function expects the token as first argument. + auto c = with_cancellation(src) | [](cancellation_token token, int x) { + if (token.canceled()) return 0; + return x * 2; + } | [](int y) { return y + 10; }; // token only needed by first step + + auto f = start(std::move(c), 5); + REQUIRE(f.get_ready() == 20); // (5*2)+10 + + // Demonstrate cancel before start + src.cancel(); + auto c2 = with_cancellation(src) | [](cancellation_token token, int x) { + if (token.canceled()) return 0; + return x * 3; + }; + auto f2 = start(std::move(c2), 7); + REQUIRE(f2.get_ready() == 0); + } + + //{ + // cancellation_source src; + + // // Build a chain where each function expects the token as first argument. + // // First function uses the token, returns an int. + // auto c = with_cancellation(src) | [](cancellation_token token, int x) { + // if (token.canceled()) return 0; + // return x * 2; + // } | [](int y) { return y + 10; }; // token only needed by first step + + // auto f = start(std::move(c), 5); + // REQUIRE(f.get_ready() == 20); // (5*2)+10 + //} +} + +// --- Example test demonstrating split --------------------------------------------------------- +TEST_CASE("Split fan-out", "[initial_draft]") { + auto base = on(immediate_executor) | [](int a) { return a; } | [](int x) { return x + 5; }; + auto splitter = split(std::move(base)); + auto left = splitter.fan([](int v) { return v * 2; }) | [](int x) { return x + 1; }; + auto right = splitter.fan([](int v) { return std::string("v=") + std::to_string(v); }); + + auto f_right = start(std::move(right), 10); + auto f_left = start(std::move(left), 5); + REQUIRE(f_right.get_ready() == std::string("v=15")); + REQUIRE(f_left.get_ready() == 31); +} + +TEST_CASE("Split fan-out bound", "[initial_draft]") { + auto base = on(immediate_executor) | [](int a) { return a; } | [](int x) { return x + 5; }; + + // Bind upstream start argument 10 once: + auto splitter = split_bind(std::move(base), 10); + + // Branches now start with no args; upstream result (15) is injected. + auto left = splitter.fan([](int v) { return v * 2; }) | [](int x) { return x + 1; }; + auto right = splitter.fan([](int v) { return std::string("v=") + std::to_string(v); }); + + auto f_right = start(std::move(right)); // no argument + auto f_left = start(std::move(left)); // no argument + + REQUIRE(f_right.get_ready() == std::string("v=15")); + REQUIRE(f_left.get_ready() == 31); +} + +TEST_CASE("Initial draft", "[initial_draft]") { + GIVEN("a sequence of callables with different arguments") { + auto oneInt2Int = [](int a) { return a * 2; }; + auto twoInt2Int = [](int a, int b) { return a + b; }; + auto void2Int = []() { return 42; }; + + auto a0 = on(stlab::immediate_executor) | oneInt2Int | void2Int | twoInt2Int; + + auto f = start(std::move(a0), 2); + REQUIRE(f.is_ready()); + auto val = f.get_ready(); + REQUIRE(46 == val); + } + + GIVEN("a sequence of callables that just work with move only value") { + auto oneInt2Int = [](move_only a) { return move_only(a.member() * 2); }; + auto twoInt2Int = [](move_only a, move_only b) { + return move_only(a.member() + b.member()); + }; + auto void2Int = []() { return move_only(42); }; + + auto a0 = on(stlab::immediate_executor) | oneInt2Int | void2Int | twoInt2Int; + + auto f = start(std::move(a0), move_only(2)); + REQUIRE(f.is_ready()); + auto val = std::move(f).get_ready(); + REQUIRE(46 == val.member()); + } + + GIVEN("a sequence of callables in a chain of chains synchronous") { + auto a0 = on(immediate_executor) | [](int x) { return x * 2; } | on(immediate_executor) | + [](int x) { return to_string(x); } | on(immediate_executor) | + [](const string& s) { return s + "!"; }; + + auto f = start(std::move(a0), 42); + auto val = f.get_ready(); + REQUIRE(val == string("84!")); + } + + GIVEN("a sequence of callables in a chain of chains asynchronous") { + auto a0 = on(default_executor) | [](int x) { return x * 2; } | on(immediate_executor) | + [](int x) { return to_string(x); } | on(default_executor) | + [](const string& s) { return s + "!"; }; + + auto val = sync_wait(std::move(a0), 42); + REQUIRE(val == string("84!")); + } +} + +TEST_CASE("Cancellation of then()", "[initial_draft]") { + annotate_counters cnt; + GIVEN("that a ") { + auto fut = + async(default_executor, [] { + std::this_thread::sleep_for(std::chrono::seconds{3}); + std::cout << "Future did run" << std::endl; + return std::string("42"); + }).then([_counter = annotate{cnt}](const auto& s) { std::cout << s << std::endl; }); + + auto result_f = start(then(fut)); + } + std::this_thread::sleep_for(std::chrono::seconds{5}); + std::cout << cnt << std::endl; +} +} // namespace chains + +#endif From 75719b6db026b021217b30701eb0add4213e55e0 Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Fri, 20 Feb 2026 22:23:05 +0100 Subject: [PATCH 5/6] Setup tear down of chain --- test/chains_test.cpp | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/test/chains_test.cpp b/test/chains_test.cpp index 8d4c789..f2321ac 100644 --- a/test/chains_test.cpp +++ b/test/chains_test.cpp @@ -2,11 +2,24 @@ #include -#include +#include TEST_CASE("Basic chain operations", "[chain]") { - SECTION("") { - SECTION("") {} + SECTION("Can instantiate a simple chain") { + SECTION("Chain with two lambdas") { + // Create a simple chain by piping a segment with a function + auto s = chains::segment{chains::type{}, + [](auto&& f, Args&&... args) { + return f(std::forward(args)...); + }, + [](int x) { return x * 2; }}; + + auto c = std::move(s) | [](int x) { return x + 1; }; + + // c is now a chains::chain instance + // We can verify it compiles and the type is deduced correctly + (void)c; // Suppress unused variable warning + } } } From f77d8af1589709004006a1ee61ab0138051a7cca Mon Sep 17 00:00:00 2001 From: Felix Petriconi Date: Sat, 21 Feb 2026 11:06:10 +0100 Subject: [PATCH 6/6] Fix tidy warnings --- include/chains/chains.hpp | 14 +++++++------- include/chains/segment.hpp | 2 +- include/chains/tuple.hpp | 6 +++--- test/chains_test.cpp | 11 ++++++----- test/segment_tests.cpp | 13 ++++++++++--- test/tuple_tests.cpp | 17 ++++++++++------- 6 files changed, 37 insertions(+), 26 deletions(-) diff --git a/include/chains/chains.hpp b/include/chains/chains.hpp index 3ed2da6..35ca981 100644 --- a/include/chains/chains.hpp +++ b/include/chains/chains.hpp @@ -69,17 +69,17 @@ class chain { static consteval auto result_type_helper(Tail&& tail, segment&& head) { return detail::fold_over( - []([[maybe_unused]] Fold fold, - First&& first, Rest&&... rest) { + []( + [[maybe_unused]] Fold fold, First&& first, Rest&&... rest) -> auto { if constexpr (sizeof...(rest) == 0) { return [_segment = std::forward(first)]( - Args&&... args) mutable { + Args&&... args) mutable -> auto { return std::move(_segment).result_type_helper(std::forward(args)...); }; } else { return [_segment = std::forward(first).append( fold(fold, std::forward(rest)...))]( - Args&&... args) mutable { + Args&&... args) mutable -> auto { return std::move(_segment).result_type_helper(std::forward(args)...); }; } @@ -92,18 +92,18 @@ class chain { return detail::fold_over( [_receiver = std::forward(receiver)]( - [[maybe_unused]] Fold fold, First&& first, Rest&&... rest) mutable { + [[maybe_unused]] Fold fold, First&& first, Rest&&... rest) mutable -> auto { if constexpr (sizeof...(rest) == 0) { return [_receiver, _segment = std::forward(first).append( [_receiver](V&& val) { _receiver->operator()(std::forward(val)); - })](Args&&... args) mutable { + })](Args&&... args) mutable -> auto { return std::move(_segment).invoke(_receiver, std::forward(args)...); }; } else { return [_receiver, _segment = std::forward(first).append(fold( fold, std::forward(rest)...))]( - Args&&... args) mutable { + Args&&... args) mutable -> auto { return std::move(_segment).invoke(_receiver, std::forward(args)...); }; } diff --git a/include/chains/segment.hpp b/include/chains/segment.hpp index df7d6ff..6c3848c 100644 --- a/include/chains/segment.hpp +++ b/include/chains/segment.hpp @@ -48,7 +48,7 @@ class segment { The basic operations should follow those from C++ lambdas, for now default everything. and see if the compiler gets it correct. */ - explicit segment(const segment&) = default; + segment(const segment&) = default; segment(segment&&) noexcept = default; auto operator=(const segment&) -> segment& = default; auto operator=(segment&&) noexcept -> segment& = default; diff --git a/include/chains/tuple.hpp b/include/chains/tuple.hpp index 33760fa..3e37687 100644 --- a/include/chains/tuple.hpp +++ b/include/chains/tuple.hpp @@ -25,7 +25,7 @@ namespace detail { /* Map void return to std::monostate */ template auto void_to_monostate(F& f) { - return [&_f = f](Args&&... args) mutable { + return [&_f = f](Args&&... args) mutable -> auto { if constexpr (std::is_same_v(args)...)), void>) { std::move(_f)(std::forward(args)...); return std::monostate{}; @@ -83,7 +83,7 @@ constexpr auto invoke_prefix(F&& f, Tuple&& t) { return std::monostate{}; } } else { - return [&](std::index_sequence) { + return [&](std::index_sequence) -> auto { if constexpr (std::is_void_v(t))...))>) { std::invoke(f, std::move(std::get(t))...); return std::monostate{}; @@ -139,7 +139,7 @@ constexpr auto move_tuple_tail_at(Tuple&& t) { */ template constexpr auto tuple_consume(Tuple&& values) { - return [_values = std::forward(values)](F&& f) mutable { + return [_values = std::forward(values)](F&& f) mutable -> auto { using tuple_t = std::decay_t; constexpr std::size_t N = std::tuple_size_v; diff --git a/test/chains_test.cpp b/test/chains_test.cpp index f2321ac..845d6e8 100644 --- a/test/chains_test.cpp +++ b/test/chains_test.cpp @@ -1,6 +1,7 @@ -#include "catch2/catch_test_macros.hpp" - #include +#include + +#include #include @@ -9,12 +10,12 @@ TEST_CASE("Basic chain operations", "[chain]") { SECTION("Chain with two lambdas") { // Create a simple chain by piping a segment with a function auto s = chains::segment{chains::type{}, - [](auto&& f, Args&&... args) { + [](auto&& f, Args&&... args) -> auto { return f(std::forward(args)...); }, - [](int x) { return x * 2; }}; + [](int x) -> int { return x * 2; }}; - auto c = std::move(s) | [](int x) { return x + 1; }; + auto c = std::move(s) | [](int x) -> int { return x + 1; }; // c is now a chains::chain instance // We can verify it compiles and the type is deduced correctly diff --git a/test/segment_tests.cpp b/test/segment_tests.cpp index 62a7ef8..38ec5ed 100644 --- a/test/segment_tests.cpp +++ b/test/segment_tests.cpp @@ -23,7 +23,7 @@ struct mock_receiver { int _result{0}; [[nodiscard]] auto canceled() const -> bool { return _canceled; } - auto set_exception(std::exception_ptr e) -> void { _exception = std::move(e); } + auto set_exception(const std::exception_ptr& e) -> void { _exception = e; } auto set_value(int value) -> void { _result = value; } }; @@ -31,21 +31,25 @@ TEST_CASE("Basic segment operations", "[segment]") { SECTION("simple creation with variadic constructor") { auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, []() { return 42; }}; + (void)sut; } SECTION("simple creation with tuple constructor") { auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, std::make_tuple([]() { return 42; })}; + (void)sut; } SECTION("creation with multiple functions") { auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, [](int x) { return x + 1; }, [](int x) { return x * 2; }}; + (void)sut; } SECTION("creation with empty function tuple") { auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, std::tuple<>{}}; + (void)sut; } } @@ -157,10 +161,10 @@ TEST_CASE("Segment invoke with receiver", "[segment]") { auto hit = 0; // Custom applicator that doubles the argument - auto custom_apply = [](auto f, int x) { f(x * 2); }; + auto custom_apply = [](auto f, int x) -> void { f(x * 2); }; auto sut = chains::segment{chains::type>{}, std::move(custom_apply), - [&hit](int x) { hit = x; }}; + [&hit](int x) -> void { hit = x; }}; std::move(sut).invoke(receiver, 21); CHECK(hit == 42); // 21 * 2 = 42 @@ -185,12 +189,14 @@ TEST_CASE("Segment with injected types", "[segment]") { auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, []() { return 42; }}; // Segment should be constructible with injection type + (void)sut; } SECTION("segment with multiple injection types") { auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, []() { return 42; }}; // Segment should be constructible with multiple injection types + (void)sut; } } @@ -199,6 +205,7 @@ TEST_CASE("Segment edge cases", "[segment]") { auto sut = chains::segment{chains::type>{}, [](auto f) { f(); }, std::tuple<>{}}; // Should be constructible + (void)sut; } SECTION("segment with void function") { diff --git a/test/tuple_tests.cpp b/test/tuple_tests.cpp index ac35b1c..ede5368 100644 --- a/test/tuple_tests.cpp +++ b/test/tuple_tests.cpp @@ -16,8 +16,9 @@ #include // monostate TEST_CASE("Test tuple compose", "[tuple]") { - std::tuple t{[](int x) { return x + 1.0; }, [](double x) { return x * 2.0; }, - [](double x) { return std::to_string(x / 2.0); }}; + std::tuple t{[](int x) -> double { return x + 1.0; }, + [](double x) -> double { return x * 2.0; }, + [](double x) -> std::string { return std::to_string(x / 2.0); }}; auto f = chains::tuple_compose(std::move(t)); CHECK(f(1) == "2.000000"); } @@ -225,7 +226,8 @@ TEST_CASE("Test tuple consume", "[tuple]") { } SECTION("tuple -> int(float)") { std::tuple t{3.14f}; - auto result = chains::tuple_consume(t)([](float f) { return static_cast(f); }); + auto result = + chains::tuple_consume(t)([](float f) -> int { return static_cast(f); }); CHECK(result == std::make_tuple(3)); } } @@ -317,7 +319,7 @@ TEST_CASE("Test tuple consume", "[tuple]") { std::tuple t{1, 2, 3}; auto hit{0}; auto result = - chains::tuple_consume(t)([&hit](int a, int b, int c) { hit = a + b + c; }); + chains::tuple_consume(t)([&hit](int a, int b, int c) -> void { hit = a + b + c; }); CHECK(hit == 6); CHECK(result == std::make_tuple(std::monostate{})); } @@ -410,7 +412,8 @@ TEST_CASE("Test tuple consume", "[tuple]") { SECTION("Complex return types") { SECTION("tuple -> tuple(int)") { std::tuple t{42}; - auto result = chains::tuple_consume(t)([](int x) { return std::make_tuple(x, x * 2); }); + auto result = chains::tuple_consume(t)( + [](int x) -> std::tuple { return std::make_tuple(x, x * 2); }); // Result is tuple> - a 1-element tuple containing a 2-element tuple auto nested = std::get<0>(result); CHECK(nested == std::make_tuple(42, 84)); @@ -419,8 +422,8 @@ TEST_CASE("Test tuple consume", "[tuple]") { } SECTION("tuple -> pair(int, int)") { std::tuple t{3, 4}; - auto result = - chains::tuple_consume(t)([](int a, int b) { return std::make_pair(a, b); }); + auto result = chains::tuple_consume(t)( + [](int a, int b) -> std::pair { return std::make_pair(a, b); }); // Result is tuple> - a 1-element tuple containing a pair auto nested = std::get<0>(result); CHECK(nested == std::make_pair(3, 4));