diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml index 3e8e97d15..5dd2bcbf6 100644 --- a/.github/workflows/coverage.yml +++ b/.github/workflows/coverage.yml @@ -3,10 +3,25 @@ name: coverage env: GXX_VERSION: 12 TESTS_DIR: out/tests - COVERAGE_FILE: coverage.info + COBERTURA_REPORT: cobertura.xml + COVERALLS_REPORT: coveralls.json + HTML_REPORT_DIR: html/ COVERAGE_ARTIFACT_NAME: coverage-report -on: [push, pull_request] +on: + push: + branches: + - master + - development + paths-ignore: + - 'README.md' + - 'docs/' + pull_request: + branches: + - '**' + paths-ignore: + - 'README.md' + - 'docs/' jobs: create-coverage-report: @@ -14,16 +29,19 @@ jobs: steps: - uses: actions/checkout@v4 - - name: Install compiler + - name: Install prerequisites run: | sudo apt-get update + sudo apt-get install pip -y + sudo pip install gcovr sudo apt-get install g++-${{ env.GXX_VERSION }} -y sudo update-alternatives --install /usr/bin/g++ g++ /usr/bin/g++-${{ env.GXX_VERSION }} ${{ env.GXX_VERSION }}00 --slave /usr/bin/gcc gcc /usr/bin/gcc-${{ env.GXX_VERSION }} sudo update-alternatives --install /usr/bin/gcov gcov /usr/bin/gcov-${{ env.GXX_VERSION }} ${{ env.GXX_VERSION }}00 - name: Compile tests env: - CXXFLAGS: "--coverage -fno-inline -fprofile-abs-path" + LDFLAGS: "-fprofile-arcs" + CXXFLAGS: "-g -O0 --coverage -fno-inline -fprofile-abs-path -fkeep-inline-functions -fkeep-static-functions" CC: gcc CXX: g++ run: | @@ -33,19 +51,34 @@ jobs: - name: Run tests env: CTEST_OUTPUT_ON_FAILURE: 1 - run: ctest --test-dir ${{ env.TESTS_DIR }} --timeout 30 -C Debug -j4 + run: ctest --test-dir ${{env.TESTS_DIR}} -C Debug -j4 - - name: Collect data + - name: Run gcovr run: | - sudo apt-get install lcov -y - lcov --capture -d ${{env.TESTS_DIR}} -o ${{env.COVERAGE_FILE}} --include "$PWD/include/*" - lcov -l ${{env.COVERAGE_FILE}} + gcovr --root ${{env.TESTS_DIR}} --filter include/Simple-Utility --keep -j 4 \ + --exclude-lines-by-pattern "\s*assert\(" \ + --exclude-unreachable-branches \ + --exclude-noncode-lines \ + --exclude-throw-branches \ + --decisions \ + --cobertura ${{env.COBERTURA_REPORT}} --cobertura-pretty \ + --html-nested ${{env.HTML_REPORT_DIR}} --html-title "Simple-Utility Coverage Report" \ + --coveralls ${{env.COVERALLS_REPORT}} --coveralls-pretty - - name: Upload artifacts + - name: Upload gcov coverage report artifacts + uses: actions/upload-artifact@v3 + with: + name: gcov-files + path: "${{env.TESTS_DIR}}/*.gcov" + + - name: Upload generated report artifacts uses: actions/upload-artifact@v3 with: name: ${{env.COVERAGE_ARTIFACT_NAME}} - path: ${{env.COVERAGE_FILE}} + path: | + ${{env.COBERTURA_REPORT}} + ${{env.COVERALLS_REPORT}} + ${{env.HTML_REPORT_DIR}} codacy-report: needs: create-coverage-report @@ -60,8 +93,8 @@ jobs: - name: Upload coverage to Codacy uses: codacy/codacy-coverage-reporter-action@v1 with: - project-token: ${{ secrets.CODACY_PROJECT_TOKEN }} - coverage-reports: ${{ env.COVERAGE_FILE }} + project-token: ${{secrets.CODACY_PROJECT_TOKEN}} + coverage-reports: ${{env.COBERTURA_REPORT}} codecov-report: needs: create-coverage-report @@ -77,8 +110,8 @@ jobs: uses: codecov/codecov-action@v3 with: name: $GITHUB_REPOSITORY - token: ${{ secrets.CODECOV_TOKEN }} - files: ${{ env.COVERAGE_FILE }} + token: ${{secrets.CODECOV_TOKEN}} + files: ${{env.COBERTURA_REPORT}} fail_ci_if_error: true verbose: true @@ -96,6 +129,6 @@ jobs: uses: coverallsapp/github-action@v2 with: github-token: ${{secrets.GITHUB_TOKEN}} - file: ${{env.COVERAGE_FILE}} - format: lcov + file: ${{env.COVERALLS_REPORT}} + format: coveralls fail-on-error: true diff --git a/.github/workflows/run_benchmarks.yml b/.github/workflows/run_benchmarks.yml new file mode 100644 index 000000000..ce4896b66 --- /dev/null +++ b/.github/workflows/run_benchmarks.yml @@ -0,0 +1,161 @@ +name: run benchmarks + +env: + BUILD_DIR: out # root build directory + BENCHMARK_DIR: benchmarks # relative directory below BUILD_DIR + ARTIFACTS_DIR: artifacts # relative directory below BENCHMARK_DIR + +on: + push: + branches: + - master + - development + paths-ignore: + - 'README.md' + - 'docs/' + pull_request: + branches: + - '**' + paths-ignore: + - 'README.md' + - 'docs/' + +jobs: + ubuntu-22_04: + runs-on: ubuntu-22.04 + + strategy: + fail-fast: false + matrix: + build_mode: [Release] + compiler: + - pkg: g++-12 + exe: g++-12 + - pkg: g++-11 + exe: g++-11 + - pkg: g++-10 + exe: g++-10 + - pkg: clang-14 + exe: clang++-14 + - pkg: clang-13 + exe: clang++-13 + - pkg: clang-12 + exe: clang++-12 + - pkg: clang-11 + exe: clang++-11 + + steps: + - uses: actions/checkout@v3 + - name: Install compiler + run: | + sudo apt-get update + sudo apt-get install ${{ matrix.compiler.pkg }} -y + - name: Compile + env: + CXX: ${{ matrix.compiler.exe }} + run: | + cmake -DCMAKE_BUILD_TYPE=${{ matrix.build_mode }} -DSIMPLE_UTILITY_BUILD_BENCHMARKS=Yes -B ${{ env.BUILD_DIR }} -S . + cmake --build ${{ env.BUILD_DIR }} -j4 + - name: Run benchmarks + run: "${{env.BUILD_DIR}}/${{env.BENCHMARK_DIR}}/Simple-Utility-Benchmarks" + - name: Upload generated report artifacts + uses: actions/upload-artifact@v3 + with: + path: "${{env.ARTIFACTS_DIR}}" + + + ubuntu-20_04: + runs-on: ubuntu-20.04 + + strategy: + fail-fast: false + matrix: + build_mode: [Release] + compiler: + - pkg: g++-11 + exe: g++-11 + - pkg: g++-10 + exe: g++-10 + - pkg: clang-12 + exe: clang++-12 + - pkg: clang-11 + exe: clang++-11 + - pkg: clang-10 + exe: clang++-10 + + steps: + - uses: actions/checkout@v3 + - name: Install compiler + run: | + sudo apt-get update + sudo apt-get install ${{ matrix.compiler.pkg }} -y + - name: Compile + env: + CXX: ${{ matrix.compiler.exe }} + run: | + cmake -DCMAKE_BUILD_TYPE=${{ matrix.build_mode }} -DSIMPLE_UTILITY_BUILD_BENCHMARKS=Yes -B ${{ env.BUILD_DIR }} -S . + cmake --build ${{ env.BUILD_DIR }} -j4 + - name: Run benchmarks + run: "${{env.BUILD_DIR}}/${{env.BENCHMARK_DIR}}/Simple-Utility-Benchmarks" + - name: Upload generated report artifacts + uses: actions/upload-artifact@v3 + with: + path: "${{env.ARTIFACTS_DIR}}" + + + windows_2022: + runs-on: windows-2022 + + strategy: + fail-fast: false + matrix: + build_mode: [Release] + toolset: [v142, v143, ClangCl] + + steps: + - uses: actions/checkout@v3 + - name: Compile + run: | + cmake -G"Visual Studio 17 2022" ` + -T${{matrix.toolset}} ` + -DCMAKE_BUILD_TYPE=${{matrix.build_mode}} ` + -DSIMPLE_UTILITY_BUILD_BENCHMARKS=Yes ` + -B${{env.BUILD_DIR}} ` + -Ax64 ` + -S . + cmake --build ${{ env.BUILD_DIR }} --config ${{ matrix.build_mode }} -j4 + - name: Run benchmarks + run: ".\\${{env.BUILD_DIR}}\\${{env.BENCHMARK_DIR}}\\${{matrix.build_mode}}\\Simple-Utility-Benchmarks.exe" + - name: Upload generated report artifacts + uses: actions/upload-artifact@v3 + with: + path: "${{env.ARTIFACTS_DIR}}" + + + windows_2019: + runs-on: windows-2019 + + strategy: + fail-fast: false + matrix: + build_mode: [Release] + toolset: [v142, ClangCl] + + steps: + - uses: actions/checkout@v3 + - name: Compile + run: | + cmake -G"Visual Studio 16 2019" ` + -T${{matrix.toolset}} ` + -DCMAKE_BUILD_TYPE=${{matrix.build_mode}} ` + -DSIMPLE_UTILITY_BUILD_BENCHMARKS=Yes ` + -B${{env.BUILD_DIR}} ` + -Ax64 ` + -S . + cmake --build ${{env.BUILD_DIR}} --config ${{matrix.build_mode}} -j4 + - name: Run benchmarks + run: ".\\${{env.BUILD_DIR}}\\${{env.BENCHMARK_DIR}}\\${{matrix.build_mode}}\\Simple-Utility-Benchmarks.exe" + - name: Upload generated report artifacts + uses: actions/upload-artifact@v3 + with: + path: "${{env.ARTIFACTS_DIR}}" diff --git a/.github/workflows/run_tests.yml b/.github/workflows/run_tests.yml index 88ecae715..454e4b6fb 100644 --- a/.github/workflows/run_tests.yml +++ b/.github/workflows/run_tests.yml @@ -6,9 +6,18 @@ env: on: push: - branches: [master, development] + branches: + - master + - development + paths-ignore: + - 'README.md' + - 'docs/' pull_request: - branches: [master, development] + branches: + - '**' + paths-ignore: + - 'README.md' + - 'docs/' jobs: ubuntu-22_04: diff --git a/CMakeLists.txt b/CMakeLists.txt index 1c481d3f8..493c66adb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -32,6 +32,12 @@ if (SIMPLE_UTILITY_BUILD_TESTS OR CMAKE_SOURCE_DIR STREQUAL Simple-Utility_SOURC add_subdirectory("tests") endif() +OPTION(SIMPLE_UTILITY_BUILD_BENCHMARKS "Determines whether benchmarks will be built." OFF) +if (SIMPLE_UTILITY_BUILD_BENCHMARKS) + include(CTest) + add_subdirectory("benchmarks") +endif() + option(SIMPLE_UTILITY_GEN_DOCS_ENABLED "Enables the GenerateDocs target." OFF) if (SIMPLE_UTILITY_GEN_DOCS_ENABLED) add_subdirectory("docs") diff --git a/CMakeSettings.json b/CMakeSettings.json index bc058be69..bc43b563e 100644 --- a/CMakeSettings.json +++ b/CMakeSettings.json @@ -1,139 +1,162 @@ { - "configurations": [ - { - "name": "x64-Debug", - "generator": "Ninja", - "configurationType": "Debug", - "inheritEnvironments": [ "msvc_x64_x64" ], - "buildRoot": "${projectDir}\\out\\build\\${name}", - "installRoot": "${projectDir}\\out\\install\\${name}", - "cmakeCommandArgs": "", - "buildCommandArgs": "", - "ctestCommandArgs": "", - "variables": [ - { - "name": "CMAKE_EXPORT_COMPILE_COMMANDS", - "value": "True", - "type": "BOOL" - } - ] - }, - { - "name": "clang-x64-Debug", - "generator": "Ninja", - "configurationType": "Debug", - "buildRoot": "${projectDir}\\out\\build\\${name}", - "installRoot": "${projectDir}\\out\\install\\${name}", - "cmakeCommandArgs": "", - "buildCommandArgs": "", - "ctestCommandArgs": "", - "inheritEnvironments": [ "clang_cl_x64_x64" ], - "variables": [ - { - "name": "Simple-Utility_BUILD_TESTS", - "value": "True", - "type": "BOOL" - } - ] - }, - { - "name": "Linux-GCC-Debug", - "generator": "Ninja", - "configurationType": "Debug", - "cmakeExecutable": "cmake", - "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ], - "cmakeCommandArgs": "", - "buildCommandArgs": "", - "ctestCommandArgs": "", - "inheritEnvironments": [ "linux_x64" ], - "remoteMachineName": "${defaultRemoteMachineName}", - "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src", - "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}", - "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}", - "remoteCopySources": true, - "rsyncCommandArgs": "-t --delete --delete-excluded", - "remoteCopyBuildOutput": false, - "remoteCopySourcesMethod": "rsync", - "variables": [ - { - "name": "CATCH_BUILD_TESTING", - "value": "False", - "type": "BOOL" - } - ] - }, - { - "name": "Linux-Clang-Debug", - "generator": "Ninja", - "configurationType": "Debug", - "cmakeExecutable": "cmake", - "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ], - "cmakeCommandArgs": "", - "buildCommandArgs": "", - "ctestCommandArgs": "", - "inheritEnvironments": [ "linux_clang_x64" ], - "variables": [ - { - "name": "CATCH_BUILD_TESTING", - "value": "False", - "type": "BOOL" - } - ], - "remoteMachineName": "${defaultRemoteMachineName}", - "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src", - "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}", - "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}", - "remoteCopySources": true, - "rsyncCommandArgs": "-t --delete --delete-excluded", - "remoteCopyBuildOutput": false, - "remoteCopySourcesMethod": "rsync" - }, - { - "name": "GenerateDocs", - "generator": "Ninja", - "buildRoot": "${projectDir}\\out\\build\\${name}", - "installRoot": "${projectDir}\\out\\install\\${name}", - "buildCommandArgs": "GenerateDocs", - "ctestCommandArgs": "", - "inheritEnvironments": [ "msvc_x64_x64" ], - "variables": [ - { - "name": "SIMPLE_UTILITY_GEN_DOCS_ENABLED", - "value": "True", - "type": "BOOL" - } - ] - }, - { - "name": "Linux-GCC-12-Debug", - "generator": "Ninja", - "configurationType": "Debug", - "cmakeExecutable": "cmake", - "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ], - "cmakeCommandArgs": "", - "buildCommandArgs": "", - "ctestCommandArgs": "", - "inheritEnvironments": [ "linux_x64" ], - "variables": [ - { - "name": "CATCH_BUILD_TESTING", - "value": "False", - "type": "BOOL" - }, - { - "name": "CMAKE_CXX_COMPILER", - "value": "/usr/bin/g++-12", - "type": "FILEPATH" - } - ], - "remoteMachineName": "${defaultRemoteMachineName}", - "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src", - "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}", - "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}", - "remoteCopySources": true, - "rsyncCommandArgs": "-t --delete --delete-excluded", - "remoteCopyBuildOutput": false, - "remoteCopySourcesMethod": "rsync" - } - ] + "configurations": [ + { + "name": "x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "inheritEnvironments": [ "msvc_x64_x64" ], + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "", + "variables": [ + { + "name": "CMAKE_EXPORT_COMPILE_COMMANDS", + "value": "True", + "type": "BOOL" + } + ] + }, + { + "name": "clang-x64-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "", + "inheritEnvironments": [ "clang_cl_x64_x64" ], + "variables": [ + { + "name": "CMAKE_C_COMPILER_WORKS", + "value": "True", + "type": "BOOL" + } + ] + }, + { + "name": "Linux-GCC-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "cmakeExecutable": "cmake", + "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ], + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "", + "inheritEnvironments": [ "linux_x64" ], + "remoteMachineName": "${defaultRemoteMachineName}", + "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src", + "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}", + "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}", + "remoteCopySources": true, + "rsyncCommandArgs": "-t --delete --delete-excluded", + "remoteCopyBuildOutput": false, + "remoteCopySourcesMethod": "rsync", + "variables": [ + { + "name": "CATCH_BUILD_TESTING", + "value": "False", + "type": "BOOL" + } + ] + }, + { + "name": "Linux-Clang-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "cmakeExecutable": "cmake", + "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ], + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "", + "inheritEnvironments": [ "linux_clang_x64" ], + "variables": [ + { + "name": "CATCH_BUILD_TESTING", + "value": "False", + "type": "BOOL" + }, + { + "name": "CMAKE_C_COMPILER_WORKS", + "value": "True", + "type": "BOOL" + } + ], + "remoteMachineName": "${defaultRemoteMachineName}", + "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src", + "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}", + "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}", + "remoteCopySources": true, + "rsyncCommandArgs": "-t --delete --delete-excluded", + "remoteCopyBuildOutput": false, + "remoteCopySourcesMethod": "rsync" + }, + { + "name": "GenerateDocs", + "generator": "Ninja", + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "buildCommandArgs": "GenerateDocs", + "ctestCommandArgs": "", + "inheritEnvironments": [ "msvc_x64_x64" ], + "variables": [ + { + "name": "SIMPLE_UTILITY_GEN_DOCS_ENABLED", + "value": "True", + "type": "BOOL" + } + ] + }, + { + "name": "Linux-GCC-12-Debug", + "generator": "Ninja", + "configurationType": "Debug", + "cmakeExecutable": "cmake", + "remoteCopySourcesExclusionList": [ ".vs", ".git", "out" ], + "cmakeCommandArgs": "", + "buildCommandArgs": "", + "ctestCommandArgs": "", + "inheritEnvironments": [ "linux_x64" ], + "variables": [ + { + "name": "CATCH_BUILD_TESTING", + "value": "False", + "type": "BOOL" + }, + { + "name": "CMAKE_CXX_COMPILER", + "value": "/usr/bin/g++-12", + "type": "FILEPATH" + } + ], + "remoteMachineName": "${defaultRemoteMachineName}", + "remoteCMakeListsRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/src", + "remoteBuildRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/build/${name}", + "remoteInstallRoot": "$HOME/.vs/${projectDirName}/${workspaceHash}/out/install/${name}", + "remoteCopySources": true, + "rsyncCommandArgs": "-t --delete --delete-excluded", + "remoteCopyBuildOutput": false, + "remoteCopySourcesMethod": "rsync" + }, + { + "name": "x64-Debug-v142", + "generator": "Visual Studio 17 2022 Win64", + "configurationType": "Debug", + "buildRoot": "${projectDir}\\out\\build\\${name}", + "installRoot": "${projectDir}\\out\\install\\${name}", + "cmakeCommandArgs": "-Tv142", + "buildCommandArgs": "", + "ctestCommandArgs": "", + "inheritEnvironments": [ "msvc_x64_x64" ], + "variables": [ + { + "name": "CMAKE_EXPORT_COMPILE_COMMANDS", + "value": "True", + "type": "BOOL" + } + ] + } + ] } \ No newline at end of file diff --git a/Folder.DotSettings b/Folder.DotSettings index 90dda5ce4..b384d7f5c 100644 --- a/Folder.DotSettings +++ b/Folder.DotSettings @@ -1,6 +1,8 @@  True True + SUGGESTION + SUGGESTION HINT HINT SUGGESTION @@ -98,4 +100,5 @@ Distributed under the Boost Software License, Version 1.0. True True True + True diff --git a/benchmarks/CMakeLists.txt b/benchmarks/CMakeLists.txt new file mode 100644 index 000000000..2ee0fa991 --- /dev/null +++ b/benchmarks/CMakeLists.txt @@ -0,0 +1,29 @@ +CPMAddPackage("gh:catchorg/Catch2@3.4.0") +include("${Catch2_SOURCE_DIR}/extras/Catch.cmake") + +CPMAddPackage("gh:fmtlib/fmt#10.1.1") +CPMAddPackage("gh:ericniebler/range-v3#0.12.0") +CPMAddPackage("gh:kokkos/mdspan#mdspan-0.6.0") +CPMAddPackage( + name boost + URL "https://github.com/boostorg/boost/releases/download/boost-1.83.0/boost-1.83.0.zip" +) + +add_executable( + Simple-Utility-Benchmarks +) + +add_subdirectory("graph") + +target_link_libraries( + Simple-Utility-Benchmarks + PRIVATE + Simple::Utility + Catch2::Catch2WithMain + std::mdspan + Boost::graph + fmt::fmt + range-v3::range-v3 +) + +catch_discover_tests(Simple-Utility-Benchmarks) diff --git a/benchmarks/Defines.hpp b/benchmarks/Defines.hpp new file mode 100644 index 000000000..166d3781d --- /dev/null +++ b/benchmarks/Defines.hpp @@ -0,0 +1,19 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_BENCHMARKS_DEFINES_HPP +#define SIMPLE_UTILITY_BENCHMARKS_DEFINES_HPP + +#pragma once + +#include + +[[nodiscard]] +inline std::filesystem::path artifacts_root_path() +{ + return std::filesystem::current_path() / "artifacts"; +} + +#endif diff --git a/benchmarks/graph/AStarBoostComparison.cpp b/benchmarks/graph/AStarBoostComparison.cpp new file mode 100644 index 000000000..e2be92395 --- /dev/null +++ b/benchmarks/graph/AStarBoostComparison.cpp @@ -0,0 +1,469 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include + +#include +#include +#include +#include +#include + +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include "../Defines.hpp" + +#include "Simple-Utility/graph/AStarSearch.hpp" +#include "Simple-Utility/graph/mixins/graph/std_reference_wrapper.hpp" + +/* Most of this code is directly taken from https://github.com/boostorg/graph/blob/develop/example/astar_maze.cpp + * and slightly modernized. + * The example types are used for benchmarking both, boost::graph and sl::graph. + */ + +// Distance traveled in the maze +using distance = double; + +using grid = boost::grid_graph<2>; +using vertex_descriptor = boost::graph_traits::vertex_descriptor; +using edge_descriptor = boost::graph_traits::edge_descriptor; +using vertices_size_type = boost::graph_traits::vertices_size_type; + +struct vertex_hash +{ + using argument_type = vertex_descriptor; + using result_type = std::size_t; + + std::size_t operator()(const vertex_descriptor& u) const + { + std::size_t seed = 0; + boost::hash_combine(seed, u[0]); + boost::hash_combine(seed, u[1]); + return seed; + } +}; + +using vertex_set = boost::unordered_set; +using filtered_grid = boost::vertex_subset_complement_filter::type; + +// A searchable maze +// +// The maze is grid of locations which can either be empty or contain a +// barrier. You can move to an adjacent location in the grid by going up, +// down, left and right. Moving onto a barrier is not allowed. The maze can +// be solved by finding a path from the lower-left-hand corner to the +// upper-right-hand corner. If no open path exists between these two +// locations, the maze is unsolvable. +// +// The maze is implemented as a filtered grid graph where locations are +// vertices. Barrier vertices are filtered out of the graph. +// +// A-star search is used to find a path through the maze. Each edge has a +// weight of one, so the total path length is equal to the number of edges +// traversed. +class maze +{ +public: + friend std::ostream& operator<<(std::ostream&, const maze&); + friend void random_maze(maze&, std::uint32_t); + + explicit maze(const std::size_t x, const std::size_t y) + : m_Grid{grid::vertex_descriptor{x, y}}, + m_BarrierGrid(make_vertex_subset_complement_filter(m_Grid, m_Barriers)) + { + } + + // The length of the maze along the specified dimension. + vertices_size_type length(std::size_t d) const { return m_Grid.length(d); } + + bool has_barrier(vertex_descriptor u) const + { + return m_Barriers.find(u) != m_Barriers.end(); + } + + // Try to find a path from the lower-left-hand corner source (0,0) to the + // upper-right-hand corner goal (x-1, y-1). + vertex_descriptor source() const { return vertex(0, m_Grid); } + + vertex_descriptor goal() const + { + return vertex(num_vertices(m_Grid) - 1, m_Grid); + } + + std::optional> solve(); + + bool solved() const { return !m_Solution.empty(); } + + bool solution_contains(vertex_descriptor u) const + { + return m_Solution.find(u) != m_Solution.end(); + } + + const filtered_grid& get_grid() const noexcept + { + return m_BarrierGrid; + } + +private: + // The grid underlying the maze + grid m_Grid; + // The barriers in the maze + vertex_set m_Barriers{}; + // The underlying maze grid with barrier vertices filtered out + filtered_grid m_BarrierGrid; + // The vertices on a solution path through the maze + vertex_set m_Solution{}; + // The length of the solution path + distance m_SolutionLength{}; +}; + +// Euclidean heuristic for a grid +// +// This calculates the Euclidean distance between a vertex and a goal +// vertex. +class euclidean_heuristic + : public boost::astar_heuristic +{ +public: + explicit euclidean_heuristic(const vertex_descriptor& goal) + : m_Goal{goal} + { + } + + double operator()(vertex_descriptor v) const + { + return std::sqrt( + std::pow(static_cast(m_Goal[0] - v[0]), 2) + + std::pow(static_cast(m_Goal[1] - v[1]), 2)); + } + +private: + vertex_descriptor m_Goal; +}; + +// Exception thrown when the goal vertex is found +struct found_goal +{ +}; + +// Visitor that terminates when we find the goal vertex +struct astar_goal_visitor : public boost::default_astar_visitor +{ + explicit astar_goal_visitor(const vertex_descriptor& goal) + : m_Goal{goal} + { + } + + void examine_vertex(const vertex_descriptor& u, const filtered_grid&) const + { + if (u == m_Goal) + { + throw found_goal{}; + } + } + +private: + vertex_descriptor m_Goal; +}; + +// Solve the maze using A-star search. Return true if a solution was found. +std::optional> maze::solve() +{ + // The predecessor map is a vertex-to-vertex mapping. + std::unordered_map predecessors{}; + // The distance map is a vertex-to-distance mapping. + std::unordered_map distances{}; + + const vertex_descriptor g = goal(); + const vertex_descriptor s = source(); + + try + { + astar_search( + m_BarrierGrid, + source(), + euclidean_heuristic{g}, + weight_map(boost::static_property_map{distance{1}}) + .predecessor_map(boost::associative_property_map{predecessors}) + .distance_map(boost::associative_property_map{distances}) + .visitor(astar_goal_visitor{g})); + } + catch (const found_goal&) + { + // Walk backwards from the goal through the predecessor chain adding + // vertices to the solution path. + for (vertex_descriptor u = g; u != s; u = predecessors[u]) + { + m_Solution.insert(u); + } + m_Solution.insert(s); + m_SolutionLength = distances[g]; + return std::tuple{m_Solution, m_SolutionLength}; + } + + return std::nullopt; +} + +// Generate a maze with a random assignment of barriers. +void random_maze(maze& m, const std::uint32_t seed) +{ + const vertices_size_type n = num_vertices(m.m_Grid); + const vertex_descriptor s = m.source(); + const vertex_descriptor g = m.goal(); + // One quarter of the cells in the maze should be barriers. + vertices_size_type barriers{n / 4u}; + + std::mt19937 rng{seed}; + while (barriers > 0) + { + // Choose horizontal or vertical direction. + const std::size_t direction = std::uniform_int_distribution{0, 1}(rng); + // Walls range up to one quarter the dimension length in this direction. + vertices_size_type wall = std::uniform_int_distribution{1, m.length(direction) / 4}(rng); + // Create the wall while decrementing the total barrier count. + vertex_descriptor u = vertex(std::uniform_int_distribution{0, n - 1}(rng), m.m_Grid); + while (wall) + { + // Start and goal spaces should never be barriers. + if (u != s && u != g) + { + --wall; + if (!m.has_barrier(u)) + { + m.m_Barriers.insert(u); + if (0 == --barriers) + { + break; + } + } + } + vertex_descriptor v = m.m_Grid.next(u, direction); + // Stop creating this wall if we reached the maze's edge. + if (u == v) + { + break; + } + u = v; + } + } +} + +/* ############################# + * Begin sl::graph related symbols + ############################## */ + +template <> +struct sl::graph::graph::traits +{ + using edge_type = CommonWeightedEdge; + using vertex_type = edge::vertex_t; + using weight_type = edge::weight_t; +}; + +template <> +struct sl::graph::customize::out_edges_fn +{ + using edge_type = graph::edge_t; + using vertex_type = edge::vertex_t; + using weight_type = edge::weight_t; + + [[nodiscard]] + auto operator ()(const maze& m, const vertex_type& current) const + { + const auto& g = m.get_grid(); + const auto [edgesBegin, edgesEnd] = out_edges(current, g); + + return ranges::subrange{edgesBegin, edgesEnd} + | ranges::views::transform([&](const auto& e) { return edge_type{target(e, g), 1.}; }); + } +}; + +std::optional> sl_graph_solve(const maze& m) +{ + namespace sg = sl::graph; + + using Node = sg::decorator::PredecessorNode>; + using Stream = sg::astar::Stream< + std::reference_wrapper, + euclidean_heuristic, + Node, + sg::tracker::CommonHashMap>; + + Stream stream{ + m.source(), + std::make_tuple(std::ref(m)), + std::tuple{}, + std::tuple{}, + std::tuple{sg::astar::NodeFactory{euclidean_heuristic{m.goal()}}} + }; + + std::unordered_map nodes{}; + for (const auto& node : stream) + { + nodes.emplace(node.vertex, node); + if (node.vertex == m.goal()) + { + break; + } + } + + if (nodes.contains(m.goal())) + { + // Walk backwards from the goal through the predecessor chain adding + // vertices to the solution path. + vertex_set solution{}; + for (Node current = nodes[m.goal()]; current.predecessor; current = nodes[*current.predecessor]) + { + solution.emplace(current.vertex); + } + solution.emplace(m.source()); + + return std::tuple{solution, nodes[m.goal()].cost}; + } + + return std::nullopt; +} + +/* ############################# + * End sl::graph related symbols + ############################## */ + +// Print the maze as an ASCII map. +std::ostream& operator<<(std::ostream& output, const maze& m) +{ + constexpr char barrier = '#'; + constexpr char start = 'S'; + constexpr char goal = 'G'; + + // Header + for (vertices_size_type i = 0; i < m.length(0) + 2; i++) + { + output << barrier; + } + output << std::endl; + // Body + for (vertices_size_type i = 0; i < m.length(1); ++i) + { + const vertices_size_type y = m.length(1) - 1 - i; + // Enumerate rows in reverse order and columns in regular order so that + // (0,0) appears in the lower left-hand corner. This requires that y be + // int and not the unsigned vertices_size_type because the loop exit + // condition is y==-1. + for (vertices_size_type x = 0; x < m.length(0); x++) + { + // Put a barrier on the left-hand side. + if (x == 0) + { + output << barrier; + } + // Put the character representing this point in the maze grid. + vertex_descriptor u = {{x, y}}; + if (u == m.source()) + { + output << start; + } + else if (u == m.goal()) + { + output << goal; + } + else if (m.solution_contains(u)) + { + output << "."; + } + else if (m.has_barrier(u)) + { + output << barrier; + } + else + { + output << " "; + } + // Put a barrier on the right-hand side. + if (x == m.length(0) - 1) + { + output << barrier; + } + } + // Put a newline after every row except the last one. + output << std::endl; + } + // Footer + for (vertices_size_type i = 0; i < m.length(0) + 2; i++) + { + output << barrier; + } + if (m.solved()) + { + output << std::endl << "Solution length " << m.m_SolutionLength; + } + else + { + output << std::endl << "Not solvable"; + } + + return output; +} + +TEMPLATE_TEST_CASE_SIG( + "Benchmarking sl::graph vs boost::graph AStar.", + "[vs_boost][benchmark][benchmark::graph]", + ((int width, int height), width, height), + (8, 8), + (16, 16), + (32, 32), + (64, 64), + (128, 128), + (256, 256), + (512, 512), + (1024, 1024) +) +{ + const std::uint32_t seed = GENERATE( + // add static seeds here + Catch::getSeed()); + + maze m{width, height}; + random_maze(m, Catch::getSeed()); + + SECTION("Compare the results of both implementations.") + { + const std::optional boostSolution = [m, fileName = fmt::format("./{}_{}x{}.maze.txt", seed, width, height)]() mutable + { + auto result = m.solve(); + const auto path = artifacts_root_path() / "graph" / "astar_vs_boost"; + create_directories(path); + std::ofstream out{path / fileName}; + out << m; + return result; + }(); + const std::optional slSolution = [m] { return sl_graph_solve(m); }(); + + REQUIRE(boostSolution.has_value() == slSolution.has_value()); + if (boostSolution.has_value()) + { + REQUIRE(std::get<1>(*boostSolution) == Catch::Approx{std::get<1>(*slSolution)}); + } + } + + BENCHMARK("boost::graph") + { + return m.solve(); + }; + + BENCHMARK("sl::graph") + { + return sl_graph_solve(m); + }; +} diff --git a/benchmarks/graph/CMakeLists.txt b/benchmarks/graph/CMakeLists.txt new file mode 100644 index 000000000..c72da6926 --- /dev/null +++ b/benchmarks/graph/CMakeLists.txt @@ -0,0 +1,5 @@ +target_sources( + Simple-Utility-Benchmarks + PRIVATE + "AStarBoostComparison.cpp" + ) diff --git a/docs/Doxyfile.in b/docs/Doxyfile.in index 2e20b2edf..44b0bd84c 100644 --- a/docs/Doxyfile.in +++ b/docs/Doxyfile.in @@ -1,4 +1,4 @@ -# Doxyfile 1.9.6 +# Doxyfile 1.9.8 # This file describes the settings to be used by the documentation system # doxygen (www.doxygen.org) for a project. @@ -363,6 +363,17 @@ MARKDOWN_SUPPORT = YES TOC_INCLUDE_HEADINGS = 5 +# The MARKDOWN_ID_STYLE tag can be used to specify the algorithm used to +# generate identifiers for the Markdown headings. Note: Every identifier is +# unique. +# Possible values are: DOXYGEN use a fixed 'autotoc_md' string followed by a +# sequence number starting at 0 and GITHUB use the lower case version of title +# with any whitespace replaced by '-' and punctuation characters removed. +# The default value is: DOXYGEN. +# This tag requires that the tag MARKDOWN_SUPPORT is set to YES. + +MARKDOWN_ID_STYLE = DOXYGEN + # When enabled doxygen tries to link words that correspond to documented # classes, or namespaces to their corresponding documentation. Such a link can # be prevented in individual cases by putting a % sign in front of the word or @@ -487,6 +498,14 @@ LOOKUP_CACHE_SIZE = 0 NUM_PROC_THREADS = 1 +# If the TIMESTAMP tag is set different from NO then each generated page will +# contain the date or date and time when the page was generated. Setting this to +# NO can help when comparing the output of multiple runs. +# Possible values are: YES, NO, DATETIME and DATE. +# The default value is: NO. + +TIMESTAMP = NO + #--------------------------------------------------------------------------- # Build related configuration options #--------------------------------------------------------------------------- @@ -872,7 +891,14 @@ WARN_IF_UNDOC_ENUM_VAL = NO # a warning is encountered. If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS # then doxygen will continue running as if WARN_AS_ERROR tag is set to NO, but # at the end of the doxygen process doxygen will return with a non-zero status. -# Possible values are: NO, YES and FAIL_ON_WARNINGS. +# If the WARN_AS_ERROR tag is set to FAIL_ON_WARNINGS_PRINT then doxygen behaves +# like FAIL_ON_WARNINGS but in case no WARN_LOGFILE is defined doxygen will not +# write the warning messages in between other messages but write them at the end +# of a run, in case a WARN_LOGFILE is defined the warning messages will be +# besides being in the defined file also be shown at the end of a run, unless +# the WARN_LOGFILE is defined as - i.e. standard output (stdout) in that case +# the behavior will remain as with the setting FAIL_ON_WARNINGS. +# Possible values are: NO, YES, FAIL_ON_WARNINGS and FAIL_ON_WARNINGS_PRINT. # The default value is: NO. WARN_AS_ERROR = NO @@ -951,12 +977,12 @@ INPUT_FILE_ENCODING = # Note the list of default checked file patterns might differ from the list of # default file extension mappings. # -# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cpp, -# *.c++, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, *.ddl, *.odl, *.h, -# *.hh, *.hxx, *.hpp, *.h++, *.l, *.cs, *.d, *.php, *.php4, *.php5, *.phtml, -# *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be provided as doxygen C -# comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, *.f18, *.f, *.for, *.vhd, -# *.vhdl, *.ucf, *.qsf and *.ice. +# If left blank the following patterns are tested:*.c, *.cc, *.cxx, *.cxxm, +# *.cpp, *.cppm, *.c++, *.c++m, *.java, *.ii, *.ixx, *.ipp, *.i++, *.inl, *.idl, +# *.ddl, *.odl, *.h, *.hh, *.hxx, *.hpp, *.h++, *.ixx, *.l, *.cs, *.d, *.php, +# *.php4, *.php5, *.phtml, *.inc, *.m, *.markdown, *.md, *.mm, *.dox (to be +# provided as doxygen C comment), *.py, *.pyw, *.f90, *.f95, *.f03, *.f08, +# *.f18, *.f, *.for, *.vhd, *.vhdl, *.ucf, *.qsf and *.ice. FILE_PATTERNS = *.c \ *.cc \ @@ -1042,9 +1068,6 @@ EXCLUDE_PATTERNS = # output. The symbol name can be a fully qualified name, a word, or if the # wildcard * is used, a substring. Examples: ANamespace, AClass, # ANamespace::AClass, ANamespace::*Test -# -# Note that the wildcards are matched against the file with absolute path, so to -# exclude all test directories use the pattern */test/* EXCLUDE_SYMBOLS = sl:*:detail @@ -1427,15 +1450,6 @@ HTML_COLORSTYLE_SAT = 100 HTML_COLORSTYLE_GAMMA = 80 -# If the HTML_TIMESTAMP tag is set to YES then the footer of each generated HTML -# page will contain the date and time when the page was generated. Setting this -# to YES can help to show when doxygen was last run and thus if the -# documentation is up to date. -# The default value is: NO. -# This tag requires that the tag GENERATE_HTML is set to YES. - -HTML_TIMESTAMP = NO - # If the HTML_DYNAMIC_MENUS tag is set to YES then the generated HTML # documentation will contain a main index with vertical navigation menus that # are dynamically created via JavaScript. If disabled, the navigation index will @@ -1455,6 +1469,13 @@ HTML_DYNAMIC_MENUS = YES HTML_DYNAMIC_SECTIONS = NO +# If the HTML_CODE_FOLDING tag is set to YES then classes and functions can be +# dynamically folded and expanded in the generated HTML source code. +# The default value is: YES. +# This tag requires that the tag GENERATE_HTML is set to YES. + +HTML_CODE_FOLDING = YES + # With HTML_INDEX_NUM_ENTRIES one can control the preferred number of entries # shown in the various tree structured indices initially; the user can expand # and collapse entries dynamically later on. Doxygen will expand the tree to @@ -1585,6 +1606,16 @@ BINARY_TOC = NO TOC_EXPAND = NO +# The SITEMAP_URL tag is used to specify the full URL of the place where the +# generated documentation will be placed on the server by the user during the +# deployment of the documentation. The generated sitemap is called sitemap.xml +# and placed on the directory specified by HTML_OUTPUT. In case no SITEMAP_URL +# is specified no sitemap is generated. For information about the sitemap +# protocol see https://www.sitemaps.org +# This tag requires that the tag GENERATE_HTML is set to YES. + +SITEMAP_URL = + # If the GENERATE_QHP tag is set to YES and both QHP_NAMESPACE and # QHP_VIRTUAL_FOLDER are set, an additional index file will be generated that # can be used as input for Qt's qhelpgenerator to generate a Qt Compressed Help @@ -2073,9 +2104,16 @@ PDF_HYPERLINKS = YES USE_PDFLATEX = YES -# If the LATEX_BATCHMODE tag is set to YES, doxygen will add the \batchmode -# command to the generated LaTeX files. This will instruct LaTeX to keep running -# if errors occur, instead of asking the user for help. +# The LATEX_BATCHMODE tag signals the behavior of LaTeX in case of an error. +# Possible values are: NO same as ERROR_STOP, YES same as BATCH, BATCH In batch +# mode nothing is printed on the terminal, errors are scrolled as if is +# hit at every error; missing files that TeX tries to input or request from +# keyboard input (\read on a not open input stream) cause the job to abort, +# NON_STOP In nonstop mode the diagnostic message will appear on the terminal, +# but there is no possibility of user interaction just like in batch mode, +# SCROLL In scroll mode, TeX will stop only for missing files to input or if +# keyboard input is necessary and ERROR_STOP In errorstop mode, TeX will stop at +# each error, asking for user intervention. # The default value is: NO. # This tag requires that the tag GENERATE_LATEX is set to YES. @@ -2096,14 +2134,6 @@ LATEX_HIDE_INDICES = NO LATEX_BIB_STYLE = plain -# If the LATEX_TIMESTAMP tag is set to YES then the footer of each generated -# page will contain the date and time when the page was generated. Setting this -# to NO can help when comparing the output of multiple runs. -# The default value is: NO. -# This tag requires that the tag GENERATE_LATEX is set to YES. - -LATEX_TIMESTAMP = NO - # The LATEX_EMOJI_DIRECTORY tag is used to specify the (relative or absolute) # path from which the emoji images will be read. If a relative path is entered, # it will be relative to the LATEX_OUTPUT directory. If left blank the @@ -2269,7 +2299,7 @@ DOCBOOK_OUTPUT = docbook #--------------------------------------------------------------------------- # If the GENERATE_AUTOGEN_DEF tag is set to YES, doxygen will generate an -# AutoGen Definitions (see http://autogen.sourceforge.net/) file that captures +# AutoGen Definitions (see https://autogen.sourceforge.net/) file that captures # the structure of the code including all documentation. Note that this feature # is still experimental and incomplete at the moment. # The default value is: NO. @@ -2280,6 +2310,28 @@ GENERATE_AUTOGEN_DEF = NO # Configuration options related to Sqlite3 output #--------------------------------------------------------------------------- +# If the GENERATE_SQLITE3 tag is set to YES doxygen will generate a Sqlite3 +# database with symbols found by doxygen stored in tables. +# The default value is: NO. + +GENERATE_SQLITE3 = NO + +# The SQLITE3_OUTPUT tag is used to specify where the Sqlite3 database will be +# put. If a relative path is entered the value of OUTPUT_DIRECTORY will be put +# in front of it. +# The default directory is: sqlite3. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_OUTPUT = sqlite3 + +# The SQLITE3_OVERWRITE_DB tag is set to YES, the existing doxygen_sqlite3.db +# database file will be recreated with each doxygen run. If set to NO, doxygen +# will warn if an a database file is already found and not modify it. +# The default value is: YES. +# This tag requires that the tag GENERATE_SQLITE3 is set to YES. + +SQLITE3_RECREATE_DB = YES + #--------------------------------------------------------------------------- # Configuration options related to the Perl module output #--------------------------------------------------------------------------- @@ -2422,15 +2474,15 @@ TAGFILES = GENERATE_TAGFILE = -# If the ALLEXTERNALS tag is set to YES, all external class will be listed in -# the class index. If set to NO, only the inherited external classes will be -# listed. +# If the ALLEXTERNALS tag is set to YES, all external classes and namespaces +# will be listed in the class and namespace index. If set to NO, only the +# inherited external classes will be listed. # The default value is: NO. ALLEXTERNALS = NO # If the EXTERNAL_GROUPS tag is set to YES, all external groups will be listed -# in the modules index. If set to NO, only the current project's groups will be +# in the topic index. If set to NO, only the current project's groups will be # listed. # The default value is: YES. @@ -2444,16 +2496,9 @@ EXTERNAL_GROUPS = YES EXTERNAL_PAGES = YES #--------------------------------------------------------------------------- -# Configuration options related to the dot tool +# Configuration options related to diagram generator tools #--------------------------------------------------------------------------- -# You can include diagrams made with dia in doxygen documentation. Doxygen will -# then run dia to produce the diagram and insert it in the documentation. The -# DIA_PATH tag allows you to specify the directory where the dia binary resides. -# If left empty dia is assumed to be found in the default search path. - -DIA_PATH = - # If set to YES the inheritance and collaboration graphs will hide inheritance # and usage relations if the target is undocumented or is not a class. # The default value is: YES. @@ -2462,7 +2507,7 @@ HIDE_UNDOC_RELATIONS = YES # If you set the HAVE_DOT tag to YES then doxygen will assume the dot tool is # available from the path. This tool is part of Graphviz (see: -# http://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent +# https://www.graphviz.org/), a graph visualization toolkit from AT&T and Lucent # Bell Labs. The other options in this section have no effect if this option is # set to NO # The default value is: NO. @@ -2515,13 +2560,15 @@ DOT_NODE_ATTR = "shape=box,height=0.2,width=0.4" DOT_FONTPATH = -# If the CLASS_GRAPH tag is set to YES (or GRAPH) then doxygen will generate a -# graph for each documented class showing the direct and indirect inheritance -# relations. In case HAVE_DOT is set as well dot will be used to draw the graph, -# otherwise the built-in generator will be used. If the CLASS_GRAPH tag is set -# to TEXT the direct and indirect inheritance relations will be shown as texts / -# links. -# Possible values are: NO, YES, TEXT and GRAPH. +# If the CLASS_GRAPH tag is set to YES or GRAPH or BUILTIN then doxygen will +# generate a graph for each documented class showing the direct and indirect +# inheritance relations. In case the CLASS_GRAPH tag is set to YES or GRAPH and +# HAVE_DOT is enabled as well, then dot will be used to draw the graph. In case +# the CLASS_GRAPH tag is set to YES and HAVE_DOT is disabled or if the +# CLASS_GRAPH tag is set to BUILTIN, then the built-in generator will be used. +# If the CLASS_GRAPH tag is set to TEXT the direct and indirect inheritance +# relations will be shown as texts / links. +# Possible values are: NO, YES, TEXT, GRAPH and BUILTIN. # The default value is: YES. CLASS_GRAPH = YES @@ -2529,15 +2576,21 @@ CLASS_GRAPH = YES # If the COLLABORATION_GRAPH tag is set to YES then doxygen will generate a # graph for each documented class showing the direct and indirect implementation # dependencies (inheritance, containment, and class references variables) of the -# class with other documented classes. +# class with other documented classes. Explicit enabling a collaboration graph, +# when COLLABORATION_GRAPH is set to NO, can be accomplished by means of the +# command \collaborationgraph. Disabling a collaboration graph can be +# accomplished by means of the command \hidecollaborationgraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. COLLABORATION_GRAPH = YES # If the GROUP_GRAPHS tag is set to YES then doxygen will generate a graph for -# groups, showing the direct groups dependencies. See also the chapter Grouping -# in the manual. +# groups, showing the direct groups dependencies. Explicit enabling a group +# dependency graph, when GROUP_GRAPHS is set to NO, can be accomplished by means +# of the command \groupgraph. Disabling a directory graph can be accomplished by +# means of the command \hidegroupgraph. See also the chapter Grouping in the +# manual. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2597,7 +2650,9 @@ TEMPLATE_RELATIONS = YES # If the INCLUDE_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are set to # YES then doxygen will generate a graph for each documented file showing the # direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an include graph, when INCLUDE_GRAPH is is set to NO, +# can be accomplished by means of the command \includegraph. Disabling an +# include graph can be accomplished by means of the command \hideincludegraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2606,7 +2661,10 @@ INCLUDE_GRAPH = YES # If the INCLUDED_BY_GRAPH, ENABLE_PREPROCESSING and SEARCH_INCLUDES tags are # set to YES then doxygen will generate a graph for each documented file showing # the direct and indirect include dependencies of the file with other documented -# files. +# files. Explicit enabling an included by graph, when INCLUDED_BY_GRAPH is set +# to NO, can be accomplished by means of the command \includedbygraph. Disabling +# an included by graph can be accomplished by means of the command +# \hideincludedbygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2646,7 +2704,10 @@ GRAPHICAL_HIERARCHY = YES # If the DIRECTORY_GRAPH tag is set to YES then doxygen will show the # dependencies a directory has on other directories in a graphical way. The # dependency relations are determined by the #include relations between the -# files in the directories. +# files in the directories. Explicit enabling a directory graph, when +# DIRECTORY_GRAPH is set to NO, can be accomplished by means of the command +# \directorygraph. Disabling a directory graph can be accomplished by means of +# the command \hidedirectorygraph. # The default value is: YES. # This tag requires that the tag HAVE_DOT is set to YES. @@ -2662,7 +2723,7 @@ DIR_GRAPH_MAX_DEPTH = 1 # The DOT_IMAGE_FORMAT tag can be used to set the image format of the images # generated by dot. For an explanation of the image formats see the section # output formats in the documentation of the dot tool (Graphviz (see: -# http://www.graphviz.org/)). +# https://www.graphviz.org/)). # Note: If you choose svg you need to set HTML_FILE_EXTENSION to xhtml in order # to make the SVG files visible in IE 9+ (other browsers do not have this # requirement). @@ -2699,11 +2760,12 @@ DOT_PATH = @DOXY_DOT_PATH@ DOTFILE_DIRS = -# The MSCFILE_DIRS tag can be used to specify one or more directories that -# contain msc files that are included in the documentation (see the \mscfile -# command). +# You can include diagrams made with dia in doxygen documentation. Doxygen will +# then run dia to produce the diagram and insert it in the documentation. The +# DIA_PATH tag allows you to specify the directory where the dia binary resides. +# If left empty dia is assumed to be found in the default search path. -MSCFILE_DIRS = +DIA_PATH = # The DIAFILE_DIRS tag can be used to specify one or more directories that # contain dia files that are included in the documentation (see the \diafile @@ -2780,3 +2842,19 @@ GENERATE_LEGEND = YES # The default value is: YES. DOT_CLEANUP = YES + +# You can define message sequence charts within doxygen comments using the \msc +# command. If the MSCGEN_TOOL tag is left empty (the default), then doxygen will +# use a built-in version of mscgen tool to produce the charts. Alternatively, +# the MSCGEN_TOOL tag can also specify the name an external tool. For instance, +# specifying prog as the value, doxygen will call the tool as prog -T +# -o . The external tool should support +# output file formats "png", "eps", "svg", and "ismap". + +MSCGEN_TOOL = + +# The MSCFILE_DIRS tag can be used to specify one or more directories that +# contain msc files that are included in the documentation (see the \mscfile +# command). + +MSCFILE_DIRS = diff --git a/include/Simple-Utility/Config.hpp b/include/Simple-Utility/Config.hpp index 4828bd82b..82885bd1a 100644 --- a/include/Simple-Utility/Config.hpp +++ b/include/Simple-Utility/Config.hpp @@ -30,4 +30,16 @@ "." SL_UTILITY_XSTR(SL_UTILITY_VERSION_MINOR) \ "." SL_UTILITY_XSTR(SL_UTILITY_VERSION_PATCH) +#include + +#if defined(_MSC_VER) || (__cpp_lib_format >= 201907L) + #define SL_UTILITY_HAS_STD_FORMAT +#endif + +#if defined(_MSC_VER) || (__cpp_lib_ranges >= 202110L) + #if not defined(__clang__) || __clang_major__ >= 16L + #define SL_UTILITY_HAS_RANGES_VIEWS + #endif +#endif + #endif diff --git a/include/Simple-Utility/Utility.hpp b/include/Simple-Utility/Utility.hpp new file mode 100644 index 000000000..26cd0cc72 --- /dev/null +++ b/include/Simple-Utility/Utility.hpp @@ -0,0 +1,115 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_UTILITY_HPP +#define SIMPLE_UTILITY_UTILITY_HPP + +#pragma once + +#include "Simple-Utility/concepts/stl_extensions.hpp" + +#include +#include +#include + +namespace sl +{ + /** + * \defgroup GROUP_PRIORITY_TAG priority_tag + * \brief Helper type, useful when prioritize overloads of an overload set. + * \see Got the idea from here: https://quuxplusone.github.io/blog/2021/07/09/priority-tag/ + * \{ + */ + + /** + * \brief Primary template, inheriting from the other specializations with lesser priority. + */ + template + struct priority_tag + /** \cond */ + : public priority_tag + /** \endcond */ + { + }; + + /** + * \brief Specialization, denoting the least likely alternative. + */ + template <> + struct priority_tag<0> + { + }; + + /** + * \} + */ + + /** + * \defgroup GROUP_IN_PLACE in_place + * \brief Provides convenient option, to construct arguments in-place. + * \{ + */ + + /** + * \brief Helper type, enabling deferred construction, which will then (thanks to copy-elision) in-place construct the ``Type`` instance. + * \tparam Type The type to be constructed. + * \tparam Args The constructor argument types. + * \param args The constructor arguments. + * \note Instances of this type are not intended to be materialized anywhere. They should only be used during one expression. + */ + template + requires std::constructible_from + struct in_place_constructor + { + public: + in_place_constructor(const in_place_constructor&) = delete; + in_place_constructor& operator =(const in_place_constructor&) = delete; + in_place_constructor(in_place_constructor&&) = delete; + in_place_constructor& operator =(in_place_constructor&&) = delete; + + ~in_place_constructor() = default; + + [[nodiscard]] + in_place_constructor() = default; + + [[nodiscard]] + explicit constexpr operator Type() && noexcept(std::is_nothrow_constructible_v) + { + return std::make_from_tuple(std::move(m_Args)); + } + + template + friend constexpr in_place_constructor in_place(Ts&&... args) noexcept; + + private: + std::tuple m_Args; + + [[nodiscard]] + explicit constexpr in_place_constructor(std::tuple&& args) noexcept + : m_Args{std::move(args)} + { + } + }; + + /** + * \brief Forwards the given arguments as tuple into the internal storage. + * \tparam Type The type to be constructed. + * \tparam Args The constructor argument types. + * \param args The constructor arguments. + * \return in_place_constructor instance, storing the arguments as forwarding references. + */ + template + [[nodiscard]] + constexpr in_place_constructor in_place(Args&&... args) noexcept + { + return in_place_constructor{std::forward_as_tuple(std::forward(args)...)}; + } + + /** + * \} + */ +} + +#endif diff --git a/include/Simple-Utility/concepts/stl_extensions.hpp b/include/Simple-Utility/concepts/stl_extensions.hpp index a542f3e7f..90becb299 100644 --- a/include/Simple-Utility/concepts/stl_extensions.hpp +++ b/include/Simple-Utility/concepts/stl_extensions.hpp @@ -8,8 +8,11 @@ #pragma once +#include "Simple-Utility/Config.hpp" + #include #include +#include // ReSharper disable CppClangTidyClangDiagnosticDocumentation // ReSharper disable CppIdenticalOperandsInBinaryExpression @@ -68,6 +71,15 @@ namespace sl::concepts template concept not_same_as = !std::same_as; + /** + * \brief Checks whether T is not ``void``. + * \details This is the inverted counterpart of ``std::is_void_v`` trait. + * \see https://en.cppreference.com/w/cpp/types/is_void + * \tparam T Type to check. + */ + template + concept not_void = !std::is_void_v; + /** * \brief Checks whether the target type is constructible from the source type. * \details This is the symmetrical counterpart of ``std::constructible_from`` concept with a single constructor argument. @@ -271,4 +283,49 @@ namespace sl::concepts */ } +#ifdef SL_UTILITY_HAS_STD_FORMAT + +#include +#include + +namespace sl::concepts +{ + /** + * \addtogroup GROUP_STL_EXTENSION_CONCEPTS + * \{ + */ + + /** + * \brief Determines, whether a complete specialization of ``std::formatter`` for the given (possibly cv-ref qualified) type exists. + * \tparam T Type to check. + * \tparam Char Used character type. + * \details This is an adapted implementation of the ``std::formattable`` concept, which is added c++23. + * \note This implementation takes a simple but reasonable shortcut in assuming, that ```Char`` is either ``char`` or ``wchar_t``, + * which must not necessarily true. + * \see Adapted from here: https://www.open-std.org/jtc1/sc22/wg21/docs/papers/2022/p2286r8.html#concept-formattable + * \see https://en.cppreference.com/w/cpp/utility/format/formattable + */ + template + concept formattable = + std::semiregular, Char>> + && requires( + std::formatter, Char> formatter, + T t, + std::conditional_t, std::format_context, std::wformat_context> formatContext, + std::basic_format_parse_context parseContext + ) + { + { formatter.parse(parseContext) } -> std::same_as::iterator>; + { + std::as_const(formatter).format(t, formatContext) + } -> std::same_as::iterator>; + }; + + /** + * \} + */ +} + +#endif + #endif diff --git a/include/Simple-Utility/graph/AStarSearch.hpp b/include/Simple-Utility/graph/AStarSearch.hpp new file mode 100644 index 000000000..85f3f2bfb --- /dev/null +++ b/include/Simple-Utility/graph/AStarSearch.hpp @@ -0,0 +1,184 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_A_STAR_SEARCH_HPP +#define SIMPLE_UTILITY_GRAPH_A_STAR_SEARCH_HPP + +#pragma once + +#include "Simple-Utility/graph/Traverse.hpp" +#include "Simple-Utility/graph/mixins/queue/std_priority_queue.hpp" +#include "Simple-Utility/graph/mixins/tracker/std_unordered_map.hpp" + +namespace sl::graph::concepts +{ + template + concept heuristic_for = ranked_node + && sl::concepts::unqualified + && std::movable + && std::invocable> + && std::convertible_to>, node::rank_t>; +} + +namespace sl::graph::astar::detail +{ + template + constexpr void check_bounds(const Rank& base, const Rank& increase) noexcept + { + assert(0 <= base && "base must be non-negative."); + assert(0 <= increase && "increase must be non-negative."); + assert(increase <= std::numeric_limits::max() - base && "Rank is about to overflow."); + } +} + +namespace sl::graph::astar +{ + template + struct CommonNode + { + using vertex_type = Vertex; + using rank_type = Rank; + + vertex_type vertex{}; + rank_type cost{}; + rank_type estimatedPendingCost{}; + + [[nodiscard]] + friend constexpr rank_type rank(const CommonNode& node) noexcept(noexcept(node.cost + node.cost)) + { + detail::check_bounds(node.cost, node.estimatedPendingCost); + + return node.cost + node.estimatedPendingCost; + } + + [[nodiscard]] + friend bool operator==(const CommonNode&, const CommonNode&) = default; + }; + + template Heuristic> + struct NodeFactory; + + template > Heuristic> + struct NodeFactory, Heuristic> + { + public: + using node_type = CommonNode; + using vertex_type = Vertex; + using rank_type = Rank; + + [[nodiscard]] + explicit NodeFactory(Heuristic heuristic) noexcept(std::is_nothrow_move_constructible_v) + : m_Heuristic{std::move(heuristic)} + { + } + + [[nodiscard]] + constexpr node_type operator ()(const vertex_type& origin) const + { + node_type node{ + .vertex = origin, + .cost = {0}, + .estimatedPendingCost = std::invoke(m_Heuristic, origin) + }; + detail::check_bounds(node.cost, node.estimatedPendingCost); + + return node; + } + + template Edge> + [[nodiscard]] + constexpr node_type operator ()(const node_type& current, const Edge& edge) const + { + detail::check_bounds(current.cost, edge::weight(edge)); + + node_type node{ + .vertex = edge::destination(edge), + .cost = current.cost + edge::weight(edge), + .estimatedPendingCost = std::invoke(m_Heuristic, edge::destination(edge)) + }; + detail::check_bounds(node.cost, node.estimatedPendingCost); + + return node; + } + + private: + SL_UTILITY_NO_UNIQUE_ADDRESS Heuristic m_Heuristic; + }; + + template + struct NodeFactoryTemplate + { + template + using type = NodeFactory; + }; + + template Heuristic> + requires requires { typename decorator::NodeFactory::template type>::node_type; } + struct NodeFactory + : public decorator::NodeFactory::template type> + { + private: + using Super = decorator::NodeFactory::template type>; + + public: + using Super::Super; + using Super::operator (); + }; + + template Strategy> + class SingleDestinationHeuristic + { + public: + using vertex_type = Vertex; + + [[nodiscard]] + explicit constexpr SingleDestinationHeuristic( + Vertex destination + ) noexcept(std::is_nothrow_move_constructible_v + && std::is_nothrow_default_constructible_v) + : m_Destination{std::move(destination)} + { + } + + [[nodiscard]] + explicit constexpr SingleDestinationHeuristic( + Vertex destination, + Strategy strategy + ) noexcept(std::is_nothrow_move_constructible_v + && std::is_nothrow_move_constructible_v) + : m_Destination{std::move(destination)}, + m_Strategy{std::move(strategy)} + { + } + + [[nodiscard]] + constexpr auto operator ()(const Vertex& current) const noexcept(std::is_nothrow_invocable_v) + { + return std::invoke(m_Strategy, m_Destination, current); + } + + private: + Vertex m_Destination{}; + Strategy m_Strategy{}; + }; + + template < + concepts::basic_graph Graph, + typename Heuristic, + concepts::ranked_node Node = CommonNode>, edge::weight_t>>, + concepts::tracker_for> Tracker = tracker::CommonHashMap>> + requires concepts::heuristic_for + using Stream = IterableTraverser< + sl::graph::detail::BasicTraverser< + Node, + Graph, + queue::CommonPriorityQueue, + Tracker, + sl::graph::detail::default_explorer_t< + Node, + NodeFactory>>>; +} + +#endif diff --git a/include/Simple-Utility/graph/BreadthFirstSearch.hpp b/include/Simple-Utility/graph/BreadthFirstSearch.hpp new file mode 100644 index 000000000..fc2c52bd4 --- /dev/null +++ b/include/Simple-Utility/graph/BreadthFirstSearch.hpp @@ -0,0 +1,40 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_BREADTH_FIRST_SEARCH_HPP +#define SIMPLE_UTILITY_GRAPH_BREADTH_FIRST_SEARCH_HPP + +#pragma once + +#include "Simple-Utility/graph/Traverse.hpp" +#include "Simple-Utility/graph/mixins/queue/std_queue.hpp" +#include "Simple-Utility/graph/mixins/tracker/std_unordered_map.hpp" + +namespace sl::graph::dfs +{ + template + struct NodeFactory + : public detail::NodeFactory + { + }; + + template + using CommonNode = CommonBasicNode; + + template < + concepts::basic_graph Graph, + concepts::basic_node Node = CommonNode>>, + concepts::tracker_for> Tracker = tracker::CommonHashMap>> + requires (!concepts::ranked_node) + using Stream = IterableTraverser< + detail::BasicTraverser< + Node, + Graph, + queue::CommonQueue, + Tracker, + detail::default_explorer_t>>>; +} + +#endif diff --git a/include/Simple-Utility/graph/Common.hpp b/include/Simple-Utility/graph/Common.hpp new file mode 100644 index 000000000..ef2bb7420 --- /dev/null +++ b/include/Simple-Utility/graph/Common.hpp @@ -0,0 +1,195 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_COMMON_HPP +#define SIMPLE_UTILITY_GRAPH_COMMON_HPP + +#pragma once + +#include "Simple-Utility/concepts/operators.hpp" +#include "Simple-Utility/concepts/stl_extensions.hpp" + +#include + +/** + * \defgroup GROUP_GRAPH graph + * + * \brief This library offers various graph related algorithms. + * \details + * # Design philosophy + * This library is built completely on concepts, thus almost everything is exchangeable by users. Some parts may be worth the effort, some may not. + * Symbols in ``detail`` namespace can be used, but are less secure and may require some more insights into the internals to be used correctly. + * + * The (as far as I'm aware of) unique core idea of this library is to *somehow* sort the vertices of an arbitrary graph as sequential range. The actual + * algorithm then defines the *somehow* in an exchangeable manner. + * + * Many graph libraries utilize the visitor approach, where the visitor serves as the major customization point and gets notified about pre-defined + * events during the whole algorithm runtime. These visitor may or may not be able to abort the algorithm, e.g. if they reached their goal. For example + * boost requires the visitor throwing an exception (see [boost graph faq](https://www.boost.org/doc/libs/1_83_0/libs/graph/doc/faq.html)), which is not + * what I would consider a clean design. + * + * The major issue I see with the visitor approach is, that this pollutes the actual algorithm with too much complexity, as it must be versatile enough to + * satisfy any (or at least many) user requirements, while optimally disabling the optional features, the user does not need. It's really difficult to think about + * any possible use-case the algorithm may be utilized for and then making it generic enough, while still offering top-notch performance, being versatile + * and having a clean design. + * The iterative approach of this library therefore concentrates on being a cursor into a graph and notifying the user about the currently referred vertex + * (including possibly additional information). The users themselves can than decide how to proceed, e.g. if and how to store the yielded state or simply + * throwing the whole algorithm state away, because the job is done. Having the algorithm state present as an actual object makes it trivial to run the + * algorithm for a specific amount of steps (or until a predicate isn't satisfied, or whatever condition one may come up with) and continue later on. + * + * Nevertheless, this library neither aims to replace any existing graph library nor does it want to compete with them in performance regards. Other + * libraries are (and probably stay) more feature rich, faster and more polished. + * + * ## Concepts + * All algorithms utilize at least some of the general concepts. The term concept doesn't necessarily mean a c++ concept, its rather from a design perspective. + * Nevertheless, most of these design concepts are indeed implemented as c++ concepts. + * + * ### Graph + * A graph consists of multiple elements (vertices) and multiple edges. Most of the algorithms allow graphs to be dynamically explored during the traversal, + * thus they can be generated on the fly. + * For more info see \ref GROUP_GRAPH_GRAPH graph sub-section. + * + * ### Vertex + * A vertex denotes a unique graph element and is used by algorithms to query the graph for further information (e.g. neighboring vertices). + * + * ### Edges + * A edge denotes a connection between two neighboring vertices of a graph and may contain a ``weight``, which is used by some algorithms, to select the best + * alternatives out of the known edges. + * + * ### Node + * A node is an info structure generated by traversing algorithms, which is used to keep track of the exploration state. After each exploration step the current + * node is returned to the caller, which may cache it in its own container or simply throw it away. + * + * Nodes consist of at least a vertex, which they are related to, but may also contain additional information provided by the algorithm. + * In fact, the algorithms require a minimal property set, but users are free to extend them with their own properties. + * + * ### Tracker + * In general, a Graph may contain loops or merging branches, which will eventually lead to multiple discovery of the same vertices. The trackers task is then + * to identify these multiple discoveries and decide if the current vertex should be further investigated or skipped. There are three exploration states, a vertex + * can be in: + * - ``unknown``: The vertex is not known by the tracker and should be further investigated. + * - ``discovered``: The tracker already knows about the vertex, but there may be still a better solution, thus it should be further investigated. + * - ``visited``: The vertex has already been visited and should be skipped. + * + * The actual tracker implementation is usually one of the first optimizations users can apply. Per default, a hash map is used, which is a good general + * purpose solution, but isn't the most efficient for many cases. E.g. a grid graph could utilize a 2d bool array, which will then lead to a constant lookup time. + * + * ### Queue + * A queue stores the discovered nodes and returns them back in any order it desires, thus queue doesn't necessarily mean a FIFO structure. In fact, while the breadth- + * first-search is indeed built on top of an actual queue, the depth-first-search for example utilizes a stack (LIFO) container. A queue therefore has major influence + * about the algorithms behaviour and should therefore only exchanged with an equivalent container. + * + * ### Traverser + * The traverser couples everything together and can be queried for the next vertex, which will then either return the next visited node, or a null-object if there were + * no pending nodes left. + * + * A traverser can be decorated with ``IterableTraverser``, which in fact provides ``begin`` and ``end`` members, and can then be used as a input-range with any existing + * algorithms. + * + * ## Customization points + * As already pointed out, the library does utilize concepts very much. But to be fully exchangeable, the library introduces another layer of indirection in the + * sub-namespace ``graph:customize``. All templates in this namespace may be specialized for user types and any such specialization will be prioritized by the + * library. + */ + +// ReSharper disable CppDoxygenUnresolvedReference +/** + * \defgroup GROUP_GRAPH_CUSTOMIZATION_POINT customization points + * \ingroup GROUP_GRAPH + * + * \brief Customization points offered by the library. + * \details The library offers several customization points to the users, which may be used to make custom or third-party types interface compliant. + * In fact a special implementation strategy is used, so that each customization point favors the user defined customization point specialization in cases + * where an appropriate alternative would exist. All what has to be done is, to introduce a template specialization for the desired entry point. The specialization + * must adhere to the expectations of the customization point and should not alter observable behavior in any way. + * + * ## Example + * For example, given the type ``ThirdPartyEdge`` which shall be utilized as an edge type for this library. + * \snippet tests/graph/Edge.cpp ThirdPartyEdge definition + * + * As you can see, it offers a ``vertex`` member function but the library expects a ``destination`` member function instead. Given its a type of a third party you + * can not simply alter the function name, nor is it wise to add a free function into a third party namespace. Fortunately the library offers a third option: The + * customization points. In this case its ``sl::graph::customize::destination_fn``, which one can specialize here. + * \snippet tests/graph/Edge.cpp ThirdPartyEdge destination_fn specialization + * And that's it,``ThirdPartyEdge`` can now be used as an edge type in this library. + */ +// ReSharper restore CppDoxygenUnresolvedReference + +/** + * \defgroup GROUP_GRAPH_COMMON_TYPES common types + * \ingroup GROUP_GRAPH + * \brief Contains common types, which can be utilized for algorithms. + * \details The contained types are just default types, which can be utilized for algorithms, but may also be easily exchanged with user types. + */ + +namespace sl::graph::concepts +{ + /** + * \defgroup GROUP_GRAPH_CONCEPTS concepts + * \ingroup GROUP_GRAPH + * \brief Contains general library concepts. + *\{ + */ + + /** + * \brief Checks, whether the given type satisfies the vertex type requirements. + * \tparam T Type to check. + */ + template + concept vertex = sl::concepts::unqualified + && std::equality_comparable + && std::copyable; + + /** + * \brief Checks, whether the given type satisfies the weight type requirements. + * \tparam T Type to check. + */ + template + concept weight = sl::concepts::unqualified + && std::copyable + && sl::concepts::plus + && sl::concepts::minus + && sl::concepts::plus_assign + && sl::concepts::minus_assign; + + /** + * \brief Checks, whether the given type satisfies the rank type requirements. + * \tparam T Type to check. + */ + template + concept rank = sl::concepts::unqualified + && std::regular + && std::totally_ordered; + + /** + * \brief Checks, whether the given type contains a ``vertex_type`` member alias. + * \tparam T Type to check. + */ + template + concept readable_vertex_type = requires { typename T::vertex_type; } + && vertex; + + /** + * \brief Checks, whether the given type contains a ``weight_type`` member alias. + * \tparam T Type to check. + */ + template + concept readable_weight_type = requires { typename T::weight_type; } + && weight; + + /** + * \brief Checks, whether the given type contains a ``rank_type`` member alias. + * \tparam T Type to check. + */ + template + concept readable_rank_type = requires { typename T::rank_type; } + && rank; + + /** + * \} + */ +} + +#endif diff --git a/include/Simple-Utility/graph/DepthFirstSearch.hpp b/include/Simple-Utility/graph/DepthFirstSearch.hpp new file mode 100644 index 000000000..a16ec7ddf --- /dev/null +++ b/include/Simple-Utility/graph/DepthFirstSearch.hpp @@ -0,0 +1,40 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_DEPTH_FIRST_SEARCH_HPP +#define SIMPLE_UTILITY_GRAPH_DEPTH_FIRST_SEARCH_HPP + +#pragma once + +#include "Simple-Utility/graph/Traverse.hpp" +#include "Simple-Utility/graph/mixins/queue/std_stack.hpp" +#include "Simple-Utility/graph/mixins/tracker/std_unordered_map.hpp" + +namespace sl::graph::dfs +{ + template + struct NodeFactory + : public detail::NodeFactory + { + }; + + template + using CommonNode = CommonBasicNode; + + template < + concepts::basic_graph Graph, + concepts::basic_node Node = CommonNode>>, + concepts::tracker_for> Tracker = tracker::CommonHashMap>> + requires (!concepts::ranked_node) + using Stream = IterableTraverser< + detail::BasicTraverser< + Node, + Graph, + queue::CommonStack, + Tracker, + detail::default_explorer_t>>>; +} + +#endif diff --git a/include/Simple-Utility/graph/Edge.hpp b/include/Simple-Utility/graph/Edge.hpp new file mode 100644 index 000000000..5d6e36b47 --- /dev/null +++ b/include/Simple-Utility/graph/Edge.hpp @@ -0,0 +1,344 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_EDGE_HPP +#define SIMPLE_UTILITY_GRAPH_EDGE_HPP + +#pragma once + +#include "Simple-Utility/Utility.hpp" +#include "Simple-Utility/concepts/stl_extensions.hpp" +#include "Simple-Utility/graph/Common.hpp" + +namespace sl::graph::edge +{ + /** + * \defgroup GROUP_GRAPH_EDGE edge + * \ingroup GROUP_GRAPH + * \brief Contains edge related definitions. + * \details An edge connects two vertices inside a graph. A minimal edge contains a ``destination`` vertex; a weighted edge contains also a + * ``weight`` property. + * + * \{ + */ + + /** + * \brief Primary template is purposely undefined. + */ + template + struct traits; + + /** + * \brief Convenience alias, exposing the ``vertex_type`` member alias of the \ref sl::graph::edge::traits "traits" type. + * \tparam Edge Type to retrieve the info for. + */ + template + using vertex_t = typename traits::vertex_type; + + /** + * \brief Convenience alias, exposing the ``weight_type`` member alias of the \ref sl::graph::edge::traits "traits" type. + * \tparam Edge Type to retrieve the info for. + */ + template + using weight_t = typename traits::weight_type; + + /** + * \brief General trait specialization for edges, which contain a valid ``vertex_type`` member alias. + * \tparam T + */ + template + requires concepts::readable_vertex_type + struct traits + { + using vertex_type = typename T::vertex_type; + }; + + /** + * \brief General trait specialization for edges, which contain both, a valid ``vertex_type`` and ``weight_type`` member alias. + * \tparam T + */ + template + requires concepts::readable_vertex_type + && concepts::readable_weight_type + struct traits + { + using vertex_type = typename T::vertex_type; + using weight_type = typename T::weight_type; + }; + + /** + * \} + */ +} + +namespace sl::graph::customize +{ + /** + * \brief Primary template for the ``weight`` customization point. Is purposely undefined. + * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT_WEIGHT + */ + template + struct weight_fn; + + /** + * \brief Primary template for the ``destination`` customization point. Is purposely undefined. + * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT_DESTINATION + */ + template + struct destination_fn; +} + +namespace sl::graph::edge::detail +{ + template + requires requires(const Node& node, customize::destination_fn fn) + { + requires concepts::vertex>; + } + constexpr decltype(auto) destination( + const Node& node, + const priority_tag<3> + ) noexcept(noexcept(customize::destination_fn{}(node))) + { + return customize::destination_fn{}(node); + } + + template + requires requires(const Node& node) + { + requires concepts::vertex>; + } + constexpr auto& destination(const Node& node, const priority_tag<2>) noexcept + { + return node.destination; + } + + template + requires requires(const Node& node) + { + requires concepts::vertex>; + } + constexpr decltype(auto) destination(const Node& node, const priority_tag<1>) noexcept(noexcept(node.destination())) + { + return node.destination(); + } + + template + requires requires(const Node& node) + { + requires concepts::vertex>; + } + constexpr decltype(auto) destination(const Node& node, const priority_tag<0>) noexcept(noexcept(destination(node))) + { + return destination(node); + } + + struct destination_fn + { + template + requires requires(const Node& node, const priority_tag<3> tag) + { + requires concepts::vertex>; + } + constexpr decltype(auto) operator ()(const Node& node) const noexcept(noexcept(detail::destination(node, priority_tag<3>{}))) + { + return detail::destination(node, priority_tag<3>{}); + } + }; + + template + requires requires(const Edge& node, customize::weight_fn fn) + { + requires concepts::weight>; + } + constexpr decltype(auto) weight(const Edge& node, const priority_tag<3>) noexcept(noexcept(customize::weight_fn{}(node))) + { + return customize::weight_fn{}(node); + } + + template + requires requires(const Edge& node) + { + requires concepts::weight>; + } + constexpr auto& weight(const Edge& node, const priority_tag<2>) noexcept + { + return node.weight; + } + + template + requires requires(const Edge& node) + { + requires concepts::weight>; + } + constexpr decltype(auto) weight(const Edge& node, const priority_tag<1>) noexcept(noexcept(node.weight())) + { + return node.weight(); + } + + template + requires requires(const Edge& node) + { + requires concepts::weight>; + } + constexpr decltype(auto) weight(const Edge& node, const priority_tag<0>) noexcept(noexcept(weight(node))) + { + return weight(node); + } + + struct weight_fn + { + template + requires requires(const Edge& node, const priority_tag<3> tag) + { + requires concepts::weight>; + } + constexpr decltype(auto) operator ()(const Edge& node) const noexcept(noexcept(detail::weight(node, priority_tag<3>{}))) + { + return detail::weight(node, priority_tag<3>{}); + } + }; +} + +namespace sl::graph::edge +{ + /** + * \defgroup GROUP_GRAPH_CUSTOMIZATION_POINT_DESTINATION destination + * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT + * \ingroup GROUP_GRAPH_EDGE + * \brief Queries the edge for its destination vertex. + * \details This function internally dispatches the call in regards of the following priority list: + * - ``graph::customize::destination_fn`` specialization + * - ``destination`` member variable + * - ``destination`` member function + * - ``destination`` free function (with ADL enabled) + * + * Specialized ``destination_fn`` should offer an ``operator ()`` definition matching the following signature: + * \code{.cpp} + * sl::graph::edge::vertex_t operator ()(const Edge&) const; + * \endcode + * ``Edge`` itself is the user type, for which the entry point is specialized for. + *\{ + */ + + /** + * \brief Customization point, retrieving the destination vertex of the given edge. + */ + inline constexpr detail::destination_fn destination{}; + + /** + * \} + */ + + /** + * \defgroup GROUP_GRAPH_CUSTOMIZATION_POINT_WEIGHT weight + * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT + * \ingroup GROUP_GRAPH_EDGE + * \brief Queries the edge for its weight. + * \details This function internally dispatches the call in regards of the following priority list: + * - ``graph::customize::weight_fn`` specialization + * - ``weight`` member variable + * - ``weight`` member function + * - ``weight`` free function (with ADL enabled) + * + * Specialized ``weight_fn`` should offer an ``operator ()`` definition matching the following signature: + * \code{.cpp} + * sl::graph::edge::weight_t operator ()(const Edge&) const; + * \endcode + * ``Edge`` itself is the user type, for which the entry point is specialized for. + *\{ + */ + + /** + * \brief Customization point, retrieving the weight of the given edge. + */ + inline constexpr detail::weight_fn weight{}; + + /** + * \} + */ +} + +namespace sl::graph::concepts +{ + /** + * \brief Determines, whether the given type satisfies the requirements. + * \tparam T Type to check. + */ + template + concept edge = sl::concepts::unqualified + && std::copyable + && std::destructible + && vertex::vertex_type> + && requires(const T& edge) + { + // fixes compile error on msvc v142 + // ReSharper disable once CppRedundantTemplateKeyword + { edge::destination(edge) } -> std::convertible_to::vertex_type>; + }; + + /** + * \brief Determines, whether the given type satisfies the requirements. + * \tparam T Type to check. + */ + template + concept weighted_edge = edge + && weight::weight_type> + && requires(const T& edge) + { + // fixes compile error on msvc v142 + // ReSharper disable once CppRedundantTemplateKeyword + { edge::weight(edge) } -> std::convertible_to::weight_type>; + }; +} + +namespace sl::graph +{ + /** + * \addtogroup GROUP_GRAPH_COMMON_TYPES + * \{ + */ + + /** + * \brief A basic edge type. + * \tparam Vertex The used vertex type. + * \note This type is also equality comparable, which is not an actual requirement for basic edge types. + */ + template + struct CommonBasicEdge + { + using vertex_type = Vertex; + + vertex_type destination; + + [[nodiscard]] + friend bool operator ==(const CommonBasicEdge&, const CommonBasicEdge&) = default; + }; + + /** + * \brief A weighted edge type. + * \tparam Vertex The used vertex type. + * \tparam Weight The used weight type. + * \note This type is also equality comparable, which is not an actual requirement for weighted edge types. + */ + template + struct CommonWeightedEdge + { + using vertex_type = Vertex; + using weight_type = Weight; + + vertex_type destination; + weight_type weight; + + [[nodiscard]] + friend bool operator==(const CommonWeightedEdge&, const CommonWeightedEdge&) = default; + }; + + /** + * \} + */ +} + +#endif diff --git a/include/Simple-Utility/graph/Formatter.hpp b/include/Simple-Utility/graph/Formatter.hpp new file mode 100644 index 000000000..385c3b031 --- /dev/null +++ b/include/Simple-Utility/graph/Formatter.hpp @@ -0,0 +1,163 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_FORMATTER_HPP +#define SIMPLE_UTILITY_GRAPH_FORMATTER_HPP + +#pragma once + +#include "Simple-Utility/Config.hpp" +#include "Simple-Utility/graph/Edge.hpp" +#include "Simple-Utility/graph/Node.hpp" + +#ifdef SL_UTILITY_HAS_STD_FORMAT + +#include + +namespace sl::graph +{ + template + requires sl::concepts::formattable, Char> + class NodeFormatter + { + public: + static constexpr auto parse(std::basic_format_parse_context& ctx) noexcept + { + return ctx.begin(); + } + + template + static auto format(const Node& node, FormatContext& ctx) + { + return std::format_to(ctx.out(), "vertex: {}", node::vertex(node)); + } + }; + + template + requires sl::concepts::formattable, Char> + && sl::concepts::formattable, Char> + class NodeFormatter + { + public: + static constexpr auto parse(std::basic_format_parse_context& ctx) noexcept + { + return ctx.begin(); + } + + template + static auto format(const Node& node, FormatContext& ctx) + { + return std::format_to(ctx.out(), "vertex: {}, rank: {}", node::vertex(node), node::rank(node)); + } + }; + + template + class NodeFormatter, Char> + { + public: + constexpr auto parse(std::basic_format_parse_context& ctx) noexcept + { + return m_Formatter.parse(ctx); + } + + template + auto format(const decorator::PredecessorNode& node, FormatContext& ctx) const + { + return std::format_to( + m_Formatter.format(node, ctx), + ", predecessor: {}", + node.predecessor ? std::format("{}", *node.predecessor) : "null"); + } + + private: + SL_UTILITY_NO_UNIQUE_ADDRESS NodeFormatter m_Formatter{}; + }; + + template + class NodeFormatter, Char> + { + public: + constexpr auto parse(std::basic_format_parse_context& ctx) noexcept + { + return m_Formatter.parse(ctx); + } + + template + auto format(const decorator::DepthNode& node, FormatContext& ctx) const + { + return std::format_to( + m_Formatter.format(node, ctx), + ", depth: {}", + node.depth); + } + + private: + SL_UTILITY_NO_UNIQUE_ADDRESS NodeFormatter m_Formatter{}; + }; +} + +template +struct std::formatter // NOLINT(cert-dcl58-cpp) +{ +public: + constexpr auto parse(std::basic_format_parse_context& ctx) + { + return m_Formatter.parse(ctx); + } + + template + auto format(const Node& node, FormatContext& ctx) const + { + auto out = std::format_to(ctx.out(), "{}", "{"); + out = m_Formatter.format(node, ctx); + return std::format_to(out, "{}", "}"); + } + +private: + SL_UTILITY_NO_UNIQUE_ADDRESS sl::graph::NodeFormatter m_Formatter{}; +}; + +template + requires sl::concepts::formattable, Char> +struct std::formatter // NOLINT(cert-dcl58-cpp) +{ + static constexpr auto parse(std::basic_format_parse_context& ctx) noexcept + { + return ctx.begin(); + } + + template + auto format(const Edge& edge, FormatContext& ctx) const + { + return std::format_to(ctx.out(), "{}destination: {}{}", "{", sl::graph::edge::destination(edge), "}"); + } +}; + +template + requires sl::concepts::formattable, Char> + && sl::concepts::formattable, Char> +struct std::formatter // NOLINT(cert-dcl58-cpp) +{ + static constexpr auto parse(std::basic_format_parse_context& ctx) noexcept + { + return ctx.begin(); + } + + template + auto format(const Edge& edge, FormatContext& ctx) const + { + return std::format_to( + ctx.out(), + "{}destination: {}, weight: {}{}", + "{", + sl::graph::edge::destination(edge), + sl::graph::edge::weight(edge), + "}"); + } +}; + +#endif + +#endif diff --git a/include/Simple-Utility/graph/Graph.hpp b/include/Simple-Utility/graph/Graph.hpp new file mode 100644 index 000000000..c2b49a6fc --- /dev/null +++ b/include/Simple-Utility/graph/Graph.hpp @@ -0,0 +1,218 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_VIEW_HPP +#define SIMPLE_UTILITY_GRAPH_VIEW_HPP + +#pragma once + +#include "Simple-Utility/concepts/stl_extensions.hpp" +#include "Simple-Utility/graph/Edge.hpp" +#include "Simple-Utility/graph/Node.hpp" + +#include +// ReSharper disable once CppUnusedIncludeDirective +#include // std::ranges::input_range, etc. + +namespace sl::graph::graph +{ + /** + * \defgroup GROUP_GRAPH_GRAPH graph + * \ingroup GROUP_GRAPH + * \brief Contains graph related definitions. + * \details A graph is a compilation of vertices and edges. Vertices are uniquely identifiable elements, which are connected via edges. + * A minimal graph can be queried for outgoing edges of a specific vertex, one at a time. Thus, the graph doesn't have to be know from + * the get go; it can be generated during algorithm runs. + * + * Algorithms expect graphs by value, but its generally enough to provide a shallow graph (aka view) to the library, which just references + * an actual graph object (e.g. a graph wrapped as ``std::reference_wrapper`` is fine). Thus, this library sometimes refers to graphs as + * ``view``. + *\{ + */ + + /** + * \brief Primary template is purposely undefined. + */ + template + struct traits; + + /** + * \brief Convenience alias, exposing the ``edge_type`` member alias of the \ref sl::graph::graph::traits "traits" type. + * \tparam T Type to retrieve the info for. + */ + template + using edge_t = typename traits::edge_type; + + /** + * \brief Convenience alias, exposing the ``vertex_type`` member alias of the \ref sl::graph::graph::traits "traits" type. + * \tparam T Type to retrieve the info for. + */ + template + using vertex_t = typename traits::vertex_type; + + /** + * \brief General trait specialization for graphs, which have both, a valid ``vertex_type`` and ``edge_type`` member alias. + * \tparam T + */ + template + requires concepts::readable_vertex_type + && requires { requires concepts::edge; } + struct traits + { + using vertex_type = typename T::vertex_type; + using edge_type = typename T::edge_type; + }; + + /** + * \} + */ +} + +namespace sl::graph::customize +{ + /** + * \brief Primary template for the ``out_edges`` customization point. Is purposely undefined. + * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT_OUT_EDGES + */ + template + struct out_edges_fn; +} + +namespace sl::graph::detail +{ + template + requires requires { customize::out_edges_fn{}; } + && std::ranges::input_range, const Graph&, const graph::vertex_t&>> + && std::convertible_to< + std::ranges::range_reference_t, const Graph&, const graph::vertex_t&>>, + graph::edge_t> + constexpr decltype(auto) out_edges( + const Graph& graph, + const graph::vertex_t& vertex, + const priority_tag<2> + ) noexcept(noexcept(customize::out_edges_fn{}(graph, vertex))) + { + return customize::out_edges_fn{}(graph, vertex); + } + + // pleases msvc v142 + // ReSharper disable CppRedundantTemplateKeyword + // ReSharper disable CppRedundantTypenameKeyword + template + requires requires(const Graph& graph, const typename graph::template vertex_t& vertex) + { + { graph.out_edges(vertex) } -> std::ranges::input_range; + requires std::convertible_to< + std::ranges::range_reference_t, + typename graph::template edge_t>; + } + constexpr decltype(auto) out_edges( + const Graph& graph, + const graph::vertex_t& vertex, + const priority_tag<1> + ) noexcept(noexcept(graph.out_edges(vertex))) + { + return graph.out_edges(vertex); + } + + template + requires requires(const Graph& graph, const typename graph::template vertex_t& vertex) + { + { out_edges(graph, vertex) } -> std::ranges::input_range; + requires std::convertible_to< + std::ranges::range_reference_t, + typename graph::template edge_t>; + } + constexpr decltype(auto) out_edges( + const Graph& graph, + const graph::vertex_t& vertex, + const priority_tag<0> + ) noexcept(noexcept(out_edges(graph, vertex))) + { + return out_edges(graph, vertex); + } + + struct out_edges_fn + { + template + requires requires(const Graph& graph, const typename graph::template vertex_t& vertex, const priority_tag<2> tag) + { + { detail::out_edges(graph, vertex, tag) } -> std::ranges::input_range; + requires std::convertible_to< + std::ranges::range_reference_t, + typename graph::template edge_t>; + } + constexpr decltype(auto) operator ()( + const Graph& graph, + const graph::vertex_t& vertex + ) const noexcept(noexcept(detail::out_edges(graph, vertex, priority_tag<2>{}))) + { + return detail::out_edges(graph, vertex, priority_tag<2>{}); + } + }; + + // ReSharper restore CppRedundantTemplateKeyword + // ReSharper restore CppRedundantTypenameKeyword +} + +namespace sl::graph::graph +{ + /** + * \defgroup GROUP_GRAPH_CUSTOMIZATION_POINT_OUT_EDGES out_edges + * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT + * \ingroup GROUP_GRAPH_GRAPH + * \brief Queries the outgoing edges of a specific vertex from a graph. + * \details This function internally dispatches the call in regards of the following priority list: + * - ``graph::customize::out_edges_fn`` specialization + * - ``out_edges`` member function + * - ``out_edges`` free function (with ADL enabled) + * + * Specialized ``out_edges_fn`` should offer an ``operator ()`` definition matching the following signature: + * \code{.cpp} + * input_edge_view operator ()(const Graph&, const sl::graph::graph::vertex_t&) const; + * \endcode + * ``input_edge_view`` may be any type satisfying the ``std::ranges::input_range`` concept and having a ``reference_type`` convertible to + * the ``sl::graph::graph::edge_t`` type. ``Graph`` itself is the user type, for which the entry point is specialized for. + *\{ + */ + + /** + * \brief Customization point, querying the outgoing edges of a specific vertex from a graph. + */ + inline constexpr detail::out_edges_fn out_edges{}; + + /** + * \} + */ +} + +namespace sl::graph::concepts +{ + /** + * \brief Determines, whether the given type satisfies the requirements of a graph type. + */ + template + concept basic_graph = sl::concepts::unqualified + && std::destructible + && requires(const T& graph) + { + // fixes compile error on msvc v142 + // ReSharper disable CppRedundantTemplateKeyword + // ReSharper disable CppRedundantTypenameKeyword + requires vertex::vertex_type>; + requires edge::edge_type>; + { graph::out_edges(graph, std::declval&>()) } -> std::ranges::input_range; + requires std::convertible_to< + std::ranges::range_value_t&>>, + typename graph::template edge_t>; + // ReSharper restore CppRedundantTemplateKeyword + // ReSharper restore CppRedundantTypenameKeyword + }; +} + +#endif diff --git a/include/Simple-Utility/graph/Node.hpp b/include/Simple-Utility/graph/Node.hpp new file mode 100644 index 000000000..8fc1aac2b --- /dev/null +++ b/include/Simple-Utility/graph/Node.hpp @@ -0,0 +1,488 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_NODE_HPP +#define SIMPLE_UTILITY_GRAPH_NODE_HPP + +#pragma once + +#include "Simple-Utility/Utility.hpp" +#include "Simple-Utility/concepts/stl_extensions.hpp" +#include "Simple-Utility/graph/Common.hpp" +#include "Simple-Utility/graph/Edge.hpp" + +// ReSharper disable once CppUnusedIncludeDirective +#include // std::invoke +#include + +namespace sl::graph::customize +{ + /** + * \brief Primary template for the ``vertex`` customization point. Is purposely undefined. + * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT_VERTEX + */ + template + struct vertex_fn; + + /** + * \brief Primary template for the ``rank`` customization point. Is purposely undefined. + * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT_RANK + */ + template + struct rank_fn; +} + +namespace sl::graph::detail +{ + template + requires requires(const Node& node, customize::vertex_fn fn) + { + requires concepts::vertex>; + } + constexpr decltype(auto) vertex(const Node& node, const priority_tag<3>) noexcept(noexcept(customize::vertex_fn{}(node))) + { + return customize::vertex_fn{}(node); + } + + template + requires requires + { + requires concepts::vertex().vertex)>>; + } + constexpr auto& vertex(const Node& node, const priority_tag<2>) noexcept + { + return node.vertex; + } + + template + requires requires(const Node& node) + { + requires concepts::vertex>; + } + constexpr decltype(auto) vertex(const Node& node, const priority_tag<1>) noexcept(noexcept(node.vertex())) + { + return node.vertex(); + } + + template + requires requires(const Node& node) + { + requires concepts::vertex>; + } + constexpr decltype(auto) vertex(const Node& node, const priority_tag<0>) noexcept(noexcept(vertex(node))) + { + return vertex(node); + } + + struct vertex_fn + { + template + requires requires(const Node& node, const priority_tag<3> tag) + { + requires concepts::vertex>; + } + constexpr decltype(auto) operator ()(const Node& node) const noexcept(noexcept(detail::vertex(node, priority_tag<3>{}))) + { + return detail::vertex(node, priority_tag<3>{}); + } + }; + + template + requires requires(const Node& node, customize::rank_fn fn) + { + requires concepts::rank>; + } + constexpr decltype(auto) rank(const Node& node, const priority_tag<3>) noexcept(noexcept(customize::rank_fn{}(node))) + { + return customize::rank_fn{}(node); + } + + template + requires requires(const Node& node) + { + requires concepts::rank>; + } + constexpr auto& rank(const Node& node, const priority_tag<2>) noexcept + { + return node.rank; + } + + template + requires requires(const Node& node) + { + requires concepts::rank>; + } + constexpr decltype(auto) rank(const Node& node, const priority_tag<1>) noexcept(noexcept(node.rank())) + { + return node.rank(); + } + + template + requires requires(const Node& node) + { + requires concepts::rank>; + } + constexpr decltype(auto) rank(const Node& node, const priority_tag<0>) noexcept(noexcept(rank(node))) + { + return rank(node); + } + + struct rank_fn + { + template + requires requires(const Node& node, const priority_tag<3> tag) + { + requires concepts::rank>; + } + constexpr decltype(auto) operator ()(const Node& node) const noexcept(noexcept(detail::rank(node, priority_tag<3>{}))) + { + return detail::rank(node, priority_tag<3>{}); + } + }; +} + +namespace sl::graph::node +{ + /** + * \defgroup GROUP_GRAPH_CUSTOMIZATION_POINT_VERTEX vertex + * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT + * \ingroup GROUP_GRAPH_NODE + * \brief Queries the node for its related vertex. + * \details This function internally dispatches the call in regards of the following priority list: + * - ``graph::customize::vertex_fn`` specialization + * - ``vertex`` member variable + * - ``vertex`` member function + * - ``vertex`` free function (with ADL enabled) + * + * Specialized ``vertex_fn`` should offer an ``operator ()`` definition matching the following signature: + * \code{.cpp} + * sl::graph::node::vertex_t operator ()(const Node&) const; + * \endcode + * ``Node`` itself is the user type, for which the entry point is specialized for. + *\{ + */ + + /** + * \brief Customization point, retrieving the related vertex of the given node. + */ + + inline constexpr detail::vertex_fn vertex{}; + + /** + * \} + */ + + /** + * \defgroup GROUP_GRAPH_CUSTOMIZATION_POINT_RANK rank + * \ingroup GROUP_GRAPH_CUSTOMIZATION_POINT + * \ingroup GROUP_GRAPH_NODE + * \brief Queries the node for its rank. + * \details This function internally dispatches the call in regards of the following priority list: + * - ``graph::customize::rank_fn`` specialization + * - ``rank`` member variable + * - ``rank`` member function + * - ``rank`` free function (with ADL enabled) + * + * Specialized ``rank_fn`` should offer an ``operator ()`` definition matching the following signature: + * \code{.cpp} + * sl::graph::node::rank_t operator ()(const Node&) const; + * \endcode + * ``Node`` itself is the user type, for which the entry point is specialized for. + *\{ + */ + + /** + * \brief Customization point, retrieving the rank of the given node. + */ + + inline constexpr detail::rank_fn rank{}; + + /** + * \} + */ + + /** + * \defgroup GROUP_GRAPH_NODE node + * \ingroup GROUP_GRAPH + * \brief Contains node related definitions. + * \details A node represents the state of a vertex during an algorithm run. A minimal node contains its referred vertex but may also contain + * addition information. + * + * In general a node is built by a node_factory, which receives the source node and one of its edges and then builds the sub-node with the provided + * information. In fact the specified node for the algorithm determines the minimal information a graph must provide via its returned edges. + * Or more formally:
+ *
information(node) ∩ information(edge) = information(node)
+ * + * \{ + */ + + /** + * \brief Primary template is purposely undefined. + */ + template + struct traits; + + /** + * \brief Convenience alias, exposing the ``vertex_type`` member alias of the \ref sl::graph::node::traits "traits" type. + * \tparam Node Type to retrieve the info for. + */ + template + using vertex_t = typename traits::vertex_type; + + /** + * \brief Convenience alias, exposing the ``rank_type`` member alias of the \ref sl::graph::node::traits "traits" type. + * \tparam Node Type to retrieve the info for. + */ + template + using rank_t = typename traits::rank_type; + + /** + * \brief General trait specialization for nodes, which contain a valid ``vertex_type`` member alias. + * \tparam T + */ + template + requires concepts::readable_vertex_type + struct traits + { + using vertex_type = typename T::vertex_type; + }; + + /** + * \brief General trait specialization for nodes, which contain both, a valid ``vertex_type`` and ``rank_type`` member alias. + * \tparam T + */ + template + requires concepts::readable_vertex_type + && concepts::readable_rank_type + struct traits + { + using vertex_type = typename T::vertex_type; + using rank_type = typename T::rank_type; + }; + + /** + * \} + */ +} + +namespace sl::graph::concepts +{ + /** + * \brief Determines, whether the given type satisfies the requirements. + * \tparam T Type to check. + */ + template + concept basic_node = sl::concepts::unqualified + && std::copyable + && std::destructible + && requires { typename node::traits::vertex_type; } + && vertex> + && requires(const T& node) + { + // ReSharper disable once CppRedundantTemplateKeyword + // ReSharper disable once CppRedundantTypenameKeyword + { node::vertex(node) } -> std::convertible_to>; // pleases msvc v142 + }; + + /** + * \brief Determines, whether the given type satisfies the requirements. + * \tparam Node Type to check. + */ + template + concept ranked_node = basic_node + && requires { typename node::traits::rank_type; } + && rank> + && requires(const Node& node) + { + // ReSharper disable once CppRedundantTemplateKeyword + // ReSharper disable once CppRedundantTypenameKeyword + { node::rank(node) } -> std::convertible_to>; // pleases msvc v142 + }; + + /** + * \brief Determines, whether the ``Edge`` type provides the minimal information required by the ``Node`` type. + * \tparam Edge Type to check. + * \tparam Node Baseline for the minimal required information. + */ + template + concept edge_for = basic_node + && edge + && std::same_as, node::vertex_t> + && (!ranked_node + || requires(const Edge& edge) + { + requires weighted_edge; + { edge::weight(edge) } -> std::convertible_to>; + }); +} + +namespace sl::graph +{ + /** + * \brief Generic node type, satisfying the requirements of the ``basic_node`` concept. + * \tparam Vertex Vertex type. + */ + template + struct CommonBasicNode + { + using vertex_type = Vertex; + + vertex_type vertex; + + [[nodiscard]] + friend bool operator==(const CommonBasicNode&, const CommonBasicNode&) = default; + }; + + /** + * \brief Generic node type, satisfying the requirements of the ``ranked_node`` concept. + * \tparam Vertex Vertex type. + * \tparam Rank Rank type. + */ + template + struct CommonRankedNode + { + using vertex_type = Vertex; + using rank_type = Rank; + + vertex_type vertex; + rank_type rank; + + [[nodiscard]] + friend bool operator ==(const CommonRankedNode&, const CommonRankedNode&) = default; + }; +} + +namespace sl::graph::decorator +{ + /** + * \brief Node decorator, extending the provided Node type with a ``predecessor`` property. + * \tparam Node The decorated node type. + */ + template + struct PredecessorNode + : public Node + { + using vertex_type = node::vertex_t; + + std::optional predecessor{}; + + [[nodiscard]] + friend bool operator ==(const PredecessorNode&, const PredecessorNode&) = default; + }; + + /** + * \brief Primary template node factory decorator, extending the given base node-factory. Purposely undefined. + * \tparam Node The node type. + * \tparam BaseNodeFactory The base node-factory template. + * \details This complexity is necessary, so users can freely nest library-provided and user-defined + * node decorators in arbitrary depth. + */ + template typename BaseNodeFactory> + class NodeFactory; + + /** + * \brief Node factory decorator specialization for PredecessorNode decorator. + * \tparam Node The decorated node type. + * \tparam BaseNodeFactory The base node-factory template. + */ + template typename BaseNodeFactory> + class NodeFactory, BaseNodeFactory> + : private BaseNodeFactory + { + private: + using Super = BaseNodeFactory; + + public: + using node_type = PredecessorNode; + using vertex_type = node::vertex_t; + + using Super::Super; + + template + [[nodiscard]] + constexpr node_type operator ()(vertex_type origin, Args&&... args) const + { + // leave code as-is, because directly returning the temporary results in an ICE on gcc10 + node_type node{ + {std::invoke(static_cast(*this), std::move(origin), std::forward(args)...)}, + std::nullopt + }; + return node; + } + + template Edge, typename... Args> + [[nodiscard]] + constexpr node_type operator ()(const node_type& current, const Edge& edge, Args&&... args) const + { + // leave code as-is, because directly returning the temporary results in an ICE on gcc10 + node_type node{ + {std::invoke(static_cast(*this), current, edge, std::forward(args)...)}, + node::vertex(current) + }; + return node; + } + }; + + /** + * \brief Node decorator, extending the provided Node type with a ``depth`` property. + * \tparam Node The decorated node type. + */ + template + struct DepthNode + : public Node + { + using vertex_type = node::vertex_t; + + int depth{}; + + [[nodiscard]] + friend bool operator ==(const DepthNode&, const DepthNode&) = default; + }; + + /** + * \brief Node factory decorator specialization for DepthNode decorator. + * \tparam Node The decorated node type. + * \tparam BaseNodeFactory The base node-factory template. + */ + template typename BaseNodeFactory> + class NodeFactory, BaseNodeFactory> + : private BaseNodeFactory + { + private: + using Super = BaseNodeFactory; + + public: + using node_type = DepthNode; + using vertex_type = node::vertex_t; + + using Super::Super; + + template + [[nodiscard]] + constexpr node_type operator ()(vertex_type origin, Args&&... args) const + { + // leave code as-is, because directly returning the temporary results in an ICE on gcc10 + node_type node{ + {std::invoke(static_cast(*this), std::move(origin), std::forward(args)...)}, + {0} + }; + return node; + } + + template Edge, typename... Args> + [[nodiscard]] + constexpr node_type operator ()(const node_type& current, const Edge& edge, Args&&... args) const + { + // leave code as-is, because directly returning the temporary results in an ICE on gcc10 + node_type node{ + {std::invoke(static_cast(*this), current, edge, std::forward(args)...)}, + {current.depth + 1} + }; + return node; + } + }; +} + +#endif diff --git a/include/Simple-Utility/graph/Queue.hpp b/include/Simple-Utility/graph/Queue.hpp new file mode 100644 index 000000000..28a345793 --- /dev/null +++ b/include/Simple-Utility/graph/Queue.hpp @@ -0,0 +1,207 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_QUEUE_HPP +#define SIMPLE_UTILITY_GRAPH_QUEUE_HPP + +#pragma once + +#include "Simple-Utility/Utility.hpp" +#include "Simple-Utility/graph/Common.hpp" +#include "Simple-Utility/graph/Node.hpp" + +#include +#include + +namespace sl::graph::customize +{ + template + struct empty_fn; + + template + struct insert_fn; + + template + struct next_fn; +} + +namespace sl::graph::queue::detail +{ + template + requires requires(const T& t) { { customize::empty_fn{}(t) } -> std::convertible_to; } + constexpr bool empty(const T& queue, const priority_tag<2>) noexcept(noexcept(customize::empty_fn{}(queue))) + { + return customize::empty_fn{}(queue); + } + + template + requires requires(const T& t) { { t.empty() } -> std::convertible_to; } + constexpr bool empty(const T& queue, const priority_tag<1>) noexcept(noexcept(queue.empty())) + { + return queue.empty(); + } + + template + requires requires(const T& t) { { empty(t) } -> std::convertible_to; } + constexpr decltype(auto) empty(const T& queue, const priority_tag<0>) noexcept(noexcept(empty(queue))) + { + return empty(queue); + } + + struct empty_fn + { + template + requires requires(const T& t) { { detail::empty(t, priority_tag<2>{}) } -> std::convertible_to; } + constexpr bool operator ()(const T& queue) const noexcept(noexcept(detail::empty(queue, priority_tag<2>{}))) + { + return detail::empty(queue, priority_tag<2>{}); + } + }; + + template + requires requires(Queue& queue) { customize::insert_fn{}(queue, std::declval()); } + constexpr void insert( + Queue& queue, + Range&& elements, + const priority_tag<2> + ) noexcept(noexcept(customize::insert_fn{}(queue, std::forward(elements)))) + { + customize::insert_fn{}(queue, std::forward(elements)); + } + + template + requires requires(Queue& queue) { queue.insert(std::declval()); } + constexpr void insert( + Queue& container, + Range&& elements, + const priority_tag<1> + ) noexcept(noexcept(container.insert(std::forward(elements)))) + { + container.insert(std::forward(elements)); + } + + template + requires requires(Queue& queue) { insert(queue, std::declval()); } + constexpr void insert( + Queue& container, + Range&& elements, + const priority_tag<0> + ) noexcept(noexcept(insert(container, std::forward(elements)))) + { + insert(container, std::forward(elements)); + } + + struct insert_fn + { + template + requires requires(Queue& queue) { detail::insert(queue, std::declval(), priority_tag<2>{}); } + constexpr void operator ()( + Queue& queue, + Range&& elements + ) const noexcept(noexcept(detail::insert(queue, std::forward(elements), priority_tag<2>{}))) + { + detail::insert(queue, std::forward(elements), priority_tag<2>{}); + } + }; + + template + requires requires(Queue& queue) + { + requires concepts::basic_node{}(queue))>>; + } + constexpr decltype(auto) next(Queue& queue, const priority_tag<2>) noexcept(noexcept(customize::next_fn{}(queue))) + { + return customize::next_fn{}(queue); + } + + template + requires requires(Queue& queue) + { + requires concepts::basic_node>; + } + constexpr decltype(auto) next(Queue& queue, const priority_tag<1>) noexcept(noexcept(queue.next())) + { + return queue.next(); + } + + template + requires requires(Queue& queue) + { + requires concepts::basic_node>; + } + constexpr decltype(auto) next(Queue& queue, const priority_tag<0>) noexcept(noexcept(next(queue))) + { + return next(queue); + } + + struct next_fn + { + template + requires requires(Queue& queue) + { + requires concepts::basic_node{}))>>; + } + constexpr decltype(auto) operator ()(Queue& queue) const noexcept(noexcept(detail::next(queue, priority_tag<2>{}))) + { + return detail::next(queue, priority_tag<2>{}); + } + }; +} + +namespace sl::graph::queue +{ + inline constexpr detail::empty_fn empty{}; + inline constexpr detail::insert_fn insert{}; + inline constexpr detail::next_fn next{}; +} + +namespace sl::graph::queue::detail +{ + // LCOV_EXCL_START + + template + struct dummy_input_range + { + struct iterator + { + using iterator_concept = std::input_iterator_tag; + using element_type = T; + using difference_type = std::ptrdiff_t; + + T operator *() const { throw std::runtime_error{"Dummy"}; } + + iterator& operator ++() { return *this; } + + // ReSharper disable once CppDiscardedPostfixOperatorResult + void operator ++(int) { (*this)++; } + + bool operator==(const iterator&) const = default; + }; + + static iterator begin() { return {}; } + + static iterator end() { return {}; } + }; + + static_assert(std::ranges::input_range>); + + // LCOV_EXCL_STOP +} + +namespace sl::graph::concepts +{ + template + concept queue_for = sl::concepts::unqualified + && basic_node + // ReSharper disable once CppRedundantTemplateKeyword // pleases msvc v142 + && requires(T& queue, queue::detail::template dummy_input_range inputRange) + { + { queue::empty(std::as_const(queue)) } -> std::convertible_to; + queue::insert(queue, inputRange); + { queue::next(queue) } -> std::convertible_to; + }; +} + +#endif diff --git a/include/Simple-Utility/graph/Tracker.hpp b/include/Simple-Utility/graph/Tracker.hpp new file mode 100644 index 000000000..eddbf7b44 --- /dev/null +++ b/include/Simple-Utility/graph/Tracker.hpp @@ -0,0 +1,129 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_TRACKER_HPP +#define SIMPLE_UTILITY_GRAPH_TRACKER_HPP + +#pragma once + +#include "Simple-Utility/Utility.hpp" +#include "Simple-Utility/concepts/stl_extensions.hpp" +#include "Simple-Utility/graph/Common.hpp" + +// ReSharper disable once CppUnusedIncludeDirective +#include + +namespace sl::graph::customize +{ + template + struct set_discovered_fn; + + template + struct set_visited_fn; +} + +namespace sl::graph::tracker::detail +{ + template + requires requires(T& t, const Vertex& v) { { customize::set_discovered_fn{}(t, v) } -> std::convertible_to; } + constexpr bool set_discovered( + T& tracker, + const Vertex& v, + const priority_tag<2> + ) noexcept(noexcept(customize::set_discovered_fn{}(tracker, v))) + { + return customize::set_discovered_fn{}(tracker, v); + } + + template + requires requires(T& t, const Vertex& v) { { t.set_discovered(v) } -> std::convertible_to; } + constexpr bool set_discovered(T& tracker, const Vertex& v, const priority_tag<1>) noexcept(noexcept(tracker.set_discovered(v))) + { + return tracker.set_discovered(v); + } + + template + requires requires(T& t, const Vertex& v) { { set_discovered(t, v) } -> std::convertible_to; } + constexpr bool set_discovered(T& tracker, const Vertex& v, const priority_tag<0>) noexcept(noexcept(set_discovered(tracker, v))) + { + return set_discovered(tracker, v); + } + + struct set_discovered_fn + { + template + requires requires(T& t, const Vertex& v) + { + { detail::set_discovered(t, v, priority_tag<2>{}) } -> std::convertible_to; + } + [[nodiscard]] + constexpr bool operator ()( + T& tracker, + const Vertex& v + ) const noexcept(noexcept(detail::set_discovered(tracker, v, priority_tag<2>{}))) + { + return detail::set_discovered(tracker, v, priority_tag<2>{}); + } + }; + + template + requires requires(T& t, const Vertex& v) { { customize::set_visited_fn{}(t, v) } -> std::convertible_to; } + constexpr bool set_visited( + T& tracker, + const Vertex& v, + const priority_tag<2> + ) noexcept(noexcept(customize::set_visited_fn{}(tracker, v))) + { + return customize::set_visited_fn{}(tracker, v); + } + + template + requires requires(T& t, const Vertex& v) { { t.set_visited(v) } -> std::convertible_to; } + constexpr bool set_visited(T& tracker, const Vertex& v, const priority_tag<1>) noexcept(noexcept(tracker.set_visited(v))) + { + return tracker.set_visited(v); + } + + template + requires requires(T& t, const Vertex& v) { { set_visited(t, v) } -> std::convertible_to; } + constexpr bool set_visited(T& tracker, const Vertex& v, const priority_tag<0>) noexcept(noexcept(set_visited(tracker, v))) + { + return set_visited(tracker, v); + } + + struct set_visited_fn + { + template + requires requires(T& t, const Vertex& v) { { detail::set_visited(t, v, priority_tag<2>{}) } -> std::convertible_to; } + [[nodiscard]] + constexpr bool operator ()( + T& tracker, + const Vertex& v + ) const noexcept(noexcept(detail::set_visited(tracker, v, priority_tag<2>{}))) + { + return detail::set_visited(tracker, v, priority_tag<2>{}); + } + }; +} + +namespace sl::graph::tracker +{ + inline constexpr detail::set_discovered_fn set_discovered{}; + inline constexpr detail::set_visited_fn set_visited{}; +} + +namespace sl::graph::concepts +{ + template + concept tracker_for = sl::concepts::unqualified + && vertex + && requires(T& tracker, const Vertex& v) + { + { tracker::set_discovered(tracker, v) } -> std::convertible_to; + { tracker::set_visited(tracker, v) } -> std::convertible_to; + }; +} + +#endif diff --git a/include/Simple-Utility/graph/Traverse.hpp b/include/Simple-Utility/graph/Traverse.hpp new file mode 100644 index 000000000..6c40390c3 --- /dev/null +++ b/include/Simple-Utility/graph/Traverse.hpp @@ -0,0 +1,425 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_TRAVERSE_HPP +#define SIMPLE_UTILITY_GRAPH_TRAVERSE_HPP + +#pragma once + +#include "Simple-Utility/Config.hpp" +#include "Simple-Utility/functional/Tuple.hpp" +#include "Simple-Utility/graph/Edge.hpp" +#include "Simple-Utility/graph/Graph.hpp" +#include "Simple-Utility/graph/Node.hpp" +#include "Simple-Utility/graph/Queue.hpp" +#include "Simple-Utility/graph/Tracker.hpp" + +#include +#include +#include +#include +#include +#include +#include + +namespace sl::graph::concepts +{ + template + concept explorer = basic_node + && basic_graph + && tracker_for> + && sl::concepts::unqualified + && std::destructible + && requires(const T& explorer, const Node& node, const Graph& view, Tracker& tracker) + { + { std::invoke(explorer, node::vertex(node), tracker) } -> std::convertible_to; + { std::invoke(explorer, view, node, tracker) } -> std::ranges::input_range; + requires std::convertible_to< + std::ranges::range_reference_t>, + Node>; + }; + + template + concept traverser_kernel = basic_graph + && sl::concepts::unqualified + && std::destructible + && requires( + T& kernel, + const Graph& view, + const Explorer& explorer, + Queue& queue, + Tracker& tracker + ) + { + { !std::invoke(kernel, view, explorer, queue, tracker) } -> std::convertible_to; + { + *std::invoke(kernel, view, explorer, queue, tracker) + } -> std::convertible_to>; + }; + + template + concept traverser = std::destructible + && requires(T& traverser) + { + typename T::node_type; + { !traverser.next() } -> std::convertible_to; + { *traverser.next() } -> std::convertible_to; + }; +} + +namespace sl::graph::detail +{ + template + struct NodeFactory; + + template + struct NodeFactory + { + [[nodiscard]] + constexpr Node operator ()(const node::vertex_t& vertex) const + { + return Node{vertex}; + } + + template Edge> + [[nodiscard]] + constexpr Node operator ()([[maybe_unused]] const Node& predecessor, const Edge& edge) const + { + if constexpr (concepts::ranked_node) + { + return Node{ + edge::destination(edge), + node::rank(predecessor) + edge::weight(edge) + }; + } + else + { + return Node{edge::destination(edge)}; + } + } + }; + + template + requires requires { typename decorator::NodeFactory::node_type; } + struct NodeFactory + : public decorator::NodeFactory + { + }; + + template + class BasicExplorer + { + public: + [[nodiscard]] + explicit BasicExplorer() = default; + + [[nodiscard]] + explicit constexpr BasicExplorer( + NodeFactory nodeFactory + ) noexcept(std::is_nothrow_move_constructible_v) + : m_NodeFactory{std::move(nodeFactory)} + { + } + + template + [[nodiscard]] + constexpr Node operator ()(const node::vertex_t& vertex, Tracker& tracker) const + { + [[maybe_unused]] const bool result = tracker::set_discovered(tracker, vertex); + assert(result && "Tracker returned false (already visited) for the origin node."); + + return std::invoke(m_NodeFactory, vertex); + } + + template + requires std::convertible_to< + std::invoke_result_t< + NodeFactory, + const Node&, + graph::edge_t>, + Node> + [[nodiscard]] + constexpr auto operator ()(const View& graph, const Node& current, Tracker& tracker) const + { + return std::invoke( + CollectorStrategy{}, + graph::out_edges(graph, node::vertex(current)), + current, + m_NodeFactory, + tracker); + } + + private: + SL_UTILITY_NO_UNIQUE_ADDRESS NodeFactory m_NodeFactory{}; + }; + + struct BufferedCollector + { + template + constexpr auto operator ()(Edges&& edges, const Node& current, const NodeFactory& nodeFactory, Tracker& tracker) + { + std::vector results{}; + if constexpr (std::ranges::sized_range) + { + results.reserve(std::ranges::size(edges)); + } + + for (const auto& edge : edges) + { + if (tracker::set_discovered(tracker, edge::destination(edge))) + { + results.emplace_back(std::invoke(nodeFactory, current, edge)); + } + } + + return results; + } + }; + + template + using BufferedExplorer = BasicExplorer; + +#ifdef SL_UTILITY_HAS_RANGES_VIEWS + + struct LazyCollector + { + template + constexpr auto operator ()(Edges&& edges, const Node& current, const NodeFactory& nodeFactory, Tracker& tracker) + { + return std::views::all(std::forward(edges)) + | std::views::filter([&](const auto& edge) { return tracker::set_discovered(tracker, edge::destination(edge)); }) + | std::views::transform([&](const auto& edge) { return std::invoke(nodeFactory, current, edge); }); + } + }; + + template + using LazyExplorer = BasicExplorer; + + template + using default_explorer_t = LazyExplorer; +#else + template + using default_explorer_t = BufferedExplorer; +#endif + + struct PreOrderKernel + { + template + [[nodiscard]] + constexpr auto operator()(const Graph& graph, const Explorer& explorer, Queue& queue, Tracker& tracker) const + { + using node_type = std::remove_cvref_t; + + const auto queueNext = [&]() -> std::optional + { + if (!queue::empty(queue)) + { + return {queue::next(queue)}; + } + + return std::nullopt; + }; + + std::optional result = queueNext(); + for (; + result && !tracker::set_visited(tracker, node::vertex(*result)); + result = queueNext()) + { + } + + if (result) + { + queue::insert( + queue, + std::invoke(explorer, graph, *result, tracker)); + } + + return result; + } + }; + + template < + concepts::basic_node Node, + concepts::basic_graph Graph, + concepts::queue_for QueueStrategy, + concepts::tracker_for> TrackingStrategy, + concepts::explorer ExplorationStrategy = default_explorer_t>, + concepts::traverser_kernel KernelStrategy = PreOrderKernel> + requires concepts::edge_for, Node> + class BasicTraverser + { + public: + using node_type = Node; + using edge_type = graph::edge_t; + using vertex_type = node::vertex_t; + using graph_type = Graph; + using queue_type = QueueStrategy; + using tracker_type = TrackingStrategy; + + ~BasicTraverser() = default; + + BasicTraverser(const BasicTraverser&) = delete; + BasicTraverser& operator =(const BasicTraverser&) = delete; + BasicTraverser(BasicTraverser&&) = default; + BasicTraverser& operator =(BasicTraverser&&) = default; + + template < + typename... GraphArgs, + typename... QueueArgs, + typename... TrackerArgs, + typename... ExplorerArgs> + requires std::constructible_from + && std::constructible_from + && std::constructible_from + && std::constructible_from + [[nodiscard]] + explicit constexpr BasicTraverser( + const vertex_type& origin, + std::tuple graphArgs, + std::tuple queueArgs, + std::tuple trackerArgs, + std::tuple explorerArgs + ) + : m_Explorer{std::make_from_tuple(std::move(explorerArgs))}, + m_Queue{std::make_from_tuple(std::move(queueArgs))}, + m_Tracker{std::make_from_tuple(std::move(trackerArgs))}, + m_Graph{std::make_from_tuple(std::move(graphArgs))} + { + assert(queue::empty(m_Queue) && "Queue already contains elements."); + + queue::insert(m_Queue, std::array{std::invoke(m_Explorer, origin, m_Tracker)}); + } + + [[nodiscard]] + constexpr std::optional next() + { + return std::invoke(m_Kernel, m_Graph, m_Explorer, m_Queue, m_Tracker); + } + + [[nodiscard]] + constexpr const queue_type& queue() const noexcept + { + return m_Queue; + } + + [[nodiscard]] + constexpr const tracker_type& tracker() const noexcept + { + return m_Tracker; + } + + [[nodiscard]] + constexpr const graph_type& view() const noexcept + { + return m_Graph; + } + + private: + ExplorationStrategy m_Explorer{}; + KernelStrategy m_Kernel{}; + queue_type m_Queue; + tracker_type m_Tracker; + graph_type m_Graph; + }; +} + +namespace sl::graph +{ + template + class IterableTraverser + { + public: + using node_type = typename Traverser::node_type; + using vertex_type = node::vertex_t; + + ~IterableTraverser() = default; + + IterableTraverser(const IterableTraverser&) = delete; + IterableTraverser& operator =(const IterableTraverser&) = delete; + IterableTraverser(IterableTraverser&&) = default; + IterableTraverser& operator =(IterableTraverser&&) = default; + + template + requires std::constructible_from + [[nodiscard]] + explicit constexpr IterableTraverser( + TraverserArgs&&... traverserArgs + ) noexcept(std::is_nothrow_constructible_v) + : m_Traverser{std::forward(traverserArgs)...} + { + } + + struct Sentinel final + { + }; + + struct Iterator final + { + friend IterableTraverser; + + public: + using iterator_concept = std::input_iterator_tag; + using element_type = node_type; + using difference_type = std::ptrdiff_t; + + [[nodiscard]] + constexpr const node_type& operator *() const noexcept + { + return *m_Value; + } + + constexpr Iterator& operator ++() + { + m_Value = m_Source->next(); + return *this; + } + + constexpr void operator ++(int) + { + operator++(); + } + + [[nodiscard]] + constexpr bool operator==([[maybe_unused]] const Sentinel) const noexcept + { + return !m_Value; + } + + private: + Traverser* m_Source{}; + std::optional m_Value{}; + + [[nodiscard]] + constexpr explicit Iterator(Traverser& source) + : m_Source{std::addressof(source)}, + m_Value{source.next()} + { + } + }; + + [[nodiscard]] + constexpr Iterator begin() & + { + return Iterator{m_Traverser}; + } + + void begin() const & = delete; + + [[nodiscard]] + constexpr Sentinel end() const noexcept + { + return Sentinel{}; + } + + private: + Traverser m_Traverser; + }; + + template + requires concepts::traverser> + IterableTraverser(Traverser&&) -> IterableTraverser; +} + +#endif diff --git a/include/Simple-Utility/graph/UniformCostSearch.hpp b/include/Simple-Utility/graph/UniformCostSearch.hpp new file mode 100644 index 000000000..4523fe1bb --- /dev/null +++ b/include/Simple-Utility/graph/UniformCostSearch.hpp @@ -0,0 +1,39 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_UNIFORM_COST_SEARCH_HPP +#define SIMPLE_UTILITY_GRAPH_UNIFORM_COST_SEARCH_HPP + +#pragma once + +#include "Simple-Utility/graph/Traverse.hpp" +#include "Simple-Utility/graph/mixins/queue/std_priority_queue.hpp" +#include "Simple-Utility/graph/mixins/tracker/std_unordered_map.hpp" + +namespace sl::graph::ucs +{ + template + struct NodeFactory + : public detail::NodeFactory + { + }; + + template + using CommonNode = CommonRankedNode; + + template < + concepts::basic_graph Graph, + concepts::ranked_node Node = CommonNode>, edge::weight_t>>, + concepts::tracker_for> Tracker = tracker::CommonHashMap>> + using Stream = IterableTraverser< + detail::BasicTraverser< + Node, + Graph, + queue::CommonPriorityQueue, + Tracker, + detail::default_explorer_t>>>; +} + +#endif diff --git a/include/Simple-Utility/graph/mixins/graph/std_reference_wrapper.hpp b/include/Simple-Utility/graph/mixins/graph/std_reference_wrapper.hpp new file mode 100644 index 000000000..905076e99 --- /dev/null +++ b/include/Simple-Utility/graph/mixins/graph/std_reference_wrapper.hpp @@ -0,0 +1,37 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_MIXINS_GRAPH_STD_REFERENCE_WRAPPER_HPP +#define SIMPLE_UTILITY_GRAPH_MIXINS_GRAPH_STD_REFERENCE_WRAPPER_HPP + +#pragma once + +#include + +#include "Simple-Utility/graph/Graph.hpp" + +template + requires sl::graph::concepts::basic_graph> +struct sl::graph::graph::traits> +{ + using edge_type = edge_t>; + using vertex_type = vertex_t>; +}; + +template + requires sl::graph::concepts::basic_graph> +struct sl::graph::customize::out_edges_fn> +{ + using edge_type = graph::edge_t>; + using vertex_type = graph::vertex_t>; + + [[nodiscard]] + constexpr auto operator ()(const Graph& graph, const vertex_type& current) const + { + return graph::out_edges(graph, current); + } +}; + +#endif diff --git a/include/Simple-Utility/graph/mixins/queue/std_priority_queue.hpp b/include/Simple-Utility/graph/mixins/queue/std_priority_queue.hpp new file mode 100644 index 000000000..1685b8f69 --- /dev/null +++ b/include/Simple-Utility/graph/mixins/queue/std_priority_queue.hpp @@ -0,0 +1,60 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_MIXINS_QUEUE_STD_PRIORITY_QUEUE_HPP +#define SIMPLE_UTILITY_GRAPH_MIXINS_QUEUE_STD_PRIORITY_QUEUE_HPP + +#pragma once + +#include +#include +#include +#include + +#include "Simple-Utility/graph/Node.hpp" +#include "Simple-Utility/graph/Queue.hpp" + +template +struct sl::graph::customize::insert_fn> +{ + template + requires std::convertible_to, T> + constexpr void operator ()(std::priority_queue& container, Range&& elements) const + { + for (auto&& element : std::forward(elements)) + { + container.push(std::forward(element)); + } + } +}; + +template +struct sl::graph::customize::next_fn> +{ + constexpr T operator ()(std::priority_queue& container) const + { + auto element = container.top(); + container.pop(); + return element; + } +}; + +namespace sl::graph::queue +{ + struct PriorityAfterRelation + { + template + [[nodiscard]] + constexpr bool operator ()(const Node& lhs, const Node& rhs) const + { + return node::rank(lhs) > node::rank(rhs); + } + }; + + template > + using CommonPriorityQueue = std::priority_queue; +} + +#endif diff --git a/include/Simple-Utility/graph/mixins/queue/std_queue.hpp b/include/Simple-Utility/graph/mixins/queue/std_queue.hpp new file mode 100644 index 000000000..3d15fee5a --- /dev/null +++ b/include/Simple-Utility/graph/mixins/queue/std_queue.hpp @@ -0,0 +1,50 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_MIXINS_QUEUE_STD_QUEUE_HPP +#define SIMPLE_UTILITY_GRAPH_MIXINS_QUEUE_STD_QUEUE_HPP + +#pragma once + +#include +#include +#include +#include + +#include "Simple-Utility/graph/Node.hpp" +#include "Simple-Utility/graph/Queue.hpp" + +template +struct sl::graph::customize::insert_fn> +{ + template + requires std::convertible_to, T> + constexpr void operator ()(std::queue& container, Range&& elements) const + { + for (auto&& element : std::forward(elements)) + { + container.push(std::forward(element)); + } + } +}; + +template +struct sl::graph::customize::next_fn> +{ + constexpr T operator ()(std::queue& container) const + { + auto element = std::move(container.front()); + container.pop(); + return element; + } +}; + +namespace sl::graph::queue +{ + template > + using CommonQueue = std::queue; +} + +#endif diff --git a/include/Simple-Utility/graph/mixins/queue/std_stack.hpp b/include/Simple-Utility/graph/mixins/queue/std_stack.hpp new file mode 100644 index 000000000..e26c6d4ac --- /dev/null +++ b/include/Simple-Utility/graph/mixins/queue/std_stack.hpp @@ -0,0 +1,49 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_MIXINS_QUEUE_STD_STACK_HPP +#define SIMPLE_UTILITY_GRAPH_MIXINS_QUEUE_STD_STACK_HPP + +#pragma once + +#include +#include +#include +#include + +#include "Simple-Utility/graph/Queue.hpp" + +template +struct sl::graph::customize::insert_fn> +{ + template + requires std::convertible_to, T> + constexpr void operator ()(std::stack& container, Range&& elements) const + { + for (auto&& element : std::forward(elements)) + { + container.push(std::forward(element)); + } + } +}; + +template +struct sl::graph::customize::next_fn> +{ + constexpr T operator ()(std::stack& container) const + { + auto element = std::move(container.top()); + container.pop(); + return element; + } +}; + +namespace sl::graph::queue +{ + template > + using CommonStack = std::stack; +} + +#endif diff --git a/include/Simple-Utility/graph/mixins/tracker/Null.hpp b/include/Simple-Utility/graph/mixins/tracker/Null.hpp new file mode 100644 index 000000000..7f98dea48 --- /dev/null +++ b/include/Simple-Utility/graph/mixins/tracker/Null.hpp @@ -0,0 +1,35 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_MIXINS_TRACKER_NULL_HPP +#define SIMPLE_UTILITY_GRAPH_MIXINS_TRACKER_NULL_HPP + +#pragma once + +#include "Simple-Utility/graph/Common.hpp" +#include "Simple-Utility/graph/Tracker.hpp" + +namespace sl::graph::tracker +{ + class Null + { + public: + template + [[nodiscard]] + static constexpr bool set_discovered([[maybe_unused]] const Vertex& vertex) noexcept + { + return true; + } + + template + [[nodiscard]] + static constexpr bool set_visited([[maybe_unused]] const Vertex& vertex) noexcept + { + return true; + } + }; +} + +#endif diff --git a/include/Simple-Utility/graph/mixins/tracker/std_map.hpp b/include/Simple-Utility/graph/mixins/tracker/std_map.hpp new file mode 100644 index 000000000..40e69c05a --- /dev/null +++ b/include/Simple-Utility/graph/mixins/tracker/std_map.hpp @@ -0,0 +1,45 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_MIXINS_TRACKER_STD_MAP_HPP +#define SIMPLE_UTILITY_GRAPH_MIXINS_TRACKER_STD_MAP_HPP + +#pragma once + +#include +#include + +#include "Simple-Utility/graph/Common.hpp" +#include "Simple-Utility/graph/Tracker.hpp" + +template +struct sl::graph::customize::set_discovered_fn> +{ + template K> + bool operator ()(std::map& container, K&& vertex) const + { + const auto [iter, inserted] = container.try_emplace(std::forward(vertex), false); + return inserted || !iter->second; + } +}; + +template +struct sl::graph::customize::set_visited_fn> +{ + bool operator ()(std::map& container, const Key& vertex) const + { + const auto iter = container.find(vertex); + assert(iter != std::cend(container) && "Visited a vertex which hasn't been discovered yet."); + return !std::exchange(iter->second, true); + } +}; + +namespace sl::graph::tracker +{ + template + using CommonMap = std::map; +} + +#endif diff --git a/include/Simple-Utility/graph/mixins/tracker/std_unordered_map.hpp b/include/Simple-Utility/graph/mixins/tracker/std_unordered_map.hpp new file mode 100644 index 000000000..91a86393e --- /dev/null +++ b/include/Simple-Utility/graph/mixins/tracker/std_unordered_map.hpp @@ -0,0 +1,46 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_GRAPH_MIXINS_TRACKER_STD_UNORDERED_MAP_HPP +#define SIMPLE_UTILITY_GRAPH_MIXINS_TRACKER_STD_UNORDERED_MAP_HPP + +#pragma once + +#include +#include + +#include "Simple-Utility/graph/Common.hpp" +#include "Simple-Utility/graph/Tracker.hpp" + +template +struct sl::graph::customize::set_discovered_fn> +{ + template K> + bool operator ()(std::unordered_map& container, K&& vertex) const + { + const auto [iter, inserted] = container.try_emplace(std::forward(vertex), false); + return inserted || !iter->second; + } +}; + +template +struct sl::graph::customize::set_visited_fn> +{ + bool operator ()(std::unordered_map& container, const Key& vertex) const + { + const auto iter = container.find(vertex); + assert(iter != std::cend(container) && "Visited a vertex which hasn't been discovered yet."); + return !std::exchange(iter->second, true); + } +}; + +namespace sl::graph::tracker +{ + template > + requires std::is_invocable_r_v + using CommonHashMap = std::unordered_map; +} + +#endif diff --git a/include/Simple-Utility/test_util/Catch2Ext.hpp b/include/Simple-Utility/test_util/Catch2Ext.hpp new file mode 100644 index 000000000..1de791aa4 --- /dev/null +++ b/include/Simple-Utility/test_util/Catch2Ext.hpp @@ -0,0 +1,53 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_TEST_UTIL_CATCH2_EXT_HPP +#define SIMPLE_UTILITY_TEST_UTIL_CATCH2_EXT_HPP + +#pragma once + +#include "Simple-Utility/Config.hpp" +#include "Simple-Utility/concepts/stl_extensions.hpp" + +#include + +#include + +namespace catch_ext +{ + class RangesEmpty final + : public Catch::Matchers::MatcherGenericBase + { + public: + static constexpr bool match(const std::ranges::range auto& range) + { + return std::ranges::empty(range); + } + + std::string describe() const override + { + return "Empty"; + } + }; +} + +#ifdef SL_UTILITY_HAS_STD_FORMAT + +#include + +template + requires sl::concepts::formattable + && (!Catch::Detail::IsStreamInsertable::value) +struct Catch::StringMaker +{ + static std::string convert(const T& value) + { + return std::format("{}", value); + } +}; + +#endif + +#endif diff --git a/include/Simple-Utility/test_util/TrompeloeilExt.hpp b/include/Simple-Utility/test_util/TrompeloeilExt.hpp new file mode 100644 index 000000000..8c3280ce1 --- /dev/null +++ b/include/Simple-Utility/test_util/TrompeloeilExt.hpp @@ -0,0 +1,69 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#ifndef SIMPLE_UTILITY_TEST_UTIL_TROMPELOEIL_EXT_HPP +#define SIMPLE_UTILITY_TEST_UTIL_TROMPELOEIL_EXT_HPP + +#pragma once + +// ReSharper disable once CppUnusedIncludeDirective +#include +#include + +// strictly include this after the catch header +#include + +#include +#include + +#include "Simple-Utility/Config.hpp" +#include "Simple-Utility/concepts/stl_extensions.hpp" + +namespace trompeloeil_ext +{ + template + struct matches_matcher_fn; + + template + requires std::derived_from, Catch::Matchers::MatcherGenericBase> // new style + || std::derived_from, Catch::Matchers::MatcherUntypedBase> // old style + struct matches_matcher_fn + { + constexpr auto operator ()(Matcher&& matcher) const + { + using matcher_type = std::remove_cvref_t; + return trompeloeil::make_matcher( + [](const auto& value, const matcher_type& matcher) -> bool + { + return matcher.match(value); + }, + [](std::ostream& os, const matcher_type& matcher) + { + os << " matching matcher: " << matcher.describe(); + }, + std::forward(matcher)); + } + }; + + inline constexpr auto matches = [](Matcher&& matcher) + { + return matches_matcher_fn>{}(std::forward(matcher)); + }; +} + +#ifdef SL_UTILITY_HAS_STD_FORMAT + +template T> +struct trompeloeil::printer +{ + static void print(std::ostream& os, const T& obj) + { + std::format_to(std::ostream_iterator{os}, "{}", obj); + } +}; + +#endif + +#endif diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index 936002559..5a6d71a81 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -9,15 +9,18 @@ add_executable( "CRTPBase.cpp" "helper.cpp" "math.cpp" - "unique_handle.cpp" "Tuple.cpp" "TypeList.cpp" "TypeTraits.cpp" + "unique_handle.cpp" + "Utility.cpp" ) add_subdirectory("nullables") add_subdirectory("concepts") add_subdirectory("functional") +add_subdirectory("graph") +add_subdirectory("test_util") target_link_libraries( Simple-Utility-Tests diff --git a/tests/Utility.cpp b/tests/Utility.cpp new file mode 100644 index 000000000..8868886f3 --- /dev/null +++ b/tests/Utility.cpp @@ -0,0 +1,226 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/Utility.hpp" + +#include +#include +#include +#include + +#include "Simple-Utility/test_util/Catch2Ext.hpp" + +#include + +TEMPLATE_TEST_CASE_SIG( + "Higher priority_tag always inherit from the lower ones.", + "[utility]", + ((bool expected, class Derived, class Base), expected, Derived, Base), + (true, sl::priority_tag<10>, sl::priority_tag<0>), + (true, sl::priority_tag<10>, sl::priority_tag<1>), + (true, sl::priority_tag<1>, sl::priority_tag<0>), + (true, sl::priority_tag<1>, sl::priority_tag<1>), + (false, sl::priority_tag<0>, sl::priority_tag<1>), + (false, sl::priority_tag<1>, sl::priority_tag<10>) +) +{ + STATIC_REQUIRE(expected == std::derived_from); +} + +namespace +{ + template + class immobile + { + public: + ~immobile() = default; + + immobile(const immobile&) = delete; + immobile& operator =(const immobile&) = delete; + immobile(immobile&&) = delete; + immobile& operator =(immobile&&) = delete; + + template + requires std::constructible_from, Args...> + explicit immobile(Args&&... args) + : value{std::forward(args)...} + { + } + + std::tuple value; + }; + + template + class ForwardingCTor + { + public: + template + explicit ForwardingCTor(Arg&& arg) + : value{static_cast(std::forward(arg))} + { + } + + T value; + }; + + template + class InPlaceExpecting + { + public: + template + explicit InPlaceExpecting(sl::in_place_constructor&& args) + : value{static_cast(std::move(args))} + { + } + + T value; + }; + + template + class MultiInPlaceExpecting + { + public: + template + explicit MultiInPlaceExpecting( + sl::in_place_constructor&& args1, + sl::in_place_constructor&& args2, + sl::in_place_constructor&& args3 + ) + : value1{static_cast(std::move(args1))}, + value2{static_cast(std::move(args2))}, + value3{static_cast(std::move(args3))} + { + } + + T1 value1; + T2 value2; + T3 value3; + }; +} + +TEST_CASE("in_place_constructor can be utilized from types, which expects it as its ctor argument.", "[utility]") +{ + SECTION("Supports convenient default construction") + { + const InPlaceExpecting> obj{{}}; + + REQUIRE(std::nullopt == obj.value); + } + + SECTION("Supports CTAD") + { + SECTION("When move constructing.") + { + std::string str{"Hello, World!"}; + const InPlaceExpecting obj{sl::in_place(std::move(str))}; + + REQUIRE("Hello, World!" == obj.value); + } + + SECTION("When copy constructing.") + { + const std::string str{"Hello, World!"}; + const InPlaceExpecting obj{sl::in_place(str)}; + + REQUIRE("Hello, World!" == obj.value); + } + + SECTION("When argument constructing.") + { + const InPlaceExpecting obj{sl::in_place("Hello, World!")}; + + REQUIRE("Hello, World!" == obj.value); + } + + SECTION("Even when constructing immobile types.") + { + const InPlaceExpecting obj{sl::in_place>("Hello, World!", 42)}; + + REQUIRE(std::tuple{"Hello, World!", 42} == obj.value.value); + } + } +} + +TEST_CASE("in_place_constructor can be utilized on custruction, when used on perfect forwarding ctors..", "[utility]") +{ + SECTION("Supports default construction") + { + const ForwardingCTor> obj{sl::in_place>()}; + + REQUIRE(std::nullopt == obj.value); + } + + SECTION("When move constructing.") + { + std::string str{"Hello, World!"}; + const ForwardingCTor obj{sl::in_place(std::move(str))}; + + REQUIRE("Hello, World!" == obj.value); + } + + SECTION("When copy constructing.") + { + const std::string str{"Hello, World!"}; + const ForwardingCTor obj{sl::in_place(str)}; + + REQUIRE("Hello, World!" == obj.value); + } + + SECTION("When argument constructing.") + { + const ForwardingCTor obj{sl::in_place("Hello, World!")}; + + REQUIRE("Hello, World!" == obj.value); + } + + SECTION("Even when constructing immobile types.") + { + const ForwardingCTor> obj{sl::in_place>("Hello, World!", 42)}; + + REQUIRE(std::tuple{"Hello, World!", 42} == obj.value.value); + } +} + +TEST_CASE("in_place_constructor can be utilized from types, which expects some of them as its ctor arguments.", "[utility]") +{ + SECTION("Supports convenient default construction") + { + const MultiInPlaceExpecting, std::string, int> obj{{}, {}, {}}; + + REQUIRE(std::nullopt == obj.value1); + REQUIRE(obj.value2.empty()); + REQUIRE(0 == obj.value3); + } + + SECTION("Supports CTAD") + { + const MultiInPlaceExpecting obj{ + sl::in_place>(1337), + sl::in_place("Hello, World!"), + sl::in_place(42) + }; + + using Expected = MultiInPlaceExpecting, std::string, int>; + STATIC_REQUIRE(std::same_as>); + REQUIRE(1337 == obj.value1); + REQUIRE("Hello, World!" == obj.value2); + REQUIRE(42 == obj.value3); + } + + SECTION("Supports immobile types") + { + const MultiInPlaceExpecting obj{ + sl::in_place>("World, Hello!", 1337), + sl::in_place("Hello, World!"), + sl::in_place(42) + }; + + using Expected = MultiInPlaceExpecting, std::string, int>; + STATIC_REQUIRE(std::same_as>); + REQUIRE(std::tuple{"World, Hello!", 1337} == obj.value1.value); + REQUIRE("Hello, World!" == obj.value2); + REQUIRE(42 == obj.value3); + } +} diff --git a/tests/concepts/stl_extensions.cpp b/tests/concepts/stl_extensions.cpp index eba672518..80f9b8041 100644 --- a/tests/concepts/stl_extensions.cpp +++ b/tests/concepts/stl_extensions.cpp @@ -8,6 +8,7 @@ #include "../helper.hpp" #include +#include #include "Simple-Utility/concepts/stl_extensions.hpp" @@ -92,6 +93,19 @@ TEMPLATE_TEST_CASE_SIG( STATIC_REQUIRE(not_same_as == VExpected); } +TEMPLATE_TEST_CASE_SIG( + "not_void should behave as the inverted counterpart of std::is_void(_v).", + "[concepts][stl_ext]", + ((bool expected, class T), expected, T), + (false, void), + (false, const void), + (true, int), + (true, int&) +) +{ + STATIC_REQUIRE(expected == not_void); +} + TEMPLATE_TEST_CASE_SIG( "weakly_equality_comparable checks whether the given type is comparable via operator == and !=.", "[concepts][stl_ext]", @@ -344,3 +358,51 @@ TEMPLATE_TEST_CASE_SIG( { STATIC_REQUIRE(expected == nothrow_weakly_three_way_comparable_with); } + +#ifdef SL_UTILITY_HAS_STD_FORMAT + +namespace +{ + struct Formattable + { + int value{}; + }; + + struct NonFormattable + { + }; +} + +template +struct std::formatter + : public std::formatter +{ + template + auto format(Formattable t, FormatContext& fc) const + { + return std::formatter::format(t.value, fc); + } +}; + +TEMPLATE_TEST_CASE_SIG( + "formattable checks, whether a complete specialization of std::formatter for the given type T exists.", + "[concepts][stl_ext]", + ((bool expected, class T, class Char), expected, T, Char), + (false, NonFormattable, char), + (false, NonFormattable, wchar_t), + (true, Formattable, char), + (true, const Formattable, char), + (true, const Formattable&, char), + (true, Formattable, wchar_t), + (true, int, char), + (true, int, wchar_t), + (true, std::string, char), + (false, std::string, wchar_t), + (false, std::wstring, char), + (true, std::wstring, wchar_t) +) +{ + STATIC_REQUIRE(expected == formattable); +} + +#endif diff --git a/tests/graph/AStarSearch.cpp b/tests/graph/AStarSearch.cpp new file mode 100644 index 000000000..6bb944798 --- /dev/null +++ b/tests/graph/AStarSearch.cpp @@ -0,0 +1,200 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/AStarSearch.hpp" + +#include +#include +#include +#include + +#include "Defines.hpp" + +namespace +{ + inline const std::vector< + std::tuple< + std::vector>>>, + std::string, // origin + std::string // destination + >> testResults{ + { + {{{{"42", 0, 30}, std::nullopt}, 0}}, + "42", + "12" + }, + { + {{{{"3", 0, 6}, std::nullopt}, 0}, {{{"5", 2, 4}, "3"}, 1}, {{{"6", 3, 3}, "3"}, 1}, {{{"2", 7, 7}, "6"}, 2}}, + "3", + "9" + }, + { + {{{{"6", 0, 3}, std::nullopt}, 0}, {{{"2", 4, 7}, "6"}, 1}}, + "6", + "9" + }, + // non-deterministic, as 6 may have the predecessor 2 or 3 + //{ + // {{{{"1", 0, 8}, std::nullopt}, 0}, {{{"2", 1, 7}, "1"}, 1}, {{{"3", 2, 6}, "1"}, 1}, {{{"6", 5, 3}, "2"}, 2}, {{{"5", 4, 5}, "3"}}, 2}, + // "1", + // "9" + //}, + { + {{{{"8", 0, 1}, std::nullopt}, 0}, {{{"7", 1, 2}, "8"}, 1}, {{{"4", 4, 5}, "7"}, 2}, {{{"9", 1, 0}, "8"}, 1}}, + "8", + "9" + } + }; + + struct manhattan_distance + { + [[nodiscard]] + int operator ()(const std::string& dest, const std::string& cur) const noexcept + { + return std::abs(std::stoi(dest) - std::stoi(cur)); + } + }; + + using Heuristic = sg::astar::SingleDestinationHeuristic; + + constexpr auto toCommonAStarNode = [](const Node& node) + { + return sg::astar::CommonNode, sg::node::rank_t>{ + sg::node::vertex(node), + node.cost, + node.estimatedPendingCost + }; + }; + + constexpr auto toDepthAStarNode = [](const sg::decorator::DepthNode& node) + { + return sg::decorator::DepthNode, sg::node::rank_t>>{ + {toCommonAStarNode(node)}, + node.depth + }; + }; + + constexpr auto toPredecessorAStarNode = [](const sg::decorator::PredecessorNode& node) + { + return sg::decorator::PredecessorNode, sg::node::rank_t>>{ + {toCommonAStarNode(node)}, + node.predecessor + }; + }; +} + +TEST_CASE("astar::CommonNode members have expected values.", "[graph][graph::astar]") +{ + using Node = sg::astar::CommonNode; + const auto vertex = GENERATE("42", "Hello, World!"); + const auto cost = GENERATE(1337, 42); + const auto estimatedPendingCost = GENERATE(1338, 41); + + const Node node{.vertex = vertex, .cost = cost, .estimatedPendingCost = estimatedPendingCost}; + + REQUIRE(node.vertex == vertex); + REQUIRE(node.cost == cost); + REQUIRE(node.estimatedPendingCost == estimatedPendingCost); +} + +TEST_CASE("astar::CommonNode is equality comparable.", "[graph][graph::astar]") +{ + using Node = sg::astar::CommonNode; + const auto& [expectedEquality, first, second] = GENERATE( + (table)({ + {true, {"42", 1337, 1338}, {"42", 1337, 1338}}, + {false, {"41", 1337, 1338}, {"42", 1337, 1338}}, + {false, {"42", 1336, 1338}, {"42", 1337, 1338}}, + {false, {"42", 1337, 1339}, {"42", 1337, 1338}} + })); + + REQUIRE(expectedEquality == (first == second)); + REQUIRE(expectedEquality == (second == first)); + REQUIRE(expectedEquality != (first != second)); + REQUIRE(expectedEquality != (second != first)); +} + +TEMPLATE_TEST_CASE( + "astar::Stream visits all reachable vertices.", + "[graph][graph::astar]", + WeightedGraphStub) +{ + using Node = sg::astar::CommonNode; + const auto& [expected, origin, destination] = GENERATE(from_range(slice_test_expectations(testResults, toCommonAStarNode))); + + sg::astar::Stream stream{ + origin, + std::tuple{TestType{}}, + std::tuple{}, + std::tuple{}, + std::tuple{sg::astar::NodeFactory{Heuristic{destination}}} + }; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::UnorderedRangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "astar::Stream node can be decorated with DepthNodeDecorator.", + "[graph][graph::astar]", + WeightedGraphStub +) +{ + using Node = sg::decorator::DepthNode>; + const auto& [expected, origin, destination] = GENERATE(from_range(slice_test_expectations(testResults, toDepthAStarNode))); + + sg::astar::Stream stream{ + origin, + std::tuple{TestType{}}, + std::tuple{}, + std::tuple{}, + std::tuple{sg::astar::NodeFactory{Heuristic{destination}}} + }; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::UnorderedRangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "astar::Stream node can be decorated with PredecessorNodeDecorator.", + "[graph][graph::astar]", + WeightedGraphStub +) +{ + using Node = sg::decorator::PredecessorNode>; + const auto& [expected, origin, destination] = GENERATE(from_range(slice_test_expectations(testResults, toPredecessorAStarNode))); + + sg::astar::Stream stream{ + origin, + std::tuple{TestType{}}, + std::tuple{}, + std::tuple{}, + std::tuple{sg::astar::NodeFactory{Heuristic{destination}}} + }; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::UnorderedRangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "astar::Stream can be used with arbitrary decorated nodes.", + "[graph][graph::astar]", + WeightedGraphStub +) +{ + using Node = sg::decorator::DepthNode>>; + const auto& [expected, origin, destination] = GENERATE(from_range(testResults)); + + sg::astar::Stream stream{ + origin, + std::tuple{TestType{}}, + std::tuple{}, + std::tuple{}, + std::tuple{sg::astar::NodeFactory{Heuristic{destination}}} + }; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::UnorderedRangeEquals(expected)); +} diff --git a/tests/graph/BreadthFirstSearch.cpp b/tests/graph/BreadthFirstSearch.cpp new file mode 100644 index 000000000..a96ce6864 --- /dev/null +++ b/tests/graph/BreadthFirstSearch.cpp @@ -0,0 +1,107 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/BreadthFirstSearch.hpp" + +#include +#include +#include +#include + +#include "Defines.hpp" + +namespace +{ + inline const std::vector< + std::tuple< + std::vector>>>, + std::string + >> testResults{ + { + {{{{"42"}, std::nullopt}, 0}}, + "42" + }, + { + {{{{"3"}, std::nullopt}, 0}, {{{"5"}, "3"}, 1}, {{{"6"}, "3"}, 1}, {{{"2"}, "6"}, 2}}, + "3" + }, + { + {{{{"6"}, std::nullopt}, 0}, {{{"2"}, "6"}, 1}}, + "6" + }, + { + {{{{"1"}, std::nullopt}, 0}, {{{"2"}, "1"}, 1}, {{{"3"}, "1"}, 1}, {{{"6"}, "2"}, 2}, {{{"5"}, "3"}, 2}}, + "1" + }, + { + {{{{"8"}, std::nullopt}, 0}, {{{"7"}, "8"}, 1}, {{{"9"}, "8"}, 1}, {{{"4"}, "7"}, 2}}, + "8" + } + }; +} + +TEMPLATE_TEST_CASE( + "bfs::Stream visits all reachable vertices.", + "[graph][graph::bfs]", + BasicGraphStub, + WeightedGraphStub +) +{ + using Node = sg::CommonBasicNode; + const auto& [expected, origin] = GENERATE(from_range(slice_test_expectations(testResults, toCommonBasicNode))); + + sg::dfs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::RangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "bfs::Stream can be used with depth decorated nodes.", + "[graph][graph::bfs]", + BasicGraphStub, + WeightedGraphStub +) +{ + using Node = sg::decorator::DepthNode>; + const auto& [expected, origin] = GENERATE(from_range(slice_test_expectations(testResults, toDepthBasicNode))); + + sg::dfs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::RangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "bfs::Stream can be used with predecessor decorated nodes.", + "[graph][graph::bfs]", + BasicGraphStub, + WeightedGraphStub +) +{ + using Node = sg::decorator::PredecessorNode>; + const auto& [expected, origin] = GENERATE(from_range(slice_test_expectations(testResults, toPredecessorBasicNode))); + + sg::dfs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::RangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "bfs::Stream can be used with arbitrary decorated nodes.", + "[graph][graph::bfs]", + BasicGraphStub, + WeightedGraphStub +) +{ + using Node = sg::decorator::DepthNode>>; + const auto& [expected, origin] = GENERATE(from_range(testResults)); + + sg::dfs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::RangeEquals(expected)); +} diff --git a/tests/graph/CMakeLists.txt b/tests/graph/CMakeLists.txt new file mode 100644 index 000000000..9323546f6 --- /dev/null +++ b/tests/graph/CMakeLists.txt @@ -0,0 +1,16 @@ +target_sources( + Simple-Utility-Tests + PRIVATE + "AStarSearch.cpp" + "BreadthFirstSearch.cpp" + "Common.cpp" + "DepthFirstSearch.cpp" + "Edge.cpp" + "Formatter.cpp" + "Graph.cpp" + "Node.cpp" + "Queue.cpp" + "Tracker.cpp" + "Traverse.cpp" + "UniformCostSearch.cpp" +) diff --git a/tests/graph/Common.cpp b/tests/graph/Common.cpp new file mode 100644 index 000000000..d795ba006 --- /dev/null +++ b/tests/graph/Common.cpp @@ -0,0 +1,210 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/Common.hpp" + +#include +#include +#include +#include + +#include "Defines.hpp" + +// ReSharper disable CppDeclaratorNeverUsed +// ReSharper disable CppTypeAliasNeverUsed +// ReSharper disable CppClangTidyClangDiagnosticUnusedMemberFunction +// ReSharper disable CppClangTidyClangDiagnosticUnneededMemberFunction +// ReSharper disable CppClangTidyClangDiagnosticUnneededInternalDeclaration + +namespace +{ + struct non_equality_comparable + { + bool operator ==(const non_equality_comparable&) const = delete; + auto operator <=>(const non_equality_comparable&) const = default; + }; + + struct non_copyable // NOLINT(cppcoreguidelines-special-member-functions) + { + non_copyable(const non_copyable&) = delete; + non_copyable& operator =(const non_copyable&) = delete; + auto operator <=>(const non_copyable&) const = default; + }; + + struct valid_vertex + { + friend bool operator==(const valid_vertex&, const valid_vertex&) = default; + }; + + struct non_totally_ordered + { + bool operator ==(const non_totally_ordered&) const = default; + }; + + struct non_mutable_plus + { + non_mutable_plus operator +([[maybe_unused]] const non_mutable_plus&) const; + }; + + struct non_immutable_plus + { + non_immutable_plus& operator +=([[maybe_unused]] const non_immutable_plus&); + }; + + struct non_mutable_minus + { + non_mutable_minus operator +([[maybe_unused]] const non_mutable_minus&) const; + }; + + struct non_immutable_minus + { + non_immutable_minus& operator +=([[maybe_unused]] const non_immutable_minus&); + }; + + struct valid_weight + { + valid_weight& operator +=([[maybe_unused]] const valid_weight&); + valid_weight operator +([[maybe_unused]] const valid_weight&) const; + valid_weight& operator -=([[maybe_unused]] const valid_weight&); + valid_weight operator -([[maybe_unused]] const valid_weight&) const; + }; + + struct valid_rank + { + auto operator <=>(const valid_rank&) const = default; + }; +} + +TEMPLATE_TEST_CASE_SIG( + "graph::concepts::vertex determines whether the given type can be used as vertex type.", + "[graph][graph::concepts]", + ((bool expected, class T), expected, T), + (true, int), + (true, float), + (true, valid_vertex), + (false, const int), + (false, int&), + (false, non_equality_comparable), + (false, non_copyable) +) +{ + STATIC_REQUIRE(expected == sg::concepts::vertex); +} + +TEMPLATE_TEST_CASE_SIG( + "graph::concepts::weight determines whether the given type can be used as weight type.", + "[graph][graph::concepts]", + ((bool expected, class T), expected, T), + (true, int), + (true, float), + (true, valid_weight), + (false, const int), + (false, int&), + (false, non_copyable), + (false, non_mutable_plus), + (false, non_immutable_plus), + (false, non_mutable_minus), + (false, non_immutable_minus) +) +{ + STATIC_REQUIRE(expected == sg::concepts::weight); +} + +TEMPLATE_TEST_CASE_SIG( + "graph::concepts::rank determines whether the given type can be used as rank type.", + "[graph][graph::concepts]", + ((bool expected, class T), expected, T), + (true, int), + (true, float), + (true, valid_rank), + (false, const int), + (false, int&), + (false, non_equality_comparable), + (false, non_copyable) +) +{ + STATIC_REQUIRE(expected == sg::concepts::rank); +} + +namespace +{ + struct non_readable_vertex_type + { + }; + + struct readable_but_unsatisfied_vertex_type + { + using vertex_type = non_equality_comparable; + }; + + struct readable_vertex_type + { + using vertex_type = valid_vertex; + }; + + struct non_readable_weight_type + { + }; + + struct readable_but_unsatisfied_weight_type + { + using weight_type = non_mutable_plus; + }; + + struct readable_weight_type + { + using weight_type = valid_weight; + }; + + struct non_readable_rank_type + { + }; + + struct readable_but_unsatisfied_rank_type + { + using vertex_type = non_totally_ordered; + }; + + struct readable_rank_type + { + using rank_type = valid_rank; + }; +} + +TEMPLATE_TEST_CASE_SIG( + "graph::concepts::readable_vertex_type determines whether T contains a \"vertex_type\" member alias.", + "[graph][graph::concepts]", + ((bool expected, class T), expected, T), + (false, non_readable_vertex_type), + (false, readable_but_unsatisfied_vertex_type), + (true, readable_vertex_type) +) +{ + STATIC_REQUIRE(expected == sg::concepts::readable_vertex_type); +} + +TEMPLATE_TEST_CASE_SIG( + "graph::concepts::readable_weight_type determines whether T contains a \"weight_type\" member alias.", + "[graph][graph::concepts]", + ((bool expected, class T), expected, T), + (false, non_readable_weight_type), + (false, readable_but_unsatisfied_weight_type), + (true, readable_weight_type) +) +{ + STATIC_REQUIRE(expected == sg::concepts::readable_weight_type); +} + +TEMPLATE_TEST_CASE_SIG( + "graph::concepts::readable_rank_type determines whether T contains a \"rank_type\" member alias.", + "[graph][graph::concepts]", + ((bool expected, class T), expected, T), + (false, non_readable_rank_type), + (false, readable_but_unsatisfied_rank_type), + (true, readable_rank_type) +) +{ + STATIC_REQUIRE(expected == sg::concepts::readable_rank_type); +} diff --git a/tests/graph/Defines.hpp b/tests/graph/Defines.hpp new file mode 100644 index 000000000..59a4384c6 --- /dev/null +++ b/tests/graph/Defines.hpp @@ -0,0 +1,306 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#pragma once + +#include +#include + +#include "Simple-Utility/TypeList.hpp" +#include "Simple-Utility/functional/Tuple.hpp" +#include "Simple-Utility/graph/Common.hpp" +#include "Simple-Utility/graph/Edge.hpp" +#include "Simple-Utility/graph/Formatter.hpp" +#include "Simple-Utility/graph/Graph.hpp" +#include "Simple-Utility/graph/Node.hpp" +#include "Simple-Utility/graph/Tracker.hpp" +#include "Simple-Utility/test_util/Catch2Ext.hpp" +#include "Simple-Utility/test_util/TrompeloeilExt.hpp" + +namespace sg = sl::graph; + +template +struct GenericBasicNode +{ + using vertex_type = Vertex; + + vertex_type vertex; + + friend bool operator==(const GenericBasicNode&, const GenericBasicNode&) = default; +}; + +template +struct GenericRankedNode +{ + using vertex_type = Vertex; + using rank_type = Rank; + + vertex_type vertex; + rank_type rank; + + friend bool operator==(const GenericRankedNode&, const GenericRankedNode&) = default; +}; + +template +struct GenericBasicEdge +{ + using vertex_type = Vertex; + + vertex_type destination; + + friend bool operator==(const GenericBasicEdge&, const GenericBasicEdge&) = default; +}; + +template +struct GenericWeightedEdge +{ + using vertex_type = Vertex; + using weight_type = Weight; + + vertex_type destination; + weight_type weight; + + friend bool operator==(const GenericWeightedEdge&, const GenericWeightedEdge&) = default; +}; + +template +class QueueMock +{ +public: + inline static constexpr bool trompeloeil_movable_mock = true; + + MAKE_CONST_MOCK0(empty, bool()); + // Can't expect a template here, so just expect a std::vector + MAKE_MOCK1(do_insert, void(std::vector)); + MAKE_MOCK0(next, Node()); + + template + requires std::convertible_to, Node> + friend constexpr void insert(QueueMock& queue, Range&& range) + { + std::vector> vector{}; + std::ranges::copy(range, std::back_inserter(vector)); + queue.do_insert(std::move(vector)); + } +}; + +template +struct EmptyQueueStub +{ + [[nodiscard]] + static constexpr bool empty() noexcept { return true; } + + static constexpr void insert(auto&&) noexcept + { + } + + static constexpr Node next() + { + return {}; + } +}; + +template +class TrackerMock +{ +public: + inline static constexpr bool trompeloeil_movable_mock = true; + + MAKE_MOCK1(set_discovered, bool(const Vertex&)); + MAKE_MOCK1(set_visited, bool(const Vertex&)); +}; + +template +class BasicGraphMock +{ +public: + inline static constexpr bool trompeloeil_movable_mock = true; + + using vertex_type = Vertex; + using edge_type = GenericBasicEdge; + + MAKE_CONST_MOCK1(out_edges, std::vector(const vertex_type&)); +}; + +template +class WeightedGraphMock +{ +public: + inline static constexpr bool trompeloeil_movable_mock = true; + + using vertex_type = Vertex; + using weight_type = Weight; + using edge_type = GenericWeightedEdge; + + MAKE_CONST_MOCK1(out_edges, std::vector(const vertex_type&)); +}; + +template +class EmptyGraphStub +{ +public: + using vertex_type = Vertex; + using edge_type = GenericBasicEdge; + + static constexpr std::array out_edges([[maybe_unused]] const vertex_type&) noexcept + { + return {}; + } +}; + +inline static const std::unordered_map> graph{ + {1, {2, 3}}, + {2, {6}}, + {3, {5, 6}}, + {5, {5}}, + {6, {2}}, + + // begin isolated sub-graph + {4, {7}}, + {7, {4, 7, 9}}, + {8, {7, 9}}, + {9, {4}} +}; + +struct BasicGraphStub +{ + using vertex_type = std::string; + using edge_type = sg::CommonBasicEdge; + + static std::vector out_edges(const vertex_type& current) + { + const auto vertex = std::stoi(current); + if (!graph.contains(vertex)) + { + return {}; + } + + const auto& vertices = graph.at(vertex); + std::vector infos{}; + infos.reserve(std::ranges::size(vertices)); + std::ranges::transform( + vertices, + std::back_inserter(infos), + [](const int v) { return edge_type{.destination = std::to_string(v)}; }); + return infos; + } +}; + +struct WeightedGraphStub +{ + using vertex_type = std::string; + using edge_type = sg::CommonWeightedEdge; + + static std::vector out_edges(const vertex_type& current) + { + const auto vertex = std::stoi(current); + if (!graph.contains(vertex)) + { + return {}; + } + + const auto& vertices = graph.at(vertex); + std::vector infos{}; + infos.reserve(std::ranges::size(vertices)); + std::ranges::transform( + vertices, + std::back_inserter(infos), + [&](const int v) + { + return edge_type{ + .destination = std::to_string(v), + .weight = std::abs(v - vertex) + }; + }); + return infos; + } +}; + +template +constexpr auto buffer_nodes(Range& range) +{ + std::vector nodes{}; + std::ranges::copy(range, std::back_inserter(nodes)); + return nodes; +} + +template +constexpr auto slice_test_expectations(const Range& range, Transform transform) +{ + using TargetNode = std::invoke_result_t< + Transform, + std::ranges::range_value_t>>>; + std::vector< + sl::type_list::prepend_t< + sl::type_list::pop_front_t< + std::ranges::range_value_t>, + std::vector>> results{}; + std::ranges::transform( + range, + std::back_inserter(results), + sl::functional::envelop( + [&](const Nodes& nodes, Args&&... args) + { + std::vector targetNodes{}; + std::ranges::transform(nodes, std::back_inserter(targetNodes), transform); + return std::tuple{ + std::move(targetNodes), + std::forward(args)... + }; + })); + + return results; +} + +constexpr auto toCommonBasicNode = [](const Node& node) +{ + return sg::CommonBasicNode>{sg::node::vertex(node)}; +}; + +constexpr auto toDepthBasicNode = [](const sg::decorator::DepthNode& node) +{ + return sg::decorator::DepthNode>>{ + {toCommonBasicNode(node)}, + node.depth + }; +}; + +constexpr auto toPredecessorBasicNode = [](const sg::decorator::PredecessorNode& node) +{ + return sg::decorator::PredecessorNode>>{ + {toCommonBasicNode(node)}, + node.predecessor + }; +}; + +constexpr auto toCommonRankedNode = [](const Node& node) +{ + return sg::CommonRankedNode, sg::node::rank_t>{ + sg::node::vertex(node), + sg::node::rank(node) + }; +}; + +constexpr auto toDepthRankedNode = [](const sg::decorator::DepthNode& node) +{ + // leave code as-is, because directly returning the temporary results in an ICE on gcc10 + sg::decorator::DepthNode, sg::node::rank_t>> sliced{ + {toCommonRankedNode(node)}, + node.depth + }; + + return sliced; +}; + +constexpr auto toPredecessorRankedNode = [](const sg::decorator::PredecessorNode& node) +{ + // leave code as-is, because directly returning the temporary results in an ICE on gcc10 + sg::decorator::PredecessorNode, sg::node::rank_t>> sliced{ + {toCommonRankedNode(node)}, + node.predecessor + }; + + return sliced; +}; diff --git a/tests/graph/DepthFirstSearch.cpp b/tests/graph/DepthFirstSearch.cpp new file mode 100644 index 000000000..a19a0edc8 --- /dev/null +++ b/tests/graph/DepthFirstSearch.cpp @@ -0,0 +1,107 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/DepthFirstSearch.hpp" + +#include +#include +#include +#include + +#include "Defines.hpp" + +namespace +{ + inline const std::vector< + std::tuple< + std::vector>>>, + std::string + >> testResults{ + { + {{{{"42"}, std::nullopt}, 0}}, + "42" + }, + { + {{{{"3"}, std::nullopt}, 0}, {{{"6"}, "3"}, 1}, {{{"2"}, "6"}, 2}, {{{"5"}, "3"}, 1}}, + "3" + }, + { + {{{{"6"}, std::nullopt}, 0}, {{{"2"}, "6"}, 1}}, + "6" + }, + { + {{{{"1"}, std::nullopt}, 0}, {{{"3"}, "1"}, 1}, {{{"6"}, "3"}, 2}, {{{"2"}, "6"}, 3}, {{{"5"}, "3"}, 2}}, + "1" + }, + { + {{{{"8"}, std::nullopt}, 0}, {{{"9"}, "8"}, 1}, {{{"4"}, "9"}, 2}, {{{"7"}, "4"}, 3}}, + "8" + } + }; +} + +TEMPLATE_TEST_CASE( + "dfs::Stream visits all reachable vertices.", + "[graph][graph::dfs]", + BasicGraphStub, + WeightedGraphStub +) +{ + using Node = sg::CommonBasicNode; + const auto& [expected, origin] = GENERATE(from_range(slice_test_expectations(testResults, toCommonBasicNode))); + + sg::dfs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::RangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "dfs::Stream can be used with depth decorated nodes.", + "[graph][graph::dfs]", + BasicGraphStub, + WeightedGraphStub +) +{ + using Node = sg::decorator::DepthNode>; + const auto& [expected, origin] = GENERATE(from_range(slice_test_expectations(testResults, toDepthBasicNode))); + + sg::dfs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::RangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "dfs::Stream can be used with predecessor decorated nodes.", + "[graph][graph::dfs]", + BasicGraphStub, + WeightedGraphStub +) +{ + using Node = sg::decorator::PredecessorNode>; + const auto& [expected, origin] = GENERATE(from_range(slice_test_expectations(testResults, toPredecessorBasicNode))); + + sg::dfs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::RangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "dfs::Stream can be used with arbitrary decorated nodes.", + "[graph][graph::dfs]", + BasicGraphStub, + WeightedGraphStub +) +{ + using Node = sg::decorator::DepthNode>>; + const auto& [expected, origin] = GENERATE(from_range(testResults)); + + sg::dfs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::RangeEquals(expected)); +} diff --git a/tests/graph/Edge.cpp b/tests/graph/Edge.cpp new file mode 100644 index 000000000..e6aabd603 --- /dev/null +++ b/tests/graph/Edge.cpp @@ -0,0 +1,244 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/Edge.hpp" + +#include +#include +#include +#include + +#include "Defines.hpp" + +// ReSharper disable CppDeclaratorNeverUsed + +namespace +{ + struct member_destination + { + int destination; + }; + + struct member_fun_destination + { + MAKE_CONST_MOCK0(destination, int()); + }; + + struct free_fun_destination + { + MAKE_CONST_MOCK0(my_destination, int()); + + friend int destination(const free_fun_destination& v) + { + return v.my_destination(); + } + }; + + struct custom_fun_destination + { + MAKE_CONST_MOCK0(my_destination, int()); + }; + + struct member_weight + { + int weight; + }; + + struct member_fun_weight + { + MAKE_CONST_MOCK0(weight, int()); + }; + + struct free_fun_weight + { + MAKE_CONST_MOCK0(my_weight, int()); + + friend int weight(const free_fun_weight& v) + { + return v.my_weight(); + } + }; + + struct custom_fun_weight + { + MAKE_CONST_MOCK0(my_weight, int()); + }; +} + +template <> +struct sg::customize::destination_fn +{ + [[nodiscard]] + decltype(auto) operator ()(const custom_fun_destination& e) const + { + return e.my_destination(); + } +}; + +template <> +struct sg::customize::weight_fn +{ + [[nodiscard]] + decltype(auto) operator ()(const custom_fun_weight& e) const + { + return e.my_weight(); + } +}; + +//! [ThirdPartyEdge definition] +class ThirdPartyEdge +{ +public: + using vertex_type = int; + + vertex_type vertex() const + { + return m_Vertex; + } + +private: + vertex_type m_Vertex{}; +}; +//! [ThirdPartyEdge definition] + +//! [ThirdPartyEdge destination_fn specialization] +template <> +struct sl::graph::customize::destination_fn +{ + using vertex_type = typename ThirdPartyEdge::vertex_type; + vertex_type operator ()(const ThirdPartyEdge& edge) const + { + return edge.vertex(); + } +}; +//! [ThirdPartyEdge destination_fn specialization] + +TEST_CASE("graph::edge::destination serves as a customization point accessing the destination.", "[graph][detail]") +{ + const int expected = GENERATE(take(5, random(0, std::numeric_limits::max()))); + + SECTION("Access via the destination member.") + { + REQUIRE(expected == sg::edge::destination(member_destination{expected})); + } + + SECTION("Access via the destination member function.") + { + member_fun_destination mock{}; + REQUIRE_CALL(mock, destination()) + .RETURN(expected); + REQUIRE(expected == sg::edge::destination(std::as_const(mock))); + } + + SECTION("Access via the destination free function.") + { + free_fun_destination mock{}; + REQUIRE_CALL(mock, my_destination()) + .RETURN(expected); + REQUIRE(expected == sg::edge::destination(std::as_const(mock))); + } + + SECTION("Access via custom function.") + { + custom_fun_destination mock{}; + REQUIRE_CALL(mock, my_destination()) + .RETURN(expected); + REQUIRE(expected == sg::edge::destination(std::as_const(mock))); + } +} + +TEST_CASE("graph::edge::weight serves as a customization point accessing the edge weight.", "[graph][graph::edge]") +{ + const int expected = GENERATE(take(5, random(0, std::numeric_limits::max()))); + + SECTION("Access via member.") + { + REQUIRE(expected == sg::edge::weight(member_weight{expected})); + } + + SECTION("Access via member function.") + { + member_fun_weight mock{}; + REQUIRE_CALL(mock, weight()) + .RETURN(expected); + REQUIRE(expected == sg::edge::weight(std::as_const(mock))); + } + + SECTION("Access via free function.") + { + free_fun_weight mock{}; + REQUIRE_CALL(mock, my_weight()) + .RETURN(expected); + REQUIRE(expected == sg::edge::weight(std::as_const(mock))); + } + + SECTION("Access via custom function.") + { + custom_fun_weight mock{}; + REQUIRE_CALL(mock, my_weight()) + .RETURN(expected); + REQUIRE(expected == sg::edge::weight(std::as_const(mock))); + } +} + +TEST_CASE( + "Default graph::edge::traits exposes vertex_type, if readable.", + "[graph][graph::edge]" +) +{ + using TestType = GenericBasicEdge; + + STATIC_REQUIRE(std::same_as::vertex_type>); + STATIC_REQUIRE(std::same_as>); +} + +TEST_CASE( + "Default graph::edge::traits exposes vertex_type and weight_type, if readable.", + "[graph][graph::edge]" +) +{ + using TestType = GenericWeightedEdge; + + STATIC_REQUIRE(std::same_as::vertex_type>); + STATIC_REQUIRE(std::same_as>); + + STATIC_REQUIRE(std::same_as::weight_type>); + STATIC_REQUIRE(std::same_as>); +} + +TEMPLATE_TEST_CASE_SIG( + "concepts::edge determines, whether the given type satisfies the requirements.", + "[graph][graph::concepts]", + ((bool expected, class T), expected, T), + (false, member_weight), + (false, member_fun_weight), + (false, free_fun_weight), + (false, custom_fun_weight), + (true, ThirdPartyEdge), + (true, GenericBasicEdge), + (true, GenericWeightedEdge), + (true, sg::CommonBasicEdge), + (true, sg::CommonWeightedEdge) +) +{ + STATIC_REQUIRE(expected == sg::concepts::edge); +} + +TEMPLATE_TEST_CASE_SIG( + "concepts::weighted_edge determines, whether the given type satisfies the requirements.", + "[graph][graph::concepts]", + ((bool expected, class T), expected, T), + (false, member_weight), + (false, member_fun_weight), + (false, free_fun_weight), + (false, custom_fun_weight), + (false, GenericBasicEdge), + (true, GenericWeightedEdge), + (false, sg::CommonBasicEdge), + (true, sg::CommonWeightedEdge) +) +{ + STATIC_REQUIRE(expected == sg::concepts::weighted_edge); +} diff --git a/tests/graph/Formatter.cpp b/tests/graph/Formatter.cpp new file mode 100644 index 000000000..790f6defb --- /dev/null +++ b/tests/graph/Formatter.cpp @@ -0,0 +1,58 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/Formatter.hpp" + +#include + +#include "Defines.hpp" + +#ifdef SL_UTILITY_HAS_STD_FORMAT + +TEST_CASE("node types can be formatted.", "[graph][graph::node][graph::format]") +{ + using TestType = GenericBasicNode; + + REQUIRE("{vertex: Hello, World!}" == std::format("{}", TestType{.vertex = "Hello, World!"})); +} + +TEST_CASE("ranked_node types can be formatted.", "[graph][graph::node][graph::format]") +{ + using TestType = GenericRankedNode; + + REQUIRE("{vertex: Hello, World!, rank: 42}" == std::format("{}", TestType{.vertex = "Hello, World!", .rank = 42})); +} + +TEST_CASE("predecessor decorated ranked_node types can be formatted.", "[graph][graph::node][graph::format]") +{ + using TestType = sg::decorator::PredecessorNode>; + + REQUIRE("{vertex: 42, rank: 1337, predecessor: 41}" == std::format("{}", TestType{{.vertex = "42", .rank = 1337}, "41"})); + REQUIRE( + "{vertex: 42, rank: 1337, predecessor: null}" == std::format("{}", TestType{{.vertex = "42", .rank = 1337}, std::nullopt})); +} + +TEST_CASE("depth decorated ranked_node types can be formatted.", "[graph][graph::node][graph::format]") +{ + using TestType = sg::decorator::DepthNode>; + + REQUIRE("{vertex: -42, rank: 1337, depth: 42}" == std::format("{}", TestType{{.vertex = "-42", .rank = 1337}, 42})); +} + +TEST_CASE("edge types can be formatted.", "[graph][graph::edge][graph::format]") +{ + using TestType = GenericBasicEdge; + + REQUIRE("{destination: Hello, World!}" == std::format("{}", TestType{.destination = "Hello, World!"})); +} + +TEST_CASE("weighted_edge types can be formatted.", "[graph][graph::edge][graph::format]") +{ + using TestType = GenericWeightedEdge; + + REQUIRE("{destination: Hello, World!, weight: 42}" == std::format("{}", TestType{.destination = "Hello, World!", .weight = 42})); +} + +#endif diff --git a/tests/graph/Graph.cpp b/tests/graph/Graph.cpp new file mode 100644 index 000000000..123408ab3 --- /dev/null +++ b/tests/graph/Graph.cpp @@ -0,0 +1,158 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/Graph.hpp" +#include "Simple-Utility/graph/mixins/graph/std_reference_wrapper.hpp" + +#include +#include + +#include "Defines.hpp" + +namespace +{ + struct member_fun_out_edges + { + using vertex_type = std::string; + using edge_type = GenericBasicEdge; + + MAKE_CONST_MOCK1(out_edges, std::vector(const vertex_type&)); + }; + + struct free_fun_out_edges + { + using vertex_type = std::string; + using edge_type = GenericBasicEdge; + + MAKE_CONST_MOCK1(get_out_edges, std::vector(const vertex_type&)); + + friend std::vector out_edges(const free_fun_out_edges& obj, const vertex_type& vertex) + { + return obj.get_out_edges(vertex); + } + }; + + struct customized_out_edges + { + using vertex_type = std::string; + using edge_type = GenericBasicEdge; + + MAKE_CONST_MOCK1(get_out_edges, std::vector(const vertex_type&)); + }; +} + +template <> +struct sl::graph::customize::out_edges_fn +{ + [[nodiscard]] + auto operator ()(const customized_out_edges& e, const std::string& vertex) const + { + return e.get_out_edges(vertex); + } +}; + +TEMPLATE_TEST_CASE_SIG( + "graph::traits extracts edge type.", + "[graph][graph::graph]", + ((bool dummy, class Expected, class Graph), dummy, Expected, Graph), + (true, GenericBasicEdge, BasicGraphMock), + (true, GenericBasicEdge, BasicGraphMock), + (true, GenericWeightedEdge, WeightedGraphMock), + (true, GenericBasicEdge, std::reference_wrapper>), + (true, GenericBasicEdge, std::reference_wrapper>) +) +{ + STATIC_REQUIRE(std::same_as::edge_type>); + STATIC_REQUIRE(std::same_as>); +} + +TEMPLATE_TEST_CASE_SIG( + "graph::traits extracts vertex type.", + "[graph][graph::graph]", + ((bool dummy, class Expected, class Graph), dummy, Expected, Graph), + (true, int, BasicGraphMock), + (true, std::string, BasicGraphMock), + (true, std::string, WeightedGraphMock), + (true, int, std::reference_wrapper>), + (true, int, std::reference_wrapper>) +) +{ + STATIC_REQUIRE(std::same_as::vertex_type>); + STATIC_REQUIRE(std::same_as>); +} + +TEMPLATE_TEST_CASE_SIG( + "concepts::basic_graph determines, whether the given type satisfies the minimal requirements.", + "[graph][graph::concepts]", + ((bool expected, class Graph), expected, Graph), + (true, BasicGraphMock), + (true, WeightedGraphMock), + (true, EmptyGraphStub), + (true, BasicGraphStub), + (true, WeightedGraphStub), + (true, std::reference_wrapper>), + (true, std::reference_wrapper>) +) +{ + STATIC_REQUIRE(expected == sg::concepts::basic_graph); +} + +TEST_CASE( + "graph::graph::out_edges serves as a customization point, returning the outgoing edges of the given vertex.", + "[graph][graph::graph]" +) +{ + const std::string vertex{"Hello, World!"}; + const std::vector> expected{ + {"Edge0"}, + {"Edge1"}, + {"Edge2"} + }; + + SECTION("Access via the member function.") + { + const member_fun_out_edges mock{}; + REQUIRE_CALL(mock, out_edges(vertex)) + .RETURN(expected); + REQUIRE_THAT(sg::graph::out_edges(mock, vertex), Catch::Matchers::RangeEquals(expected)); + } + + SECTION("Access via the free function.") + { + const free_fun_out_edges mock{}; + REQUIRE_CALL(mock, get_out_edges(vertex)) + .RETURN(expected); + REQUIRE_THAT(sg::graph::out_edges(mock, vertex), Catch::Matchers::RangeEquals(expected)); + } + + SECTION("Access via customized function.") + { + const customized_out_edges mock{}; + REQUIRE_CALL(mock, get_out_edges(vertex)) + .RETURN(expected); + REQUIRE_THAT(sg::graph::out_edges(mock, vertex), Catch::Matchers::RangeEquals(expected)); + } +} + +TEST_CASE( + "std::reference_wrapper forwards calls to graph::out_edges to the actual graph object.", + "[graph][graph::graph]" +) +{ + const std::vector>> expected{{41}, {43}, {44}}; + BasicGraphMock graph{}; + REQUIRE_CALL(graph, out_edges(42)) + .RETURN(expected); + + SECTION("as std::ref") + { + REQUIRE_THAT(sg::graph::out_edges(std::ref(graph), 42), Catch::Matchers::RangeEquals(expected)); + } + + SECTION("as std::cref") + { + REQUIRE_THAT(sg::graph::out_edges(std::cref(graph), 42), Catch::Matchers::RangeEquals(expected)); + } +} diff --git a/tests/graph/Node.cpp b/tests/graph/Node.cpp new file mode 100644 index 000000000..42f729734 --- /dev/null +++ b/tests/graph/Node.cpp @@ -0,0 +1,295 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/Node.hpp" + +#include +#include +#include +#include + +#include "Defines.hpp" + +#include + +// ReSharper disable CppDeclaratorNeverUsed + +namespace +{ + struct member_vertex + { + int vertex; + }; + + struct member_fun_vertex + { + MAKE_CONST_MOCK0(vertex, int()); + }; + + struct free_fun_vertex + { + MAKE_CONST_MOCK0(my_vertex, int()); + + friend int vertex(const free_fun_vertex& v) + { + return v.my_vertex(); + } + }; + + struct custom_fun_vertex + { + MAKE_CONST_MOCK0(my_vertex, int()); + }; + + struct member_rank + { + int rank; + }; + + struct member_fun_rank + { + MAKE_CONST_MOCK0(rank, int()); + }; + + struct free_fun_rank + { + MAKE_CONST_MOCK0(my_rank, int()); + + friend int rank(const free_fun_rank& v) + { + return v.my_rank(); + } + }; + + struct custom_fun_rank + { + MAKE_CONST_MOCK0(my_rank, int()); + }; + + struct minimal_node + { + using vertex_type = int; + + vertex_type vertex; + }; + + struct ranked_node + { + using vertex_type = std::string; + using rank_type = int; + + vertex_type vertex; + rank_type rank; + }; + + struct node_with_custom_trait + { + int vertex; + float rank; + }; +} + +template <> +struct sg::customize::vertex_fn +{ + [[nodiscard]] + decltype(auto) operator ()(const custom_fun_vertex& e) const + { + return e.my_vertex(); + } +}; + +template <> +struct sg::node::traits +{ + using vertex_type = int; + using rank_type = float; +}; + +template <> +struct sg::customize::rank_fn +{ + [[nodiscard]] + decltype(auto) operator ()(const custom_fun_rank& e) const + { + return e.my_rank(); + } +}; + +TEST_CASE("graph::node::vertex serves as a customization point accessing the vertex.", "[graph][detail]") +{ + const int expected = GENERATE(take(5, random(0, std::numeric_limits::max()))); + + SECTION("Access via the vertex member.") + { + REQUIRE(expected == sg::node::vertex(member_vertex{expected})); + } + + SECTION("Access via the vertex member function.") + { + member_fun_vertex mock{}; + REQUIRE_CALL(mock, vertex()) + .RETURN(expected); + REQUIRE(expected == sg::node::vertex(std::as_const(mock))); + } + + SECTION("Access via the vertex free function.") + { + free_fun_vertex mock{}; + REQUIRE_CALL(mock, my_vertex()) + .RETURN(expected); + REQUIRE(expected == sg::node::vertex(std::as_const(mock))); + } + + SECTION("Access via custom function.") + { + custom_fun_vertex mock{}; + REQUIRE_CALL(mock, my_vertex()) + .RETURN(expected); + REQUIRE(expected == sg::node::vertex(std::as_const(mock))); + } +} + +TEST_CASE("graph::node::rank serves as a customization point accessing the node rank.", "[graph][graph::node]") +{ + const int expected = GENERATE(take(5, random(0, std::numeric_limits::max()))); + + SECTION("Access via member.") + { + REQUIRE(expected == sg::node::rank(member_rank{expected})); + } + + SECTION("Access via member function.") + { + member_fun_rank mock{}; + REQUIRE_CALL(mock, rank()) + .RETURN(expected); + REQUIRE(expected == sg::node::rank(std::as_const(mock))); + } + + SECTION("Access via free function.") + { + free_fun_rank mock{}; + REQUIRE_CALL(mock, my_rank()) + .RETURN(expected); + REQUIRE(expected == sg::node::rank(std::as_const(mock))); + } + + SECTION("Access via custom function.") + { + custom_fun_rank mock{}; + REQUIRE_CALL(mock, my_rank()) + .RETURN(expected); + REQUIRE(expected == sg::node::rank(std::as_const(mock))); + } +} + +TEMPLATE_TEST_CASE_SIG( + "concepts::node determines, whether the given type satisfies the requirements.", + "[graph][graph::concepts]", + ((bool expected, class T), expected, T), + (true, minimal_node), + (true, ranked_node), + (true, GenericBasicNode), + (true, GenericRankedNode), + (true, sg::CommonBasicNode), + (true, sg::CommonRankedNode), + + (true, sg::decorator::PredecessorNode>), + (true, sg::decorator::PredecessorNode>), + (true, sg::decorator::DepthNode>), + (true, sg::decorator::DepthNode>), + (true, sg::decorator::PredecessorNode>>), + (true, sg::decorator::PredecessorNode>>), + (true, sg::decorator::DepthNode>>), + (true, sg::decorator::DepthNode>>) +) +{ + STATIC_REQUIRE(expected == sg::concepts::basic_node); +} + +TEMPLATE_TEST_CASE_SIG( + "concepts::ranked_node determines, whether the given type satisfies the requirements.", + "[graph][graph::concepts]", + ((bool expected, class T), expected, T), + (false, member_rank), + (false, member_fun_rank), + (false, free_fun_rank), + (false, minimal_node), + (true, ranked_node), + (false, GenericBasicNode), + (true, GenericRankedNode), + (false, sg::CommonBasicNode), + (true, sg::CommonRankedNode), + + (false, sg::decorator::PredecessorNode>), + (true, sg::decorator::PredecessorNode>), + (false, sg::decorator::DepthNode>), + (true, sg::decorator::DepthNode>), + (false, sg::decorator::PredecessorNode>>), + (true, sg::decorator::PredecessorNode>>), + (false, sg::decorator::DepthNode>>), + (true, sg::decorator::DepthNode>>) +) +{ + STATIC_REQUIRE(expected == sg::concepts::ranked_node); +} + +TEST_CASE( + "Default graph::node::traits exposes vertex_type if readable.", + "[graph][graph::node]" +) +{ + using TestType = minimal_node; + + STATIC_REQUIRE(std::same_as::vertex_type>); + STATIC_REQUIRE(std::same_as>); +} + +TEST_CASE( + "Default graph::node::traits exposes vertex_type and rank_type if readable.", + "[graph][graph::node]" +) +{ + using TestType = ranked_node; + + STATIC_REQUIRE(std::same_as::vertex_type>); + STATIC_REQUIRE(std::same_as>); + + STATIC_REQUIRE(std::same_as::rank_type>); + STATIC_REQUIRE(std::same_as>); +} + +TEST_CASE( + "graph::node::traits can be specialized.", + "[graph][graph::node]" +) +{ + using TestType = node_with_custom_trait; + + STATIC_REQUIRE(std::same_as::vertex_type>); + STATIC_REQUIRE(std::same_as>); + + STATIC_REQUIRE(std::same_as::rank_type>); + STATIC_REQUIRE(std::same_as>); +} + +TEMPLATE_TEST_CASE_SIG( + "concepts::edge_for determines, whether the Edge type satisfies the minimal requirements of the Node type.", + "[graph][graph::concepts]", + ((bool expected, class Edge, class Node), expected, Edge, Node), + (false, GenericBasicEdge, GenericBasicNode), + (true, GenericBasicEdge, GenericBasicNode), + (true, (GenericWeightedEdge), GenericBasicNode), + + (false, GenericBasicEdge, (GenericRankedNode)), + (true, (GenericWeightedEdge), (GenericRankedNode)), + + (true, sg::CommonBasicEdge, sg::CommonBasicNode), + (true, sg::CommonWeightedEdge, sg::CommonRankedNode) +) +{ + STATIC_REQUIRE(expected == sg::concepts::edge_for); +} diff --git a/tests/graph/Queue.cpp b/tests/graph/Queue.cpp new file mode 100644 index 000000000..e89857008 --- /dev/null +++ b/tests/graph/Queue.cpp @@ -0,0 +1,310 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/Queue.hpp" +#include "Simple-Utility/graph/mixins/queue/std_priority_queue.hpp" +#include "Simple-Utility/graph/mixins/queue/std_queue.hpp" +#include "Simple-Utility/graph/mixins/queue/std_stack.hpp" + +#include +#include +#include +#include + +#include "Defines.hpp" + +#include + +// ReSharper disable CppDeclaratorNeverUsed + +namespace +{ + struct member_fun_empty + { + MAKE_CONST_MOCK0(empty, bool()); + }; + + struct free_fun_empty + { + MAKE_CONST_MOCK0(is_empty, bool()); + + friend bool empty(const free_fun_empty& obj) + { + return obj.is_empty(); + } + }; + + struct customized_fun_empty + { + MAKE_CONST_MOCK0(is_empty, bool()); + }; + + using TestNode = GenericBasicNode; + using TestRankedNode = GenericRankedNode; + + struct member_fun_insert + { + MAKE_MOCK1(insert, void(std::vector)); + }; + + struct free_fun_insert + { + MAKE_MOCK1(do_insert, void(std::vector)); + + friend void insert(free_fun_insert& obj, std::vector input) + { + obj.do_insert(std::move(input)); + } + }; + + struct customized_fun_insert + { + MAKE_MOCK1(do_insert, void(std::vector)); + }; + + struct member_fun_next + { + MAKE_MOCK0(next, TestNode()); + }; + + struct free_fun_next + { + MAKE_MOCK0(get_next, TestNode()); + + friend TestNode next(free_fun_next& obj) + { + return obj.get_next(); + } + }; + + struct customized_fun_next + { + MAKE_MOCK0(get_next, TestNode()); + }; +} + +template <> +struct sl::graph::customize::empty_fn +{ + [[nodiscard]] + bool operator ()(const customized_fun_empty& e) const + { + return e.is_empty(); + } +}; + +template <> +struct sl::graph::customize::insert_fn +{ + void operator ()(customized_fun_insert& e, std::vector v) const + { + e.do_insert(std::move(v)); + } +}; + +template <> +struct sl::graph::customize::next_fn +{ + [[nodiscard]] + decltype(auto) operator ()(customized_fun_next& e) const + { + return e.get_next(); + } +}; + +TEST_CASE("graph::queue::empty serves as a customization point, detmerining whether the queue is empty.", "[graph][graph::queue]") +{ + const bool expected = GENERATE(false, true); + + SECTION("Access via the member function.") + { + member_fun_empty mock{}; + REQUIRE_CALL(mock, empty()) + .RETURN(expected); + REQUIRE(expected == sg::queue::empty(std::as_const(mock))); + } + + SECTION("Access via the free function.") + { + free_fun_empty mock{}; + REQUIRE_CALL(mock, is_empty()) + .RETURN(expected); + REQUIRE(expected == sg::queue::empty(std::as_const(mock))); + } + + SECTION("Access via the customizization point.") + { + customized_fun_empty mock{}; + REQUIRE_CALL(mock, is_empty()) + .RETURN(expected); + REQUIRE(expected == sg::queue::empty(std::as_const(mock))); + } +} + +TEST_CASE("graph::queue::insert serves as a customization point, inserting the range elements.", "[graph][graph::queue]") +{ + const auto& expected = GENERATE( + std::vector{}, + (std::vector{{42}, {1337} })); + + SECTION("Access via the member function.") + { + member_fun_insert mock{}; + REQUIRE_CALL(mock, insert(expected)); + + sg::queue::insert(mock, expected); + } + + SECTION("Access via the free function.") + { + free_fun_insert mock{}; + REQUIRE_CALL(mock, do_insert(expected)); + + sg::queue::insert(mock, expected); + } + + SECTION("Access via the customization point.") + { + customized_fun_insert mock{}; + REQUIRE_CALL(mock, do_insert(expected)); + + sg::queue::insert(mock, expected); + } +} + +TEST_CASE("graph::queue::next serves as a customization point, retrieving the next node.", "[graph][graph::queue]") +{ + const auto expected = GENERATE(TestNode{42}, TestNode{1337}); + + SECTION("Access via the member function.") + { + member_fun_next mock{}; + REQUIRE_CALL(mock, next()) + .RETURN(expected); + + REQUIRE(expected == sg::queue::next(mock)); + } + + SECTION("Access via the free function.") + { + free_fun_next mock{}; + REQUIRE_CALL(mock, get_next()) + .RETURN(expected); + + REQUIRE(expected == sg::queue::next(mock)); + } + + SECTION("Access via the customization point.") + { + customized_fun_next mock{}; + REQUIRE_CALL(mock, get_next()) + .RETURN(expected); + + REQUIRE(expected == sg::queue::next(mock)); + } +} + +TEMPLATE_TEST_CASE_SIG( + "concepts::queue_for determines, whether the given type satisfies the requirements of a queue for the specified node type.", + "[graph][graph::concepts]", + ((bool expected, class Queue, class Node), expected, Queue, Node), + (false, member_fun_empty, TestNode), + (false, free_fun_empty, TestNode), + (false, member_fun_insert, TestNode), + (false, free_fun_insert, TestNode), + (false, member_fun_next, TestNode), + (false, free_fun_next, TestNode), + (true, QueueMock, TestNode), + (true, sg::queue::CommonStack, TestNode), + (true, sg::queue::CommonQueue, TestNode), + (true, sg::queue::CommonPriorityQueue, TestRankedNode) +) +{ + STATIC_REQUIRE(expected == sg::concepts::queue_for); +} + +TEST_CASE("graph::queue::CommonStack follows the queue protocol.", "[graph][graph::queue]") +{ + sg::queue::CommonStack queue{}; + + REQUIRE(sg::queue::empty(queue)); + + TestNode node{.vertex = 42}; + + SECTION("When a single node is inserted.") + { + sg::queue::insert(queue, std::array{node}); + } + + SECTION("When multiple nodes are inserted.") + { + sg::queue::insert(queue, std::array{TestNode{44}, node}); + } + + SECTION("When multiple nodes are inserted during multiple insertions.") + { + sg::queue::insert(queue, std::array{TestNode{41}}); + sg::queue::insert(queue, std::array{TestNode{44}, node}); + } + + REQUIRE(!sg::queue::empty(queue)); + REQUIRE(node == sg::queue::next(queue)); +} + +TEST_CASE("graph::queue::CommonQueue follows the queue protocol.", "[graph][graph::queue]") +{ + sg::queue::CommonQueue queue{}; + + REQUIRE(sg::queue::empty(queue)); + + TestNode node{.vertex = 42}; + + SECTION("When a single node is inserted.") + { + sg::queue::insert(queue, std::array{node}); + } + + SECTION("When multiple nodes are inserted.") + { + sg::queue::insert(queue, std::array{node, TestNode{44}}); + } + + SECTION("When multiple nodes are inserted during multiple insertions.") + { + sg::queue::insert(queue, std::array{node, TestNode{44}}); + sg::queue::insert(queue, std::array{TestNode{41}}); + } + + REQUIRE(!sg::queue::empty(queue)); + REQUIRE(node == sg::queue::next(queue)); +} + +TEST_CASE("graph::queue::CommonPriorityQueue follows the queue protocol.", "[graph][graph::queue]") +{ + sg::queue::CommonPriorityQueue queue{}; + + REQUIRE(sg::queue::empty(queue)); + + TestRankedNode node{.vertex = "V42", .rank = 1}; + + SECTION("When a single node is inserted.") + { + sg::queue::insert(queue, std::array{node}); + } + + SECTION("When multiple nodes are inserted.") + { + sg::queue::insert(queue, std::array{node, TestRankedNode{"V44", 2}}); + } + + SECTION("When multiple nodes are inserted during multiple insertions.") + { + sg::queue::insert(queue, std::array{node, TestRankedNode{"V44", 2}}); + sg::queue::insert(queue, std::array{TestRankedNode{"V41", 3}}); + } + + REQUIRE(!sg::queue::empty(queue)); + REQUIRE(node == sg::queue::next(queue)); +} diff --git a/tests/graph/Tracker.cpp b/tests/graph/Tracker.cpp new file mode 100644 index 000000000..604cd778b --- /dev/null +++ b/tests/graph/Tracker.cpp @@ -0,0 +1,243 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/Tracker.hpp" + +#include +#include +#include +#include + +#include "Defines.hpp" + +#include "Simple-Utility/graph/mixins/tracker/Null.hpp" +#include "Simple-Utility/graph/mixins/tracker/std_map.hpp" +#include "Simple-Utility/graph/mixins/tracker/std_unordered_map.hpp" + +#include + +// ReSharper disable CppDeclaratorNeverUsed + +namespace +{ + struct member_fun_set_discovered + { + MAKE_MOCK1(set_discovered, bool(int)); + }; + + struct free_fun_set_discovered + { + MAKE_MOCK1(do_set_discovered, bool(int)); + + friend bool set_discovered(free_fun_set_discovered& obj, const int vertex) + { + return obj.do_set_discovered(vertex); + } + }; + + struct customized_set_discovered + { + MAKE_MOCK1(do_set_discovered, bool(int)); + }; + + struct member_fun_set_visited + { + MAKE_MOCK1(set_visited, bool(int)); + }; + + struct free_fun_set_visited + { + MAKE_MOCK1(do_set_visited, bool(int)); + + friend bool set_visited(free_fun_set_visited& obj, const int vertex) + { + return obj.do_set_visited(vertex); + } + }; + + struct customized_set_visited + { + MAKE_MOCK1(do_set_visited, bool(int)); + }; +} + +template <> +struct sl::graph::customize::set_discovered_fn +{ + [[nodiscard]] + bool operator ()(customized_set_discovered& e, const int v) const + { + return e.do_set_discovered(v); + } +}; + +template <> +struct sl::graph::customize::set_visited_fn +{ + bool operator ()(customized_set_visited& e, const int v) const + { + return e.do_set_visited(v); + } +}; + +TEST_CASE("graph::queue::set_discovered serves as a customization point, modifing the tracker state.", "[graph][graph::tracker]") +{ + const int vertex = GENERATE(take(5, random(0, std::numeric_limits::max()))); + const bool expected = GENERATE(true, false); + + SECTION("Access via the member function.") + { + member_fun_set_discovered mock{}; + REQUIRE_CALL(mock, set_discovered(vertex)) + .RETURN(expected); + REQUIRE(expected == sg::tracker::set_discovered(mock, vertex)); + } + + SECTION("Access via the free function.") + { + free_fun_set_discovered mock{}; + REQUIRE_CALL(mock, do_set_discovered(vertex)) + .RETURN(expected); + REQUIRE(expected == sg::tracker::set_discovered(mock, vertex)); + } + + SECTION("Access via customized function.") + { + customized_set_discovered mock{}; + REQUIRE_CALL(mock, do_set_discovered(vertex)) + .RETURN(expected); + REQUIRE(expected == sg::tracker::set_discovered(mock, vertex)); + } +} + +TEST_CASE("graph::queue::set_visited serves as a customization point, modifing the tracker state.", "[graph][graph::tracker]") +{ + const int vertex = GENERATE(take(5, random(0, std::numeric_limits::max()))); + const bool expected = GENERATE(true, false); + + SECTION("Access via the member function.") + { + member_fun_set_visited mock{}; + REQUIRE_CALL(mock, set_visited(vertex)) + .RETURN(expected); + + REQUIRE(expected == sg::tracker::set_visited(mock, vertex)); + } + + SECTION("Access via the free function.") + { + free_fun_set_visited mock{}; + REQUIRE_CALL(mock, do_set_visited(vertex)) + .RETURN(expected); + + REQUIRE(expected == sg::tracker::set_visited(mock, vertex)); + } + + SECTION("Access via custom function.") + { + customized_set_visited mock{}; + REQUIRE_CALL(mock, do_set_visited(vertex)) + .RETURN(expected); + + REQUIRE(expected == sg::tracker::set_visited(mock, vertex)); + } +} + +TEMPLATE_TEST_CASE_SIG( + "concepts::tracker_for determines, whether the given type can be used to track the visitation state of the specified vertex.", + "[graph][graph::concepts][graph::tracker]", + ((bool expected, class T, class Vertex), expected, T, Vertex), + (false, member_fun_set_discovered, int), + (false, free_fun_set_discovered, int), + (false, member_fun_set_visited, int), + (false, free_fun_set_visited, int), + (false, TrackerMock, std::string), + (true, TrackerMock, int), + (true, sg::tracker::CommonHashMap, int), + (true, sg::tracker::CommonMap, int), + (true, sg::tracker::Null, int) +) +{ + STATIC_REQUIRE(expected == sg::concepts::tracker_for); +} + +TEMPLATE_TEST_CASE( + "Concrete tracker types behave as expected.", + "[graph][graph::tracker]", + (sg::tracker::CommonHashMap), + (sg::tracker::CommonMap) +) +{ + TestType tracker{}; + + SECTION("Discovering a new vertex yields true.") + { + const int vertex = GENERATE(take(5, random(std::numeric_limits::min() + 1, std::numeric_limits::max()))); + + REQUIRE(sg::tracker::set_discovered(tracker, vertex)); + + SECTION("Discovering the same vertex again, yields also true.") + { + REQUIRE(sg::tracker::set_discovered(tracker, vertex)); + } + + SECTION("Discovering another vertex yields true.") + { + REQUIRE(sg::tracker::set_discovered(tracker, -vertex)); + } + + SECTION("Visiting a discovered vertex yields true.") + { + REQUIRE(sg::tracker::set_visited(tracker, vertex)); + + SECTION("Discovering an already visited vertex, yields false.") + { + REQUIRE(!sg::tracker::set_discovered(tracker, vertex)); + } + + SECTION("Visiting an already visited vertex, yields false.") + { + REQUIRE(!sg::tracker::set_visited(tracker, vertex)); + } + } + } +} + +TEST_CASE("tracker::Null always returns true.", "[graph][graph::tracker]") +{ + sg::tracker::Null tracker{}; + + SECTION("Discovering a new vertex yields true.") + { + const int vertex = GENERATE(take(5, random(std::numeric_limits::min() + 1, std::numeric_limits::max()))); + + REQUIRE(sg::tracker::set_discovered(tracker, vertex)); + + SECTION("Discovering the same vertex again, yields also true.") + { + REQUIRE(sg::tracker::set_discovered(tracker, vertex)); + } + + SECTION("Discovering another vertex yields true.") + { + REQUIRE(sg::tracker::set_discovered(tracker, -vertex)); + } + + SECTION("Visiting a discovered vertex yields true.") + { + REQUIRE(sg::tracker::set_visited(tracker, vertex)); + + SECTION("Discovering an already visited vertex, yields true.") + { + REQUIRE(sg::tracker::set_discovered(tracker, vertex)); + } + + SECTION("Visiting an already visited vertex, yields true.") + { + REQUIRE(sg::tracker::set_visited(tracker, vertex)); + } + } + } +} diff --git a/tests/graph/Traverse.cpp b/tests/graph/Traverse.cpp new file mode 100644 index 000000000..942aea69e --- /dev/null +++ b/tests/graph/Traverse.cpp @@ -0,0 +1,347 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/Traverse.hpp" + +#include +#include +#include +#include +#include + +#include "Defines.hpp" + +#include "Simple-Utility/graph/mixins/tracker/Null.hpp" +#include "Simple-Utility/test_util/Catch2Ext.hpp" +#include "Simple-Utility/test_util/TrompeloeilExt.hpp" + +// ReSharper disable CppDeclaratorNeverUsed +// ReSharper disable CppTypeAliasNeverUsed +// ReSharper disable CppClangTidyClangDiagnosticUnneededMemberFunction + +using DefaultNode = GenericBasicNode; +using DefaultEdge = GenericBasicEdge; +using DefaultQueue = QueueMock; +using DefaultTracker = TrackerMock>; +using DefaultView = BasicGraphMock>; +using DefaultTraverser = sg::detail::BasicTraverser< + DefaultNode, + DefaultView, + DefaultQueue, + DefaultTracker>; + +using MovableTraverser = sg::detail::BasicTraverser< + DefaultNode, + EmptyGraphStub, + EmptyQueueStub, + sg::tracker::Null>; + +namespace +{ + template + struct TraverserMock + { + inline static constexpr bool trompeloeil_movable_mock = true; + + using node_type = Node; + + MAKE_MOCK0(next, std::optional()); + }; +} + +namespace +{ + template + struct NodeFactoryMock + { + inline static constexpr bool trompeloeil_movable_mock = true; + + MAKE_CONST_MOCK1(MakeOrigin, Node(const sg::node::vertex_t& vertex)); + MAKE_CONST_MOCK2(MakeSuccessor, Node(const Node& current, const Edge& edge)); + + [[nodiscard]] + constexpr Node operator ()(const sg::node::vertex_t& vertex) const + { + return MakeOrigin(vertex); + } + + [[nodiscard]] + constexpr Node operator ()(const Node& current, const Edge& edge) const + { + return MakeSuccessor(current, edge); + } + }; +} + +using TestExplorers = std::tuple< +#ifdef SL_UTILITY_HAS_RANGES_VIEWS + sg::detail::LazyExplorer>, +#endif + sg::detail::BufferedExplorer>>; + +TEMPLATE_LIST_TEST_CASE( + "Explorer implementations satisfy concepts::explorer.", + "[graph][graph::detail][graph::concept]", + TestExplorers +) +{ + STATIC_REQUIRE(sg::concepts::explorer, TrackerMock>); +} + +TEMPLATE_LIST_TEST_CASE( + "Explorer implementations behave as expected.", + "[graph][graph::detail]", + TestExplorers +) +{ + using namespace Catch::Matchers; + + TrackerMock tracker{}; + + SECTION("When creating origin.") + { + REQUIRE_CALL(tracker, set_discovered(42)) + .RETURN(true); + + NodeFactoryMock nodeFactory{}; + REQUIRE_CALL(nodeFactory, MakeOrigin(42)) + .RETURN(DefaultNode{.vertex = 42}); + + TestType explorer{std::move(nodeFactory)}; + REQUIRE(DefaultNode{.vertex = 42} == std::invoke(explorer, 42, tracker)); + } + + SECTION("When creating successor(s).") + { + constexpr DefaultNode current{.vertex = 42}; + const auto& [expected, visited, edgeCollection] = GENERATE( + (table, std::vector, std::vector>)({ + {{}, {}, {}}, + {{{.vertex = 43}}, {false}, {{.destination = 43}}}, + {{}, {true}, {{.destination = 43}}}, + {{{.vertex = 43}, {.vertex = 41}}, {false, false}, {{.destination = 43}, {.destination = 41}}}, + {{{.vertex = 41}}, {true, false}, {{.destination = 43}, {.destination = 41}}}, + {{{.vertex = 43}}, {false, true}, {{.destination = 43}, {.destination = 41}}} + })); + CHECK(std::ssize(edgeCollection) == std::ssize(visited)); + + const BasicGraphMock view{}; + // clang, honestly. I hate you. This workaround is necessary for the whole range clang[11, 14] + clangCl + const std::vector& edgeCollectionRef = edgeCollection; + REQUIRE_CALL(view, out_edges(sg::node::vertex(current))) + .LR_RETURN(edgeCollectionRef); + + NodeFactoryMock nodeFactory{}; + std::vector> expectations{}; + for (std::size_t i{0}; i < std::ranges::size(visited); ++i) + { + const auto& edge = edgeCollection[i]; + const bool isVisited = visited[i]; + + expectations.emplace_back( + NAMED_REQUIRE_CALL(tracker, set_discovered(sg::edge::destination(edge))) + .RETURN(!isVisited)); + + if (!isVisited) + { + expectations.emplace_back( + NAMED_REQUIRE_CALL(nodeFactory, MakeSuccessor(current, edge)) + .RETURN(DefaultNode{.vertex = sg::edge::destination(edge)})); + } + } + + TestType explorer{std::move(nodeFactory)}; + REQUIRE_THAT(std::invoke(explorer, view, current, tracker), RangeEquals(expected)); + } +} + +using TestKernels = std::tuple< + sg::detail::PreOrderKernel>; + +TEMPLATE_LIST_TEST_CASE( + "Kernel implementations satisfy concepts::traverser_kernel", + "[graph][graph::detail][graph::concept]", + TestKernels +) +{ + using Explorer = sg::detail::BufferedExplorer>; + + STATIC_REQUIRE(sg::concepts::traverser_kernel, Explorer, QueueMock, TrackerMock>); +} + +TEST_CASE( + "detail::BasicTraverser is not copyable but movable, when strategies support it.", + "[graph][graph::traverser][graph::detail]" +) +{ + STATIC_REQUIRE(!std::is_copy_constructible_v); + STATIC_REQUIRE(!std::is_copy_assignable_v); + STATIC_REQUIRE(std::is_move_constructible_v); + STATIC_REQUIRE(std::is_move_assignable_v); +} + +TEMPLATE_TEST_CASE( + "detail::BasicTraverser satisfies graph::concepts::traverser.", + "[graph][graph::traverser][graph::concepts]", + MovableTraverser, + DefaultTraverser, + TraverserMock +) +{ + STATIC_REQUIRE(sg::concepts::traverser); +} + +TEST_CASE("detail::BasicTraverser can be constructed with an origin.", "[graph][graph::traverser][graph::detail]") +{ + using namespace trompeloeil_ext; + using namespace Catch::Matchers; + + const auto origin = GENERATE(take(5, random(std::numeric_limits::min(), std::numeric_limits::max()))); + + DefaultQueue queue{}; + ALLOW_CALL(queue, empty()) // internal assertion + .RETURN(true); + REQUIRE_CALL(queue, do_insert(matches(RangeEquals(std::array{DefaultNode{origin}})))); + + DefaultTracker tracker{}; + REQUIRE_CALL(tracker, set_discovered(origin)) + .RETURN(true); + + const DefaultTraverser traverser{ + origin, + std::forward_as_tuple(DefaultView{}), + std::forward_as_tuple(std::move(queue)), + std::forward_as_tuple(std::move(tracker)), + std::tuple{} + }; +} + +TEST_CASE("detail::BasicTraverser::next returns the current node, or std::nullopt.", "[graph][graph::traverser][graph::detail]") +{ + using trompeloeil::_; + using namespace trompeloeil_ext; + using namespace catch_ext; + using namespace Catch::Matchers; + + constexpr DefaultNode originNode{42}; + auto traverser = [&] + { + DefaultTracker trackerMock{}; + REQUIRE_CALL(trackerMock, set_discovered(42)) + .RETURN(true); + + DefaultQueue queue{}; + ALLOW_CALL(queue, empty()) // internal assertion + .RETURN(true); + REQUIRE_CALL(queue, do_insert(matches(RangeEquals(std::array{originNode})))); + + return DefaultTraverser{ + originNode.vertex, + std::forward_as_tuple(DefaultView{}), + std::forward_as_tuple(std::move(queue)), + std::forward_as_tuple(std::move(trackerMock)), + std::tuple{} + }; + }(); + + using VertexInfo = DefaultView::edge_type; + auto& view = const_cast(traverser.view()); + auto& queue = const_cast(traverser.queue()); + auto& tracker = const_cast(traverser.tracker()); + + SECTION("Next returns a node, when queue contains elements.") + { + // vertex 43 will be skipped on purpose + REQUIRE_CALL(view, out_edges(sg::node::vertex(originNode))) + .RETURN(std::vector{{41}, {43}, {44}}); + + REQUIRE_CALL(queue, do_insert(matches(RangeEquals(std::vector{{41}, {44}})))); + REQUIRE_CALL(queue, empty()) + .RETURN(false); + REQUIRE_CALL(queue, next()) + .RETURN(DefaultNode{.vertex = 42}); + + REQUIRE_CALL(tracker, set_discovered(41)) + .RETURN(true); + REQUIRE_CALL(tracker, set_discovered(43)) + .RETURN(false); + REQUIRE_CALL(tracker, set_discovered(44)) + .RETURN(true); + REQUIRE_CALL(tracker, set_visited(42)) + .RETURN(true); + + REQUIRE(DefaultNode{.vertex = 42} == traverser.next()); + } + + SECTION("Next returns std::nullopt, when queue is empty.") + { + REQUIRE_CALL(queue, empty()) + .RETURN(true); + + REQUIRE(std::nullopt == traverser.next()); + } +} + +TEST_CASE("graph::IterableTraverser can be used as a range.", "[graph][graph::traverser]") +{ + SECTION("Is empty, when traverser returned std::nullopt.") + { + TraverserMock traverser{}; + REQUIRE_CALL(traverser, next()) + .RETURN(std::nullopt); + + sg::IterableTraverser range{std::move(traverser)}; + STATIC_REQUIRE(std::ranges::input_range); + + REQUIRE(0 == std::ranges::distance(range)); + } + + SECTION("Returns nodes as given from traverser.") + { + TraverserMock traverser{}; + trompeloeil::sequence seq{}; + REQUIRE_CALL(traverser, next()) + .RETURN(DefaultNode{.vertex = 42}) + .IN_SEQUENCE(seq); + + REQUIRE_CALL(traverser, next()) + .RETURN(DefaultNode{.vertex = 41}) + .IN_SEQUENCE(seq); + + REQUIRE_CALL(traverser, next()) + .RETURN(DefaultNode{.vertex = -42}) + .IN_SEQUENCE(seq); + + REQUIRE_CALL(traverser, next()) + .RETURN(std::nullopt) + .IN_SEQUENCE(seq); + + sg::IterableTraverser range{std::move(traverser)}; + STATIC_REQUIRE(std::ranges::input_range); + + REQUIRE_THAT(range, Catch::Matchers::RangeEquals(std::to_array({{42}, {41}, {-42}}))); + } +} + +// Test case needs to be a template, because the negative requires statements will fail. +TEMPLATE_TEST_CASE( + "graph::IterableTraverser::begin can only be called from an lvalue-ref.", + "[graph][graph::traverser]", + sg::IterableTraverser> +) +{ + STATIC_REQUIRE(requires{ {std::declval().begin()} -> std::input_iterator; }); + STATIC_REQUIRE(!requires{std::declval().begin(); }); + STATIC_REQUIRE(!requires{std::declval().begin(); }); + STATIC_REQUIRE(!requires{std::declval().begin(); }); +} + +TEST_CASE("graph::IterableTraverser can be used with std::ranges traits.", "[graph][graph::traverser]") +{ + using Range = sg::IterableTraverser>; + + STATIC_REQUIRE(std::same_as>); + STATIC_REQUIRE(std::same_as>); +} diff --git a/tests/graph/UniformCostSearch.cpp b/tests/graph/UniformCostSearch.cpp new file mode 100644 index 000000000..b5f73ec4f --- /dev/null +++ b/tests/graph/UniformCostSearch.cpp @@ -0,0 +1,104 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/graph/UniformCostSearch.hpp" + +#include +#include +#include +#include + +#include "Defines.hpp" + +namespace +{ + inline const std::vector< + std::tuple< + std::vector>>>, + std::string + >> testResults{ + { + {{{{"42", 0}, std::nullopt}, 0}}, + "42" + }, + { + {{{{"3", 0}, std::nullopt}, 0}, {{{"5", 2}, "3"}, 1}, {{{"6", 3}, "3"}, 1}, {{{"2", 7}, "6"}, 2}}, + "3" + }, + { + {{{{"6", 0}, std::nullopt}, 0}, {{{"2", 4}, "6"}, 1}}, + "6" + }, + // non-deterministic, as 6 may have the predecessor 2 or 3 + //{ + // {{{{"1", 0}, std::nullopt}, 0}, {{{"2", 1}, "1"}, 1}, {{{"3", 2}, "1"}, 1}, {{{"6", 5}, "2"}, 2}, {{{"5", 4}, "3"}}, 2}, + // "1" + //}, + { + {{{{"8", 0}, std::nullopt}, 0}, {{{"7", 1}, "8"}, 1}, {{{"4", 4}, "7"}, 2}, {{{"9", 1}, "8"}, 1}}, + "8" + } + }; +} + +TEMPLATE_TEST_CASE( + "ucs::Stream visits all reachable vertices.", + "[graph][graph::ucs]", + WeightedGraphStub +) +{ + using Node = sg::CommonRankedNode; + const auto& [expected, origin] = GENERATE(from_range(slice_test_expectations(testResults, toCommonRankedNode))); + + sg::ucs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::UnorderedRangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "ucs::Stream node can be decorated with DepthNode.", + "[graph][graph::ucs]", + WeightedGraphStub +) +{ + using Node = sg::decorator::DepthNode>; + const auto& [expected, origin] = GENERATE(from_range(slice_test_expectations(testResults, toDepthRankedNode))); + + sg::ucs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::UnorderedRangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "ucs::Stream node can be decorated with PredecessorNode.", + "[graph][graph::ucs]", + WeightedGraphStub +) +{ + using Node = sg::decorator::PredecessorNode>; + const auto& [expected, origin] = GENERATE(from_range(slice_test_expectations(testResults, toPredecessorRankedNode))); + + sg::ucs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::UnorderedRangeEquals(expected)); +} + +TEMPLATE_TEST_CASE( + "ucs::Stream can be used with arbitrary decorated nodes.", + "[graph][graph::ucs]", + WeightedGraphStub +) +{ + using Node = sg::decorator::DepthNode>>; + const auto& [expected, origin] = GENERATE(from_range(testResults)); + + sg::ucs::Stream stream{origin, std::tuple{TestType{}}, std::tuple{}, std::tuple{}, std::tuple{}}; + STATIC_CHECK(std::ranges::input_range); + + REQUIRE_THAT(buffer_nodes(stream), Catch::Matchers::UnorderedRangeEquals(expected)); +} diff --git a/tests/test_util/CMakeLists.txt b/tests/test_util/CMakeLists.txt new file mode 100644 index 000000000..432b97f4a --- /dev/null +++ b/tests/test_util/CMakeLists.txt @@ -0,0 +1,6 @@ +target_sources( + Simple-Utility-Tests + PRIVATE + "Catch2Ext.cpp" + "TrompeloeilExt.cpp" +) diff --git a/tests/test_util/Catch2Ext.cpp b/tests/test_util/Catch2Ext.cpp new file mode 100644 index 000000000..be7a88a3b --- /dev/null +++ b/tests/test_util/Catch2Ext.cpp @@ -0,0 +1,51 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/test_util/Catch2Ext.hpp" + +#include +#include +#include + +TEST_CASE("catch_ext::RangesEmpty matches empty ranges.", "[test_util][test_util::catch2]") +{ + REQUIRE_THAT(std::vector{}, catch_ext::RangesEmpty{}); + REQUIRE_THAT(std::vector{42}, !catch_ext::RangesEmpty{}); +} + +TEST_CASE("catch_ext::RangesEmpty::describe prints a description.", "[test_util][test_util::catch2]") +{ + REQUIRE_THAT(catch_ext::RangesEmpty{}.describe(), Catch::Matchers::Equals("Empty")); +} + +#ifdef SL_UTILITY_HAS_STD_FORMAT + +namespace +{ + struct TestType + { + int value{}; + }; +} + +template +struct std::formatter + : public std::formatter +{ + template + auto format(TestType t, FormatContext& fc) const + { + return std::format_to(fc.out(), "TestType: {}", t.value); + } +}; + +TEST_CASE("Catch::StringMaker is extended by std::format compatible types.", "[test_util][test_util::catch2]") +{ + STATIC_CHECK(sl::concepts::formattable); + + REQUIRE_THAT(Catch::StringMaker{}.convert(TestType{42}), Catch::Matchers::Equals("TestType: 42")); +} + +#endif diff --git a/tests/test_util/TrompeloeilExt.cpp b/tests/test_util/TrompeloeilExt.cpp new file mode 100644 index 000000000..a3b4d92f1 --- /dev/null +++ b/tests/test_util/TrompeloeilExt.cpp @@ -0,0 +1,64 @@ +// Copyright Dominic Koepke 2019 - 2023. +// Distributed under the Boost Software License, Version 1.0. +// (See accompanying file LICENSE_1_0.txt or copy at +// https://www.boost.org/LICENSE_1_0.txt) + +#include "Simple-Utility/test_util/TrompeloeilExt.hpp" + +#include +#include +#include + +TEST_CASE("Catch2 matchers can be used as argument for the trompeloeil_ext::matches matcher", "[test_util][test_util::trompeloeil]") +{ + const std::vector data{42, 43, 47}; + + // new style + REQUIRE(!trompeloeil_ext::matches(Catch::Matchers::RangeEquals(std::array{42, 47})).matches(data)); + REQUIRE(trompeloeil_ext::matches(Catch::Matchers::RangeEquals(std::array{42, 43, 47})).matches(data)); + + // old style + REQUIRE(trompeloeil_ext::matches(Catch::Matchers::VectorContains(43)).matches(data)); + REQUIRE(!trompeloeil_ext::matches(Catch::Matchers::VectorContains(1337)).matches(data)); +} + +TEST_CASE("trompeloeil_ext::matches can be used to print something to an ostream.", "[test_util][test_util::trompeloeil]") +{ + std::ostringstream ss{}; + ss << trompeloeil_ext::matches(Catch::Matchers::RangeEquals(std::vector{42, 43, 47})); + + REQUIRE(!std::ranges::empty(ss.str())); +} + +#ifdef SL_UTILITY_HAS_STD_FORMAT + +namespace +{ + struct TestType + { + int value{}; + }; +} + +template +struct std::formatter + : public std::formatter +{ + template + auto format(TestType t, FormatContext& fc) const + { + return std::format_to(fc.out(), "TestType: {}", t.value); + } +}; + +TEST_CASE("trompeloeil::printer is extended by std::format compatible types.", "[test_util][test_util::trompeloeil]") +{ + STATIC_CHECK(sl::concepts::formattable); + + std::ostringstream os{}; + trompeloeil::print(os, TestType{42}); + + REQUIRE("TestType: 42" == os.str()); +} + +#endif