diff --git a/.github/workflows/asciidoctor-ghpages.yml b/.github/workflows/asciidoctor-ghpages.yml index a252e20..2657d76 100644 --- a/.github/workflows/asciidoctor-ghpages.yml +++ b/.github/workflows/asciidoctor-ghpages.yml @@ -33,7 +33,7 @@ jobs: runs-on: ${{ github.repository_owner == 'intel' && 'intel-' || '' }}ubuntu-24.04 steps: - name: Checkout source - uses: actions/checkout@d632683dd7b4114ad314bca15554477dd762a938 # v4.2.0 + uses: actions/checkout@eef61447b9ff4aafe5dcd4e0bbf5d482be7e7871 # v4.2.1 - name: Setup Node.js uses: actions/setup-node@0a44ba7841725637a19e28fa30b79a866c81b0a6 # v4.0.4 with: diff --git a/.github/workflows/unit_tests.yml b/.github/workflows/unit_tests.yml index db5085d..a93dda5 100644 --- a/.github/workflows/unit_tests.yml +++ b/.github/workflows/unit_tests.yml @@ -98,7 +98,12 @@ jobs: - name: Install build tools run: | ${{ matrix.install }} - sudo apt install -y ninja-build + sudo apt install -y ninja-build python3-venv python3-pip + python3 -m venv ${{github.workspace}}/test_venv + source ${{github.workspace}}/test_venv/bin/activate + pip install -r ${{github.workspace}}/requirements.txt + echo "${{github.workspace}}/test_venv/bin" >> $GITHUB_PATH + - name: Restore CPM cache env: @@ -177,7 +182,11 @@ jobs: - name: Install build tools run: | ${{ matrix.install }} - sudo apt install -y ninja-build + sudo apt install -y ninja-build python3-venv python3-pip + python3 -m venv ${{github.workspace}}/test_venv + source ${{github.workspace}}/test_venv/bin/activate + pip install -r ${{github.workspace}}/requirements.txt + echo "${{github.workspace}}/test_venv/bin" >> $GITHUB_PATH - name: Restore CPM cache env: @@ -292,7 +301,11 @@ jobs: - name: Install build tools run: | ${{ matrix.install }} - sudo apt install -y ninja-build + sudo apt install -y ninja-build python3-venv python3-pip + python3 -m venv ${{github.workspace}}/test_venv + source ${{github.workspace}}/test_venv/bin/activate + pip install -r ${{github.workspace}}/requirements.txt + echo "${{github.workspace}}/test_venv/bin" >> $GITHUB_PATH - name: Restore CPM cache env: @@ -338,7 +351,11 @@ jobs: - name: Install build tools run: | - sudo apt update && sudo apt install -y gcc-${{env.DEFAULT_GCC_VERSION}} g++-${{env.DEFAULT_GCC_VERSION}} ninja-build valgrind + sudo apt update && sudo apt install -y gcc-${{env.DEFAULT_GCC_VERSION}} g++-${{env.DEFAULT_GCC_VERSION}} ninja-build valgrind python3-venv python3-pip + python3 -m venv ${{github.workspace}}/test_venv + source ${{github.workspace}}/test_venv/bin/activate + pip install -r ${{github.workspace}}/requirements.txt + echo "${{github.workspace}}/test_venv/bin" >> $GITHUB_PATH - name: Restore CPM cache env: @@ -409,7 +426,11 @@ jobs: - name: Install build tools run: | - sudo apt update && sudo apt install -y clang-${{env.MULL_LLVM_VERSION}} ninja-build + sudo apt update && sudo apt install -y clang-${{env.MULL_LLVM_VERSION}} ninja-build python3-venv python3-pip + python3 -m venv ${{github.workspace}}/test_venv + source ${{github.workspace}}/test_venv/bin/activate + pip install -r ${{github.workspace}}/requirements.txt + echo "${{github.workspace}}/test_venv/bin" >> $GITHUB_PATH - name: Install mull env: diff --git a/.gitignore b/.gitignore index 570978f..29c2d78 100644 --- a/.gitignore +++ b/.gitignore @@ -10,3 +10,7 @@ .cmake-format.yaml CMakePresets.json /toolchains +__pycache__ +.mypy_cache +.pytest_cache +.hypothesis \ No newline at end of file diff --git a/include/stdx/tuple_algorithms.hpp b/include/stdx/tuple_algorithms.hpp index ab89303..036c878 100644 --- a/include/stdx/tuple_algorithms.hpp +++ b/include/stdx/tuple_algorithms.hpp @@ -319,16 +319,16 @@ template constexpr auto cartesian_product_copy(Ts &&...ts) { return [](First &&first, Rest &&...rest) { auto const c = cartesian_product_copy(std::forward(rest)...); - return std::forward(first).apply( - [&](Elems &&...elems) { - auto const prepend = [&](E &&e) { - return c.apply([&](auto... subs) { - return make_tuple(tuple_cat( - make_tuple(std::forward(e)), subs)...); - }); - }; - return tuple_cat(prepend(std::forward(elems))...); - }); + return std::forward(first).apply([&]( + Elems &&...elems) { + [[maybe_unused]] auto const prepend = [&](E &&e) { + return c.apply([&](auto... subs) { + return make_tuple( + tuple_cat(make_tuple(std::forward(e)), subs)...); + }); + }; + return tuple_cat(prepend(std::forward(elems))...); + }); }(std::forward(ts)...); } } diff --git a/requirements.txt b/requirements.txt new file mode 100644 index 0000000..19a90b9 --- /dev/null +++ b/requirements.txt @@ -0,0 +1,11 @@ +pytest==8.3.3 +pytest-forked==1.6.0 +pytest-xdist==3.6.1 +hypothesis==6.112.5 +attrs==24.2.0 +execnet==2.1.1 +pluggy==1.5.0 +sortedcontainers==2.4.0 +iniconfig==2.0.0 +packaging==24.1 +py==1.11.0 \ No newline at end of file diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index e57a339..4ce0eea 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -64,3 +64,4 @@ if(${CMAKE_CXX_STANDARD} GREATER_EQUAL 20) endif() add_subdirectory(fail) +add_subdirectory(pbt) diff --git a/test/pbt/CMakeLists.txt b/test/pbt/CMakeLists.txt new file mode 100644 index 0000000..b6f8ad2 --- /dev/null +++ b/test/pbt/CMakeLists.txt @@ -0,0 +1,16 @@ +if(${CMAKE_CXX_STANDARD} GREATER_EQUAL 20) + add_executable(pbt_prototype_driver EXCLUDE_FROM_ALL + pbt_prototype_driver.cpp) + target_link_libraries(pbt_prototype_driver PUBLIC sanitizers warnings stdx) + + add_unit_test( + tuple + PYTEST + FILES + tuple.py + EXTRA_ARGS + -vv + -n2 + --compile-commands=${CMAKE_BINARY_DIR}/compile_commands.json + --prototype-driver=${CMAKE_CURRENT_SOURCE_DIR}/pbt_prototype_driver.cpp) +endif() diff --git a/test/pbt/conftest.py b/test/pbt/conftest.py new file mode 100644 index 0000000..e02d40a --- /dev/null +++ b/test/pbt/conftest.py @@ -0,0 +1,98 @@ +import pytest +import hypothesis +import json +import subprocess +import tempfile +import os +import re + +hypothesis.settings.register_profile("ci", max_examples=500) +hypothesis.settings.register_profile("fast", max_examples=10) + + + +def pytest_addoption(parser): + parser.addoption("--compiler", action="store", help="C++ compiler", default=None, required=False) + parser.addoption("--compiler-args", action="store", help="C++ compiler arguments", default="", required=False) + parser.addoption("--includes", action="store", help="C++ include directories", default="", required=False) + + parser.addoption("--compile-commands", action="store", help="cmake compiler commands", default=None, required=False) + parser.addoption("--prototype-driver", action="store", help="Prototype .cpp filename to gather compilation command from", default=None, required=False) + +@pytest.fixture(scope="module") +def cmake_compilation_command(pytestconfig): + compile_commands_filename = pytestconfig.getoption("compile_commands") + prototype_driver_filename = pytestconfig.getoption("prototype_driver") + + if compile_commands_filename is None or prototype_driver_filename is None: + return None + + def f(filename): + with open(compile_commands_filename, "r") as f: + db = json.load(f) + for obj in db: + if obj["file"] == prototype_driver_filename: + cmd = obj["command"] + cmd = cmd.replace(prototype_driver_filename, filename) + cmd = re.sub(r"-o .*?\.cpp\.o", f"-o {filename}.o", cmd) + return cmd.split(" ") + + return f + +@pytest.fixture(scope="module") +def args_compilation_command(pytestconfig): + compiler = pytestconfig.getoption("compiler") + if compiler is None: + return None + + include_dirs = [f"-I{i}" for i in pytestconfig.getoption("includes").split(",") if i] + compiler_args = [i for i in pytestconfig.getoption("compiler_args").split(",") if i] + + def f(filename): + compile_command = [ + compiler, temp_cpp_file_path, + "-o", temp_cpp_file_path + ".o" + ] + compiler_args + include_args + return compile_command + + return f + + + +@pytest.fixture(scope="module") +def compile(cmake_compilation_command, args_compilation_command): + cmd = cmake_compilation_command + if cmd is None: + cmd = args_compilation_command + + def f(code_str): + code_str += "\n" + with tempfile.NamedTemporaryFile(delete=False, suffix=".cpp") as temp_cpp_file: + temp_cpp_file.write(code_str.encode('utf-8')) + temp_cpp_file_path = temp_cpp_file.name + + try: + compile_command = cmd(temp_cpp_file_path) + result = subprocess.run(compile_command, stdout=subprocess.PIPE, stderr=subprocess.PIPE) + + if result.returncode == 0: + return True + else: + error_message = ( + f"Compiler returned non-zero exit code: {result.returncode}\n" + f"Compilation command: {' '.join(compile_command)}\n" + f"Source code:\n{code_str}\n" + f"Compiler stderr:\n{result.stderr.decode('utf-8')}\n" + f"Compiler stdout:\n{result.stdout.decode('utf-8')}\n" + ) + pytest.fail(error_message) + + except Exception as e: + pytest.fail(str(e)) + finally: + os.remove(temp_cpp_file_path) + if os.path.exists(temp_cpp_file_path + ".out"): + os.remove(temp_cpp_file_path + ".out") + + return f + diff --git a/test/pbt/pbt_prototype_driver.cpp b/test/pbt/pbt_prototype_driver.cpp new file mode 100644 index 0000000..237c8ce --- /dev/null +++ b/test/pbt/pbt_prototype_driver.cpp @@ -0,0 +1 @@ +int main() {} diff --git a/test/pbt/tuple.py b/test/pbt/tuple.py new file mode 100644 index 0000000..23d9ef7 --- /dev/null +++ b/test/pbt/tuple.py @@ -0,0 +1,193 @@ +from hypothesis import strategies as st, given, settings, event, assume + +def unpack(l): + return ", ".join([str(i) for i in l]) + +small_ints = st.integers(min_value=-100, max_value=100) + +@st.composite +def tuples(draw, children): + values = draw(st.lists(children)) + return f"stdx::make_tuple({unpack(values)})" + +@st.composite +def list_trees(draw, leaves=st.integers(), max_leaves=100): + l = draw(st.recursive(leaves, lambda children: st.lists(children), max_leaves=max_leaves)) + if not isinstance(l, list): + l = [l] + return l + +def as_tuple_tree(value): + if isinstance(value, list): + values = [as_tuple_tree(v) for v in value] + return f"stdx::make_tuple({unpack(values)})" + else: + return value + +@st.composite +def tuple_trees(draw, leaves=st.integers()): + return draw(st.recursive(leaves, lambda children: tuples(children))) + +@settings(deadline=50000) +@given(tuple_trees(small_ints)) +def test_tuple_trees(compile, t): + assert compile(f""" + #include + + [[maybe_unused]] constexpr auto t = {t}; + + int main() {{ + return 0; + }} + """) + +@settings(deadline=50000) +@given(list_trees(small_ints)) +def test_tuple_size(compile, l): + t = as_tuple_tree(l) + assert compile(f""" + #include + + constexpr auto t = {t}; + static_assert(stdx::tuple_size_v == {len(l)}); + static_assert(std::size(t) == {len(l)}); + + int main() {{ + return 0; + }} + """) + + +@settings(deadline=50000) +@given(list_trees(small_ints), st.integers()) +def test_get_by_index(compile, l, i): + assume(len(l) > 0) + t = as_tuple_tree(l) + i = i % len(l) + + expected_v = as_tuple_tree(l[i]) + + assert compile(f""" + #include + + using namespace stdx::literals; + + constexpr auto t = {t}; + constexpr auto expected = {expected_v}; + + static_assert(stdx::get<{i}>(t) == expected); + static_assert(get<{i}>(t) == expected); + + static_assert(t[{i}_idx] == expected); + static_assert(t[stdx::index<{i}>] == expected); + + int main() {{ + return 0; + }} + """) + + +@settings(deadline=50000) +@given(st.lists(list_trees(small_ints))) +def test_tuple_cat(compile, ls): + ts = [as_tuple_tree(l) for l in ls] + + flattened_ls = [i for subl in ls for i in subl] + expected = as_tuple_tree(flattened_ls) + + assert compile(f""" + #include + #include + + static_assert(stdx::tuple_cat({unpack(ts)}) == {expected}); + + int main() {{ + return 0; + }} + """) + + +@settings(deadline=50000) +@given(list_trees(small_ints), st.one_of(list_trees(small_ints), small_ints)) +def test_push(compile, l, elem): + expected_back = as_tuple_tree(l + [elem]) + expected_front = as_tuple_tree([elem] + l) + + if isinstance(elem, list): + elem = as_tuple_tree(elem) + else: + elem = str(elem) + + t = as_tuple_tree(l) + + assert compile(f""" + #include + #include + + constexpr auto t = {t}; + constexpr auto elem = {elem}; + + constexpr auto expected_back = {expected_back}; + static_assert(stdx::tuple_push_back(t, elem) == expected_back); + static_assert(stdx::tuple_snoc(t, elem) == expected_back); + + constexpr auto expected_front = {expected_front}; + static_assert(stdx::tuple_push_front(elem, t) == expected_front); + static_assert(stdx::tuple_cons(elem, t) == expected_front); + + int main() {{ + return 0; + }} + """) + +from itertools import product + +def put_in_list(i): + if isinstance(i, list): + return i + else: + return [i] + +@settings(deadline=50000) +@given(list_trees(small_ints, max_leaves=15)) +def test_cartesian_product(compile, ls): + ls = [put_in_list(i) for i in ls] + ts = [as_tuple_tree(l) for l in ls] + expected = as_tuple_tree([list(p) for p in product(*ls)]) + + assert compile(f""" + #include + #include + + static_assert(stdx::cartesian_product_copy({unpack(ts)}) == {expected}); + + int main() {{ + return 0; + }} + """) + +from functools import reduce + +@settings(deadline=50000) +@given(st.lists(small_ints)) +def test_star_of(compile, l): + expected_any_of = any([i > 50 for i in l]) + expected_all_of = all([i > 50 for i in l]) + expected_none_of = not expected_any_of + + t = as_tuple_tree(l) + + assert compile(f""" + #include + #include + + constexpr auto f = [](int i) {{ return i > 50; }}; + + static_assert(stdx::any_of(f, {t}) == {str(expected_any_of).lower()}); + static_assert(stdx::all_of(f, {t}) == {str(expected_all_of).lower()}); + static_assert(stdx::none_of(f, {t}) == {str(expected_none_of).lower()}); + + int main() {{ + return 0; + }} + """) \ No newline at end of file