├── infra ├── .github │ ├── CODEOWNERS │ └── workflows │ │ ├── beman-submodule.yml │ │ ├── reusable-beman-create-issue-when-fault.yml │ │ └── pre-commit.yml ├── .beman_submodule ├── .pre-commit-hooks.yaml ├── .gitignore ├── cmake │ ├── llvm-libc++-toolchain.cmake │ ├── gnu-toolchain.cmake │ ├── llvm-toolchain.cmake │ ├── msvc-toolchain.cmake │ ├── appleclang-toolchain.cmake │ ├── beman-install-library-config.cmake │ └── use-fetch-content.cmake ├── .pre-commit-config.yaml ├── tools │ └── beman-submodule │ │ ├── README.md │ │ ├── beman-submodule │ │ └── test │ │ └── test_beman_submodule.py ├── README.md └── LICENSE ├── paper ├── images │ ├── utf16.png │ ├── utf32.png │ ├── utf8-1.png │ ├── utf8-2.png │ ├── utf8-3.png │ ├── utf8.png │ ├── utf8_2.png │ ├── iterator.png │ ├── utf16_2.png │ ├── error_handling.png │ ├── viewoptimization2-pt1.png │ └── utf_transcoding_error_old.png ├── diagram_sources │ ├── utf8.psd │ ├── 3-8.graffle │ ├── 3-9.graffle │ ├── 3-10.graffle │ ├── 3-11.graffle │ ├── utf32.graffle │ ├── iterator.graffle │ ├── utf16_2.graffle │ ├── utf8-1.graffle │ ├── utf8-2.graffle │ ├── utf8-3.graffle │ ├── utf8_2.graffle │ ├── filterview.graffle │ ├── error_handling.graffle │ ├── viewoptimization.graffle │ ├── viewoptimization2.graffle │ ├── viewoptimization2-pt1.graffle │ ├── viewoptimization2-pt2.graffle │ └── viewoptimization2-pt1-alternate.graffle ├── CMakeLists.txt ├── generator │ ├── .clang-format │ ├── generator.sh │ └── post_clang_format.py └── P3705.md ├── .gitmodules ├── .gitattributes ├── src └── beman │ └── utf_view │ ├── utf_view.cpp │ ├── beman.utf_view-config.cmake.in │ └── CMakeLists.txt ├── .github ├── CODEOWNERS ├── pull_request_template.md ├── workflows │ ├── pre-commit.yml │ └── ci_tests.yml └── ISSUE_TEMPLATE │ ├── infrastructure-issues.md │ ├── paper-discussion.md │ └── implementation-deficiency.md ├── .gitignore ├── include └── beman │ └── utf_view │ ├── utf_view.hpp │ ├── detail │ ├── constexpr_unless_msvc.hpp │ ├── nontype_t_polyfill.hpp │ ├── fake_inplace_vector.hpp │ └── concepts.hpp │ ├── null_term.hpp │ └── code_unit_view.hpp ├── lockfile.json ├── examples ├── null_term_argv_environ.cpp ├── CMakeLists.txt └── readme_examples.cpp ├── .devcontainer └── devcontainer.json ├── tests └── beman │ └── utf_view │ ├── main.t.cpp │ ├── framework.cpp │ ├── std_archetypes │ ├── exposition_only.t.cpp │ ├── iterator.t.cpp │ ├── exposition_only.hpp │ └── iterator.hpp │ ├── framework.hpp │ ├── detail │ └── concepts.t.cpp │ ├── CMakeLists.txt │ ├── null_term.t.cpp │ ├── code_unit_view.t.cpp │ └── test_iterators.hpp ├── .pre-commit-config.yaml ├── LICENSE ├── CMakeLists.txt ├── CMakePresets.json └── README.md /infra/.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | * @ednolan @neatudarius @rishyak @wusatosi @JeffGarland 2 | -------------------------------------------------------------------------------- /paper/images/utf16.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/utf16.png -------------------------------------------------------------------------------- /paper/images/utf32.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/utf32.png -------------------------------------------------------------------------------- /paper/images/utf8-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/utf8-1.png -------------------------------------------------------------------------------- /paper/images/utf8-2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/utf8-2.png -------------------------------------------------------------------------------- /paper/images/utf8-3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/utf8-3.png -------------------------------------------------------------------------------- /paper/images/utf8.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/utf8.png -------------------------------------------------------------------------------- /paper/images/utf8_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/utf8_2.png -------------------------------------------------------------------------------- /.gitmodules: -------------------------------------------------------------------------------- 1 | [submodule "deps/wg21"] 2 | path = deps/wg21 3 | url = https://github.com/mpark/wg21.git 4 | -------------------------------------------------------------------------------- /paper/images/iterator.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/iterator.png -------------------------------------------------------------------------------- /paper/images/utf16_2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/utf16_2.png -------------------------------------------------------------------------------- /paper/diagram_sources/utf8.psd: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/utf8.psd -------------------------------------------------------------------------------- /paper/diagram_sources/3-8.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/3-8.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/3-9.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/3-9.graffle -------------------------------------------------------------------------------- /paper/images/error_handling.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/error_handling.png -------------------------------------------------------------------------------- /paper/diagram_sources/3-10.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/3-10.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/3-11.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/3-11.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/utf32.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/utf32.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/iterator.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/iterator.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/utf16_2.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/utf16_2.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/utf8-1.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/utf8-1.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/utf8-2.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/utf8-2.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/utf8-3.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/utf8-3.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/utf8_2.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/utf8_2.graffle -------------------------------------------------------------------------------- /paper/images/viewoptimization2-pt1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/viewoptimization2-pt1.png -------------------------------------------------------------------------------- /paper/diagram_sources/filterview.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/filterview.graffle -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | infra/** linguist-vendored 2 | *.bib -linguist-detectable 3 | *.tex -linguist-detectable 4 | papers/* linguist-documentation 5 | -------------------------------------------------------------------------------- /paper/images/utf_transcoding_error_old.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/images/utf_transcoding_error_old.png -------------------------------------------------------------------------------- /src/beman/utf_view/utf_view.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | 3 | #include 4 | -------------------------------------------------------------------------------- /.github/CODEOWNERS: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | # Codeowners for reviews on PRs 3 | 4 | * @ednolan @camio 5 | -------------------------------------------------------------------------------- /paper/diagram_sources/error_handling.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/error_handling.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/viewoptimization.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/viewoptimization.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/viewoptimization2.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/viewoptimization2.graffle -------------------------------------------------------------------------------- /infra/.beman_submodule: -------------------------------------------------------------------------------- 1 | [beman_submodule] 2 | remote=https://github.com/bemanproject/infra.git 3 | commit_hash=bb58b2a1cc894d58a55bf745be78f5d27029e245 4 | -------------------------------------------------------------------------------- /paper/diagram_sources/viewoptimization2-pt1.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/viewoptimization2-pt1.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/viewoptimization2-pt2.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/viewoptimization2-pt2.graffle -------------------------------------------------------------------------------- /paper/diagram_sources/viewoptimization2-pt1-alternate.graffle: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/bemanproject/utf_view/HEAD/paper/diagram_sources/viewoptimization2-pt1-alternate.graffle -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | 3 | /.cache 4 | /compile_commands.json 5 | /build 6 | 7 | # ignore emacs temp files 8 | *~ 9 | \#*\# 10 | 11 | # ignore vscode settings 12 | .vscode 13 | -------------------------------------------------------------------------------- /infra/.pre-commit-hooks.yaml: -------------------------------------------------------------------------------- 1 | - id: beman-tidy 2 | name: "beman-tidy: bemanification your repo" 3 | entry: ./tools/beman-tidy/beman-tidy 4 | language: script 5 | pass_filenames: false 6 | always_run: true 7 | args: [".", "--verbose"] 8 | -------------------------------------------------------------------------------- /src/beman/utf_view/beman.utf_view-config.cmake.in: -------------------------------------------------------------------------------- 1 | set(BEMAN_UTF_VIEW_VERSION @PROJECT_VERSION@) 2 | 3 | @PACKAGE_INIT@ 4 | 5 | include(${CMAKE_CURRENT_LIST_DIR}/@PROJECT_NAME@-targets.cmake) 6 | 7 | check_required_components(@PROJECT_NAME@) 8 | -------------------------------------------------------------------------------- /.github/pull_request_template.md: -------------------------------------------------------------------------------- 1 | 6 | -------------------------------------------------------------------------------- /include/beman/utf_view/utf_view.hpp: -------------------------------------------------------------------------------- 1 | #ifndef BEMAN_UTF_VIEW_UTF_VIEW_HPP 2 | #define BEMAN_UTF_VIEW_UTF_VIEW_HPP 3 | 4 | #include 5 | #include 6 | #include 7 | 8 | #endif // BEMAN_UTF_VIEW_UTF_VIEW_HPP 9 | -------------------------------------------------------------------------------- /lockfile.json: -------------------------------------------------------------------------------- 1 | { 2 | "dependencies": [ 3 | { 4 | "name": "transform_view", 5 | "package_name": "beman::transform_view", 6 | "git_repository": "https://github.com/bemanproject/transform_view.git", 7 | "git_tag": "82e982882c79004ce748b32c3bd6481def185bb5" 8 | } 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: Lint Check (pre-commit) 2 | 3 | on: 4 | # We have to use pull_request_target here as pull_request does not grant 5 | # enough permission for reviewdog 6 | pull_request_target: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | pre-commit: 13 | uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-pre-commit.yml@1.0.0 14 | -------------------------------------------------------------------------------- /examples/null_term_argv_environ.cpp: -------------------------------------------------------------------------------- 1 | #include 2 | #include 3 | 4 | extern char** environ; 5 | 6 | #if __cpp_lib_format_ranges >= 202207 7 | int main([[maybe_unused]] int argc, char** argv) { 8 | std::println("argv: {}", beman::utf_view::null_term(argv)); 9 | std::println("environ: {}", beman::utf_view::null_term(environ)); 10 | } 11 | #else 12 | int main() {} 13 | #endif 14 | -------------------------------------------------------------------------------- /examples/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | 3 | set(ALL_EXAMPLES readme_examples null_term_argv_environ) 4 | 5 | message("Examples to be built: ${ALL_EXAMPLES}") 6 | 7 | foreach(example ${ALL_EXAMPLES}) 8 | add_executable(beman.utf_view.examples.${example}) 9 | target_sources(beman.utf_view.examples.${example} PRIVATE ${example}.cpp) 10 | target_link_libraries( 11 | beman.utf_view.examples.${example} 12 | PRIVATE beman::utf_view 13 | ) 14 | endforeach() 15 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | // For format details, see https://aka.ms/devcontainer.json. For config options, see the 2 | // README at: https://github.com/devcontainers/templates/tree/main/src/cpp 3 | 4 | { 5 | "name": "Beman Project Generic Devcontainer", 6 | "image": "ghcr.io/bemanproject/infra-containers-devcontainer-gcc:14", 7 | "postCreateCommand": "pre-commit", 8 | "customizations": { 9 | "vscode": { 10 | "extensions": [ 11 | "ms-vscode.cpptools", 12 | "ms-vscode.cmake-tools" 13 | ] 14 | } 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /tests/beman/utf_view/main.t.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | int main() { 13 | for (auto& [test_name, test] : beman::utf_view::tests::framework::tests()) { 14 | if (!test()) { 15 | std::cerr << test_name << " failed"; 16 | return EXIT_FAILURE; 17 | } 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | # See https://pre-commit.com for more information 2 | # See https://pre-commit.com/hooks.html for more hooks 3 | repos: 4 | - repo: https://github.com/pre-commit/pre-commit-hooks 5 | rev: v6.0.0 6 | hooks: 7 | - id: trailing-whitespace 8 | - id: end-of-file-fixer 9 | - id: check-yaml 10 | - id: check-added-large-files 11 | 12 | # CMake linting and formatting 13 | - repo: https://github.com/BlankSpruce/gersemi 14 | rev: 0.22.3 15 | hooks: 16 | - id: gersemi 17 | name: CMake linting 18 | 19 | - repo: https://github.com/codespell-project/codespell 20 | rev: v2.4.1 21 | hooks: 22 | - id: codespell 23 | 24 | exclude: 'paper/' 25 | -------------------------------------------------------------------------------- /tests/beman/utf_view/framework.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #include 9 | #include 10 | #include 11 | 12 | namespace beman::utf_view::tests { 13 | 14 | namespace framework { 15 | 16 | std::map>& tests() { 17 | static std::map> result{}; 18 | return result; 19 | } 20 | 21 | } // namespace framework 22 | 23 | } // namespace beman::utf_view::tests 24 | -------------------------------------------------------------------------------- /tests/beman/utf_view/std_archetypes/exposition_only.t.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #include 9 | 10 | namespace beman::utf_view::tests::std_archetypes { 11 | 12 | #if defined(_GLIBCXX_VERSION_INCLUDED) || defined(_LIBCPP_VERSION) 13 | static_assert(__boolean_testable); 14 | static_assert(__boolean_testable); 15 | #endif 16 | 17 | } // namespace beman::utf_view::tests::std_archetypes 18 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/infrastructure-issues.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Infrastructure Issues 3 | about: Report a bug or feature request with our Infrastructure 4 | title: '' 5 | labels: infra 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | 15 | ## I am attempting to 16 | 17 | Describe what you were attempting to do. 18 | 19 | ## Expected Behavior 20 | 21 | A clear and concise description of what you expected to happen. 22 | 23 | ## Current Behavior 24 | 25 | A clear and concise description of what actually happened. 26 | 27 | ## Additional Discussions 28 | 29 | Add any other context about the problem here. 30 | -------------------------------------------------------------------------------- /tests/beman/utf_view/framework.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #ifndef BEMAN_UTF_VIEW_TESTS_FRAMEWORK_HPP 9 | #define BEMAN_UTF_VIEW_TESTS_FRAMEWORK_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | namespace beman::utf_view::tests { 16 | 17 | namespace framework { 18 | 19 | std::map>& tests(); 20 | 21 | } // namespace framework 22 | 23 | } // namespace beman::utf_view::tests 24 | 25 | #endif // BEMAN_UTF_VIEW_TESTS_FRAMEWORK_HPP 26 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/paper-discussion.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Paper Discussion 3 | about: Provide feedback to current API 4 | title: '' 5 | labels: '' 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | 15 | ## Use case 16 | 17 | Describe your concerns about adding this change to the C++ Standard Library. 18 | 19 | ```c++ 20 | // example snippet 21 | ``` 22 | 23 | ## What I like 24 | 25 | Let us know what you find positive about current approach / design. 26 | 27 | ## What I dislike 28 | 29 | Let us know what you find negative about current approach / design. 30 | 31 | ## Discussion 32 | 33 | Let us know if you have any more remarks. 34 | -------------------------------------------------------------------------------- /infra/.gitignore: -------------------------------------------------------------------------------- 1 | # Prerequisites 2 | *.d 3 | 4 | # Compiled Object files 5 | *.slo 6 | *.lo 7 | *.o 8 | *.obj 9 | 10 | # Precompiled Headers 11 | *.gch 12 | *.pch 13 | 14 | # Compiled Dynamic libraries 15 | *.so 16 | *.dylib 17 | *.dll 18 | 19 | # Fortran module files 20 | *.mod 21 | *.smod 22 | 23 | # Compiled Static libraries 24 | *.lai 25 | *.la 26 | *.a 27 | *.lib 28 | 29 | # Executables 30 | *.exe 31 | *.out 32 | *.app 33 | 34 | # Python 35 | __pycache__/ 36 | .pytest_cache/ 37 | *.pyc 38 | *.pyo 39 | *.pyd 40 | *.pyw 41 | *.pyz 42 | *.pywz 43 | *.pyzw 44 | *.pyzwz 45 | *.delete_me 46 | 47 | # MAC OS 48 | *.DS_Store 49 | 50 | # Editor files 51 | .vscode/ 52 | .idea/ 53 | 54 | # Build directories 55 | infra.egg-info/ 56 | beman_tidy.egg-info/ 57 | *.egg-info/ 58 | build/ 59 | dist/ 60 | -------------------------------------------------------------------------------- /infra/.github/workflows/beman-submodule.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | 3 | name: beman-submodule tests 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | workflow_dispatch: 11 | 12 | jobs: 13 | beman-submodule-script-ci: 14 | name: beman_module.py ci 15 | runs-on: ubuntu-latest 16 | steps: 17 | - name: Checkout repository 18 | uses: actions/checkout@v4 19 | 20 | - name: Set up Python 21 | uses: actions/setup-python@v5 22 | with: 23 | python-version: 3.13 24 | 25 | - name: Install pytest 26 | run: | 27 | python3 -m pip install pytest 28 | 29 | - name: Run pytest 30 | run: | 31 | cd tools/beman-submodule/ 32 | pytest 33 | -------------------------------------------------------------------------------- /infra/cmake/llvm-libc++-toolchain.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSL-1.0 2 | 3 | # This toolchain file is not meant to be used directly, 4 | # but to be invoked by CMake preset and GitHub CI. 5 | # 6 | # This toolchain file configures for LLVM family of compiler. 7 | # 8 | # BEMAN_BUILDSYS_SANITIZER: 9 | # This optional CMake parameter is not meant for public use and is subject to 10 | # change. 11 | # Possible values: 12 | # - MaxSan: configures clang and clang++ to use all available non-conflicting 13 | # sanitizers. 14 | # - TSan: configures clang and clang++ to enable the use of thread sanitizer. 15 | 16 | include(${CMAKE_CURRENT_LIST_DIR}/llvm-toolchain.cmake) 17 | 18 | if(NOT CMAKE_CXX_FLAGS MATCHES "-stdlib=libc\\+\\+") 19 | string(APPEND CMAKE_CXX_FLAGS " -stdlib=libc++") 20 | endif() 21 | -------------------------------------------------------------------------------- /tests/beman/utf_view/detail/concepts.t.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #include 9 | #include 10 | 11 | namespace beman::utf_view::tests { 12 | 13 | static_assert(exposition_only_code_unit); 14 | static_assert(exposition_only_code_unit); 15 | static_assert(exposition_only_code_unit); 16 | static_assert(!exposition_only_code_unit); 17 | static_assert(!exposition_only_code_unit); 18 | static_assert(!exposition_only_code_unit); 19 | 20 | } // namespace beman::utf_view::detail::tests 21 | -------------------------------------------------------------------------------- /paper/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSL-1.0 2 | 3 | # Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | # Distributed under the Boost Software License, Version 1.0. 5 | # (See accompanying file LICENSE.txt or copy at 6 | # https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | add_custom_command( 9 | OUTPUT P2728.html 10 | COMMAND SRCDIR=${CMAKE_CURRENT_SOURCE_DIR} OUTDIR=${CMAKE_CURRENT_BINARY_DIR} make -C ${CMAKE_CURRENT_SOURCE_DIR}/../deps/wg21 html 11 | DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/P2728.md 12 | VERBATIM) 13 | add_custom_target(p2728 ALL DEPENDS P2728.html) 14 | 15 | add_custom_command( 16 | OUTPUT P3705.html 17 | COMMAND SRCDIR=${CMAKE_CURRENT_SOURCE_DIR} OUTDIR=${CMAKE_CURRENT_BINARY_DIR} make -C ${CMAKE_CURRENT_SOURCE_DIR}/../deps/wg21 html 18 | DEPENDS ${CMAKE_CURRENT_SOURCE_DIR}/P3705.md 19 | VERBATIM) 20 | add_custom_target(p3705 ALL DEPENDS P3705.html p2728) 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/implementation-deficiency.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Implementation Deficiency 3 | about: Report a bug or performance issue of our implementation 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | 14 | 15 | ## Describe the deficiency 16 | 17 | A clear and concise description of what the deficiency is. 18 | Link all relevant issues. 19 | This could be a bug, or a performance problem. 20 | 21 | ## To Reproduce 22 | 23 | ```c++ 24 | // Use case 25 | ``` 26 | 27 | ## Expected Behavior 28 | 29 | A clear and concise description of what you expected to happen. 30 | 31 | ## Additional Discussions 32 | 33 | Add any other context about the problem here. 34 | If you believe your issue is platform dependent, 35 | please post your compiler versions here. 36 | -------------------------------------------------------------------------------- /paper/generator/.clang-format: -------------------------------------------------------------------------------- 1 | BasedOnStyle: WebKit 2 | ColumnLimit: 200 3 | AlwaysBreakTemplateDeclarations: Yes 4 | AlignAfterOpenBracket: Align 5 | AllowShortFunctionsOnASingleLine: All 6 | BreakConstructorInitializers: AfterColon 7 | Cpp11BracedListStyle: True 8 | SpaceBeforeCpp11BracedList: False 9 | BreakBeforeBraces: Attach 10 | AllowShortEnumsOnASingleLine: False 11 | BreakStringLiterals: False 12 | AlwaysBreakAfterReturnType: None 13 | PenaltyReturnTypeOnItsOwnLine: 200 14 | ContinuationIndentWidth: 2 15 | IndentWidth: 2 16 | BreakBeforeBinaryOperators: None 17 | IndentAccessModifiers: False 18 | AccessModifierOffset: -2 19 | NamespaceIndentation: None 20 | SpaceAfterTemplateKeyword: False 21 | PackConstructorInitializers: NextLine 22 | AlignTrailingComments: Leave 23 | RequiresClausePosition: WithPreceding 24 | IndentRequiresClause: True 25 | SpaceBeforeParens: Custom 26 | SpaceBeforeParensOptions: 27 | AfterRequiresInClause: True 28 | AfterRequiresInExpression: True 29 | -------------------------------------------------------------------------------- /tests/beman/utf_view/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSL-1.0 2 | 3 | # Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | # Distributed under the Boost Software License, Version 1.0. 5 | # (See accompanying file LICENSE.txt or copy at 6 | # https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | include_directories(${CMAKE_CURRENT_SOURCE_DIR}) 9 | 10 | add_library( 11 | beman_utf_view_test_lib 12 | STATIC 13 | code_unit_view.t.cpp 14 | detail/concepts.t.cpp 15 | framework.cpp 16 | null_term.t.cpp 17 | std_archetypes/exposition_only.t.cpp 18 | std_archetypes/iterator.t.cpp 19 | to_utf_view.t.cpp 20 | ) 21 | 22 | target_link_libraries(beman_utf_view_test_lib beman::utf_view) 23 | 24 | add_executable(beman_utf_view_test main.t.cpp) 25 | 26 | target_link_libraries( 27 | beman_utf_view_test 28 | "$" 29 | ) 30 | 31 | add_test(NAME beman_utf_view_test COMMAND beman_utf_view_test) 32 | -------------------------------------------------------------------------------- /tests/beman/utf_view/std_archetypes/iterator.t.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #include 9 | #include 10 | 11 | namespace beman::utf_view::tests::std_archetypes { 12 | 13 | static_assert(std::input_iterator); 14 | static_assert(std::input_iterator>); 15 | static_assert(std::forward_iterator); 16 | static_assert(std::forward_iterator>); 17 | static_assert(std::bidirectional_iterator); 18 | static_assert(std::bidirectional_iterator>); 19 | 20 | } // namespace beman::utf_view::tests::std_archetypes 21 | -------------------------------------------------------------------------------- /include/beman/utf_view/detail/constexpr_unless_msvc.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #ifndef BEMAN_UTF_VIEW_DETAIL_CONSTEXPR_UNLESS_MSVC_HPP 9 | #define BEMAN_UTF_VIEW_DETAIL_CONSTEXPR_UNLESS_MSVC_HPP 10 | 11 | // msvc constexpr bugs can be induced by a light breeze 12 | // https://developercommunity.visualstudio.com/t/std::initializer_list-data-member-of-cla/10622889 13 | // https://developercommunity.visualstudio.com/t/MSVC-complains-about-uninvoked-implicitl/10585513 14 | // https://developercommunity.visualstudio.com/t/Constraint-not-applied-to-defaulted-cons/10715859 15 | #ifdef _MSC_VER 16 | #define CONSTEXPR_UNLESS_MSVC 17 | #else 18 | #define CONSTEXPR_UNLESS_MSVC constexpr 19 | #endif 20 | 21 | #endif // BEMAN_UTF_VIEW_DETAIL_CONSTEXPR_UNLESS_MSVC_HPP 22 | -------------------------------------------------------------------------------- /include/beman/utf_view/detail/nontype_t_polyfill.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #ifndef BEMAN_UTF_VIEW_NONTYPE_T_POLYFILL_HPP 9 | #define BEMAN_UTF_VIEW_NONTYPE_T_POLYFILL_HPP 10 | 11 | #include 12 | #include 13 | 14 | namespace beman::utf_view::detail { 15 | 16 | #ifdef __cpp_lib_function_ref 17 | 18 | template 19 | using nontype_t = std::nontype_t; 20 | 21 | template constexpr auto nontype{std::nontype}; 22 | 23 | #else 24 | 25 | template 26 | struct nontype_t { 27 | explicit nontype_t() = default; 28 | }; 29 | 30 | template constexpr nontype_t nontype{}; 31 | 32 | #endif 33 | 34 | 35 | } // namespace beman::utf_view::detail 36 | 37 | #endif // BEMAN_UTF_VIEW_NONTYPE_T_POLYFILL_HPP 38 | -------------------------------------------------------------------------------- /paper/generator/generator.sh: -------------------------------------------------------------------------------- 1 | #/usr/bin/env bash 2 | 3 | set -euo pipefail 4 | 5 | set -x 6 | 7 | declare generator_script_dir=$(dirname $BASH_SOURCE) 8 | 9 | function process_file() { 10 | local file="$1"; shift 11 | local build_dir="$1" ; shift 12 | cp $generator_script_dir/../../$file $build_dir 13 | local file_copy=$build_dir/${file##*/} 14 | clang-format-21 -i -style=file $file_copy 15 | $generator_script_dir/post_clang_format.py $file_copy > ${file_copy}.md 16 | rm $file_copy 17 | } 18 | 19 | function main() { 20 | local files=( 21 | include/beman/utf_view/code_unit_view.hpp 22 | include/beman/utf_view/detail/concepts.hpp 23 | include/beman/utf_view/null_term.hpp 24 | include/beman/utf_view/to_utf_view.hpp) 25 | local build_dir=$generator_script_dir/build 26 | mkdir $build_dir 27 | cp $generator_script_dir/.clang-format $build_dir 28 | for file in ${files[@]} ; do 29 | process_file "$file" "$build_dir" 30 | done 31 | } 32 | 33 | main "$@" 34 | -------------------------------------------------------------------------------- /infra/.pre-commit-config.yaml: -------------------------------------------------------------------------------- 1 | repos: 2 | - repo: https://github.com/pre-commit/pre-commit-hooks 3 | rev: v6.0.0 4 | hooks: 5 | - id: trailing-whitespace 6 | - id: end-of-file-fixer 7 | - id: check-yaml 8 | - id: check-added-large-files 9 | 10 | - repo: https://github.com/codespell-project/codespell 11 | rev: v2.4.1 12 | hooks: 13 | - id: codespell 14 | 15 | # CMake linting and formatting 16 | - repo: https://github.com/BlankSpruce/gersemi 17 | rev: 0.22.3 18 | hooks: 19 | - id: gersemi 20 | name: CMake linting 21 | exclude: ^.*/tests/.*/data/ # Exclude test data directories 22 | 23 | # Python linting and formatting 24 | # config file: ruff.toml (not currently present but add if needed) 25 | # https://docs.astral.sh/ruff/configuration/ 26 | - repo: https://github.com/astral-sh/ruff-pre-commit 27 | rev: v0.13.2 28 | hooks: 29 | - id: ruff-check 30 | files: ^tools/beman-tidy/ 31 | - id: ruff-format 32 | files: ^tools/beman-tidy/ 33 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Boost Software License - Version 1.0 - August 17th, 2003 2 | 3 | Permission is hereby granted, free of charge, to any person or organization 4 | obtaining a copy of the software and accompanying documentation covered by 5 | this license (the "Software") to use, reproduce, display, distribute, 6 | execute, and transmit the Software, and to prepare derivative works of the 7 | Software, and to permit third-parties to whom the Software is furnished to 8 | do so, all subject to the following: 9 | 10 | The copyright notices in the Software and this entire statement, including 11 | the above license grant, this restriction and the following disclaimer, 12 | must be included in all copies of the Software, in whole or in part, and 13 | all derivative works of the Software, unless such copies or derivative 14 | works are solely in the form of machine-executable object code generated by 15 | a source language processor. 16 | 17 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 18 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 19 | FITNESS FOR A PARTICULAR PURPOSE, TITLE AND NON-INFRINGEMENT. IN NO EVENT 20 | SHALL THE COPYRIGHT HOLDERS OR ANYONE DISTRIBUTING THE SOFTWARE BE LIABLE 21 | FOR ANY DAMAGES OR OTHER LIABILITY, WHETHER IN CONTRACT, TORT OR OTHERWISE, 22 | ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER 23 | DEALINGS IN THE SOFTWARE. 24 | -------------------------------------------------------------------------------- /include/beman/utf_view/detail/fake_inplace_vector.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #ifndef BEMAN_UTF_VIEW_FAKE_INPLACE_VECTOR_HPP 9 | #define BEMAN_UTF_VIEW_FAKE_INPLACE_VECTOR_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | namespace beman::utf_view::detail { 16 | 17 | template 18 | class fake_inplace_vector { 19 | public: 20 | constexpr fake_inplace_vector() = default; 21 | constexpr fake_inplace_vector(std::initializer_list init) 22 | { 23 | std::ranges::copy(init, storage_); 24 | } 25 | 26 | constexpr std::size_t size() const { 27 | return size_; 28 | } 29 | constexpr void clear() { 30 | size_ = 0; 31 | } 32 | constexpr void push_back(T t) { 33 | storage_[size_++] = t; 34 | } 35 | constexpr T operator[](std::size_t n) const { 36 | return storage_[n]; 37 | } 38 | T* begin() { 39 | return &storage_[0]; 40 | } 41 | T* end() { 42 | return &storage_[size_]; 43 | } 44 | 45 | private: 46 | T storage_[N]; 47 | size_t size_{}; 48 | }; 49 | 50 | } // namespace beman::utf_view::detail 51 | 52 | #endif // BEMAN_UTF_VIEW_FAKE_INPLACE_VECTOR_HPP 53 | -------------------------------------------------------------------------------- /infra/.github/workflows/reusable-beman-create-issue-when-fault.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | 3 | name: 'Beman issue creation workflow' 4 | on: 5 | workflow_call: 6 | workflow_dispatch: 7 | jobs: 8 | create-issue: 9 | runs-on: ubuntu-latest 10 | steps: 11 | # See https://github.com/cli/cli/issues/5075 12 | - uses: actions/checkout@v4 13 | - name: Create issue 14 | run: | 15 | issue_num=$(gh issue list -s open -S "[SCHEDULED-BUILD] infra repo CI job failure" -L 1 --json number | jq 'if length == 0 then -1 else .[0].number end') 16 | body="**CI job failure Report** 17 | - **Time of Failure**: $(date -u '+%B %d, %Y, %H:%M %Z') 18 | - **Commit**: [${{ github.sha }}](${{ github.server_url }}/${{ github.repository }}/commit/${{ github.sha }}) 19 | - **Action Run**: [View logs](${{ github.server_url }}/${{ github.repository }}/actions/runs/${{ github.run_id }}) 20 | The scheduled job triggered by cron has failed. 21 | Please investigate the logs and recent changes associated with this commit or rerun the workflow if you believe this is an error." 22 | if [[ $issue_num -eq -1 ]]; then 23 | gh issue create --repo ${{ github.repository }} --title "[SCHEDULED-BUILD] infra repo CI job failure" --body "$body" --assignee ${{ github.actor }} 24 | else 25 | gh issue comment --repo ${{ github.repository }} $issue_num --body "$body" 26 | fi 27 | env: 28 | GH_TOKEN: ${{ github.token }} 29 | -------------------------------------------------------------------------------- /include/beman/utf_view/detail/concepts.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #ifndef BEMAN_UTF_VIEW_DETAIL_CONCEPTS_HPP 9 | #define BEMAN_UTF_VIEW_DETAIL_CONCEPTS_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace beman::utf_view { 17 | 18 | /* PAPER */ 19 | 20 | template 21 | concept exposition_only_code_unit = std::same_as, char8_t> || 22 | std::same_as, char16_t> || 23 | std::same_as, char32_t>; 24 | 25 | /* !PAPER */ 26 | 27 | namespace detail { 28 | 29 | template 30 | constexpr bool is_empty_view = false; 31 | template 32 | constexpr bool is_empty_view> = true; 33 | 34 | template 35 | concept is_not_array_of_char = 36 | !(std::is_array_v> && 37 | exposition_only_code_unit>>); 38 | 39 | } // namespace detail 40 | 41 | /* PAPER: namespace std::ranges { */ 42 | 43 | template 44 | using exposition_only_maybe_const = 45 | std::conditional_t; // exposition only 46 | 47 | } // namespace beman::utf_view 48 | 49 | /* PAPER: } */ 50 | 51 | #endif // BEMAN_UTF_VIEW_DETAIL_CONCEPTS_HPP 52 | -------------------------------------------------------------------------------- /infra/cmake/gnu-toolchain.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | 3 | # This toolchain file is not meant to be used directly, 4 | # but to be invoked by CMake preset and GitHub CI. 5 | # 6 | # This toolchain file configures for GNU family of compiler. 7 | # 8 | # BEMAN_BUILDSYS_SANITIZER: 9 | # This optional CMake parameter is not meant for public use and is subject to 10 | # change. 11 | # Possible values: 12 | # - MaxSan: configures gcc and g++ to use all available non-conflicting 13 | # sanitizers. 14 | # - TSan: configures gcc and g++ to enable the use of thread sanitizer 15 | 16 | include_guard(GLOBAL) 17 | 18 | set(CMAKE_C_COMPILER gcc) 19 | set(CMAKE_CXX_COMPILER g++) 20 | 21 | if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") 22 | set(SANITIZER_FLAGS 23 | "-fsanitize=address -fsanitize=leak -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined -fsanitize-undefined-trap-on-error" 24 | ) 25 | elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") 26 | set(SANITIZER_FLAGS "-fsanitize=thread") 27 | endif() 28 | 29 | set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") 30 | set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") 31 | 32 | set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") 33 | 34 | set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") 35 | set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") 36 | 37 | set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") 38 | set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") 39 | 40 | # Add this dir to the module path so that `find_package(beman-install-library)` works 41 | list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") 42 | -------------------------------------------------------------------------------- /tests/beman/utf_view/std_archetypes/exposition_only.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #ifndef BEMAN_UTF_VIEW_TESTS_STD_ARCHETYPES_EXPOSITION_ONLY_HPP 9 | #define BEMAN_UTF_VIEW_TESTS_STD_ARCHETYPES_EXPOSITION_ONLY_HPP 10 | 11 | namespace beman::utf_view::tests::std_archetypes { 12 | 13 | class boolean_testable_archetype2; 14 | 15 | class boolean_testable_archetype1 { 16 | public: 17 | constexpr explicit boolean_testable_archetype1(bool inner_in) 18 | : inner_{inner_in} { } 19 | constexpr operator bool() const noexcept { 20 | return inner_; 21 | } 22 | constexpr boolean_testable_archetype2 operator!() const noexcept; 23 | 24 | private: 25 | bool inner_; 26 | }; 27 | 28 | class boolean_testable_archetype2 { 29 | public: 30 | constexpr explicit boolean_testable_archetype2(bool inner_in) 31 | : inner_{inner_in} { } 32 | constexpr operator bool() const noexcept { 33 | return inner_; 34 | } 35 | constexpr boolean_testable_archetype1 operator!() const noexcept { 36 | return boolean_testable_archetype1{!inner_}; 37 | } 38 | 39 | private: 40 | bool inner_; 41 | }; 42 | 43 | constexpr boolean_testable_archetype2 boolean_testable_archetype1::operator!() 44 | const noexcept { 45 | return boolean_testable_archetype2{inner_}; 46 | } 47 | 48 | } // namespace beman::utf_view::tests::std_archetypes 49 | 50 | #endif // BEMAN_UTF_VIEW_TESTS_STD_ARCHETYPES_EXPOSITION_ONLY_HPP 51 | -------------------------------------------------------------------------------- /infra/cmake/llvm-toolchain.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | 3 | # This toolchain file is not meant to be used directly, 4 | # but to be invoked by CMake preset and GitHub CI. 5 | # 6 | # This toolchain file configures for LLVM family of compiler. 7 | # 8 | # BEMAN_BUILDSYS_SANITIZER: 9 | # This optional CMake parameter is not meant for public use and is subject to 10 | # change. 11 | # Possible values: 12 | # - MaxSan: configures clang and clang++ to use all available non-conflicting 13 | # sanitizers. 14 | # - TSan: configures clang and clang++ to enable the use of thread sanitizer. 15 | 16 | include_guard(GLOBAL) 17 | 18 | set(CMAKE_C_COMPILER clang) 19 | set(CMAKE_CXX_COMPILER clang++) 20 | 21 | if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") 22 | set(SANITIZER_FLAGS 23 | "-fsanitize=address -fsanitize=leak -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined -fsanitize-undefined-trap-on-error" 24 | ) 25 | elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") 26 | set(SANITIZER_FLAGS "-fsanitize=thread") 27 | endif() 28 | 29 | set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") 30 | set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") 31 | 32 | set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") 33 | 34 | set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") 35 | set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") 36 | 37 | set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") 38 | set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") 39 | 40 | # Add this dir to the module path so that `find_package(beman-install-library)` works 41 | list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") 42 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSL-1.0 2 | 3 | # Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | # Distributed under the Boost Software License, Version 1.0. 5 | # (See accompanying file LICENSE.txt or copy at 6 | # https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | cmake_minimum_required(VERSION 3.27) 9 | project( 10 | beman.utf_view 11 | DESCRIPTION "C++29 UTF Transcoding Views" 12 | LANGUAGES CXX 13 | VERSION 0.0.1 14 | ) 15 | 16 | # [CMAKE.SKIP_TESTS] 17 | option( 18 | BEMAN_UTF_VIEW_BUILD_TESTS 19 | "Enable building tests and test infrastructure. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." 20 | ${PROJECT_IS_TOP_LEVEL} 21 | ) 22 | 23 | # [CMAKE.SKIP_EXAMPLES] 24 | option( 25 | BEMAN_UTF_VIEW_BUILD_EXAMPLES 26 | "Enable building examples. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." 27 | ${PROJECT_IS_TOP_LEVEL} 28 | ) 29 | 30 | option( 31 | BEMAN_UTF_VIEW_BUILD_PAPER 32 | "Enable building paper. Default: OFF. Values: { ON, OFF }." 33 | OFF 34 | ) 35 | 36 | option( 37 | BEMAN_UTF_VIEW_INSTALL_CONFIG_FILE_PACKAGE 38 | "Enable creating and installing a CMake config-file package. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." 39 | ${PROJECT_IS_TOP_LEVEL} 40 | ) 41 | 42 | find_package(beman::transform_view REQUIRED) 43 | 44 | add_subdirectory(src/beman/utf_view) 45 | 46 | if(BEMAN_UTF_VIEW_BUILD_TESTS) 47 | enable_testing() 48 | add_subdirectory(tests/beman/utf_view) 49 | endif() 50 | 51 | if(BEMAN_UTF_VIEW_BUILD_EXAMPLES) 52 | add_subdirectory(examples) 53 | endif() 54 | 55 | if(BEMAN_UTF_VIEW_BUILD_PAPER) 56 | add_subdirectory(paper) 57 | endif() 58 | -------------------------------------------------------------------------------- /infra/cmake/msvc-toolchain.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | 3 | # This toolchain file is not meant to be used directly, 4 | # but to be invoked by CMake preset and GitHub CI. 5 | # 6 | # This toolchain file configures for MSVC family of compiler. 7 | # 8 | # BEMAN_BUILDSYS_SANITIZER: 9 | # This optional CMake parameter is not meant for public use and is subject to 10 | # change. 11 | # Possible values: 12 | # - MaxSan: configures cl to use all available non-conflicting sanitizers. 13 | # 14 | # Note that in other toolchain files, TSan is also a possible value for 15 | # BEMAN_BUILDSYS_SANITIZER, however, MSVC does not support thread sanitizer, 16 | # thus this value is omitted. 17 | 18 | include_guard(GLOBAL) 19 | 20 | set(CMAKE_C_COMPILER cl) 21 | set(CMAKE_CXX_COMPILER cl) 22 | 23 | if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") 24 | # /Zi flag (add debug symbol) is needed when using address sanitizer 25 | # See C5072: https://learn.microsoft.com/en-us/cpp/error-messages/compiler-warnings/compiler-warning-c5072 26 | set(SANITIZER_FLAGS "/fsanitize=address /Zi") 27 | endif() 28 | 29 | set(CMAKE_CXX_FLAGS_DEBUG_INIT "/EHsc /permissive- ${SANITIZER_FLAGS}") 30 | set(CMAKE_C_FLAGS_DEBUG_INIT "/EHsc /permissive- ${SANITIZER_FLAGS}") 31 | 32 | set(RELEASE_FLAGS "/EHsc /permissive- /O2 ${SANITIZER_FLAGS}") 33 | 34 | set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") 35 | set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") 36 | 37 | set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") 38 | set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") 39 | 40 | # Add this dir to the module path so that `find_package(beman-install-library)` works 41 | list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") 42 | -------------------------------------------------------------------------------- /include/beman/utf_view/null_term.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #ifndef BEMAN_UTF_VIEW_NULL_TERM_HPP 9 | #define BEMAN_UTF_VIEW_NULL_TERM_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | 15 | namespace beman::utf_view { 16 | 17 | /* PAPER: namespace std { */ 18 | 19 | /* PAPER */ 20 | 21 | template 22 | concept exposition_only_default_initializable_and_equality_comparable_iter_value = 23 | std::default_initializable> && 24 | std::equality_comparable_with, 25 | std::iter_value_t>; // @*exposition only*@ 26 | 27 | struct null_sentinel_t { 28 | template 29 | requires exposition_only_default_initializable_and_equality_comparable_iter_value 30 | friend constexpr bool operator==(I const& it, null_sentinel_t) { 31 | return *it == std::iter_value_t(); 32 | } 33 | }; 34 | 35 | inline constexpr null_sentinel_t null_sentinel; 36 | 37 | /* !PAPER */ 38 | 39 | namespace detail { 40 | 41 | struct null_term_impl { 42 | template 43 | constexpr auto operator()(I&& it) const { 44 | return std::ranges::subrange(std::forward(it), null_sentinel); 45 | } 46 | }; 47 | 48 | } // namespace detail 49 | 50 | inline constexpr detail::null_term_impl null_term; 51 | 52 | /* PAPER: inline constexpr @*unspecified*@ null_term; */ 53 | /* PAPER: */ 54 | 55 | } // namespace beman::utf_view 56 | 57 | /* PAPER: } */ 58 | 59 | #endif // BEMAN_UTF_VIEW_NULL_TERM_HPP 60 | -------------------------------------------------------------------------------- /infra/cmake/appleclang-toolchain.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | 3 | # This toolchain file is not meant to be used directly, 4 | # but to be invoked by CMake preset and GitHub CI. 5 | # 6 | # This toolchain file configures for apple clang family of compiler. 7 | # Note this is different from LLVM toolchain. 8 | # 9 | # BEMAN_BUILDSYS_SANITIZER: 10 | # This optional CMake parameter is not meant for public use and is subject to 11 | # change. 12 | # Possible values: 13 | # - MaxSan: configures clang and clang++ to use all available non-conflicting 14 | # sanitizers. Note that apple clang does not support leak sanitizer. 15 | # - TSan: configures clang and clang++ to enable the use of thread sanitizer. 16 | 17 | include_guard(GLOBAL) 18 | 19 | # Prevent PATH collision with an LLVM clang installation by using the system 20 | # compiler shims 21 | set(CMAKE_C_COMPILER cc) 22 | set(CMAKE_CXX_COMPILER c++) 23 | 24 | if(BEMAN_BUILDSYS_SANITIZER STREQUAL "MaxSan") 25 | set(SANITIZER_FLAGS 26 | "-fsanitize=address -fsanitize=pointer-compare -fsanitize=pointer-subtract -fsanitize=undefined" 27 | ) 28 | elseif(BEMAN_BUILDSYS_SANITIZER STREQUAL "TSan") 29 | set(SANITIZER_FLAGS "-fsanitize=thread") 30 | endif() 31 | 32 | set(CMAKE_C_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") 33 | set(CMAKE_CXX_FLAGS_DEBUG_INIT "${SANITIZER_FLAGS}") 34 | 35 | set(RELEASE_FLAGS "-O3 ${SANITIZER_FLAGS}") 36 | 37 | set(CMAKE_C_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") 38 | set(CMAKE_CXX_FLAGS_RELWITHDEBINFO_INIT "${RELEASE_FLAGS}") 39 | 40 | set(CMAKE_C_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") 41 | set(CMAKE_CXX_FLAGS_RELEASE_INIT "${RELEASE_FLAGS}") 42 | 43 | # Add this dir to the module path so that `find_package(beman-install-library)` works 44 | list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") 45 | -------------------------------------------------------------------------------- /paper/generator/post_clang_format.py: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | import re 4 | import sys 5 | 6 | def convert_snake_to_kebab(text): 7 | def snake_to_kebab(match): 8 | word = match.group(0) 9 | word = word.replace('exposition_only_', '', 1) 10 | kebab_case_word = word.replace('_', '-') 11 | return f"@*{kebab_case_word}*@" 12 | 13 | pattern = r'\bexposition_only_[a-zA-Z_]+\b' 14 | result = re.sub(pattern, snake_to_kebab, text) 15 | return result 16 | 17 | 18 | def process_file(file_path): 19 | in_paper_section = False 20 | result_lines = [] 21 | 22 | with open(file_path, 'r') as file: 23 | for line in file: 24 | line = convert_snake_to_kebab(line); 25 | if "/* PAPER: " in line: 26 | result_lines.append(line[line.find("/* PAPER:") + len("/* PAPER: "):-4]) 27 | result_lines.append("\n") 28 | continue 29 | 30 | line = line.replace("std::ranges::", "") 31 | line = line.replace("std::", "") 32 | line = line.replace("detail::", "") 33 | line = line.replace("boost::stl_interfaces::", "") 34 | line = line.replace("CONSTEXPR_UNLESS_MSVC", "constexpr") 35 | line = line.replace("move", "std::move") 36 | 37 | if "/* !PAPER */" in line: 38 | in_paper_section = False 39 | continue 40 | elif "/* PAPER */" in line: 41 | in_paper_section = True 42 | continue 43 | 44 | if in_paper_section: 45 | result_lines.append(line) 46 | 47 | return result_lines 48 | 49 | if __name__ == "__main__": 50 | if len(sys.argv) != 2: 51 | print("Usage: python post_clang_format.py ") 52 | sys.exit(1) 53 | 54 | file_path = sys.argv[1] 55 | 56 | processed_lines = process_file(file_path) 57 | 58 | for line in processed_lines: 59 | print(line, end="") 60 | -------------------------------------------------------------------------------- /tests/beman/utf_view/null_term.t.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace beman::utf_view::tests { 18 | 19 | static_assert(std::default_initializable< 20 | std::iter_value_t>); 21 | static_assert(std::equality_comparable_with< 22 | std::iter_reference_t, 23 | std::iter_value_t>); 24 | static_assert( 25 | std::sentinel_for); 26 | static_assert(std::default_initializable< 27 | std::iter_value_t>); 28 | static_assert(std::equality_comparable_with< 29 | std::iter_reference_t, 30 | std::iter_value_t>); 31 | static_assert( 32 | std::sentinel_for); 33 | 34 | constexpr bool null_sentinel_input_iterator_test() { 35 | int total = 0; 36 | std_archetypes::input_iterator_archetype it{254}; 37 | for (auto x : null_term(std::move(it))) { 38 | total += x.x; 39 | } 40 | return total == 254 + 255; 41 | } 42 | 43 | constexpr bool null_sentinel_forward_iterator_test() { 44 | int total = 0; 45 | std_archetypes::forward_iterator_archetype it{254}; 46 | for (auto x : null_term(it)) { 47 | total += x.x; 48 | } 49 | return total == 254 + 255; 50 | } 51 | 52 | static_assert(null_sentinel_input_iterator_test()); 53 | static_assert(null_sentinel_forward_iterator_test()); 54 | 55 | } // namespace beman::utf_view::tests 56 | -------------------------------------------------------------------------------- /infra/tools/beman-submodule/README.md: -------------------------------------------------------------------------------- 1 | # beman-submodule 2 | 3 | 4 | 5 | ## What is this script? 6 | 7 | `beman-submodule` provides some of the features of `git submodule`, adding child git 8 | repositories to a parent git repository, but unlike with `git submodule`, the entire child 9 | repo is directly checked in, so only maintainers, not users, need to run this script. The 10 | command line interface mimics `git submodule`'s. 11 | 12 | ## How do I add a beman submodule to my repository? 13 | 14 | The first beman submodule you should add is this repository, `infra/`, which you can 15 | bootstrap by running: 16 | 17 | 18 | ```sh 19 | curl -s https://raw.githubusercontent.com/bemanproject/infra/refs/heads/main/tools/beman-submodule/beman-submodule | python3 - add https://github.com/bemanproject/infra.git 20 | ``` 21 | 22 | Once that's added, you can run the script from `infra/tools/beman-submodule/beman-submodule`. 23 | 24 | ## How do I update a beman submodule to the latest trunk? 25 | 26 | You can run `beman-submodule update --remote` to update all beman submodule to latest 27 | trunk, or e.g. `beman-submodule update --remote infra` to update only a specific one. 28 | 29 | ## How does it work under the hood? 30 | 31 | Along with the files from the child repository, it creates a dotfile called 32 | `.beman_submodule`, which looks like this: 33 | 34 | ```ini 35 | [beman_submodule] 36 | remote=https://github.com/bemanproject/infra.git 37 | commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77 38 | ``` 39 | 40 | ## How do I update a beman submodule to a specific commit or change the remote URL? 41 | 42 | You can edit the corresponding lines in the `.beman_submodule` file and run 43 | `beman-submodule update` to update the state of the beman submodule to the new 44 | `.beman_submodule` settings. 45 | 46 | ## How can I make CI ensure that my beman submodules are in a valid state? 47 | 48 | Add this job to your CI workflow: 49 | 50 | ```yaml 51 | beman-submodule-test: 52 | runs-on: ubuntu-latest 53 | name: "Check beman submodules for consistency" 54 | steps: 55 | - name: Checkout 56 | uses: actions/checkout@v4 57 | - name: beman submodule consistency check 58 | run: | 59 | (set -o pipefail; ./infra/tools/beman-submodule/beman-submodule status | grep -qvF '+') 60 | ``` 61 | 62 | This will fail if the contents of any beman submodule don't match what's specified in the 63 | `.beman_submodule` file. 64 | -------------------------------------------------------------------------------- /infra/README.md: -------------------------------------------------------------------------------- 1 | # Beman Project Infrastructure Repository 2 | 3 | 4 | 5 | This repository contains the infrastructure for The Beman Project. This is NOT a library repository, 6 | so it does not respect the usual structure of a Beman library repository nor The Beman Standard! 7 | 8 | ## Description 9 | 10 | * `cmake/`: CMake modules and toolchain files used by Beman libraries. 11 | * `containers/`: Containers used for CI builds and tests in the Beman org. 12 | * `tools/`: Tools used to manage the infrastructure and the codebase (e.g., linting, formatting, etc.). 13 | 14 | ## Usage 15 | 16 | This repository is intended to be used as a beman-submodule in other Beman repositories. See 17 | [the Beman Submodule documentation](./tools/beman-submodule/README.md) for details. 18 | 19 | 20 | ### CMake Modules 21 | 22 | 23 | #### `beman_install_library` 24 | 25 | The CMake modules in this repository are intended to be used by Beman libraries. Use the 26 | `beman_add_install_library_config()` function to install your library, along with header 27 | files, any metadata files, and a CMake config file for `find_package()` support. 28 | 29 | ```cmake 30 | add_library(beman.something) 31 | add_library(beman::something ALIAS beman.something) 32 | 33 | # ... configure your target as needed ... 34 | 35 | find_package(beman-install-library REQUIRED) 36 | beman_install_library(beman.something) 37 | ``` 38 | 39 | Note that the target must be created before calling `beman_install_library()`. The module 40 | also assumes that the target is named using the `beman.something` convention, and it 41 | uses that assumption to derive the names to match other Beman standards and conventions. 42 | If your target does not follow that convention, raise an issue or pull request to add 43 | more configurability to the module. 44 | 45 | The module will configure the target to install: 46 | 47 | * The library target itself 48 | * Any public headers associated with the target 49 | * CMake files for `find_package(beman.something)` support 50 | 51 | Some options for the project and target will also be supported: 52 | 53 | * `BEMAN_INSTALL_CONFIG_FILE_PACKAGES` - a list of package names (e.g., `beman.something`) for which to install the config file 54 | (default: all packages) 55 | * `_INSTALL_CONFIG_FILE_PACKAGE` - a per-project option to enable/disable config file installation (default: `ON` if the project is top-level, `OFF` otherwise). For instance for `beman.something`, the option would be `BEMAN_SOMETHING_INSTALL_CONFIG_FILE_PACKAGE`. 56 | -------------------------------------------------------------------------------- /infra/.github/workflows/pre-commit.yml: -------------------------------------------------------------------------------- 1 | name: Lint Check (pre-commit) 2 | 3 | on: 4 | # We have to use pull_request_target here as pull_request does not grant 5 | # enough permission for reviewdog 6 | pull_request_target: 7 | push: 8 | branches: 9 | - main 10 | 11 | jobs: 12 | pre-commit-push: 13 | name: Pre-Commit check on Push 14 | runs-on: ubuntu-latest 15 | if: ${{ github.event_name == 'push' }} 16 | 17 | steps: 18 | - name: Checkout repository 19 | uses: actions/checkout@v4 20 | 21 | - name: Set up Python 22 | uses: actions/setup-python@v5 23 | with: 24 | python-version: 3.13 25 | 26 | # We wish to run pre-commit on all files instead of the changes 27 | # only made in the push commit. 28 | # 29 | # So linting error persists when there's formatting problem. 30 | - uses: pre-commit/action@v3.0.1 31 | 32 | pre-commit-pr: 33 | name: Pre-Commit check on PR 34 | runs-on: ubuntu-latest 35 | if: ${{ github.event_name == 'pull_request_target' }} 36 | 37 | permissions: 38 | contents: read 39 | checks: write 40 | issues: write 41 | pull-requests: write 42 | 43 | steps: 44 | - name: Checkout repository 45 | uses: actions/checkout@v4 46 | 47 | # pull_request_target checkout the base of the repo 48 | # We need to checkout the actual pr to lint the changes. 49 | - name: Checkout pr 50 | run: gh pr checkout ${{ github.event.number }} 51 | env: 52 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 53 | 54 | - name: Set up Python 55 | uses: actions/setup-python@v5 56 | with: 57 | python-version: 3.13 58 | 59 | # we only lint on the changed file in PR. 60 | - name: Get Changed Files 61 | id: changed-files 62 | uses: tj-actions/changed-files@v45 63 | 64 | # See: 65 | # https://github.com/tj-actions/changed-files?tab=readme-ov-file#using-local-git-directory- 66 | - uses: pre-commit/action@v3.0.1 67 | id: run-pre-commit 68 | with: 69 | extra_args: --files ${{ steps.changed-files.outputs.all_changed_files }} 70 | 71 | # Review dog posts the suggested change from pre-commit to the pr. 72 | - name: suggester / pre-commit 73 | uses: reviewdog/action-suggester@v1 74 | if: ${{ failure() && steps.run-pre-commit.conclusion == 'failure' }} 75 | with: 76 | tool_name: pre-commit 77 | level: warning 78 | reviewdog_flags: "-fail-level=error" 79 | -------------------------------------------------------------------------------- /src/beman/utf_view/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: BSL-1.0 2 | 3 | # Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | # Distributed under the Boost Software License, Version 1.0. 5 | # (See accompanying file LICENSE.txt or copy at 6 | # https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | add_library(beman_utf_view INTERFACE) 9 | 10 | include(GNUInstallDirs) 11 | 12 | add_library(beman.utf_view) 13 | add_library(beman::utf_view ALIAS beman.utf_view) 14 | 15 | target_sources(beman.utf_view PRIVATE utf_view.cpp) 16 | 17 | target_sources( 18 | beman.utf_view 19 | PUBLIC 20 | FILE_SET HEADERS 21 | BASE_DIRS ${CMAKE_CURRENT_SOURCE_DIR}/../../../include 22 | FILES 23 | ${CMAKE_CURRENT_SOURCE_DIR}/../../../include/beman/utf_view/utf_view.hpp 24 | ) 25 | 26 | set_target_properties( 27 | beman.utf_view 28 | PROPERTIES VERIFY_INTERFACE_HEADER_SETS ON EXPORT_NAME utf_view 29 | ) 30 | 31 | target_link_libraries(beman.utf_view PUBLIC beman::transform_view) 32 | 33 | install( 34 | TARGETS beman.utf_view beman.transform_view 35 | COMPONENT beman.utf_view 36 | EXPORT beman.utf_view 37 | DESTINATION ${CMAKE_INSTALL_LIBDIR}$<$:/debug> 38 | RUNTIME DESTINATION ${CMAKE_INSTALL_BINDIR}$<$:/debug> 39 | FILE_SET HEADERS DESTINATION ${CMAKE_INSTALL_INCLUDEDIR} 40 | ) 41 | 42 | if(BEMAN_UTF_VIEW_INSTALL_CONFIG_FILE_PACKAGE) 43 | include(CMakePackageConfigHelpers) 44 | 45 | configure_package_config_file( 46 | "${CMAKE_CURRENT_SOURCE_DIR}/beman.utf_view-config.cmake.in" 47 | "${CMAKE_CURRENT_BINARY_DIR}/beman.utf_view-config.cmake" 48 | INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/beman.utf_view" 49 | PATH_VARS PROJECT_NAME PROJECT_VERSION 50 | ) 51 | 52 | write_basic_package_version_file( 53 | "${CMAKE_CURRENT_BINARY_DIR}/beman.utf_view-config-version.cmake" 54 | VERSION ${PROJECT_VERSION} 55 | COMPATIBILITY ExactVersion 56 | ) 57 | 58 | install( 59 | FILES 60 | "${CMAKE_CURRENT_BINARY_DIR}/beman.utf_view-config.cmake" 61 | "${CMAKE_CURRENT_BINARY_DIR}/beman.utf_view-config-version.cmake" 62 | DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/beman.utf_view" 63 | COMPONENT beman.utf_view 64 | ) 65 | 66 | install( 67 | EXPORT beman.utf_view 68 | DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/beman.utf_view" 69 | NAMESPACE beman:: 70 | FILE beman.utf_view-targets.cmake 71 | COMPONENT beman.utf_view 72 | ) 73 | endif() 74 | -------------------------------------------------------------------------------- /include/beman/utf_view/code_unit_view.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #ifndef BEMAN_UTF_VIEW_CODE_UNIT_VIEW_HPP 9 | #define BEMAN_UTF_VIEW_CODE_UNIT_VIEW_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace beman::utf_view { 17 | 18 | /* PAPER: namespace std::ranges::views { */ 19 | 20 | namespace detail { 21 | 22 | /* PAPER */ 23 | 24 | template 25 | struct exposition_only_implicit_cast_to { 26 | constexpr T operator()(auto x) const noexcept { 27 | return x; 28 | } 29 | }; 30 | 31 | /* !PAPER */ 32 | 33 | } // namespace detail 34 | 35 | namespace detail { 36 | 37 | template 38 | struct as_code_unit_impl 39 | : std::ranges::range_adaptor_closure> { 40 | template 41 | requires std::convertible_to, Char> && 42 | is_not_array_of_char 43 | constexpr auto operator()(R&& r) const { 44 | using T = std::remove_cvref_t; 45 | if constexpr (detail::is_empty_view) { 46 | return std::ranges::empty_view{}; 47 | } else if constexpr (std::is_bounded_array_v) { 48 | constexpr auto n = std::extent_v; 49 | auto first{std::ranges::begin(r)}; 50 | auto last{std::ranges::end(r)}; 51 | if (n && !r[n - 1]) { 52 | --last; 53 | } 54 | std::ranges::subrange subrange(first, last); 55 | return beman::transform_view::transform_view( 56 | std::move(subrange), exposition_only_implicit_cast_to{}); 57 | } else { 58 | return beman::transform_view::transform_view( 59 | std::move(r), exposition_only_implicit_cast_to{}); 60 | } 61 | } 62 | }; 63 | 64 | } // namespace detail 65 | 66 | inline constexpr detail::as_code_unit_impl as_char8_t; 67 | 68 | inline constexpr detail::as_code_unit_impl as_char16_t; 69 | 70 | inline constexpr detail::as_code_unit_impl as_char32_t; 71 | 72 | /* PAPER: inline constexpr @*unspecified*@ as_char8_t; */ 73 | /* PAPER: */ 74 | /* PAPER: inline constexpr @*unspecified*@ as_char16_t; */ 75 | /* PAPER: */ 76 | /* PAPER: inline constexpr @*unspecified*@ as_char32_t; */ 77 | 78 | /* PAPER: } */ 79 | 80 | } // namespace beman::utf_view 81 | 82 | #endif // BEMAN_UTF_VIEW_CODE_UNIT_VIEW_HPP 83 | -------------------------------------------------------------------------------- /.github/workflows/ci_tests.yml: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | 3 | name: Continuous Integration Tests 4 | 5 | on: 6 | push: 7 | branches: 8 | - main 9 | pull_request: 10 | workflow_dispatch: 11 | schedule: 12 | - cron: '20 19 * * *' 13 | - cron: "0 16 * * 0" 14 | 15 | jobs: 16 | beman-submodule-check: 17 | uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-submodule-check.yml@1.0.0 18 | 19 | preset-test: 20 | uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-preset-test.yml@1.0.0 21 | with: 22 | matrix_config: > 23 | [ 24 | {"preset": "gcc-debug", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, 25 | {"preset": "gcc-release", "image": "ghcr.io/bemanproject/infra-containers-gcc:latest"}, 26 | {"preset": "msvc-debug", "runner": "windows-latest"}, 27 | {"preset": "msvc-release", "runner": "windows-latest"} 28 | ] 29 | 30 | build-and-test: 31 | uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-build-and-test.yml@1.0.0 32 | with: 33 | matrix_config: > 34 | { 35 | "gcc": [ 36 | { "versions": ["trunk"], 37 | "tests": [ 38 | { "cxxversions": ["c++26"], 39 | "tests": [ 40 | { "stdlibs": ["libstdc++"], 41 | "tests": [ 42 | "Debug.Default", "Release.Default", "Release.MaxSan", 43 | "Debug.Werror", "Debug.Dynamic", "Debug.Coverage", 44 | "Debug.-DBEMAN_UTF_VIEW_BUILD_PAPER=ON" 45 | ] 46 | } 47 | ] 48 | }, 49 | { "cxxversions": ["c++23"], 50 | "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] 51 | } 52 | ] 53 | }, 54 | { "versions": ["15", "14"], 55 | "tests": [ 56 | { "cxxversions": ["c++26", "c++23"], 57 | "tests": [{ "stdlibs": ["libstdc++"], "tests": ["Release.Default"]}] 58 | } 59 | ] 60 | } 61 | ], 62 | "clang": [ 63 | { "versions": ["trunk"], 64 | "tests": [ 65 | { "cxxversions": ["c++26"], 66 | "tests": [ 67 | { "stdlibs": ["libc++"], 68 | "tests": [ 69 | "Debug.Default", "Release.Default", "Release.MaxSan", 70 | "Debug.Werror", "Debug.Dynamic" 71 | ] 72 | } 73 | ] 74 | }, 75 | { "cxxversions": ["c++23"], 76 | "tests": [{"stdlibs": ["libc++"], "tests": ["Release.Default"]}] 77 | } 78 | ] 79 | }, 80 | { "versions": ["21", "20", "19"], 81 | "tests": [ 82 | { "cxxversions": ["c++26", "c++23"], 83 | "tests": [ 84 | {"stdlibs": ["libc++"], "tests": ["Release.Default"]} 85 | ] 86 | } 87 | ] 88 | } 89 | ], 90 | "msvc": [ 91 | { "versions": ["latest"], 92 | "tests": [ 93 | { "cxxversions": ["c++23"], 94 | "tests": [ 95 | { "stdlibs": ["stl"], 96 | "tests": ["Debug.Default", "Release.Default", "Release.MaxSan"] 97 | } 98 | ] 99 | } 100 | ] 101 | } 102 | ] 103 | } 104 | 105 | create-issue-when-fault: 106 | needs: [preset-test, build-and-test] 107 | if: failure() && github.event.schedule == '20 19 * * *' 108 | uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-create-issue-when-fault.yml@1.0.0 109 | 110 | auto-update-pre-commit: 111 | if: github.event.schedule == '00 16 * * 0' 112 | uses: bemanproject/infra-workflows/.github/workflows/reusable-beman-update-pre-commit.yml@1.0.0 113 | -------------------------------------------------------------------------------- /tests/beman/utf_view/code_unit_view.t.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | 16 | namespace beman::utf_view::tests { 17 | 18 | static_assert( 19 | std::input_iterator< 20 | std::ranges::iterator_t< 21 | decltype( 22 | std::declval< 23 | std::ranges::subrange, std::default_sentinel_t>>() 24 | | as_char8_t)>>); 25 | static_assert( 26 | std::input_iterator< 27 | std::ranges::iterator_t< 28 | decltype( 29 | std::declval< 30 | std::ranges::subrange< 31 | test_comparable_input_iterator, std::default_sentinel_t>>() 32 | | as_char8_t)>>); 33 | static_assert( 34 | std::input_iterator< 35 | std::ranges::iterator_t< 36 | decltype( 37 | std::declval< 38 | std::ranges::subrange< 39 | test_copyable_input_iterator, std::default_sentinel_t>>() 40 | | as_char8_t)>>); 41 | static_assert( 42 | !std::forward_iterator< 43 | std::ranges::iterator_t< 44 | decltype( 45 | std::declval< 46 | std::ranges::subrange< 47 | test_copyable_input_iterator, std::default_sentinel_t>>() 48 | | as_char8_t)>>); 49 | 50 | static_assert( 51 | std::forward_iterator< 52 | std::ranges::iterator_t< 53 | decltype( 54 | std::declval< 55 | std::ranges::subrange< 56 | test_forward_iterator, std::default_sentinel_t>>() 57 | | as_char8_t)>>); 58 | static_assert( 59 | std::forward_iterator< 60 | std::ranges::iterator_t< 61 | decltype( 62 | std::declval< 63 | std::ranges::subrange< 64 | test_forward_iterator, test_forward_iterator>>() 65 | | as_char8_t)>>); 66 | 67 | static_assert( 68 | std::bidirectional_iterator< 69 | std::ranges::iterator_t< 70 | decltype( 71 | std::declval< 72 | std::ranges::subrange, std::default_sentinel_t>>() 73 | | as_char8_t)>>); 74 | static_assert( 75 | std::bidirectional_iterator< 76 | std::ranges::sentinel_t< 77 | decltype( 78 | std::declval< 79 | std::ranges::subrange, test_bidi_iterator>>() 80 | | as_char8_t)>>); 81 | 82 | static_assert( 83 | std::random_access_iterator< 84 | std::ranges::iterator_t< 85 | decltype( 86 | std::declval< 87 | std::ranges::subrange< 88 | test_random_access_iterator, std::default_sentinel_t>>() 89 | | as_char8_t)>>); 90 | static_assert( 91 | std::random_access_iterator< 92 | std::ranges::sentinel_t< 93 | decltype( 94 | std::declval< 95 | std::ranges::subrange< 96 | test_random_access_iterator, test_random_access_iterator>>() 97 | | as_char8_t)>>); 98 | 99 | constexpr bool smoke_test() { 100 | std::string_view foo{"foo"}; 101 | auto bar{foo | as_char8_t}; 102 | static_assert(std::ranges::borrowed_range); 103 | auto baz{bar | std::ranges::to()}; 104 | if (baz != std::u8string_view{u8"foo"}) { 105 | return false; 106 | } 107 | return true; 108 | } 109 | 110 | constexpr bool special_case_test() { 111 | std::ranges::empty_view empty_int_view{}; 112 | auto empty_char8_view{empty_int_view | as_char8_t}; 113 | static_assert( 114 | std::is_same_v>); 115 | auto char_string_literal_as_char8_view{"foo" | as_char8_t}; 116 | auto char_string_literal_as_char8_view_it{char_string_literal_as_char8_view.begin()}; 117 | if (*char_string_literal_as_char8_view_it != u8'f') { 118 | return false; 119 | } 120 | ++char_string_literal_as_char8_view_it; 121 | if (*char_string_literal_as_char8_view_it != u8'o') { 122 | return false; 123 | } 124 | ++char_string_literal_as_char8_view_it; 125 | if (*char_string_literal_as_char8_view_it != u8'o') { 126 | return false; 127 | } 128 | ++char_string_literal_as_char8_view_it; 129 | if (char_string_literal_as_char8_view_it != char_string_literal_as_char8_view.end()) { 130 | return false; 131 | } 132 | return true; 133 | } 134 | 135 | CONSTEXPR_UNLESS_MSVC bool code_unit_view_test() { 136 | if (!smoke_test()) { 137 | return false; 138 | } 139 | if (!special_case_test()) { 140 | return false; 141 | } 142 | return true; 143 | } 144 | 145 | #ifndef _MSC_VER 146 | static_assert(code_unit_view_test()); 147 | #endif 148 | 149 | static auto const init{[] { 150 | framework::tests().insert({"code_unit_view_test", &code_unit_view_test}); 151 | struct { 152 | } result{}; 153 | return result; 154 | }()}; 155 | 156 | } // namespace beman::utf_view::tests 157 | -------------------------------------------------------------------------------- /infra/cmake/beman-install-library-config.cmake: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | include_guard(GLOBAL) 3 | 4 | # This file defines the function `beman_install_library` which is used to 5 | # install a library target and its headers, along with optional CMake 6 | # configuration files. 7 | # 8 | # The function is designed to be reusable across different Beman libraries. 9 | 10 | function(beman_install_library name) 11 | # Usage 12 | # ----- 13 | # 14 | # beman_install_library(NAME) 15 | # 16 | # Brief 17 | # ----- 18 | # 19 | # This function installs the specified library target and its headers. 20 | # It also handles the installation of the CMake configuration files if needed. 21 | # 22 | # CMake variables 23 | # --------------- 24 | # 25 | # Note that configuration of the installation is generally controlled by CMake 26 | # cache variables so that they can be controlled by the user or tool running the 27 | # `cmake` command. Neither `CMakeLists.txt` nor `*.cmake` files should set these 28 | # variables directly. 29 | # 30 | # - BEMAN_INSTALL_CONFIG_FILE_PACKAGES: 31 | # List of packages that require config file installation. 32 | # If the package name is in this list, it will install the config file. 33 | # 34 | # - _INSTALL_CONFIG_FILE_PACKAGE: 35 | # Boolean to control config file installation for the specific library. 36 | # The prefix `` is the uppercased name of the library with dots 37 | # replaced by underscores. 38 | # 39 | if(NOT TARGET "${name}") 40 | message(FATAL_ERROR "Target '${name}' does not exist.") 41 | endif() 42 | 43 | if(NOT ARGN STREQUAL "") 44 | message( 45 | FATAL_ERROR 46 | "beman_install_library does not accept extra arguments: ${ARGN}" 47 | ) 48 | endif() 49 | 50 | # Given foo.bar, the component name is bar 51 | string(REPLACE "." ";" name_parts "${name}") 52 | # fail if the name doesn't look like foo.bar 53 | list(LENGTH name_parts name_parts_length) 54 | if(NOT name_parts_length EQUAL 2) 55 | message( 56 | FATAL_ERROR 57 | "beman_install_library expects a name of the form 'beman.', got '${name}'" 58 | ) 59 | endif() 60 | 61 | set(target_name "${name}") 62 | set(install_component_name "${name}") 63 | set(export_name "${name}") 64 | set(package_name "${name}") 65 | list(GET name_parts -1 component_name) 66 | 67 | install( 68 | TARGETS "${target_name}" 69 | COMPONENT "${install_component_name}" 70 | EXPORT "${export_name}" 71 | FILE_SET HEADERS 72 | ) 73 | 74 | set_target_properties( 75 | "${target_name}" 76 | PROPERTIES EXPORT_NAME "${component_name}" 77 | ) 78 | 79 | include(GNUInstallDirs) 80 | 81 | # Determine the prefix for project-specific variables 82 | string(TOUPPER "${name}" project_prefix) 83 | string(REPLACE "." "_" project_prefix "${project_prefix}") 84 | 85 | option( 86 | ${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE 87 | "Enable building examples. Default: ${PROJECT_IS_TOP_LEVEL}. Values: { ON, OFF }." 88 | ${PROJECT_IS_TOP_LEVEL} 89 | ) 90 | 91 | # By default, install the config package 92 | set(install_config_package ON) 93 | 94 | # Turn OFF installation of config package by default if, 95 | # in order of precedence: 96 | # 1. The specific package variable is set to OFF 97 | # 2. The package name is not in the list of packages to install config files 98 | if(DEFINED BEMAN_INSTALL_CONFIG_FILE_PACKAGES) 99 | if( 100 | NOT "${install_component_name}" 101 | IN_LIST 102 | BEMAN_INSTALL_CONFIG_FILE_PACKAGES 103 | ) 104 | set(install_config_package OFF) 105 | endif() 106 | endif() 107 | if(DEFINED ${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE) 108 | set(install_config_package 109 | ${${project_prefix}_INSTALL_CONFIG_FILE_PACKAGE} 110 | ) 111 | endif() 112 | 113 | if(install_config_package) 114 | message( 115 | DEBUG 116 | "beman-install-library: Installing a config package for '${name}'" 117 | ) 118 | 119 | include(CMakePackageConfigHelpers) 120 | 121 | find_file( 122 | config_file_template 123 | NAMES "${package_name}-config.cmake.in" 124 | PATHS "${CMAKE_CURRENT_SOURCE_DIR}" 125 | NO_DEFAULT_PATH 126 | NO_CACHE 127 | REQUIRED 128 | ) 129 | set(config_package_file 130 | "${CMAKE_CURRENT_BINARY_DIR}/${package_name}-config.cmake" 131 | ) 132 | set(package_install_dir "${CMAKE_INSTALL_LIBDIR}/cmake/${package_name}") 133 | configure_package_config_file( 134 | "${config_file_template}" 135 | "${config_package_file}" 136 | INSTALL_DESTINATION "${package_install_dir}" 137 | PATH_VARS PROJECT_NAME PROJECT_VERSION 138 | ) 139 | 140 | set(config_version_file 141 | "${CMAKE_CURRENT_BINARY_DIR}/${package_name}-config-version.cmake" 142 | ) 143 | write_basic_package_version_file( 144 | "${config_version_file}" 145 | VERSION "${PROJECT_VERSION}" 146 | COMPATIBILITY ExactVersion 147 | ) 148 | 149 | install( 150 | FILES "${config_package_file}" "${config_version_file}" 151 | DESTINATION "${package_install_dir}" 152 | COMPONENT "${install_component_name}" 153 | ) 154 | 155 | set(config_targets_file "${package_name}-targets.cmake") 156 | install( 157 | EXPORT "${export_name}" 158 | DESTINATION "${package_install_dir}" 159 | NAMESPACE beman:: 160 | FILE "${config_targets_file}" 161 | COMPONENT "${install_component_name}" 162 | ) 163 | else() 164 | message( 165 | DEBUG 166 | "beman-install-library: Not installing a config package for '${name}'" 167 | ) 168 | endif() 169 | endfunction() 170 | -------------------------------------------------------------------------------- /infra/cmake/use-fetch-content.cmake: -------------------------------------------------------------------------------- 1 | cmake_minimum_required(VERSION 3.24) 2 | 3 | include(FetchContent) 4 | 5 | if(NOT BEMAN_EXEMPLAR_LOCKFILE) 6 | set(BEMAN_EXEMPLAR_LOCKFILE 7 | "lockfile.json" 8 | CACHE FILEPATH 9 | "Path to the dependency lockfile for the Beman Exemplar." 10 | ) 11 | endif() 12 | 13 | set(BemanExemplar_projectDir "${CMAKE_CURRENT_LIST_DIR}/../..") 14 | message(TRACE "BemanExemplar_projectDir=\"${BemanExemplar_projectDir}\"") 15 | 16 | message(TRACE "BEMAN_EXEMPLAR_LOCKFILE=\"${BEMAN_EXEMPLAR_LOCKFILE}\"") 17 | file( 18 | REAL_PATH 19 | "${BEMAN_EXEMPLAR_LOCKFILE}" 20 | BemanExemplar_lockfile 21 | BASE_DIRECTORY "${BemanExemplar_projectDir}" 22 | EXPAND_TILDE 23 | ) 24 | message(DEBUG "Using lockfile: \"${BemanExemplar_lockfile}\"") 25 | 26 | # Force CMake to reconfigure the project if the lockfile changes 27 | set_property( 28 | DIRECTORY "${BemanExemplar_projectDir}" 29 | APPEND 30 | PROPERTY CMAKE_CONFIGURE_DEPENDS "${BemanExemplar_lockfile}" 31 | ) 32 | 33 | # For more on the protocol for this function, see: 34 | # https://cmake.org/cmake/help/latest/command/cmake_language.html#provider-commands 35 | function(BemanExemplar_provideDependency method package_name) 36 | # Read the lockfile 37 | file(READ "${BemanExemplar_lockfile}" BemanExemplar_rootObj) 38 | 39 | # Get the "dependencies" field and store it in BemanExemplar_dependenciesObj 40 | string( 41 | JSON 42 | BemanExemplar_dependenciesObj 43 | ERROR_VARIABLE BemanExemplar_error 44 | GET "${BemanExemplar_rootObj}" 45 | "dependencies" 46 | ) 47 | if(BemanExemplar_error) 48 | message(FATAL_ERROR "${BemanExemplar_lockfile}: ${BemanExemplar_error}") 49 | endif() 50 | 51 | # Get the length of the libraries array and store it in BemanExemplar_dependenciesObj 52 | string( 53 | JSON 54 | BemanExemplar_numDependencies 55 | ERROR_VARIABLE BemanExemplar_error 56 | LENGTH "${BemanExemplar_dependenciesObj}" 57 | ) 58 | if(BemanExemplar_error) 59 | message(FATAL_ERROR "${BemanExemplar_lockfile}: ${BemanExemplar_error}") 60 | endif() 61 | 62 | if(BemanExemplar_numDependencies EQUAL 0) 63 | return() 64 | endif() 65 | 66 | # Loop over each dependency object 67 | math(EXPR BemanExemplar_maxIndex "${BemanExemplar_numDependencies} - 1") 68 | foreach(BemanExemplar_index RANGE "${BemanExemplar_maxIndex}") 69 | set(BemanExemplar_errorPrefix 70 | "${BemanExemplar_lockfile}, dependency ${BemanExemplar_index}" 71 | ) 72 | 73 | # Get the dependency object at BemanExemplar_index 74 | # and store it in BemanExemplar_depObj 75 | string( 76 | JSON 77 | BemanExemplar_depObj 78 | ERROR_VARIABLE BemanExemplar_error 79 | GET "${BemanExemplar_dependenciesObj}" 80 | "${BemanExemplar_index}" 81 | ) 82 | if(BemanExemplar_error) 83 | message( 84 | FATAL_ERROR 85 | "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" 86 | ) 87 | endif() 88 | 89 | # Get the "name" field and store it in BemanExemplar_name 90 | string( 91 | JSON 92 | BemanExemplar_name 93 | ERROR_VARIABLE BemanExemplar_error 94 | GET "${BemanExemplar_depObj}" 95 | "name" 96 | ) 97 | if(BemanExemplar_error) 98 | message( 99 | FATAL_ERROR 100 | "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" 101 | ) 102 | endif() 103 | 104 | # Get the "package_name" field and store it in BemanExemplar_pkgName 105 | string( 106 | JSON 107 | BemanExemplar_pkgName 108 | ERROR_VARIABLE BemanExemplar_error 109 | GET "${BemanExemplar_depObj}" 110 | "package_name" 111 | ) 112 | if(BemanExemplar_error) 113 | message( 114 | FATAL_ERROR 115 | "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" 116 | ) 117 | endif() 118 | 119 | # Get the "git_repository" field and store it in BemanExemplar_repo 120 | string( 121 | JSON 122 | BemanExemplar_repo 123 | ERROR_VARIABLE BemanExemplar_error 124 | GET "${BemanExemplar_depObj}" 125 | "git_repository" 126 | ) 127 | if(BemanExemplar_error) 128 | message( 129 | FATAL_ERROR 130 | "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" 131 | ) 132 | endif() 133 | 134 | # Get the "git_tag" field and store it in BemanExemplar_tag 135 | string( 136 | JSON 137 | BemanExemplar_tag 138 | ERROR_VARIABLE BemanExemplar_error 139 | GET "${BemanExemplar_depObj}" 140 | "git_tag" 141 | ) 142 | if(BemanExemplar_error) 143 | message( 144 | FATAL_ERROR 145 | "${BemanExemplar_errorPrefix}: ${BemanExemplar_error}" 146 | ) 147 | endif() 148 | 149 | if(method STREQUAL "FIND_PACKAGE") 150 | if(package_name STREQUAL BemanExemplar_pkgName) 151 | string( 152 | APPEND 153 | BemanExemplar_debug 154 | "Redirecting find_package calls for ${BemanExemplar_pkgName} " 155 | "to FetchContent logic.\n" 156 | ) 157 | string( 158 | APPEND 159 | BemanExemplar_debug 160 | "Fetching ${BemanExemplar_repo} at " 161 | "${BemanExemplar_tag} according to ${BemanExemplar_lockfile}." 162 | ) 163 | message(DEBUG "${BemanExemplar_debug}") 164 | FetchContent_Declare( 165 | "${BemanExemplar_name}" 166 | GIT_REPOSITORY "${BemanExemplar_repo}" 167 | GIT_TAG "${BemanExemplar_tag}" 168 | EXCLUDE_FROM_ALL 169 | ) 170 | set(INSTALL_GTEST OFF) # Disable GoogleTest installation 171 | FetchContent_MakeAvailable("${BemanExemplar_name}") 172 | 173 | # Important! _FOUND tells CMake that `find_package` is 174 | # not needed for this package anymore 175 | set("${BemanExemplar_pkgName}_FOUND" TRUE PARENT_SCOPE) 176 | endif() 177 | endif() 178 | endforeach() 179 | endfunction() 180 | 181 | cmake_language( 182 | SET_DEPENDENCY_PROVIDER BemanExemplar_provideDependency 183 | SUPPORTED_METHODS FIND_PACKAGE 184 | ) 185 | 186 | # Add this dir to the module path so that `find_package(beman-install-library)` works 187 | list(APPEND CMAKE_PREFIX_PATH "${CMAKE_CURRENT_LIST_DIR}") 188 | -------------------------------------------------------------------------------- /tests/beman/utf_view/std_archetypes/iterator.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #ifndef BEMAN_UTF_VIEW_TESTS_STD_ARCHETYPES_ITERATOR_HPP 9 | #define BEMAN_UTF_VIEW_TESTS_STD_ARCHETYPES_ITERATOR_HPP 10 | 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | 17 | namespace beman::utf_view::tests::std_archetypes { 18 | 19 | #ifdef _MSC_VER 20 | // https://developercommunity.visualstudio.com/t/MSVC-fails-to-resolve-operator-overloa/10560905 21 | using boolean_testable_archetype = bool; 22 | #else 23 | using boolean_testable_archetype = boolean_testable_archetype1; 24 | #endif 25 | 26 | namespace detail { 27 | 28 | template 29 | struct reference_type_archetype; 30 | 31 | template 32 | struct value_type_archetype { 33 | constexpr value_type_archetype() 34 | : x{} { } 35 | constexpr value_type_archetype(reference_type_archetype const& ref); 36 | value_type_archetype(value_type_archetype const&) = default; 37 | value_type_archetype& operator=(value_type_archetype const&) = default; 38 | value_type_archetype(value_type_archetype&&) = default; 39 | value_type_archetype& operator=(value_type_archetype&&) = default; 40 | constexpr boolean_testable_archetype operator==( 41 | value_type_archetype const& other) const { 42 | return boolean_testable_archetype{x == other.x}; 43 | } 44 | constexpr boolean_testable_archetype operator!=( 45 | value_type_archetype const& other) const { 46 | return boolean_testable_archetype{x != other.x}; 47 | } 48 | State x; 49 | }; 50 | 51 | template 52 | struct reference_type_archetype { 53 | constexpr boolean_testable_archetype operator==( 54 | value_type_archetype const& value) const { 55 | return boolean_testable_archetype{x == value.x}; 56 | } 57 | constexpr boolean_testable_archetype operator!=( 58 | value_type_archetype const& value) const { 59 | return boolean_testable_archetype{x != value.x}; 60 | } 61 | State const& x; 62 | }; 63 | 64 | template 65 | constexpr value_type_archetype::value_type_archetype( 66 | reference_type_archetype const& ref) 67 | : x{ref.x} { } 68 | 69 | } // namespace detail 70 | 71 | template , 72 | typename ReferenceType = detail::reference_type_archetype> 73 | struct basic_input_iterator_archetype { 74 | using value_type = ValueType; 75 | using reference_type = ReferenceType; 76 | using difference_type = std::ptrdiff_t; 77 | using iterator_concept = std::input_iterator_tag; 78 | constexpr explicit basic_input_iterator_archetype(State x_in) 79 | : x{std::move(x_in)} { } 80 | basic_input_iterator_archetype(basic_input_iterator_archetype const&) = delete; 81 | basic_input_iterator_archetype& operator=(basic_input_iterator_archetype const&) = 82 | delete; 83 | basic_input_iterator_archetype(basic_input_iterator_archetype&&) = default; 84 | basic_input_iterator_archetype& operator=(basic_input_iterator_archetype&&) = default; 85 | constexpr reference_type operator*() const { 86 | return reference_type{x}; 87 | } 88 | constexpr basic_input_iterator_archetype& operator++() { 89 | ++x; 90 | return *this; 91 | } 92 | constexpr void operator++(int) { 93 | ++x; 94 | } 95 | State x; 96 | }; 97 | 98 | using input_iterator_archetype = basic_input_iterator_archetype; 99 | 100 | template , 101 | typename ReferenceType = detail::reference_type_archetype> 102 | struct basic_forward_iterator_archetype { 103 | using value_type = ValueType; 104 | using reference_type = ReferenceType; 105 | using difference_type = std::ptrdiff_t; 106 | using iterator_concept = std::forward_iterator_tag; 107 | constexpr basic_forward_iterator_archetype() 108 | : x{} { } 109 | constexpr explicit basic_forward_iterator_archetype(State x_in) 110 | : x{std::move(x_in)} { } 111 | basic_forward_iterator_archetype(basic_forward_iterator_archetype const&) = default; 112 | basic_forward_iterator_archetype& operator=(basic_forward_iterator_archetype const&) = 113 | default; 114 | basic_forward_iterator_archetype(basic_forward_iterator_archetype&&) = default; 115 | basic_forward_iterator_archetype& operator=(basic_forward_iterator_archetype&&) = 116 | default; 117 | 118 | constexpr reference_type operator*() const { 119 | return reference_type{x}; 120 | } 121 | constexpr basic_forward_iterator_archetype& operator++() { 122 | ++x; 123 | return *this; 124 | } 125 | constexpr basic_forward_iterator_archetype operator++(int) { 126 | basic_forward_iterator_archetype result{*this}; 127 | ++x; 128 | return result; 129 | } 130 | 131 | friend boolean_testable_archetype operator==( 132 | basic_forward_iterator_archetype const& lhs, 133 | basic_forward_iterator_archetype const& rhs) { 134 | return boolean_testable_archetype{lhs.x == rhs.x}; 135 | } 136 | friend boolean_testable_archetype operator!=( 137 | basic_forward_iterator_archetype const& lhs, 138 | basic_forward_iterator_archetype const& rhs) { 139 | return boolean_testable_archetype{lhs.x != rhs.x}; 140 | } 141 | 142 | State x; 143 | }; 144 | 145 | using forward_iterator_archetype = basic_forward_iterator_archetype; 146 | 147 | template , 148 | typename ReferenceType = detail::reference_type_archetype> 149 | struct basic_bidirectional_iterator_archetype { 150 | using value_type = ValueType; 151 | using reference_type = ReferenceType; 152 | using difference_type = std::ptrdiff_t; 153 | using iterator_concept = std::bidirectional_iterator_tag; 154 | constexpr basic_bidirectional_iterator_archetype() 155 | : x{} { } 156 | constexpr explicit basic_bidirectional_iterator_archetype(State x_in) 157 | : x{std::move(x_in)} { } 158 | basic_bidirectional_iterator_archetype(basic_bidirectional_iterator_archetype const&) = 159 | default; 160 | basic_bidirectional_iterator_archetype& operator=( 161 | basic_bidirectional_iterator_archetype const&) = default; 162 | basic_bidirectional_iterator_archetype(basic_bidirectional_iterator_archetype&&) = 163 | default; 164 | basic_bidirectional_iterator_archetype& operator=( 165 | basic_bidirectional_iterator_archetype&&) = default; 166 | 167 | constexpr reference_type operator*() const { 168 | return reference_type{x}; 169 | } 170 | constexpr basic_bidirectional_iterator_archetype& operator++() { 171 | ++x; 172 | return *this; 173 | } 174 | constexpr basic_bidirectional_iterator_archetype operator++(int) { 175 | basic_bidirectional_iterator_archetype result{*this}; 176 | ++x; 177 | return result; 178 | } 179 | constexpr basic_bidirectional_iterator_archetype& operator--() { 180 | --x; 181 | return *this; 182 | } 183 | constexpr basic_bidirectional_iterator_archetype operator--(int) { 184 | basic_bidirectional_iterator_archetype result{*this}; 185 | --x; 186 | return result; 187 | } 188 | 189 | friend boolean_testable_archetype operator==( 190 | basic_bidirectional_iterator_archetype const& lhs, 191 | basic_bidirectional_iterator_archetype const& rhs) { 192 | return boolean_testable_archetype{lhs.x == rhs.x}; 193 | } 194 | friend boolean_testable_archetype operator!=( 195 | basic_bidirectional_iterator_archetype const& lhs, 196 | basic_bidirectional_iterator_archetype const& rhs) { 197 | return boolean_testable_archetype{lhs.x != rhs.x}; 198 | } 199 | 200 | State x; 201 | }; 202 | 203 | using bidirectional_iterator_archetype = 204 | basic_bidirectional_iterator_archetype; 205 | 206 | } // namespace beman::utf_view::tests::std_archetypes 207 | 208 | #endif // BEMAN_UTF_VIEW_TESTS_STD_ARCHETYPES_ITERATOR_HPP 209 | -------------------------------------------------------------------------------- /CMakePresets.json: -------------------------------------------------------------------------------- 1 | { 2 | "version": 6, 3 | "configurePresets": [ 4 | { 5 | "name": "_root-config", 6 | "hidden": true, 7 | "generator": "Ninja", 8 | "binaryDir": "${sourceDir}/build/${presetName}", 9 | "cacheVariables": { 10 | "CMAKE_CXX_STANDARD": "23", 11 | "CMAKE_EXPORT_COMPILE_COMMANDS": "ON", 12 | "CMAKE_PROJECT_TOP_LEVEL_INCLUDES": "./infra/cmake/use-fetch-content.cmake" 13 | } 14 | }, 15 | { 16 | "name": "_debug-base", 17 | "hidden": true, 18 | "cacheVariables": { 19 | "CMAKE_BUILD_TYPE": "Debug", 20 | "BEMAN_BUILDSYS_SANITIZER": "MaxSan" 21 | } 22 | }, 23 | { 24 | "name": "_release-base", 25 | "hidden": true, 26 | "cacheVariables": { 27 | "CMAKE_BUILD_TYPE": "RelWithDebInfo" 28 | } 29 | }, 30 | { 31 | "name": "gcc-debug", 32 | "displayName": "GCC Debug Build", 33 | "inherits": [ 34 | "_root-config", 35 | "_debug-base" 36 | ], 37 | "cacheVariables": { 38 | "CMAKE_TOOLCHAIN_FILE": "infra/cmake/gnu-toolchain.cmake" 39 | } 40 | }, 41 | { 42 | "name": "gcc-release", 43 | "displayName": "GCC Release Build", 44 | "inherits": [ 45 | "_root-config", 46 | "_release-base" 47 | ], 48 | "cacheVariables": { 49 | "CMAKE_TOOLCHAIN_FILE": "infra/cmake/gnu-toolchain.cmake" 50 | } 51 | }, 52 | { 53 | "name": "llvm-debug", 54 | "displayName": "Clang Debug Build", 55 | "inherits": [ 56 | "_root-config", 57 | "_debug-base" 58 | ], 59 | "cacheVariables": { 60 | "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-toolchain.cmake" 61 | } 62 | }, 63 | { 64 | "name": "llvm-release", 65 | "displayName": "Clang Release Build", 66 | "inherits": [ 67 | "_root-config", 68 | "_release-base" 69 | ], 70 | "cacheVariables": { 71 | "CMAKE_TOOLCHAIN_FILE": "infra/cmake/llvm-toolchain.cmake" 72 | } 73 | }, 74 | { 75 | "name": "appleclang-debug", 76 | "displayName": "Appleclang Debug Build", 77 | "inherits": [ 78 | "_root-config", 79 | "_debug-base" 80 | ], 81 | "cacheVariables": { 82 | "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" 83 | } 84 | }, 85 | { 86 | "name": "appleclang-release", 87 | "displayName": "Appleclang Release Build", 88 | "inherits": [ 89 | "_root-config", 90 | "_release-base" 91 | ], 92 | "cacheVariables": { 93 | "CMAKE_TOOLCHAIN_FILE": "infra/cmake/appleclang-toolchain.cmake" 94 | } 95 | }, 96 | { 97 | "name": "msvc-debug", 98 | "displayName": "MSVC Debug Build", 99 | "inherits": [ 100 | "_root-config", 101 | "_debug-base" 102 | ], 103 | "cacheVariables": { 104 | "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" 105 | } 106 | }, 107 | { 108 | "name": "msvc-release", 109 | "displayName": "MSVC Release Build", 110 | "inherits": [ 111 | "_root-config", 112 | "_release-base" 113 | ], 114 | "cacheVariables": { 115 | "CMAKE_TOOLCHAIN_FILE": "infra/cmake/msvc-toolchain.cmake" 116 | } 117 | } 118 | ], 119 | "buildPresets": [ 120 | { 121 | "name": "_root-build", 122 | "hidden": true, 123 | "jobs": 0 124 | }, 125 | { 126 | "name": "gcc-debug", 127 | "configurePreset": "gcc-debug", 128 | "inherits": [ 129 | "_root-build" 130 | ] 131 | }, 132 | { 133 | "name": "gcc-release", 134 | "configurePreset": "gcc-release", 135 | "inherits": [ 136 | "_root-build" 137 | ] 138 | }, 139 | { 140 | "name": "llvm-debug", 141 | "configurePreset": "llvm-debug", 142 | "inherits": [ 143 | "_root-build" 144 | ] 145 | }, 146 | { 147 | "name": "llvm-release", 148 | "configurePreset": "llvm-release", 149 | "inherits": [ 150 | "_root-build" 151 | ] 152 | }, 153 | { 154 | "name": "appleclang-debug", 155 | "configurePreset": "appleclang-debug", 156 | "inherits": [ 157 | "_root-build" 158 | ] 159 | }, 160 | { 161 | "name": "appleclang-release", 162 | "configurePreset": "appleclang-release", 163 | "inherits": [ 164 | "_root-build" 165 | ] 166 | }, 167 | { 168 | "name": "msvc-debug", 169 | "configurePreset": "msvc-debug", 170 | "inherits": [ 171 | "_root-build" 172 | ] 173 | }, 174 | { 175 | "name": "msvc-release", 176 | "configurePreset": "msvc-release", 177 | "inherits": [ 178 | "_root-build" 179 | ] 180 | } 181 | ], 182 | "testPresets": [ 183 | { 184 | "name": "_test_base", 185 | "hidden": true, 186 | "output": { 187 | "outputOnFailure": true 188 | }, 189 | "execution": { 190 | "noTestsAction": "error", 191 | "stopOnFailure": true 192 | } 193 | }, 194 | { 195 | "name": "gcc-debug", 196 | "inherits": "_test_base", 197 | "configurePreset": "gcc-debug" 198 | }, 199 | { 200 | "name": "gcc-release", 201 | "inherits": "_test_base", 202 | "configurePreset": "gcc-release" 203 | }, 204 | { 205 | "name": "llvm-debug", 206 | "inherits": "_test_base", 207 | "configurePreset": "llvm-debug" 208 | }, 209 | { 210 | "name": "llvm-release", 211 | "inherits": "_test_base", 212 | "configurePreset": "llvm-release" 213 | }, 214 | { 215 | "name": "appleclang-debug", 216 | "inherits": "_test_base", 217 | "configurePreset": "appleclang-debug" 218 | }, 219 | { 220 | "name": "appleclang-release", 221 | "inherits": "_test_base", 222 | "configurePreset": "appleclang-release" 223 | }, 224 | { 225 | "name": "msvc-debug", 226 | "inherits": "_test_base", 227 | "configurePreset": "msvc-debug" 228 | }, 229 | { 230 | "name": "msvc-release", 231 | "inherits": "_test_base", 232 | "configurePreset": "msvc-release" 233 | } 234 | ], 235 | "workflowPresets": [ 236 | { 237 | "name": "gcc-debug", 238 | "steps": [ 239 | { 240 | "type": "configure", 241 | "name": "gcc-debug" 242 | }, 243 | { 244 | "type": "build", 245 | "name": "gcc-debug" 246 | }, 247 | { 248 | "type": "test", 249 | "name": "gcc-debug" 250 | } 251 | ] 252 | }, 253 | { 254 | "name": "gcc-release", 255 | "steps": [ 256 | { 257 | "type": "configure", 258 | "name": "gcc-release" 259 | }, 260 | { 261 | "type": "build", 262 | "name": "gcc-release" 263 | }, 264 | { 265 | "type": "test", 266 | "name": "gcc-release" 267 | } 268 | ] 269 | }, 270 | { 271 | "name": "llvm-debug", 272 | "steps": [ 273 | { 274 | "type": "configure", 275 | "name": "llvm-debug" 276 | }, 277 | { 278 | "type": "build", 279 | "name": "llvm-debug" 280 | }, 281 | { 282 | "type": "test", 283 | "name": "llvm-debug" 284 | } 285 | ] 286 | }, 287 | { 288 | "name": "llvm-release", 289 | "steps": [ 290 | { 291 | "type": "configure", 292 | "name": "llvm-release" 293 | }, 294 | { 295 | "type": "build", 296 | "name": "llvm-release" 297 | }, 298 | { 299 | "type": "test", 300 | "name": "llvm-release" 301 | } 302 | ] 303 | }, 304 | { 305 | "name": "appleclang-debug", 306 | "steps": [ 307 | { 308 | "type": "configure", 309 | "name": "appleclang-debug" 310 | }, 311 | { 312 | "type": "build", 313 | "name": "appleclang-debug" 314 | }, 315 | { 316 | "type": "test", 317 | "name": "appleclang-debug" 318 | } 319 | ] 320 | }, 321 | { 322 | "name": "appleclang-release", 323 | "steps": [ 324 | { 325 | "type": "configure", 326 | "name": "appleclang-release" 327 | }, 328 | { 329 | "type": "build", 330 | "name": "appleclang-release" 331 | }, 332 | { 333 | "type": "test", 334 | "name": "appleclang-release" 335 | } 336 | ] 337 | }, 338 | { 339 | "name": "msvc-debug", 340 | "steps": [ 341 | { 342 | "type": "configure", 343 | "name": "msvc-debug" 344 | }, 345 | { 346 | "type": "build", 347 | "name": "msvc-debug" 348 | }, 349 | { 350 | "type": "test", 351 | "name": "msvc-debug" 352 | } 353 | ] 354 | }, 355 | { 356 | "name": "msvc-release", 357 | "steps": [ 358 | { 359 | "type": "configure", 360 | "name": "msvc-release" 361 | }, 362 | { 363 | "type": "build", 364 | "name": "msvc-release" 365 | }, 366 | { 367 | "type": "test", 368 | "name": "msvc-release" 369 | } 370 | ] 371 | } 372 | ] 373 | } 374 | -------------------------------------------------------------------------------- /infra/tools/beman-submodule/beman-submodule: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env python3 2 | 3 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 4 | 5 | import argparse 6 | import configparser 7 | import filecmp 8 | import glob 9 | import os 10 | import shutil 11 | import subprocess 12 | import sys 13 | import tempfile 14 | from pathlib import Path 15 | 16 | 17 | def directory_compare( 18 | reference: str | Path, actual: str | Path, ignore, allow_untracked_files: bool): 19 | reference, actual = Path(reference), Path(actual) 20 | 21 | compared = filecmp.dircmp(reference, actual, ignore=ignore) 22 | if (compared.left_only 23 | or (compared.right_only and not allow_untracked_files) 24 | or compared.diff_files): 25 | return False 26 | for common_dir in compared.common_dirs: 27 | path1 = reference / common_dir 28 | path2 = actual / common_dir 29 | if not directory_compare(path1, path2, ignore, allow_untracked_files): 30 | return False 31 | return True 32 | 33 | class BemanSubmodule: 34 | def __init__( 35 | self, dirpath: str | Path, remote: str, commit_hash: str, 36 | allow_untracked_files: bool): 37 | self.dirpath = Path(dirpath) 38 | self.remote = remote 39 | self.commit_hash = commit_hash 40 | self.allow_untracked_files = allow_untracked_files 41 | 42 | def parse_beman_submodule_file(path): 43 | config = configparser.ConfigParser() 44 | read_result = config.read(path) 45 | def fail(): 46 | raise Exception(f'Failed to parse {path} as a .beman_submodule file') 47 | if not read_result: 48 | fail() 49 | if not 'beman_submodule' in config: 50 | fail() 51 | if not 'remote' in config['beman_submodule']: 52 | fail() 53 | if not 'commit_hash' in config['beman_submodule']: 54 | fail() 55 | allow_untracked_files = config.getboolean( 56 | 'beman_submodule', 'allow_untracked_files', fallback=False) 57 | return BemanSubmodule( 58 | Path(path).resolve().parent, 59 | config['beman_submodule']['remote'], 60 | config['beman_submodule']['commit_hash'], 61 | allow_untracked_files) 62 | 63 | def get_beman_submodule(path: str | Path): 64 | beman_submodule_filepath = Path(path) / '.beman_submodule' 65 | 66 | if beman_submodule_filepath.is_file(): 67 | return parse_beman_submodule_file(beman_submodule_filepath) 68 | else: 69 | return None 70 | 71 | def find_beman_submodules_in(path): 72 | path = Path(path) 73 | assert path.is_dir() 74 | 75 | result = [] 76 | for dirpath, _, filenames in path.walk(): 77 | if '.beman_submodule' in filenames: 78 | result.append(parse_beman_submodule_file(dirpath / '.beman_submodule')) 79 | return sorted(result, key=lambda module: module.dirpath) 80 | 81 | def cwd_git_repository_path(): 82 | process = subprocess.run( 83 | ['git', 'rev-parse', '--show-toplevel'], capture_output=True, text=True, 84 | check=False) 85 | if process.returncode == 0: 86 | return process.stdout.strip() 87 | elif "fatal: not a git repository" in process.stderr: 88 | return None 89 | else: 90 | raise Exception("git rev-parse --show-toplevel failed") 91 | 92 | def clone_beman_submodule_into_tmpdir(beman_submodule, remote): 93 | tmpdir = tempfile.TemporaryDirectory() 94 | subprocess.run( 95 | ['git', 'clone', beman_submodule.remote, tmpdir.name], capture_output=True, 96 | check=True) 97 | if not remote: 98 | subprocess.run( 99 | ['git', '-C', tmpdir.name, 'reset', '--hard', beman_submodule.commit_hash], 100 | capture_output=True, check=True) 101 | return tmpdir 102 | 103 | def get_paths(beman_submodule): 104 | tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, False) 105 | paths = set(glob.glob('*', root_dir=Path(tmpdir.name), include_hidden=True)) 106 | paths.remove('.git') 107 | return paths 108 | 109 | def beman_submodule_status(beman_submodule): 110 | tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, False) 111 | if directory_compare( 112 | tmpdir.name, beman_submodule.dirpath, ['.beman_submodule', '.git'], 113 | beman_submodule.allow_untracked_files): 114 | status_character=' ' 115 | else: 116 | status_character='+' 117 | parent_repo_path = cwd_git_repository_path() 118 | if not parent_repo_path: 119 | raise Exception('this is not a git repository') 120 | relpath = Path(beman_submodule.dirpath).relative_to(Path(parent_repo_path)) 121 | return status_character + ' ' + beman_submodule.commit_hash + ' ' + str(relpath) 122 | 123 | def beman_submodule_update(beman_submodule, remote): 124 | tmpdir = clone_beman_submodule_into_tmpdir(beman_submodule, remote) 125 | tmp_path = Path(tmpdir.name) 126 | sha_process = subprocess.run( 127 | ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, 128 | cwd=tmp_path) 129 | 130 | if beman_submodule.allow_untracked_files: 131 | for path in get_paths(beman_submodule): 132 | path2 = Path(beman_submodule.dirpath) / path 133 | if Path(path2).is_dir(): 134 | shutil.rmtree(path2) 135 | elif Path(path2).is_file(): 136 | os.remove(path2) 137 | else: 138 | shutil.rmtree(beman_submodule.dirpath) 139 | 140 | submodule_path = tmp_path / '.beman_submodule' 141 | with open(submodule_path, 'w') as f: 142 | f.write('[beman_submodule]\n') 143 | f.write(f'remote={beman_submodule.remote}\n') 144 | f.write(f'commit_hash={sha_process.stdout.strip()}\n') 145 | if beman_submodule.allow_untracked_files: 146 | f.write(f'allow_untracked_files=True\n') 147 | shutil.rmtree(tmp_path / '.git') 148 | shutil.copytree(tmp_path, beman_submodule.dirpath, dirs_exist_ok=True) 149 | 150 | def update_command(remote, path): 151 | if not path: 152 | parent_repo_path = cwd_git_repository_path() 153 | if not parent_repo_path: 154 | raise Exception('this is not a git repository') 155 | beman_submodules = find_beman_submodules_in(parent_repo_path) 156 | else: 157 | beman_submodule = get_beman_submodule(path) 158 | if not beman_submodule: 159 | raise Exception(f'{path} is not a beman_submodule') 160 | beman_submodules = [beman_submodule] 161 | for beman_submodule in beman_submodules: 162 | beman_submodule_update(beman_submodule, remote) 163 | 164 | def add_command(repository, path, allow_untracked_files): 165 | tmpdir = tempfile.TemporaryDirectory() 166 | subprocess.run( 167 | ['git', 'clone', repository], capture_output=True, check=True, cwd=tmpdir.name) 168 | repository_name = os.listdir(tmpdir.name)[0] 169 | if not path: 170 | path = Path(repository_name) 171 | else: 172 | path = Path(path) 173 | if not allow_untracked_files and path.exists(): 174 | raise Exception(f'{path} exists') 175 | path.mkdir(exist_ok=allow_untracked_files) 176 | tmpdir_repo = Path(tmpdir.name) / repository_name 177 | sha_process = subprocess.run( 178 | ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, 179 | cwd=tmpdir_repo) 180 | with open(tmpdir_repo / '.beman_submodule', 'w') as f: 181 | f.write('[beman_submodule]\n') 182 | f.write(f'remote={repository}\n') 183 | f.write(f'commit_hash={sha_process.stdout.strip()}\n') 184 | if allow_untracked_files: 185 | f.write(f'allow_untracked_files=True\n') 186 | shutil.rmtree(tmpdir_repo /'.git') 187 | shutil.copytree(tmpdir_repo, path, dirs_exist_ok=True) 188 | 189 | def status_command(paths): 190 | if not paths: 191 | parent_repo_path = cwd_git_repository_path() 192 | if not parent_repo_path: 193 | raise Exception('this is not a git repository') 194 | beman_submodules = find_beman_submodules_in(parent_repo_path) 195 | else: 196 | beman_submodules = [] 197 | for path in paths: 198 | beman_submodule = get_beman_submodule(path) 199 | if not beman_submodule: 200 | raise Exception(f'{path} is not a beman_submodule') 201 | beman_submodules.append(beman_submodule) 202 | for beman_submodule in beman_submodules: 203 | print(beman_submodule_status(beman_submodule)) 204 | 205 | def get_parser(): 206 | parser = argparse.ArgumentParser(description='Beman pseudo-submodule tool') 207 | subparsers = parser.add_subparsers(dest='command', help='available commands') 208 | parser_update = subparsers.add_parser('update', help='update beman_submodules') 209 | parser_update.add_argument( 210 | '--remote', action='store_true', 211 | help='update a beman_submodule to its latest from upstream') 212 | parser_update.add_argument( 213 | 'beman_submodule_path', nargs='?', 214 | help='relative path to the beman_submodule to update') 215 | parser_add = subparsers.add_parser('add', help='add a new beman_submodule') 216 | parser_add.add_argument('repository', help='git repository to add') 217 | parser_add.add_argument( 218 | 'path', nargs='?', help='path where the repository will be added') 219 | parser_add.add_argument( 220 | '--allow-untracked-files', action='store_true', 221 | help='the beman_submodule will not occupy the subdirectory exclusively') 222 | parser_status = subparsers.add_parser( 223 | 'status', help='show the status of beman_submodules') 224 | parser_status.add_argument('paths', nargs='*') 225 | return parser 226 | 227 | def parse_args(args): 228 | return get_parser().parse_args(args); 229 | 230 | def usage(): 231 | return get_parser().format_help() 232 | 233 | def run_command(args): 234 | if args.command == 'update': 235 | update_command(args.remote, args.beman_submodule_path) 236 | elif args.command == 'add': 237 | add_command(args.repository, args.path, args.allow_untracked_files) 238 | elif args.command == 'status': 239 | status_command(args.paths) 240 | else: 241 | raise Exception(usage()) 242 | 243 | def check_for_git(path): 244 | env = os.environ.copy() 245 | if path is not None: 246 | env["PATH"] = path 247 | return shutil.which("git", path=env.get("PATH")) is not None 248 | 249 | def main(): 250 | try: 251 | if not check_for_git(None): 252 | raise Exception('git not found in PATH') 253 | args = parse_args(sys.argv[1:]) 254 | run_command(args) 255 | except Exception as e: 256 | print("Error:", e, file=sys.stderr) 257 | sys.exit(1) 258 | 259 | if __name__ == '__main__': 260 | main() 261 | -------------------------------------------------------------------------------- /paper/P3705.md: -------------------------------------------------------------------------------- 1 | --- 2 | title: "A Sentinel for Null-Terminated Strings" 3 | document: P3705R2 4 | date: 2025-06-18 5 | audience: 6 | - SG-9 Ranges 7 | - LEWG 8 | author: 9 | - name: Eddie Nolan 10 | email: 11 | toc: true 12 | monofont: "DejaVu Sans Mono" 13 | 14 | --- 15 | 16 | # Motivation 17 | 18 | Consider using `std::views::take` to get the first five characters from a string. If we 19 | pass a string literal with five or more characters, it looks okay: 20 | 21 | ```cpp 22 | static_assert(std::string{std::from_range, "Brubeck" | std::views::take(5)} == "Brube"); // passes 23 | ``` 24 | 25 | However, if we pass it a string literal with fewer than five characters, we get in trouble: 26 | 27 | ```cpp 28 | // static_assert(std::string{std::from_range, "Dave" | std::views::take(5)} == "Dave"); // fails 29 | using namespace std::string_view_literals; 30 | static_assert(std::string{std::from_range, "Dave" | std::views::take(5)} == "Dave\0"sv); // passes 31 | ``` 32 | 33 | The reason the null terminator is included in the output here is because undecayed string 34 | literals are arrays of characters, including the null terminator, which the ranges library 35 | treats like any other array: 36 | 37 | ```cpp 38 | #include 39 | #include 40 | #include 41 | 42 | static_assert(std::is_same_v, const char[4]>); 43 | static_assert(std::ranges::equal("foo", std::array{'f', 'o', 'o', '\0'})); 44 | ``` 45 | 46 | A common workaround is to wrap the string literal in `std::string_view`. But this has a 47 | performance cost: despite the fact that we only care about the first five characters, we 48 | still need to make a pass through the entire string to find the null terminator: 49 | 50 | ```cpp 51 | constexpr std::string take_five(char const* long_string) { 52 | std::string_view const long_string_view = long_string; // read all of long_string! 53 | return std::string{std::from_range, long_string_view | std::views::take(5)}; 54 | } 55 | ``` 56 | 57 | This paper introduces `null_sentinel` to solve this problem. It ends the range when it 58 | encounters a null: 59 | 60 | ```cpp 61 | constexpr std::string take_five(char const* long_string) { 62 | std::ranges::subrange const long_string_range(long_string, std::null_sentinel); // lazy! 63 | return std::string{std::from_range, long_string_range | std::views::take(5)}; 64 | } 65 | ``` 66 | 67 | It also introduces a `null_term` CPO for the common case of constructing a subrange like 68 | the one in the above example: 69 | 70 | ```cpp 71 | constexpr std::string take_five(char const* long_string) { 72 | return std::string{std::from_range, std::views::null_term(long_string) | std::views::take(5)}; 73 | } 74 | ``` 75 | 76 | The sentinel type matches any iterator position `it` at which `*it` is equal to a 77 | value-initialized object of type `iter_value_t`. This works for null-terminated 78 | strings, but can also serve as the sentinel for any range terminated by a 79 | value-initialized value. 80 | 81 | For example, `null_term` can be used to iterate `argv` and `environ`. The following program demonstrates this: 82 | 83 | ```cpp 84 | #include 85 | 86 | extern char** environ; 87 | 88 | int main(int argc, char** argv) { 89 | std::println("argv: {}", std::views::null_term(argv)); 90 | std::println("environ: {}", std::views::null_term(environ)); 91 | } 92 | ``` 93 | 94 | Output: 95 | 96 | ``` 97 | $ env --ignore-environment FOO=bar BAZ=quux ./test corge 98 | argv: ["./test", "corge"] 99 | environ: ["FOO=bar", "BAZ=quux"] 100 | ``` 101 | 102 | # Naming discussion 103 | 104 | ## Bad names 105 | 106 | ### `c_str` 107 | 108 | `range-v3` 109 | [provides](https://ericniebler.github.io/range-v3/structranges_1_1views_1_1c__str__fn.html) 110 | similar functionality to `null_term` using the name `c_str`-- unlike `null_term`, `c_str` 111 | only works with `char`, `wchar_t`, and so forth. 112 | 113 | The name `c_str` would not make sense here because, for example, `std::views::c_str(argv)` 114 | is nonsensical and misleading as to what the range is doing (terminating the sequence of 115 | strings rather than terminating the strings themselves). 116 | 117 | ### `zstring_sentinel` and `zstring` 118 | 119 | An example from Barry Revzin's [@P2210R2] provides similar functionality under the names 120 | `zstring_sentinel` and `zstring`. 121 | 122 | I dislike this name for similar reasons as `c_str`. `argv` is not a "string." 123 | 124 | Additionally, it invites confusion with [@P3655R1] `zstring_view`. 125 | 126 | ## Good names 127 | 128 | ### Status quo: `null_sentinel` and `null_term` 129 | 130 | The advantages of these names are that they are terse, they do not include the word 131 | "string," and the value-initialized `iter_value_t` value is referred to as "null," which 132 | reflects the language we use in the standard ("null terminated byte string," "null 133 | terminated multibyte string," "null terminated character sequence," "null pointer") 134 | 135 | ### `zero_termination_sentinel` 136 | 137 | This is used by think-cell's 138 | [implementation](https://github.com/think-cell/think-cell-library/blob/main/tc%2Frange%2Fmeta.h#L248) 139 | of this utility. I like it slightly less because it's more verbose but also consider it 140 | acceptable. 141 | 142 | ### Your suggestion here 143 | 144 | This is not an exhaustive list. 145 | 146 | 147 | # Wording 148 | 149 | ## Exposition-only helper concept `@*default-initializable-and-equality-comparable-iter-value*@` 150 | 151 | ```cpp 152 | namespace std { 153 | 154 | template 155 | concept @*default-initializable-and-equality-comparable-iter-value*@ = 156 | default_initializable> && 157 | equality_comparable_with, iter_value_t>; // @*exposition only*@ 158 | 159 | } 160 | ``` 161 | 162 | ## Null sentinel 163 | 164 | ```cpp 165 | namespace std { 166 | 167 | struct null_sentinel_t { 168 | template 169 | requires @*default-initializable-and-equality-comparable-iter-value*@ 170 | friend constexpr bool operator==(I const& it, null_sentinel_t); 171 | }; 172 | 173 | inline constexpr null_sentinel_t null_sentinel; 174 | 175 | } 176 | ``` 177 | 178 | ### Operations 179 | 180 | ```c++ 181 | template 182 | requires @*default-initializable-and-equality-comparable-iter-value*@ 183 | friend constexpr bool operator==(I const& it, null_sentinel_t); 184 | ``` 185 | 186 | Effects: 187 | 188 | Equivalent to `return *it == iter_value_t();`. 189 | 190 | ## `null_term` CPO 191 | 192 | ```cpp 193 | namespace std::views { 194 | 195 | inline constexpr @*unspecified*@ null_term; 196 | 197 | } 198 | ``` 199 | 200 | The name `null_term` denotes a customization point object ([customization.point.object]). 201 | Given a subexpression `E`, the expression `null_term(E)` is expression-equivalent to 202 | `ranges::subrange(E, null_sentinel)`. 203 | 204 | ## Feature test macro 205 | 206 | Header `` synopsis [version.syn] 207 | 208 | ```cpp 209 | #define __cpp_lib_ranges_null_sentinel 2026XXL 210 | ``` 211 | 212 | # History 213 | 214 | ## Changelog 215 | 216 | This proposal was originally written by Zach Laine as part of [@P2728R0], then updated and 217 | split out by Eddie Nolan. 218 | 219 | ### Changes since P2728R1 220 | 221 | - Generalize `null_sentinel_t` to a non-Unicode-specific facility. 222 | 223 | ### Changes since P2728R3 224 | 225 | - Change `null_sentinel_t` back to being Unicode-specific. 226 | 227 | ### Changes since P2728R4 228 | 229 | - Move `null_sentinel_t` to `std`, remove its `base` member function, and make 230 | it useful for more than just pointers, based on SG-9 guidance. 231 | 232 | ### Changes since P2728R5 233 | 234 | - Simplify the complicated constraint on the comparison operator for 235 | `null_sentinel_t`. 236 | 237 | ### Changes since P2728R6 238 | 239 | - Fix a bug in `null_sentinel_t` causing it not to satisfy `sentinel_for` by 240 | changing its `operator==` to return `bool`. 241 | - Fix a bug in `null_sentinel_t` where it did not support non-copyable input 242 | iterators by having `operator==` take input iterators by reference. 243 | 244 | ### Changes since P2728R7 245 | 246 | - Move `null_sentinel` and `null_term` into P3705 247 | 248 | ### Changes since P3705R0 249 | 250 | - Fix off-by-one in textual description in motivation section 251 | - Add example with argv and environ 252 | - Add `unchecked_take_before` design alternative 253 | 254 | ### Changes since P3705R1 255 | 256 | - Remove `unchecked_take_before` and pass-by-value design alternatives 257 | - Add polls and minutes from Sofia 2025 SG9 review 258 | - Move `null_term` to namespace `std::views` 259 | - Add feature test macro 260 | - Add naming discussion 261 | - Use `iter_value_t()` over `iter_value_t{}` to avoid `initializer_list` interaction 262 | 263 | ## Relevant Polls/Minutes 264 | 265 | ### SG9 review of P3705R1 on 2025-06-18 during Sofia 2025 266 | 267 | - [Minutes](https://wiki.edg.com/bin/view/Wg21sofia2025/NotesSG9P3705R0) 268 | 269 | __POLL:__ We would like something like the proposed `null_sentinel` regardless of the addition of a hypothetical `unchecked_take_before` view that may come in the future. 270 | 271 | __Outcome:__ No objection to unanimous consent 272 | 273 | __POLL:__ Always pass the iterator by reference to `operator==`, move `null_term` to namespace `std::views`, add a discussion about alternative names, and forward P3705R1 with those changes to LEWG for inclusion in C++29. 274 | 275 | |SF|F|N|A|SA| 276 | |-|-|-|-|-| 277 | |9|7|0|0|0| 278 | 279 | __# Of Authors:__ 1 280 | 281 | __Author's Position:__ SF 282 | 283 | __Attendance:__ 18 (2 abstentions) 284 | 285 | __Outcome:__ Unanimous consent 286 | 287 | ### SG9 review of D2728R4 on 2023-06-12 during Varna 2023 288 | 289 | - [Minutes](https://wiki.edg.com/bin/view/Wg21varna/P2728#SG9-2023-06-12) 290 | 291 | __POLL:__ Move null_sentinel_t to std:: namespace 292 | 293 | |SF|F|N|A|SA| 294 | |-|-|-|-|-| 295 | |1|3|1|0|0| 296 | 297 | __# Of Authors:__ 1 298 | 299 | __Author's Position:__ F 300 | 301 | __Attendance:__ 9 (4 abstentions) 302 | 303 | __Outcome:__ Consensus in favor 304 | 305 |
306 | 307 | __POLL:__ Remove null_sentinel_t::base member function from the proposal 308 | 309 | |SF|F|N|A|SA| 310 | |-|-|-|-|-| 311 | |0|4|1|0|0| 312 | 313 | __# Of Authors:__ 1 314 | 315 | __Author's Position:__ F 316 | 317 | __Attendance:__ 8 (3 abstentions) 318 | 319 | __Outcome:__ Consensus in favor 320 | 321 | ### SG16 review of P2728R3 on 2023-05-10 (Telecon) 322 | 323 | - [Minutes](https://github.com/sg16-unicode/sg16-meetings/blob/master/README-2023.md#may-10th-2023) 324 | 325 | __POLL:__ Separate `std::null_sentinel_t` from P2728 into a separate paper for 326 | SG9 and LEWG; SG16 does not need to see it again. 327 | 328 | +----+---+---+---+----+ 329 | | SF | F | N | A | SA | 330 | +====+===+===+===+====+ 331 | | 1 |1 |4 |2 | 1 | 332 | +----+---+---+---+----+ 333 | 334 | __Attendance:__ 12 (3 abstentions) 335 | 336 | __Outcome:__ No consensus; author's discretion for how to continue. 337 | -------------------------------------------------------------------------------- /examples/readme_examples.cpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #include 9 | #include 10 | #include 11 | #include 12 | #include 13 | #include 14 | #include 15 | #include 16 | #include 17 | #include 18 | #include 19 | #include 20 | #include 21 | #include 22 | #include 23 | #include 24 | 25 | namespace beman::utf_view::examples { 26 | 27 | using namespace std::string_view_literals; 28 | 29 | template 30 | std::basic_string sanitize(CharT const* str) { 31 | return null_term(str) | to_utf | std::ranges::to>(); 32 | } 33 | 34 | std::optional last_nonascii(std::ranges::view auto str) { 35 | for (auto c : str | as_char8_t | to_utf32 | std::views::reverse | 36 | std::views::filter([](char32_t c) { return c > 0x7f; })) { 37 | return c; 38 | } 39 | return std::nullopt; 40 | } 41 | 42 | #ifdef _MSC_VER 43 | bool windows_path() { 44 | std::vector path_as_ints = {U'C', U':', U'\x00010000'}; 45 | std::filesystem::path path = 46 | path_as_ints | as_char32_t | std::ranges::to(); 47 | const auto& native_path = path.native(); 48 | if (native_path != std::wstring{L'C', L':', L'\xD800', L'\xDC00'}) { 49 | return false; 50 | } 51 | return true; 52 | } 53 | #endif 54 | 55 | std::u8string as_char32_t_example() { 56 | auto get_icu_code_points = [] { return std::vector{0x1F574, 0xFFFD}; }; 57 | std::vector input = get_icu_code_points(); 58 | // This is ill-formed without the as_char32_t adaptation. 59 | auto input_utf8 = input | as_char32_t | to_utf8 | std::ranges::to(); 60 | return input_utf8; 61 | } 62 | 63 | std::string enum_to_string(utf_transcoding_error ec) { 64 | switch (ec) { 65 | case utf_transcoding_error::truncated_utf8_sequence: 66 | return "truncated_utf8_sequence"; 67 | case utf_transcoding_error::unpaired_high_surrogate: 68 | return "unpaired_high_surrogate"; 69 | case utf_transcoding_error::unpaired_low_surrogate: 70 | return "unpaired_low_surrogate"; 71 | case utf_transcoding_error::unexpected_utf8_continuation_byte: 72 | return "unexpected_utf8_continuation_byte"; 73 | case utf_transcoding_error::overlong: 74 | return "overlong"; 75 | case utf_transcoding_error::encoded_surrogate: 76 | return "encoded_surrogate"; 77 | case utf_transcoding_error::out_of_range: 78 | return "out_of_range"; 79 | case utf_transcoding_error::invalid_utf8_leading_byte: 80 | return "invalid_utf8_leading_byte"; 81 | } 82 | std::unreachable(); 83 | } 84 | 85 | template 86 | std::basic_string transcode_or_throw(std::basic_string_view input) { 87 | std::basic_string result; 88 | auto view = input | to_utf_or_error; 89 | for (auto it = view.begin(), end = view.end(); it != end; ++it) { 90 | if ((*it).has_value()) { 91 | result.push_back(**it); 92 | } else { 93 | throw std::runtime_error("error at position " + 94 | std::to_string(it.base() - input.begin()) + ": " + 95 | enum_to_string((*it).error())); 96 | } 97 | } 98 | return result; 99 | } 100 | 101 | #if 0 102 | // Deliberately broken by double-transcode optimization in the CPO 103 | template 104 | using transcode_result = std::ranges::in_out_result; 105 | 106 | template S, std::output_iterator O> 107 | transcode_result transcode_to_utf32(I first, S last, O out) { 108 | auto r = std::ranges::subrange(first, last) | to_utf32; 109 | 110 | auto copy_result = std::ranges::copy(r, out); 111 | 112 | return transcode_result{copy_result.in.base(), copy_result.out}; 113 | } 114 | 115 | bool transcode_to_utf32_test() { 116 | std::u8string_view char8_string{u8"\xf0\x9f\x95\xb4\xef\xbf\xbd"}; 117 | auto utf16_transcoding_view{char8_string | to_utf16}; 118 | std::u32string char32_string{}; 119 | auto transcode_result{transcode_to_utf32(utf16_transcoding_view.begin(), 120 | utf16_transcoding_view.end(), 121 | std::back_insert_iterator{char32_string})}; 122 | auto expected_in_it{utf16_transcoding_view.begin()}; 123 | std::ranges::advance(expected_in_it, 3); 124 | if (expected_in_it != transcode_result.in) { 125 | return false; 126 | } 127 | return true; 128 | } 129 | #endif 130 | 131 | #ifndef _MSC_VER 132 | enum class suit : std::uint8_t { 133 | spades = 0xA, 134 | hearts = 0xB, 135 | diamonds = 0xC, 136 | clubs = 0xD 137 | }; 138 | 139 | // Unicode playing card characters are laid out such that changing the second least 140 | // significant nibble changes the suit, e.g. 141 | // U+1F0A1 PLAYING CARD ACE OF SPADES 142 | // U+1F0B1 PLAYING CARD ACE OF HEARTS 143 | constexpr char32_t change_playing_card_suit(char32_t card, suit s) { 144 | if (U'\N{PLAYING CARD ACE OF SPADES}' <= card && card <= U'\N{PLAYING CARD KING OF CLUBS}') { 145 | return (card & ~(0xF << 4)) | (static_cast(s) << 4); 146 | } 147 | return card; 148 | } 149 | 150 | bool change_playing_card_suit_test() { 151 | std::u8string_view const spades = u8"🂡🂢🂣🂤🂥🂦🂧🂨🂩🂪🂫🂭🂮"; 152 | std::u8string const hearts = 153 | spades | 154 | to_utf32 | 155 | std::views::transform(std::bind_back(change_playing_card_suit, suit::hearts)) | 156 | to_utf8 | 157 | std::ranges::to(); 158 | if (hearts != u8"🂱🂲🂳🂴🂵🂶🂷🂸🂹🂺🂻🂽🂾") { 159 | return false; 160 | } 161 | return true; 162 | } 163 | #endif 164 | 165 | #ifdef __cpp_lib_containers_ranges 166 | static_assert(std::string{std::from_range, "Brubeck" | std::views::take(5)} == "Brube"); 167 | // static_assert(std::string{std::from_range, "Dave" | std::views::take(5)} == "Dave"); // fails 168 | using namespace std::string_view_literals; 169 | static_assert(std::string{std::from_range, "Dave" | std::views::take(5)} == "Dave\0"sv); // passes 170 | static_assert(std::is_same_v, const char[4]>); 171 | static_assert(std::ranges::equal("foo", std::array{'f', 'o', 'o', '\0'})); 172 | constexpr std::string take_five_a(char const* long_string) { 173 | std::string_view const long_string_view = long_string; // read all of long_string! 174 | return std::string{std::from_range, long_string_view | std::views::take(5)}; 175 | } 176 | constexpr std::string take_five_b(char const* long_string) { 177 | std::ranges::subrange const long_string_range(long_string, null_sentinel); // lazy! 178 | return std::string{std::from_range, long_string_range | std::views::take(5)}; 179 | } 180 | constexpr std::string take_five_c(char const* long_string) { 181 | return std::string{std::from_range, null_term(long_string) | std::views::take(5)}; 182 | } 183 | static_assert(take_five_a("Dave") == "Dave"sv); // passes 184 | static_assert(take_five_a("Brubeck") == "Brube"sv); // passes 185 | static_assert(take_five_b("Dave") == "Dave"sv); // passes 186 | static_assert(take_five_b("Brubeck") == "Brube"sv); // passes 187 | static_assert(take_five_c("Dave") == "Dave"sv); // passes 188 | static_assert(take_five_c("Brubeck") == "Brube"sv); // passes 189 | #endif 190 | 191 | #ifndef _MSC_VER 192 | static_assert((u8"\xf0\x9f\x99\x82"sv | to_utf32 | std::ranges::to()) == U"\x0001F642"); 193 | static_assert((u8"\xf0\x9f\x99\x82"sv | std::views::take(3) | to_utf32 | std::ranges::to()) == U"�"); 194 | static_assert( 195 | *(u8"\xf0\x9f\x99\x82"sv | std::views::take(3) | to_utf32_or_error).begin() == 196 | std::unexpected{utf_transcoding_error::truncated_utf8_sequence}); 197 | #endif 198 | 199 | #ifndef _MSC_VER 200 | bool basis_operation() { 201 | std::u8string invalid_utf8{u8"\xf0\x9f\x99\x82\xf0\x9f\x99"}; 202 | auto to_utf8_1{invalid_utf8 | to_utf8 | std::ranges::to()}; 203 | auto to_utf8_2{ 204 | invalid_utf8 205 | | to_utf8_or_error 206 | | std::views::transform( 207 | [](std::expected c) 208 | -> detail::fake_inplace_vector 209 | { 210 | if (c.has_value()) { 211 | return {c.value()}; 212 | } else { 213 | // U+FFFD 214 | return {u8'\xEF', u8'\xBF', u8'\xBD'}; 215 | } 216 | }) 217 | | std::views::join 218 | | std::ranges::to()}; 219 | return to_utf8_1 == to_utf8_2; 220 | } 221 | 222 | static_assert( 223 | !std::ranges::equal(u8"foo"sv | to_utf32, std::array{U'f', U'o', U'o', U'\0'})); 224 | static_assert( 225 | std::ranges::equal(u8"foo"sv | to_utf32, std::array{U'f', U'o', U'o'})); 226 | #endif 227 | 228 | template 229 | std::basic_string transcode_to(std::basic_string const& input) { 230 | return input | to_utf | std::ranges::to>(); 231 | } 232 | 233 | bool readme_examples() { 234 | using namespace std::string_view_literals; 235 | #ifndef _MSC_VER 236 | std::u32string hello_world = 237 | u8"こんにちは世界"sv | to_utf32 | std::ranges::to(); 238 | if (hello_world != U"こんにちは世界") { 239 | return false; 240 | } 241 | if (sanitize(u8"\xc2") != u8"\xef\xbf\xbd") { 242 | return false; 243 | } 244 | if (last_nonascii("hôtel"sv).value() != U'ô') { 245 | return false; 246 | } 247 | if (as_char32_t_example() != u8"\xf0\x9f\x95\xb4\xef\xbf\xbd") { 248 | return false; 249 | } 250 | auto foo = transcode_or_throw(u8"\xf0\x9f\x95\xb4\xef\xbf\xbd"); 251 | auto bar = std::u32string{U"\x0001F574\uFFFD"}; 252 | if (foo != bar) { 253 | return false; 254 | } 255 | try { 256 | transcode_or_throw(u8"\xc3\xa9\xff"); 257 | return false; 258 | } catch (std::exception const& e) { 259 | if (e.what() != "error at position 2: invalid_utf8_leading_byte"sv) { 260 | return false; 261 | } 262 | } 263 | if (!change_playing_card_suit_test()) { 264 | return false; 265 | } 266 | if (!basis_operation()) { 267 | return false; 268 | } 269 | #else 270 | if (!windows_path()) { 271 | return false; 272 | } 273 | #endif 274 | if (transcode_to(u8"foo") != U"foo") { 275 | return false; 276 | } 277 | return true; 278 | } 279 | 280 | } // namespace beman::utf_view::examples 281 | 282 | void windows_function(std::ranges::view auto) { 283 | 284 | } 285 | 286 | void foo(std::ranges::view auto v) { 287 | windows_function(v | beman::utf_view::to_utf16); 288 | } 289 | 290 | int main(int, char const* argv[]) { 291 | foo(beman::utf_view::null_term(argv[1]) | beman::utf_view::as_char8_t | beman::utf_view::to_utf32); 292 | beman::utf_view::examples::transcode_or_throw( 293 | u8"hi🙂" | std::views::take(5) | std::ranges::to()); 294 | return beman::utf_view::examples::readme_examples() ? EXIT_SUCCESS : EXIT_FAILURE; 295 | } 296 | -------------------------------------------------------------------------------- /infra/LICENSE: -------------------------------------------------------------------------------- 1 | 2 | Apache License 3 | Version 2.0, January 2004 4 | http://www.apache.org/licenses/ 5 | 6 | TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION 7 | 8 | 1. Definitions. 9 | 10 | "License" shall mean the terms and conditions for use, reproduction, 11 | and distribution as defined by Sections 1 through 9 of this document. 12 | 13 | "Licensor" shall mean the copyright owner or entity authorized by 14 | the copyright owner that is granting the License. 15 | 16 | "Legal Entity" shall mean the union of the acting entity and all 17 | other entities that control, are controlled by, or are under common 18 | control with that entity. For the purposes of this definition, 19 | "control" means (i) the power, direct or indirect, to cause the 20 | direction or management of such entity, whether by contract or 21 | otherwise, or (ii) ownership of fifty percent (50%) or more of the 22 | outstanding shares, or (iii) beneficial ownership of such entity. 23 | 24 | "You" (or "Your") shall mean an individual or Legal Entity 25 | exercising permissions granted by this License. 26 | 27 | "Source" form shall mean the preferred form for making modifications, 28 | including but not limited to software source code, documentation 29 | source, and configuration files. 30 | 31 | "Object" form shall mean any form resulting from mechanical 32 | transformation or translation of a Source form, including but 33 | not limited to compiled object code, generated documentation, 34 | and conversions to other media types. 35 | 36 | "Work" shall mean the work of authorship, whether in Source or 37 | Object form, made available under the License, as indicated by a 38 | copyright notice that is included in or attached to the work 39 | (an example is provided in the Appendix below). 40 | 41 | "Derivative Works" shall mean any work, whether in Source or Object 42 | form, that is based on (or derived from) the Work and for which the 43 | editorial revisions, annotations, elaborations, or other modifications 44 | represent, as a whole, an original work of authorship. For the purposes 45 | of this License, Derivative Works shall not include works that remain 46 | separable from, or merely link (or bind by name) to the interfaces of, 47 | the Work and Derivative Works thereof. 48 | 49 | "Contribution" shall mean any work of authorship, including 50 | the original version of the Work and any modifications or additions 51 | to that Work or Derivative Works thereof, that is intentionally 52 | submitted to Licensor for inclusion in the Work by the copyright owner 53 | or by an individual or Legal Entity authorized to submit on behalf of 54 | the copyright owner. For the purposes of this definition, "submitted" 55 | means any form of electronic, verbal, or written communication sent 56 | to the Licensor or its representatives, including but not limited to 57 | communication on electronic mailing lists, source code control systems, 58 | and issue tracking systems that are managed by, or on behalf of, the 59 | Licensor for the purpose of discussing and improving the Work, but 60 | excluding communication that is conspicuously marked or otherwise 61 | designated in writing by the copyright owner as "Not a Contribution." 62 | 63 | "Contributor" shall mean Licensor and any individual or Legal Entity 64 | on behalf of whom a Contribution has been received by Licensor and 65 | subsequently incorporated within the Work. 66 | 67 | 2. Grant of Copyright License. Subject to the terms and conditions of 68 | this License, each Contributor hereby grants to You a perpetual, 69 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 70 | copyright license to reproduce, prepare Derivative Works of, 71 | publicly display, publicly perform, sublicense, and distribute the 72 | Work and such Derivative Works in Source or Object form. 73 | 74 | 3. Grant of Patent License. Subject to the terms and conditions of 75 | this License, each Contributor hereby grants to You a perpetual, 76 | worldwide, non-exclusive, no-charge, royalty-free, irrevocable 77 | (except as stated in this section) patent license to make, have made, 78 | use, offer to sell, sell, import, and otherwise transfer the Work, 79 | where such license applies only to those patent claims licensable 80 | by such Contributor that are necessarily infringed by their 81 | Contribution(s) alone or by combination of their Contribution(s) 82 | with the Work to which such Contribution(s) was submitted. If You 83 | institute patent litigation against any entity (including a 84 | cross-claim or counterclaim in a lawsuit) alleging that the Work 85 | or a Contribution incorporated within the Work constitutes direct 86 | or contributory patent infringement, then any patent licenses 87 | granted to You under this License for that Work shall terminate 88 | as of the date such litigation is filed. 89 | 90 | 4. Redistribution. You may reproduce and distribute copies of the 91 | Work or Derivative Works thereof in any medium, with or without 92 | modifications, and in Source or Object form, provided that You 93 | meet the following conditions: 94 | 95 | (a) You must give any other recipients of the Work or 96 | Derivative Works a copy of this License; and 97 | 98 | (b) You must cause any modified files to carry prominent notices 99 | stating that You changed the files; and 100 | 101 | (c) You must retain, in the Source form of any Derivative Works 102 | that You distribute, all copyright, patent, trademark, and 103 | attribution notices from the Source form of the Work, 104 | excluding those notices that do not pertain to any part of 105 | the Derivative Works; and 106 | 107 | (d) If the Work includes a "NOTICE" text file as part of its 108 | distribution, then any Derivative Works that You distribute must 109 | include a readable copy of the attribution notices contained 110 | within such NOTICE file, excluding those notices that do not 111 | pertain to any part of the Derivative Works, in at least one 112 | of the following places: within a NOTICE text file distributed 113 | as part of the Derivative Works; within the Source form or 114 | documentation, if provided along with the Derivative Works; or, 115 | within a display generated by the Derivative Works, if and 116 | wherever such third-party notices normally appear. The contents 117 | of the NOTICE file are for informational purposes only and 118 | do not modify the License. You may add Your own attribution 119 | notices within Derivative Works that You distribute, alongside 120 | or as an addendum to the NOTICE text from the Work, provided 121 | that such additional attribution notices cannot be construed 122 | as modifying the License. 123 | 124 | You may add Your own copyright statement to Your modifications and 125 | may provide additional or different license terms and conditions 126 | for use, reproduction, or distribution of Your modifications, or 127 | for any such Derivative Works as a whole, provided Your use, 128 | reproduction, and distribution of the Work otherwise complies with 129 | the conditions stated in this License. 130 | 131 | 5. Submission of Contributions. Unless You explicitly state otherwise, 132 | any Contribution intentionally submitted for inclusion in the Work 133 | by You to the Licensor shall be under the terms and conditions of 134 | this License, without any additional terms or conditions. 135 | Notwithstanding the above, nothing herein shall supersede or modify 136 | the terms of any separate license agreement you may have executed 137 | with Licensor regarding such Contributions. 138 | 139 | 6. Trademarks. This License does not grant permission to use the trade 140 | names, trademarks, service marks, or product names of the Licensor, 141 | except as required for reasonable and customary use in describing the 142 | origin of the Work and reproducing the content of the NOTICE file. 143 | 144 | 7. Disclaimer of Warranty. Unless required by applicable law or 145 | agreed to in writing, Licensor provides the Work (and each 146 | Contributor provides its Contributions) on an "AS IS" BASIS, 147 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or 148 | implied, including, without limitation, any warranties or conditions 149 | of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A 150 | PARTICULAR PURPOSE. You are solely responsible for determining the 151 | appropriateness of using or redistributing the Work and assume any 152 | risks associated with Your exercise of permissions under this License. 153 | 154 | 8. Limitation of Liability. In no event and under no legal theory, 155 | whether in tort (including negligence), contract, or otherwise, 156 | unless required by applicable law (such as deliberate and grossly 157 | negligent acts) or agreed to in writing, shall any Contributor be 158 | liable to You for damages, including any direct, indirect, special, 159 | incidental, or consequential damages of any character arising as a 160 | result of this License or out of the use or inability to use the 161 | Work (including but not limited to damages for loss of goodwill, 162 | work stoppage, computer failure or malfunction, or any and all 163 | other commercial damages or losses), even if such Contributor 164 | has been advised of the possibility of such damages. 165 | 166 | 9. Accepting Warranty or Additional Liability. While redistributing 167 | the Work or Derivative Works thereof, You may choose to offer, 168 | and charge a fee for, acceptance of support, warranty, indemnity, 169 | or other liability obligations and/or rights consistent with this 170 | License. However, in accepting such obligations, You may act only 171 | on Your own behalf and on Your sole responsibility, not on behalf 172 | of any other Contributor, and only if You agree to indemnify, 173 | defend, and hold each Contributor harmless for any liability 174 | incurred by, or claims asserted against, such Contributor by reason 175 | of your accepting any such warranty or additional liability. 176 | 177 | END OF TERMS AND CONDITIONS 178 | 179 | APPENDIX: How to apply the Apache License to your work. 180 | 181 | To apply the Apache License to your work, attach the following 182 | boilerplate notice, with the fields enclosed by brackets "[]" 183 | replaced with your own identifying information. (Don't include 184 | the brackets!) The text should be enclosed in the appropriate 185 | comment syntax for the file format. We also recommend that a 186 | file or class name and description of purpose be included on the 187 | same "printed page" as the copyright notice for easier 188 | identification within third-party archives. 189 | 190 | Copyright [yyyy] [name of copyright owner] 191 | 192 | Licensed under the Apache License, Version 2.0 (the "License"); 193 | you may not use this file except in compliance with the License. 194 | You may obtain a copy of the License at 195 | 196 | http://www.apache.org/licenses/LICENSE-2.0 197 | 198 | Unless required by applicable law or agreed to in writing, software 199 | distributed under the License is distributed on an "AS IS" BASIS, 200 | WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 201 | See the License for the specific language governing permissions and 202 | limitations under the License. 203 | 204 | 205 | ---- LLVM Exceptions to the Apache 2.0 License ---- 206 | 207 | As an exception, if, as a result of your compiling your source code, portions 208 | of this Software are embedded into an Object form of such source code, you 209 | may redistribute such embedded portions in such Object form without complying 210 | with the conditions of Sections 4(a), 4(b) and 4(d) of the License. 211 | 212 | In addition, if you combine or link compiled forms of this Software with 213 | software that is licensed under the GPLv2 ("Combined Software") and if a 214 | court of competent jurisdiction determines that the patent provision (Section 215 | 3), the indemnity provision (Section 9) or other Section of the License 216 | conflicts with the conditions of the GPLv2, you may retroactively and 217 | prospectively choose to deem waived or otherwise exclude such Section(s) of 218 | the License, but only in their entirety and only with respect to the Combined 219 | Software. 220 | -------------------------------------------------------------------------------- /tests/beman/utf_view/test_iterators.hpp: -------------------------------------------------------------------------------- 1 | // SPDX-License-Identifier: BSL-1.0 2 | 3 | // Copyright Eddie Nolan and Jonathan Wakely 2023 - 2025. 4 | // Distributed under the Boost Software License, Version 1.0. 5 | // (See accompanying file LICENSE.txt or copy at 6 | // https://www.boost.org/LICENSE_1_0.txt) 7 | 8 | #ifndef BEMAN_UTF_VIEW_TESTS_TEST_ITERATORS_HPP 9 | #define BEMAN_UTF_VIEW_TESTS_TEST_ITERATORS_HPP 10 | 11 | namespace beman::utf_view::tests { 12 | 13 | template 14 | struct test_input_iterator { 15 | using value_type = CharT; 16 | using reference = CharT const&; 17 | using difference_type = std::ptrdiff_t; 18 | using iterator_concept = std::input_iterator_tag; 19 | constexpr explicit test_input_iterator(std::initializer_list const& list) 20 | : begin{list.begin()}, 21 | end{list.end()} { } 22 | test_input_iterator(test_input_iterator const&) = delete; 23 | test_input_iterator& operator=(test_input_iterator const&) = delete; 24 | test_input_iterator(test_input_iterator&&) = default; 25 | test_input_iterator& operator=(test_input_iterator&&) = default; 26 | constexpr reference operator*() const { 27 | return *begin; 28 | } 29 | constexpr test_input_iterator& operator++() { 30 | ++begin; 31 | return *this; 32 | } 33 | constexpr void operator++(int) { 34 | ++begin; 35 | } 36 | 37 | friend constexpr bool operator==(std::default_sentinel_t const&, 38 | test_input_iterator const& rhs) { 39 | return rhs.begin == rhs.end; 40 | } 41 | 42 | std::initializer_list::iterator begin; 43 | std::initializer_list::iterator end; 44 | }; 45 | 46 | template 47 | test_input_iterator(std::initializer_list) -> test_input_iterator; 48 | 49 | static_assert(std::input_iterator>); 50 | 51 | template 52 | struct test_comparable_input_iterator { 53 | using value_type = CharT; 54 | using reference = CharT const&; 55 | using difference_type = std::ptrdiff_t; 56 | using iterator_concept = std::input_iterator_tag; 57 | constexpr explicit test_comparable_input_iterator( 58 | std::initializer_list const& list) 59 | : begin{list.begin()}, 60 | end{list.end()} { } 61 | test_comparable_input_iterator(test_comparable_input_iterator const&) = delete; 62 | test_comparable_input_iterator& operator=(test_comparable_input_iterator const&) = 63 | delete; 64 | test_comparable_input_iterator(test_comparable_input_iterator&&) = default; 65 | test_comparable_input_iterator& operator=(test_comparable_input_iterator&&) = default; 66 | constexpr reference operator*() const { 67 | return *begin; 68 | } 69 | constexpr test_comparable_input_iterator& operator++() { 70 | ++begin; 71 | return *this; 72 | } 73 | constexpr void operator++(int) { 74 | ++begin; 75 | } 76 | 77 | friend constexpr bool operator==(std::default_sentinel_t const&, 78 | test_comparable_input_iterator const& rhs) { 79 | return rhs.begin == rhs.end; 80 | } 81 | 82 | friend constexpr bool operator==(test_comparable_input_iterator const&, 83 | test_comparable_input_iterator const&) = default; 84 | 85 | std::initializer_list::iterator begin; 86 | std::initializer_list::iterator end; 87 | }; 88 | 89 | template 90 | test_comparable_input_iterator(std::initializer_list) 91 | -> test_comparable_input_iterator; 92 | 93 | static_assert(std::input_iterator>); 94 | 95 | template 96 | struct test_copyable_input_iterator { 97 | static constexpr std::initializer_list empty{}; 98 | 99 | using value_type = CharT; 100 | using reference = CharT const&; 101 | using difference_type = std::ptrdiff_t; 102 | using iterator_concept = std::input_iterator_tag; 103 | constexpr test_copyable_input_iterator() 104 | : begin{empty.begin()}, 105 | end{empty.end()} { } 106 | constexpr explicit test_copyable_input_iterator( 107 | std::initializer_list const& list) 108 | : begin{list.begin()}, 109 | end{list.end()} { } 110 | test_copyable_input_iterator(test_copyable_input_iterator const&) = default; 111 | test_copyable_input_iterator& operator=(test_copyable_input_iterator const&) = default; 112 | test_copyable_input_iterator(test_copyable_input_iterator&&) = default; 113 | test_copyable_input_iterator& operator=(test_copyable_input_iterator&&) = default; 114 | constexpr reference operator*() const { 115 | return *begin; 116 | } 117 | constexpr test_copyable_input_iterator& operator++() { 118 | ++begin; 119 | return *this; 120 | } 121 | constexpr void operator++(int) { 122 | ++begin; 123 | } 124 | 125 | friend constexpr bool operator==(std::default_sentinel_t const&, 126 | test_copyable_input_iterator const& rhs) { 127 | return rhs.begin == rhs.end; 128 | } 129 | 130 | friend constexpr bool operator==(test_copyable_input_iterator const&, 131 | test_copyable_input_iterator const&) = default; 132 | 133 | std::initializer_list::iterator begin; 134 | std::initializer_list::iterator end; 135 | }; 136 | 137 | template 138 | test_copyable_input_iterator(std::initializer_list) 139 | -> test_copyable_input_iterator; 140 | 141 | static_assert(std::input_iterator>); 142 | static_assert(!std::forward_iterator>); 143 | 144 | template 145 | struct test_forward_iterator { 146 | static constexpr std::initializer_list empty{}; 147 | 148 | using value_type = CharT; 149 | using reference = CharT const&; 150 | using difference_type = std::ptrdiff_t; 151 | using iterator_concept = std::forward_iterator_tag; 152 | constexpr test_forward_iterator() 153 | : begin{empty.begin()}, 154 | end{empty.end()} { } 155 | constexpr explicit test_forward_iterator(std::initializer_list const& list) 156 | : begin{list.begin()}, 157 | end{list.end()} { } 158 | test_forward_iterator(test_forward_iterator const&) = default; 159 | test_forward_iterator& operator=(test_forward_iterator const&) = default; 160 | test_forward_iterator(test_forward_iterator&&) = default; 161 | test_forward_iterator& operator=(test_forward_iterator&&) = default; 162 | constexpr reference operator*() const { 163 | return *begin; 164 | } 165 | constexpr test_forward_iterator& operator++() { 166 | ++begin; 167 | return *this; 168 | } 169 | constexpr test_forward_iterator operator++(int) { 170 | auto ret = *this; 171 | ++begin; 172 | return ret; 173 | } 174 | 175 | friend constexpr bool operator==(std::default_sentinel_t const&, 176 | test_forward_iterator const& rhs) { 177 | return rhs.begin == rhs.end; 178 | } 179 | 180 | friend constexpr bool operator==(test_forward_iterator const&, 181 | test_forward_iterator const&) = default; 182 | 183 | std::initializer_list::iterator begin; 184 | std::initializer_list::iterator end; 185 | }; 186 | 187 | template 188 | test_forward_iterator(std::initializer_list) -> test_forward_iterator; 189 | 190 | static_assert(std::forward_iterator>); 191 | 192 | template 193 | struct test_bidi_iterator { 194 | static constexpr std::initializer_list empty{}; 195 | 196 | using value_type = CharT; 197 | using reference = CharT const&; 198 | using difference_type = std::ptrdiff_t; 199 | using iterator_concept = std::bidirectional_iterator_tag; 200 | constexpr test_bidi_iterator() 201 | : begin{empty.begin()}, 202 | end{empty.end()} { } 203 | constexpr explicit test_bidi_iterator(std::initializer_list const& list) 204 | : begin{list.begin()}, 205 | end{list.end()} { } 206 | test_bidi_iterator(test_bidi_iterator const&) = default; 207 | test_bidi_iterator& operator=(test_bidi_iterator const&) = default; 208 | test_bidi_iterator(test_bidi_iterator&&) = default; 209 | test_bidi_iterator& operator=(test_bidi_iterator&&) = default; 210 | constexpr reference operator*() const { 211 | return *begin; 212 | } 213 | constexpr test_bidi_iterator& operator++() { 214 | ++begin; 215 | return *this; 216 | } 217 | constexpr test_bidi_iterator operator++(int) { 218 | auto ret = *this; 219 | ++begin; 220 | return ret; 221 | } 222 | constexpr test_bidi_iterator& operator--() { 223 | --begin; 224 | return *this; 225 | } 226 | constexpr test_bidi_iterator operator--(int) { 227 | auto ret = *this; 228 | --begin; 229 | return ret; 230 | } 231 | 232 | friend constexpr bool operator==(std::default_sentinel_t const&, 233 | test_bidi_iterator const& rhs) { 234 | return rhs.begin == rhs.end; 235 | } 236 | friend constexpr bool operator==(test_bidi_iterator const&, 237 | test_bidi_iterator const&) = default; 238 | 239 | std::initializer_list::iterator begin; 240 | std::initializer_list::iterator end; 241 | }; 242 | 243 | template 244 | test_bidi_iterator(std::initializer_list) -> test_bidi_iterator; 245 | 246 | static_assert(std::bidirectional_iterator>); 247 | 248 | template 249 | struct test_random_access_iterator { 250 | static constexpr std::initializer_list empty{}; 251 | 252 | using value_type = CharT; 253 | using reference = CharT const&; 254 | using difference_type = std::ptrdiff_t; 255 | using iterator_concept = std::random_access_iterator_tag; 256 | constexpr test_random_access_iterator() 257 | : begin{empty.begin()}, 258 | end{empty.end()} { } 259 | constexpr explicit test_random_access_iterator(std::initializer_list const& list) 260 | : begin{list.begin()}, 261 | end{list.end()} { } 262 | test_random_access_iterator(test_random_access_iterator const&) = default; 263 | test_random_access_iterator& operator=(test_random_access_iterator const&) = default; 264 | test_random_access_iterator(test_random_access_iterator&&) = default; 265 | test_random_access_iterator& operator=(test_random_access_iterator&&) = default; 266 | constexpr reference operator*() const { 267 | return *begin; 268 | } 269 | constexpr test_random_access_iterator& operator++() { 270 | ++begin; 271 | return *this; 272 | } 273 | constexpr test_random_access_iterator operator++(int) { 274 | auto ret = *this; 275 | ++begin; 276 | return ret; 277 | } 278 | constexpr test_random_access_iterator& operator--() { 279 | --begin; 280 | return *this; 281 | } 282 | constexpr test_random_access_iterator operator--(int) { 283 | auto ret = *this; 284 | --begin; 285 | return ret; 286 | } 287 | constexpr reference operator[](difference_type n) const { 288 | auto retval = *this; 289 | retval = retval + n; 290 | return *retval; 291 | } 292 | constexpr test_random_access_iterator operator+(difference_type n) const { 293 | auto retval = *this; 294 | retval += n; 295 | return retval; 296 | } 297 | friend constexpr test_random_access_iterator operator+( 298 | difference_type n, test_random_access_iterator it) { 299 | return it + n; 300 | } 301 | constexpr test_random_access_iterator& operator+=(difference_type n) { 302 | begin += n; 303 | return *this; 304 | } 305 | constexpr test_random_access_iterator operator-(difference_type n) const { 306 | auto retval = *this; 307 | retval -= n; 308 | return retval; 309 | } 310 | constexpr difference_type operator-(test_random_access_iterator it) const { 311 | return begin - it.begin; 312 | } 313 | constexpr test_random_access_iterator& operator-=(difference_type n) { 314 | begin -= n; 315 | return *this; 316 | } 317 | friend constexpr auto operator<=>( 318 | test_random_access_iterator lhs, test_random_access_iterator rhs) { 319 | return lhs.begin <=> rhs.begin; 320 | } 321 | 322 | friend constexpr bool operator==(std::default_sentinel_t const&, 323 | test_random_access_iterator const& rhs) { 324 | return rhs.begin == rhs.end; 325 | } 326 | friend constexpr bool operator==(test_random_access_iterator const&, 327 | test_random_access_iterator const&) = default; 328 | 329 | std::initializer_list::iterator begin; 330 | std::initializer_list::iterator end; 331 | }; 332 | 333 | template 334 | test_random_access_iterator(std::initializer_list) -> 335 | test_random_access_iterator; 336 | 337 | static_assert(std::random_access_iterator>); 338 | 339 | } // namespace beman::utf_view::tests 340 | 341 | #endif // BEMAN_UTF_VIEW_TESTS_TEST_ITERATORS_HPP 342 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # beman.utf_view: C++29 UTF Transcoding Views 2 | 3 | 11 | 12 | ![Library Status](https://raw.githubusercontent.com/bemanproject/beman/refs/heads/main/images/badges/beman_badge-beman_library_under_development.svg) ![Continuous Integration Tests](https://github.com/bemanproject/utf_view/actions/workflows/ci_tests.yml/badge.svg) ![Lint Check (pre-commit)](https://github.com/bemanproject/utf_view/actions/workflows/pre-commit.yml/badge.svg) [![Coverage](https://coveralls.io/repos/github/bemanproject/utf_view/badge.svg?branch=main)](https://coveralls.io/github/bemanproject/utf_view?branch=main) ![Standard Target](https://github.com/bemanproject/beman/blob/main/images/badges/cpp29.svg) 13 | 14 | C++29 UTF transcoding features: 15 | - Transcoding UTF views `to_utf8`, `to_utf16`, and `to_utf32` 16 | - `null_sentinel` sentinel and `null_term` CPO for creating views of null-terminated strings 17 | - Casting views for creating views of `charN_t`, which are `as_char8`, `as_char16`, `as_char32` 18 | 19 | **Implements**: [Unicode in the Library, Part 1: UTF Transcoding (P2728R10)](https://isocpp.org/files/papers/P2728R10.html) and [A Sentinel for Null-Terminated Strings (P3705R2)](https://isocpp.org/files/papers/P3705R2.html) 20 | 21 | **Status**: [Under development and not yet ready for production use.](https://github.com/bemanproject/beman/blob/main/docs/beman_library_maturity_model.md#under-development-and-not-yet-ready-for-production-use) 22 | 23 | ## Examples 24 | 25 | Transcoding a UTF-8 string literal to a `std::u32string`: 26 | 27 | ```cpp 28 | std::u32string hello_world = 29 | u8"こんにちは世界"sv | beman::utf_view::to_utf32 | std::ranges::to(); 30 | ``` 31 | 32 | Sanitizing potentially invalid Unicode C strings by replacing invalid code units with replacement characters: 33 | 34 | ```cpp 35 | template 36 | std::basic_string sanitize(CharT const* str) { 37 | return beman::utf_view::null_term(str) | beman::utf_view::to_utf | std::ranges::to>(); 38 | } 39 | ``` 40 | 41 | Returning the final non-ASCII code point in a string, transcoding backwards lazily: 42 | 43 | ```cpp 44 | std::optional last_nonascii(std::ranges::view auto str) { 45 | for (auto c : str | beman::utf_view::to_utf32 | std::views::reverse 46 | | std::views::filter([](char32_t c) { return c > 0x7f; }) 47 | | std::views::take(1)) { 48 | return c; 49 | } 50 | return std::nullopt; 51 | } 52 | ``` 53 | 54 | Transcoding strings and throwing a descriptive exception on invalid UTF: 55 | 56 | (This example assumes the existence of the `enum_to_string` sample function 57 | from [P2996](https://isocpp.org/files/papers/P2996R6.html#enum-to-string)) 58 | 59 | ```cpp 60 | template 61 | std::basic_string transcode_or_throw(std::basic_string_view input) { 62 | std::basic_string result; 63 | auto view = input | to_utf; 64 | for (auto it = view.begin(), end = view.end(); it != end; ++it) { 65 | if (it.success()) { 66 | result.push_back(*it); 67 | } else { 68 | throw std::runtime_error("error at position " + 69 | std::to_string(it.base() - input.begin()) + ": " + 70 | enum_to_string(it.success().error())); 71 | } 72 | } 73 | return result; 74 | } 75 | ``` 76 | 77 | Changing the suits of Unicode playing card characters: 78 | 79 | ```cpp 80 | enum class suit : std::uint8_t { 81 | spades = 0xA, 82 | hearts = 0xB, 83 | diamonds = 0xC, 84 | clubs = 0xD 85 | }; 86 | 87 | // Unicode playing card characters are laid out such that changing the second least 88 | // significant nibble changes the suit, e.g. 89 | // U+1F0A1 PLAYING CARD ACE OF SPADES 90 | // U+1F0B1 PLAYING CARD ACE OF HEARTS 91 | constexpr char32_t change_playing_card_suit(char32_t card, suit s) { 92 | if (U'\N{PLAYING CARD ACE OF SPADES}' <= card && card <= U'\N{PLAYING CARD KING OF CLUBS}') { 93 | return (card & ~(0xF << 4)) | (static_cast(s) << 4); 94 | } 95 | return card; 96 | } 97 | 98 | void change_playing_card_suits() { 99 | std::u8string_view const spades = u8"🂡🂢🂣🂤🂥🂦🂧🂨🂩🂪🂫🂭🂮"; 100 | std::u8string const hearts = 101 | spades | 102 | to_utf32 | 103 | std::views::transform(std::bind_back(change_playing_card_suit, suit::hearts)) | 104 | to_utf8 | 105 | std::ranges::to(); 106 | assert(hearts == u8"🂱🂲🂳🂴🂵🂶🂷🂸🂹🂺🂻🂽🂾"); 107 | } 108 | ``` 109 | 110 | ## Dependencies 111 | 112 | ### Software 113 | 114 | beman.utf_view depends on [beman.transform_view_26](https://github.com/tzlaine/transform_view_26/). It brings in this library via CMake FetchContent. 115 | 116 | ### Build Environment 117 | 118 | This project requires at least the following to build: 119 | 120 | * C++23 121 | * CMake 3.27 122 | 123 | You can disable building tests by setting cmake option 124 | [`BEMAN_UTF_VIEW_BUILD_TESTS`](#beman_exemplar_build_tests) to `OFF` 125 | when configuring the project. 126 | 127 | ### Supported Platforms 128 | 129 | This project officially supports: 130 | 131 | - GCC 14 132 | - GCC 15 133 | - Clang 19/libc++ 134 | - Clang 20/libc++ 135 | - Clang 21/libc++ 136 | - MSVC 19.41 137 | 138 | Note: Building with Clang and libstdc++ is not currently supported due to various bugs. 139 | 140 | ## Development 141 | 142 | ### Develop using GitHub Codespace 143 | 144 | This project supports [GitHub Codespace](https://github.com/features/codespaces) 145 | via [Development Containers](https://containers.dev/), 146 | which allows rapid development and instant hacking in your browser. 147 | We recommend you using GitHub codespace to explore this project as this 148 | requires minimal setup. 149 | 150 | You can create a codespace for this project by clicking this badge: 151 | 152 | [![Open in GitHub Codespaces](https://github.com/codespaces/badge.svg)](https://codespaces.new/bemanproject/utf_view) 153 | 154 | For more detailed documentation regarding creating and developing inside of 155 | GitHub codespaces, please reference [this doc](https://docs.github.com/en/codespaces/). 156 | 157 | > [!NOTE] 158 | > 159 | > The codespace container may take up to 5 minutes to build and spin-up, 160 | > this is normal as we need to build a custom docker container to setup 161 | > an environment appropriate for beman projects. 162 | 163 | ### Develop locally on your machines 164 | 165 |
166 | For Linux based systems 167 | 168 | Beman libraries require [recent versions of CMake](#build-environment), 169 | we advise you to download CMake directly from [CMake's website](https://cmake.org/download/) 170 | or install it via the [Kitware apt library](https://apt.kitware.com/). 171 | 172 | A [supported compiler](#supported-platforms) should be available from your package manager. 173 | Alternatively you could use an install script from official compiler vendors. 174 | 175 | Here is an example of how to install the latest stable version of clang 176 | as per [the official LLVM install guide](https://apt.llvm.org/). 177 | 178 | ```bash 179 | bash -c "$(wget -O - https://apt.llvm.org/llvm.sh)" 180 | ``` 181 | 182 | The precise command and package name will vary depending on the Linux OS you are 183 | using. Be sure to consult documentation and the package repository for the system 184 | you are using. 185 | 186 |
187 | 188 | ### Configure and Build the Project Using CMake Presets 189 | 190 | This project recommends using [CMake Presets](https://cmake.org/cmake/help/latest/manual/cmake-presets.7.html) 191 | to configure, build and test the project. 192 | Appropriate presets for major compilers have been included by default. 193 | You can use `cmake --list-presets` to see all available presets. 194 | 195 | Here is an example to invoke the `gcc-debug` preset. 196 | 197 | ```shell 198 | cmake --workflow --preset gcc-debug 199 | ``` 200 | 201 | Generally, there are two kinds of presets, `debug` and `release`. 202 | 203 | The `debug` presets are designed to aid development, so it has debugging 204 | instrumentation enabled and as many sanitizers turned on as possible. 205 | 206 | > [!NOTE] 207 | > 208 | > The set of sanitizer supports are different across compilers. 209 | > You can checkout the exact set of compiler arguments by looking at the toolchain 210 | > files under the [`cmake`](cmake/) directory. 211 | 212 | The `release` presets are designed for use in production environments, 213 | thus they have the highest optimization turned on (e.g. `O3`). 214 | 215 | ### Configure and Build Manually 216 | 217 | While [CMake Presets](#configure-and-build-the-project-using-cmake-presets) are 218 | convenient, you might want to set different configuration or compiler arguments 219 | than any provided preset supports. 220 | 221 | To configure, build and test the project with extra arguments, 222 | you can run this set of commands. 223 | 224 | ```bash 225 | cmake -B build -S . -DCMAKE_CXX_STANDARD=23 # Your extra arguments here. 226 | cmake --build build 227 | ctest --test-dir build 228 | ``` 229 | 230 | > [!IMPORTANT] 231 | > 232 | > Beman projects are 233 | > [passive projects](https://github.com/bemanproject/beman/blob/main/docs/beman_standard.md#cmake), 234 | > therefore, 235 | > you will need to specify the C++ version via `CMAKE_CXX_STANDARD` 236 | > when manually configuring the project. 237 | 238 | ### Project specific configure arguments 239 | 240 | When configuring the project manually, 241 | you can pass an array of project specific CMake configs to customize your build. 242 | 243 | Project specific options are prefixed with `BEMAN_UTF_VIEW`. 244 | You can see the list of available options with: 245 | 246 | ```bash 247 | cmake -LH | grep "BEMAN_UTF_VIEW" -C 2 248 | ``` 249 | 250 |
251 | 252 | Details of CMake arguments. 253 | 254 | #### `BEMAN_UTF_VIEW_BUILD_TESTS` 255 | 256 | Enable building tests and test infrastructure. Default: ON. 257 | Values: { ON, OFF }. 258 | 259 | You can configure the project to have this option turned off via: 260 | 261 | ```bash 262 | cmake -B build -S . -DCMAKE_CXX_STANDARD=20 -DBEMAN_UTF_VIEW_BUILD_TESTS=OFF 263 | ``` 264 | 265 | #### `BEMAN_UTF_VIEW_BUILD_EXAMPLES` 266 | 267 | Enable building examples. Default: ON. Values: { ON, OFF }. 268 | 269 | #### `BEMAN_UTF_VIEW_BUILD_PAPER` 270 | 271 | Enable building the HTML version of P2728 and P3705 from their markdown sources. Default: ON. Values: { ON, OFF }. 272 | 273 |
274 | 275 | ## Integrate beman.utf_view into your project 276 | 277 | To use `beman.utf_view` in your C++ project, 278 | include an appropriate `beman.utf_view` header from your source code. 279 | 280 | ```c++ 281 | #include 282 | ``` 283 | 284 | > [!NOTE] 285 | > 286 | > `beman.utf_view` headers are to be included with the `beman/utf_view/` directories prefixed. 287 | > It is not supported to alter include search paths to spell the include target another way. For instance, 288 | > `#include ` is not a supported interface. 289 | 290 | How you will link your project against `beman.utf_view` will depend on your build system. 291 | CMake instructions are provided in following sections. 292 | 293 | ### Linking your project to beman.utf_view with CMake 294 | 295 | For CMake based projects, 296 | you will need to use the `beman.utf_view` CMake module 297 | to define the `beman::utf_view` CMake target: 298 | 299 | ```cmake 300 | find_package(beman.utf_view REQUIRED) 301 | ``` 302 | 303 | You will also need to add `beman::utf_view` to the link libraries of 304 | any libraries or executables that include beman.utf_view's header file. 305 | 306 | ```cmake 307 | target_link_libraries(yourlib PUBLIC beman::utf_view) 308 | ``` 309 | 310 | ### Produce beman.utf_view static library locally 311 | 312 | You can include utf_view's headers locally 313 | by producing a static `libbeman.utf_view.a` library. 314 | 315 | ```bash 316 | cmake --workflow --preset gcc-release 317 | cmake --install build/gcc-release --prefix /opt/beman.utf_view 318 | ``` 319 | 320 | This will generate such directory structure at `/opt/beman.utf_view`. 321 | 322 | ```txt 323 | /opt/beman.utf_view 324 | ├── include 325 | │ └── beman 326 | │ └── utf_view 327 | │ ├── // ... 328 | │ └── utf_view.hpp 329 | └── lib 330 | └── libbeman.utf_view.a 331 | ``` 332 | 333 | ## Paper 334 | 335 | beman.utf_view is based on P2728 and P3705. 336 | 337 | - The latest official revision of P2728 can be found at https://wg21.link/p2728 338 | - The latest official revision of P3705 can be found at https://wg21.link/p3705 339 | - The unofficial latest draft Markdown source for each paper can be found in this repository: 340 | - At [paper/P2828.md](https://github.com/bemanproject/utf_view/blob/main/paper/P2728.md) 341 | - At [paper/P3705.md](https://github.com/bemanproject/utf_view/blob/main/paper/P3705.md) 342 | - P2728's committee status page can be found at https://github.com/cplusplus/papers/issues/1422 343 | 344 | ## Authors 345 | 346 | The implementation of P2728 is a fork by Eddie Nolan of the implementation of P2728R6 in libstdc++ by Jonathan Wakely at [`gcc/libstdc++-v3/include/bits/unicode.h`](https://gcc.gnu.org/git/?p=gcc.git;a=blob;f=libstdc%2B%2B-v3/include/bits/unicode.h;h=66f8399fdfb05d85fcdb37fa9ec7c4089feb7a7d;hb=37a4c5c23a27). 347 | 348 | ## License 349 | 350 | beman.utf_view is licensed under BSL 1.0. 351 | -------------------------------------------------------------------------------- /infra/tools/beman-submodule/test/test_beman_submodule.py: -------------------------------------------------------------------------------- 1 | # SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception 2 | 3 | import glob 4 | import os 5 | import pytest 6 | import shutil 7 | import stat 8 | import subprocess 9 | import tempfile 10 | from pathlib import Path 11 | 12 | # https://stackoverflow.com/a/19011259 13 | import types 14 | import importlib.machinery 15 | loader = importlib.machinery.SourceFileLoader( 16 | 'beman_submodule', 17 | str(Path(__file__).parent.resolve().parent / 'beman-submodule')) 18 | beman_submodule = types.ModuleType(loader.name) 19 | loader.exec_module(beman_submodule) 20 | 21 | def create_test_git_repository(): 22 | tmpdir = tempfile.TemporaryDirectory() 23 | tmp_path = Path(tmpdir.name) 24 | 25 | subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) 26 | def make_commit(a_txt_contents): 27 | with open(tmp_path / 'a.txt', 'w') as f: 28 | f.write(a_txt_contents) 29 | subprocess.run( 30 | ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) 31 | subprocess.run( 32 | ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', 33 | '--author="test "', '-m', 'test'], 34 | check=True, cwd=tmpdir.name, capture_output=True) 35 | make_commit('A') 36 | make_commit('a') 37 | return tmpdir 38 | 39 | def create_test_git_repository2(): 40 | tmpdir = tempfile.TemporaryDirectory() 41 | tmp_path = Path(tmpdir.name) 42 | 43 | subprocess.run(['git', 'init'], check=True, cwd=tmpdir.name, capture_output=True) 44 | with open(tmp_path / 'a.txt', 'w') as f: 45 | f.write('a') 46 | subprocess.run( 47 | ['git', 'add', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) 48 | subprocess.run( 49 | ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', 50 | '--author="test "', '-m', 'test'], 51 | check=True, cwd=tmpdir.name, capture_output=True) 52 | os.remove(tmp_path / 'a.txt') 53 | subprocess.run( 54 | ['git', 'rm', 'a.txt'], check=True, cwd=tmpdir.name, capture_output=True) 55 | with open(tmp_path / 'b.txt', 'w') as f: 56 | f.write('b') 57 | subprocess.run( 58 | ['git', 'add', 'b.txt'], check=True, cwd=tmpdir.name, capture_output=True) 59 | subprocess.run( 60 | ['git', '-c', 'user.name=test', '-c', 'user.email=test@example.com', 'commit', 61 | '--author="test "', '-m', 'test'], 62 | check=True, cwd=tmpdir.name, capture_output=True) 63 | return tmpdir 64 | 65 | def test_directory_compare(): 66 | def create_dir_structure(dir_path: Path): 67 | bar_path = dir_path / 'bar' 68 | os.makedirs(bar_path) 69 | 70 | with open(dir_path / 'foo.txt', 'w') as f: 71 | f.write('foo') 72 | with open(bar_path / 'baz.txt', 'w') as f: 73 | f.write('baz') 74 | 75 | with tempfile.TemporaryDirectory() as dir_a, \ 76 | tempfile.TemporaryDirectory() as dir_b: 77 | path_a = Path(dir_a) 78 | path_b = Path(dir_b) 79 | 80 | create_dir_structure(path_a) 81 | create_dir_structure(path_b) 82 | 83 | assert beman_submodule.directory_compare(dir_a, dir_b, [], False) 84 | 85 | with open(path_a / 'bar' / 'quux.txt', 'w') as f: 86 | f.write('quux') 87 | 88 | assert not beman_submodule.directory_compare(path_a, path_b, [], False) 89 | assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], False) 90 | 91 | def test_directory_compare_untracked_files(): 92 | def create_dir_structure(dir_path: Path): 93 | bar_path = dir_path / 'bar' 94 | os.makedirs(bar_path) 95 | 96 | with open(dir_path / 'foo.txt', 'w') as f: 97 | f.write('foo') 98 | with open(bar_path / 'baz.txt', 'w') as f: 99 | f.write('baz') 100 | 101 | with tempfile.TemporaryDirectory() as reference, \ 102 | tempfile.TemporaryDirectory() as actual: 103 | path_a = Path(reference) 104 | path_b = Path(actual) 105 | 106 | create_dir_structure(path_a) 107 | create_dir_structure(path_b) 108 | (path_b / 'c.txt').touch() 109 | 110 | assert beman_submodule.directory_compare(reference, actual, [], True) 111 | 112 | with open(path_a / 'bar' / 'quux.txt', 'w') as f: 113 | f.write('quux') 114 | 115 | assert not beman_submodule.directory_compare(path_a, path_b, [], True) 116 | assert beman_submodule.directory_compare(path_a, path_b, ['quux.txt'], True) 117 | 118 | def test_parse_beman_submodule_file(): 119 | def valid_file(): 120 | tmpfile = tempfile.NamedTemporaryFile() 121 | tmpfile.write('[beman_submodule]\n'.encode('utf-8')) 122 | tmpfile.write( 123 | 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) 124 | tmpfile.write( 125 | 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) 126 | tmpfile.flush() 127 | module = beman_submodule.parse_beman_submodule_file(tmpfile.name) 128 | assert module.dirpath == Path(tmpfile.name).resolve().parent 129 | assert module.remote == 'git@github.com:bemanproject/infra.git' 130 | assert module.commit_hash == '9b88395a86c4290794e503e94d8213b6c442ae77' 131 | valid_file() 132 | def invalid_file_missing_remote(): 133 | threw = False 134 | try: 135 | tmpfile = tempfile.NamedTemporaryFile() 136 | tmpfile.write('[beman_submodule]\n'.encode('utf-8')) 137 | tmpfile.write( 138 | 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) 139 | tmpfile.flush() 140 | beman_submodule.parse_beman_submodule_file(tmpfile.name) 141 | except: 142 | threw = True 143 | assert threw 144 | invalid_file_missing_remote() 145 | def invalid_file_missing_commit_hash(): 146 | threw = False 147 | try: 148 | tmpfile = tempfile.NamedTemporaryFile() 149 | tmpfile.write('[beman_submodule]\n'.encode('utf-8')) 150 | tmpfile.write( 151 | 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) 152 | tmpfile.flush() 153 | beman_submodule.parse_beman_submodule_file(tmpfile.name) 154 | except: 155 | threw = True 156 | assert threw 157 | invalid_file_missing_commit_hash() 158 | def invalid_file_wrong_section(): 159 | threw = False 160 | try: 161 | tmpfile = tempfile.NamedTemporaryFile() 162 | tmpfile.write('[invalid]\n'.encode('utf-8')) 163 | tmpfile.write( 164 | 'remote=git@github.com:bemanproject/infra.git\n'.encode('utf-8')) 165 | tmpfile.write( 166 | 'commit_hash=9b88395a86c4290794e503e94d8213b6c442ae77\n'.encode('utf-8')) 167 | tmpfile.flush() 168 | beman_submodule.parse_beman_submodule_file(tmpfile.name) 169 | except: 170 | threw = True 171 | assert threw 172 | invalid_file_wrong_section() 173 | 174 | def test_get_beman_submodule(): 175 | tmpdir = create_test_git_repository() 176 | tmpdir2 = create_test_git_repository() 177 | original_cwd = Path.cwd() 178 | os.chdir(tmpdir2.name) 179 | beman_submodule.add_command(tmpdir.name, 'foo', False) 180 | assert beman_submodule.get_beman_submodule('foo') 181 | os.remove('foo/.beman_submodule') 182 | assert not beman_submodule.get_beman_submodule('foo') 183 | os.chdir(original_cwd) 184 | 185 | def test_find_beman_submodules_in(): 186 | tmpdir = create_test_git_repository() 187 | tmpdir2 = create_test_git_repository() 188 | original_cwd = Path.cwd() 189 | os.chdir(tmpdir2.name) 190 | beman_submodule.add_command(tmpdir.name, 'foo', False) 191 | beman_submodule.add_command(tmpdir.name, 'bar', False) 192 | beman_submodules = beman_submodule.find_beman_submodules_in(tmpdir2.name) 193 | sha_process = subprocess.run( 194 | ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, 195 | cwd=tmpdir.name) 196 | sha = sha_process.stdout.strip() 197 | assert beman_submodules[0].dirpath == Path(tmpdir2.name) / 'bar' 198 | assert beman_submodules[0].remote == tmpdir.name 199 | assert beman_submodules[0].commit_hash == sha 200 | assert beman_submodules[1].dirpath == Path(tmpdir2.name) / 'foo' 201 | assert beman_submodules[1].remote == tmpdir.name 202 | assert beman_submodules[1].commit_hash == sha 203 | os.chdir(original_cwd) 204 | 205 | def test_cwd_git_repository_path(): 206 | original_cwd = Path.cwd() 207 | tmpdir = tempfile.TemporaryDirectory() 208 | os.chdir(tmpdir.name) 209 | assert not beman_submodule.cwd_git_repository_path() 210 | subprocess.run(['git', 'init']) 211 | assert beman_submodule.cwd_git_repository_path() == tmpdir.name 212 | os.chdir(original_cwd) 213 | 214 | def test_clone_beman_submodule_into_tmpdir(): 215 | tmpdir = create_test_git_repository() 216 | tmpdir2 = create_test_git_repository() 217 | original_cwd = Path.cwd() 218 | os.chdir(tmpdir2.name) 219 | sha_process = subprocess.run( 220 | ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, 221 | cwd=tmpdir.name) 222 | sha = sha_process.stdout.strip() 223 | beman_submodule.add_command(tmpdir.name, 'foo', False) 224 | module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') 225 | module.commit_hash = sha 226 | tmpdir3 = beman_submodule.clone_beman_submodule_into_tmpdir(module, False) 227 | assert not beman_submodule.directory_compare( 228 | tmpdir.name, tmpdir3.name, ['.git'], False) 229 | tmpdir4 = beman_submodule.clone_beman_submodule_into_tmpdir(module, True) 230 | assert beman_submodule.directory_compare(tmpdir.name, tmpdir4.name, ['.git'], False) 231 | subprocess.run( 232 | ['git', 'reset', '--hard', sha], capture_output=True, check=True, 233 | cwd=tmpdir.name) 234 | assert beman_submodule.directory_compare(tmpdir.name, tmpdir3.name, ['.git'], False) 235 | os.chdir(original_cwd) 236 | 237 | def test_get_paths(): 238 | tmpdir = create_test_git_repository() 239 | tmpdir2 = create_test_git_repository() 240 | original_cwd = Path.cwd() 241 | os.chdir(tmpdir2.name) 242 | beman_submodule.add_command(tmpdir.name, 'foo', False) 243 | module = beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo') 244 | assert beman_submodule.get_paths(module) == set(['a.txt']) 245 | os.chdir(original_cwd) 246 | 247 | def test_beman_submodule_status(): 248 | tmpdir = create_test_git_repository() 249 | tmpdir2 = create_test_git_repository() 250 | original_cwd = Path.cwd() 251 | os.chdir(tmpdir2.name) 252 | beman_submodule.add_command(tmpdir.name, 'foo', False) 253 | sha_process = subprocess.run( 254 | ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, 255 | cwd=tmpdir.name) 256 | sha = sha_process.stdout.strip() 257 | assert ' ' + sha + ' foo' == beman_submodule.beman_submodule_status( 258 | beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) 259 | with open(Path(tmpdir2.name) / 'foo' / 'a.txt', 'w') as f: 260 | f.write('b') 261 | assert '+ ' + sha + ' foo' == beman_submodule.beman_submodule_status( 262 | beman_submodule.get_beman_submodule(Path(tmpdir2.name) / 'foo')) 263 | os.chdir(original_cwd) 264 | 265 | def test_update_command_no_paths(): 266 | tmpdir = create_test_git_repository() 267 | tmpdir2 = create_test_git_repository() 268 | original_cwd = Path.cwd() 269 | os.chdir(tmpdir2.name) 270 | orig_sha_process = subprocess.run( 271 | ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, 272 | cwd=tmpdir.name) 273 | orig_sha = orig_sha_process.stdout.strip() 274 | parent_sha_process = subprocess.run( 275 | ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, 276 | cwd=tmpdir.name) 277 | parent_sha = parent_sha_process.stdout.strip() 278 | parent_parent_sha_process = subprocess.run( 279 | ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, 280 | cwd=tmpdir.name) 281 | parent_parent_sha = parent_parent_sha_process.stdout.strip() 282 | subprocess.run( 283 | ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, 284 | cwd=tmpdir.name) 285 | beman_submodule.add_command(tmpdir.name, 'foo', False) 286 | beman_submodule.add_command(tmpdir.name, 'bar', False) 287 | subprocess.run( 288 | ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, 289 | cwd=tmpdir.name) 290 | with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: 291 | f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') 292 | with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: 293 | f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') 294 | beman_submodule.update_command(False, None) 295 | with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: 296 | assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' 297 | with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: 298 | assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' 299 | subprocess.run( 300 | ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, 301 | cwd=tmpdir.name) 302 | assert beman_submodule.directory_compare( 303 | tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) 304 | assert beman_submodule.directory_compare( 305 | tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) 306 | subprocess.run( 307 | ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, 308 | cwd=tmpdir.name) 309 | beman_submodule.update_command(True, None) 310 | with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: 311 | assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' 312 | with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: 313 | assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' 314 | assert beman_submodule.directory_compare( 315 | tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) 316 | assert beman_submodule.directory_compare( 317 | tmpdir.name, Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) 318 | os.chdir(original_cwd) 319 | 320 | def test_update_command_with_path(): 321 | tmpdir = create_test_git_repository() 322 | tmpdir2 = create_test_git_repository() 323 | original_cwd = Path.cwd() 324 | os.chdir(tmpdir2.name) 325 | orig_sha_process = subprocess.run( 326 | ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, 327 | cwd=tmpdir.name) 328 | orig_sha = orig_sha_process.stdout.strip() 329 | parent_sha_process = subprocess.run( 330 | ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, 331 | cwd=tmpdir.name) 332 | parent_sha = parent_sha_process.stdout.strip() 333 | parent_parent_sha_process = subprocess.run( 334 | ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, 335 | cwd=tmpdir.name) 336 | parent_parent_sha = parent_parent_sha_process.stdout.strip() 337 | subprocess.run( 338 | ['git', 'reset', '--hard', parent_parent_sha], capture_output=True, check=True, 339 | cwd=tmpdir.name) 340 | tmpdir_parent_parent_copy = tempfile.TemporaryDirectory() 341 | shutil.copytree(tmpdir.name, tmpdir_parent_parent_copy.name, dirs_exist_ok=True) 342 | beman_submodule.add_command(tmpdir.name, 'foo', False) 343 | beman_submodule.add_command(tmpdir.name, 'bar', False) 344 | subprocess.run( 345 | ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, 346 | cwd=tmpdir.name) 347 | with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: 348 | f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') 349 | with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'w') as f: 350 | f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n') 351 | beman_submodule.update_command(False, 'foo') 352 | with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: 353 | assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' 354 | with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: 355 | assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' 356 | subprocess.run( 357 | ['git', 'reset', '--hard', parent_sha], capture_output=True, check=True, 358 | cwd=tmpdir.name) 359 | assert beman_submodule.directory_compare( 360 | tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) 361 | assert beman_submodule.directory_compare( 362 | tmpdir_parent_parent_copy.name, 363 | Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) 364 | subprocess.run( 365 | ['git', 'reset', '--hard', orig_sha], capture_output=True, check=True, 366 | cwd=tmpdir.name) 367 | beman_submodule.update_command(True, 'foo') 368 | with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: 369 | assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={orig_sha}\n' 370 | with open(Path(tmpdir2.name) / 'bar' / '.beman_submodule', 'r') as f: 371 | assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\n' 372 | assert beman_submodule.directory_compare( 373 | tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) 374 | assert beman_submodule.directory_compare( 375 | tmpdir_parent_parent_copy.name, 376 | Path(tmpdir2.name) / 'bar', ['.git', '.beman_submodule'], False) 377 | os.chdir(original_cwd) 378 | 379 | def test_update_command_untracked_files(): 380 | tmpdir = create_test_git_repository2() 381 | tmpdir2 = create_test_git_repository() 382 | original_cwd = Path.cwd(); 383 | os.chdir(tmpdir2.name) 384 | orig_sha_process = subprocess.run( 385 | ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, 386 | cwd=tmpdir.name) 387 | orig_sha = orig_sha_process.stdout.strip() 388 | parent_sha_process = subprocess.run( 389 | ['git', 'rev-parse', 'HEAD^'], capture_output=True, check=True, text=True, 390 | cwd=tmpdir.name) 391 | parent_sha = parent_sha_process.stdout.strip() 392 | os.makedirs(Path(tmpdir2.name) / 'foo') 393 | (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() 394 | with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'w') as f: 395 | f.write(f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={parent_sha}\nallow_untracked_files=True') 396 | beman_submodule.update_command(False, 'foo') 397 | assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) 398 | beman_submodule.update_command(True, 'foo') 399 | assert set(['./foo/b.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) 400 | os.chdir(original_cwd) 401 | 402 | def test_add_command(): 403 | tmpdir = create_test_git_repository() 404 | tmpdir2 = create_test_git_repository() 405 | original_cwd = Path.cwd() 406 | os.chdir(tmpdir2.name) 407 | beman_submodule.add_command(tmpdir.name, 'foo', False) 408 | sha_process = subprocess.run( 409 | ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, 410 | cwd=tmpdir.name) 411 | sha = sha_process.stdout.strip() 412 | assert beman_submodule.directory_compare( 413 | tmpdir.name, Path(tmpdir2.name) / 'foo', ['.git', '.beman_submodule'], False) 414 | with open(Path(tmpdir2.name) / 'foo' / '.beman_submodule', 'r') as f: 415 | assert f.read() == f'[beman_submodule]\nremote={tmpdir.name}\ncommit_hash={sha}\n' 416 | os.chdir(original_cwd) 417 | 418 | def test_add_command_untracked_files(): 419 | tmpdir = create_test_git_repository() 420 | tmpdir2 = create_test_git_repository() 421 | original_cwd = Path.cwd() 422 | os.chdir(tmpdir2.name) 423 | os.makedirs(Path(tmpdir2.name) / 'foo') 424 | (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() 425 | beman_submodule.add_command(tmpdir.name, 'foo', True) 426 | assert set(['./foo/a.txt', './foo/c.txt']) == set(glob.glob('./foo/*.txt')) 427 | os.chdir(original_cwd) 428 | 429 | def test_status_command_no_paths(capsys): 430 | tmpdir = create_test_git_repository() 431 | tmpdir2 = create_test_git_repository() 432 | original_cwd = Path.cwd() 433 | os.chdir(tmpdir2.name) 434 | beman_submodule.add_command(tmpdir.name, 'foo', False) 435 | beman_submodule.add_command(tmpdir.name, 'bar', False) 436 | sha_process = subprocess.run( 437 | ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, 438 | cwd=tmpdir.name) 439 | with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: 440 | f.write('b') 441 | beman_submodule.status_command([]) 442 | sha = sha_process.stdout.strip() 443 | assert capsys.readouterr().out == '+ ' + sha + ' bar\n' + ' ' + sha + ' foo\n' 444 | os.chdir(original_cwd) 445 | 446 | def test_status_command_with_path(capsys): 447 | tmpdir = create_test_git_repository() 448 | tmpdir2 = create_test_git_repository() 449 | original_cwd = Path.cwd() 450 | os.chdir(tmpdir2.name) 451 | beman_submodule.add_command(tmpdir.name, 'foo', False) 452 | beman_submodule.add_command(tmpdir.name, 'bar', False) 453 | sha_process = subprocess.run( 454 | ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, 455 | cwd=tmpdir.name) 456 | with open(Path(tmpdir2.name) / 'bar' / 'a.txt', 'w') as f: 457 | f.write('b') 458 | beman_submodule.status_command(['bar']) 459 | sha = sha_process.stdout.strip() 460 | assert capsys.readouterr().out == '+ ' + sha + ' bar\n' 461 | os.chdir(original_cwd) 462 | 463 | def test_status_command_untracked_files(capsys): 464 | tmpdir = create_test_git_repository() 465 | tmpdir2 = create_test_git_repository() 466 | original_cwd = Path.cwd() 467 | os.chdir(tmpdir2.name) 468 | beman_submodule.add_command(tmpdir.name, 'foo', True) 469 | sha_process = subprocess.run( 470 | ['git', 'rev-parse', 'HEAD'], capture_output=True, check=True, text=True, 471 | cwd=tmpdir.name) 472 | (Path(tmpdir2.name) / 'foo' / 'c.txt').touch() 473 | beman_submodule.status_command(['foo']) 474 | sha = sha_process.stdout.strip() 475 | assert capsys.readouterr().out == ' ' + sha + ' foo\n' 476 | os.chdir(original_cwd) 477 | 478 | def test_check_for_git(): 479 | tmpdir = tempfile.TemporaryDirectory() 480 | assert not beman_submodule.check_for_git(tmpdir.name) 481 | fake_git_path = Path(tmpdir.name) / 'git' 482 | with open(fake_git_path, 'w'): 483 | pass 484 | os.chmod(fake_git_path, stat.S_IRWXU) 485 | assert beman_submodule.check_for_git(tmpdir.name) 486 | 487 | def test_parse_args(): 488 | def plain_update(): 489 | args = beman_submodule.parse_args(['update']) 490 | assert args.command == 'update' 491 | assert not args.remote 492 | assert not args.beman_submodule_path 493 | plain_update() 494 | def update_remote(): 495 | args = beman_submodule.parse_args(['update', '--remote']) 496 | assert args.command == 'update' 497 | assert args.remote 498 | assert not args.beman_submodule_path 499 | update_remote() 500 | def update_path(): 501 | args = beman_submodule.parse_args(['update', 'infra/']) 502 | assert args.command == 'update' 503 | assert not args.remote 504 | assert args.beman_submodule_path == 'infra/' 505 | update_path() 506 | def update_path_remote(): 507 | args = beman_submodule.parse_args(['update', '--remote', 'infra/']) 508 | assert args.command == 'update' 509 | assert args.remote 510 | assert args.beman_submodule_path == 'infra/' 511 | update_path_remote() 512 | def plain_add(): 513 | args = beman_submodule.parse_args(['add', 'git@github.com:bemanproject/infra.git']) 514 | assert args.command == 'add' 515 | assert args.repository == 'git@github.com:bemanproject/infra.git' 516 | assert not args.path 517 | plain_add() 518 | def add_path(): 519 | args = beman_submodule.parse_args( 520 | ['add', 'git@github.com:bemanproject/infra.git', 'infra/']) 521 | assert args.command == 'add' 522 | assert args.repository == 'git@github.com:bemanproject/infra.git' 523 | assert args.path == 'infra/' 524 | add_path() 525 | def plain_status(): 526 | args = beman_submodule.parse_args(['status']) 527 | assert args.command == 'status' 528 | assert args.paths == [] 529 | plain_status() 530 | def status_one_module(): 531 | args = beman_submodule.parse_args(['status', 'infra/']) 532 | assert args.command == 'status' 533 | assert args.paths == ['infra/'] 534 | status_one_module() 535 | def status_multiple_modules(): 536 | args = beman_submodule.parse_args(['status', 'infra/', 'foobar/']) 537 | assert args.command == 'status' 538 | assert args.paths == ['infra/', 'foobar/'] 539 | status_multiple_modules() 540 | --------------------------------------------------------------------------------