diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 498405d..8f9e2e9 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -14,6 +14,7 @@ concurrency: cancel-in-progress: true env: + OPENCPPCOVERAGE_VERSION: '0.9.9.0' PYTHON_VERSION: '3.14' jobs: @@ -88,7 +89,7 @@ jobs: disable_search: true fail_ci_if_error: true files: ./coverage/python-coverage.xml - flags: ${{ runner.os }} + flags: ${{ runner.os }}-python report_type: coverage token: ${{ secrets.CODECOV_TOKEN }} verbose: true @@ -104,7 +105,329 @@ jobs: disable_search: true fail_ci_if_error: true files: ./junit-python.xml - flags: ${{ runner.os }} + flags: ${{ runner.os }}-python + report_type: test_results + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + + build: + name: Build (${{ matrix.name }}) + needs: + - release-setup + permissions: + contents: read + runs-on: ${{ matrix.os }} + defaults: + run: + shell: ${{ matrix.shell }} + strategy: + fail-fast: false + matrix: + include: + - name: Linux-GCC + os: ubuntu-latest + shell: bash + kind: unix + cc: gcc + cxx: g++ + gcov_executable: gcov + - name: Linux-Clang + os: ubuntu-latest + shell: bash + kind: unix + cc: clang + cxx: clang++ + # Clang writes LLVM coverage notes, so gcovr needs llvm-cov's gcov compatibility mode. + gcov_executable: llvm-cov gcov + - name: macOS + os: macos-latest + shell: bash + kind: unix + cc: clang + cxx: clang++ + gcov_executable: gcov + - name: Windows-MinGW-UCRT64 + os: windows-latest + shell: msys2 {0} + kind: msys2 + cc: gcc + cxx: g++ + msystem: ucrt64 + toolchain: ucrt-x86_64 + gcov_executable: gcov + - name: Windows-MSVC + os: windows-2022 + shell: pwsh + kind: msvc + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + with: + submodules: recursive + + - name: Setup Dependencies Linux + if: runner.os == 'Linux' + run: | + sudo apt-get update + sudo apt-get install -y \ + build-essential \ + clang \ + cmake \ + llvm \ + ninja-build + + - name: Setup Dependencies macOS + if: runner.os == 'macOS' + run: | + brew install \ + cmake \ + ninja + + - name: Setup Dependencies Windows MinGW + if: matrix.kind == 'msys2' + uses: msys2/setup-msys2@66cd2cce69caa17b53920067426061ca1de3a884 # v2.32.0 + with: + msystem: ${{ matrix.msystem }} + update: true + install: >- + mingw-w64-${{ matrix.toolchain }}-cmake + mingw-w64-${{ matrix.toolchain }}-ninja + mingw-w64-${{ matrix.toolchain }}-toolchain + + - name: Setup Dependencies Windows MSVC + if: matrix.kind == 'msvc' + run: | + choco install opencppcoverage --version=${{ env.OPENCPPCOVERAGE_VERSION }} --yes --no-progress + + $openCppCoverageDir = "${env:ProgramFiles}\OpenCppCoverage" + if (!(Test-Path (Join-Path $openCppCoverageDir "OpenCppCoverage.exe"))) { + $openCppCoverageDir = "${env:ProgramFiles(x86)}\OpenCppCoverage" + } + if (!(Test-Path (Join-Path $openCppCoverageDir "OpenCppCoverage.exe"))) { + throw "OpenCppCoverage.exe was not found after Chocolatey install." + } + + $openCppCoverageDir | Out-File -FilePath $env:GITHUB_PATH -Encoding utf8 -Append + + - name: Set up Python + id: setup-python + if: matrix.kind != 'msvc' + uses: actions/setup-python@ece7cb06caefa5fff74198d8649806c4678c61a1 # v6.3.0 + with: + python-version: ${{ env.PYTHON_VERSION }} + + - name: Set up uv + if: matrix.kind != 'msvc' + uses: astral-sh/setup-uv@fac544c07dec837d0ccb6301d7b5580bf5edae39 # v8.2.0 + with: + enable-cache: true + + - name: Sync Python tools + if: matrix.kind != 'msvc' + env: + MSYS2_PATH_TYPE: inherit + UV_PYTHON: ${{ steps.setup-python.outputs.python-path }} + run: | + uv sync --locked --only-group test-c \ + --no-python-downloads \ + --no-install-project + + - name: Configure + if: matrix.kind != 'msvc' + env: + CC: ${{ matrix.cc }} + CXX: ${{ matrix.cxx }} + run: | + cmake \ + -DBUILD_DOCS=OFF \ + -DBUILD_TESTS=ON \ + -DCMAKE_BUILD_TYPE:STRING=Debug \ + -B cmake-build-ci \ + -G Ninja \ + -S . + + - name: Configure MSVC + if: matrix.kind == 'msvc' + run: | + cmake ` + -DBUILD_DOCS=OFF ` + -DBUILD_TESTS=ON ` + -A x64 ` + -B cmake-build-ci ` + -G "Visual Studio 17 2022" ` + -S . + + - name: Build + if: matrix.kind != 'msvc' + run: cmake --build cmake-build-ci -- -j2 + + - name: Build MSVC + if: matrix.kind == 'msvc' + run: cmake --build cmake-build-ci --config Debug --parallel 2 + + - name: Prepare report directory + run: cmake -E make_directory cmake-build-ci/reports + + - name: Run tests + id: test + if: matrix.kind != 'msvc' + working-directory: cmake-build-ci/tests + run: ./test_lizardbyte_common --gtest_color=yes --gtest_output=xml:../reports/junit.xml + + - name: Run tests MSVC + id: test_msvc + if: matrix.kind == 'msvc' + run: | + $openCppCoverage = (Get-Command OpenCppCoverage.exe -ErrorAction SilentlyContinue).Source + if (!$openCppCoverage) { + $candidates = @( + "${env:ProgramFiles}\OpenCppCoverage\OpenCppCoverage.exe", + "${env:ProgramFiles(x86)}\OpenCppCoverage\OpenCppCoverage.exe" + ) + $openCppCoverage = $candidates | Where-Object { Test-Path $_ } | Select-Object -First 1 + } + if (!$openCppCoverage) { + throw "OpenCppCoverage.exe was not found." + } + + & $openCppCoverage ` + --sources "$env:GITHUB_WORKSPACE\src" ` + --sources "$env:GITHUB_WORKSPACE\tests\support" ` + "--export_type=cobertura:$env:GITHUB_WORKSPACE\cmake-build-ci\reports\coverage.xml" ` + --working_dir "$env:GITHUB_WORKSPACE\cmake-build-ci\tests" ` + -- ` + "$env:GITHUB_WORKSPACE\cmake-build-ci\tests\Debug\test_lizardbyte_common.exe" ` + --gtest_color=yes ` + "--gtest_output=xml:$env:GITHUB_WORKSPACE\cmake-build-ci\reports\junit.xml" + + - name: Normalize MSVC coverage paths + if: >- + always() && + matrix.kind == 'msvc' && + (steps.test_msvc.outcome == 'success' || steps.test_msvc.outcome == 'failure') + run: | + $coveragePath = Join-Path $env:GITHUB_WORKSPACE "cmake-build-ci\reports\coverage.xml" + if (!(Test-Path $coveragePath)) { + return + } + + [xml] $coverage = Get-Content $coveragePath + $workspace = $env:GITHUB_WORKSPACE.Replace('\', '/') + foreach ($node in $coverage.SelectNodes('//*[@filename]')) { + $filename = $node.GetAttribute('filename').Replace('\', '/') + if ($filename.StartsWith("${workspace}/")) { + $filename = $filename.Substring($workspace.Length + 1) + } + + $node.SetAttribute('filename', $filename) + } + + foreach ($source in $coverage.SelectNodes('//source')) { + $source.InnerText = '.' + } + + $coverage.Save($coveragePath) + + - name: Generate gcov report + id: test_report + if: >- + always() && + matrix.kind != 'msvc' && + (steps.test.outcome == 'success' || steps.test.outcome == 'failure') + working-directory: cmake-build-ci + env: + GCOV_EXECUTABLE: ${{ matrix.gcov_executable }} + MSYS2_PATH_TYPE: inherit + run: | + uv run --project .. --locked --no-sync gcovr . -r .. \ + --filter ../src \ + --filter ../tests/support \ + --gcov-executable "${GCOV_EXECUTABLE}" \ + --exclude-noncode-lines \ + --exclude-throw-branches \ + --exclude-unreachable-branches \ + --verbose \ + --xml-pretty \ + -o reports/coverage.xml + + - name: Install + if: matrix.kind != 'msvc' + run: cmake --install cmake-build-ci --prefix cmake-build-ci/install + + - name: Install MSVC + if: matrix.kind == 'msvc' + run: cmake --install cmake-build-ci --config Debug --prefix cmake-build-ci/install + + - name: Upload report artifact + if: >- + always() && + ( + steps.test_report.outcome == 'success' || + steps.test_msvc.outcome == 'success' || + steps.test_msvc.outcome == 'failure' + ) + uses: actions/upload-artifact@043fb46d1a93c77aae656e7c1c64a875d1fc6a0a # v7.0.1 + with: + name: reports-${{ matrix.name }} + path: cmake-build-ci/reports + if-no-files-found: error + + codecov: + name: Codecov (${{ matrix.flag }}) + if: >- + always() && + (needs.build.result == 'success' || needs.build.result == 'failure') && + startsWith(github.repository, 'LizardByte/') + needs: + - build + permissions: + contents: read + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + include: + - build_name: Linux-GCC + flag: Linux-GCC + - build_name: Linux-Clang + flag: Linux-Clang + - build_name: macOS + flag: macOS + - build_name: Windows-MinGW-UCRT64 + flag: Windows-MinGW-UCRT64 + - build_name: Windows-MSVC + flag: Windows-MSVC + steps: + - name: Checkout + uses: actions/checkout@9c091bb21b7c1c1d1991bb908d89e4e9dddfe3e0 # v7.0.0 + + - name: Download report artifact + uses: actions/download-artifact@3e5f45b2cfb9172054b4087a40e8e0b5a5461e7c # v8.0.1 + with: + name: reports-${{ matrix.build_name }} + path: _reports + + - name: Debug coverage file + run: cat _reports/coverage.xml + + - name: Upload test coverage + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + disable_search: true + fail_ci_if_error: true + files: ./_reports/coverage.xml + flags: ${{ matrix.flag }} + report_type: coverage + token: ${{ secrets.CODECOV_TOKEN }} + verbose: true + + - name: Upload test results + uses: codecov/codecov-action@fb8b3582c8e4def4969c97caa2f19720cb33a72f # v7.0.0 + with: + disable_search: true + fail_ci_if_error: true + files: ./_reports/junit.xml + flags: ${{ matrix.flag }} report_type: test_results token: ${{ secrets.CODECOV_TOKEN }} verbose: true @@ -115,6 +438,7 @@ jobs: (github.event_name == 'push' && github.ref == 'refs/heads/master') && needs.release-setup.outputs.publish_release == 'true' needs: + - build - release-setup - pytest permissions: {} diff --git a/.gitignore b/.gitignore index cc32f96..23817f4 100644 --- a/.gitignore +++ b/.gitignore @@ -46,6 +46,7 @@ build/ Build/ build-*/ +cmake-build-*/ # CMake generated files CMakeFiles/ diff --git a/.gitmodules b/.gitmodules new file mode 100644 index 0000000..4e26a7d --- /dev/null +++ b/.gitmodules @@ -0,0 +1,7 @@ +[submodule "third-party/doxyconfig"] + path = third-party/doxyconfig + url = https://github.com/LizardByte/doxyconfig.git + branch = master +[submodule "third-party/googletest"] + path = third-party/googletest + url = https://github.com/google/googletest.git diff --git a/.readthedocs.yaml b/.readthedocs.yaml new file mode 100644 index 0000000..ee2f3bb --- /dev/null +++ b/.readthedocs.yaml @@ -0,0 +1,30 @@ +--- +# .readthedocs.yaml +# Read the Docs configuration file +# See https://docs.readthedocs.io/en/stable/config-file/v2.html for details + +version: 2 + +build: + os: ubuntu-24.04 + tools: + python: "miniconda-latest" + commands: + - | + if [ -f readthedocs_build.sh ]; then + doxyconfig_dir="." + else + doxyconfig_dir="./third-party/doxyconfig" + fi + chmod +x "${doxyconfig_dir}/readthedocs_build.sh" + export DOXYCONFIG_DIR="${doxyconfig_dir}" + "${doxyconfig_dir}/readthedocs_build.sh" + +# using conda, we can get newer doxygen and graphviz than ubuntu provide +# https://github.com/readthedocs/readthedocs.org/issues/8151#issuecomment-890359661 +conda: + environment: third-party/doxyconfig/environment.yml + +submodules: + include: all + recursive: true diff --git a/AGENTS.md b/AGENTS.md new file mode 100644 index 0000000..e94d873 --- /dev/null +++ b/AGENTS.md @@ -0,0 +1,27 @@ +On Windows we use msys2 and ucrt64 to compile. +You need to prefix commands with `C:\msys64\msys2_shell.cmd -defterm -here -no-start -ucrt64 -c`. + +Prefix build directories with `cmake-build-`. + +The test executable is named `test_lizardbyte_common` and will be located inside the `tests` directory within +the build directory. + +The project uses gtest as a test framework. GoogleTest is vendored as a submodule under `third-party/googletest`. + +Keep the public c++ API platform-neutral. Project-specific details from consuming projects should not leak into +consumer code. + +The production c++ target is `lizardbyte::common`; keep it free of project-specific dependencies. + +The reusable gtest support target is `lizardbyte::test_support`; it may depend on GoogleTest, but should not depend +on project-specific libraries from Sunshine, tray, libvirtualhid, libdisplaydevice, or Moonlight-XboxOG. + +Public production headers live under `src/include/lizardbyte/common/`. + +Shared test-support headers live under `tests/support/include/lizardbyte/common/`. + +Documentation uses the shared `third-party/doxyconfig` submodule and Read the Docs configuration. + +Always update public documentation when changing headers or consumer-facing behavior. + +Always follow the style guidelines defined in .clang-format for c/c++ code when that file is present. diff --git a/CMakeLists.txt b/CMakeLists.txt new file mode 100644 index 0000000..cb621a3 --- /dev/null +++ b/CMakeLists.txt @@ -0,0 +1,111 @@ +# +# Project configuration +# +cmake_minimum_required(VERSION 3.24) +project( + lizardbyte_common + VERSION 0.0.0 + DESCRIPTION "Common helpers and tooling for LizardByte projects." + HOMEPAGE_URL "https://app.lizardbyte.dev" + LANGUAGES CXX) + +set(PROJECT_LICENSE "MIT") +set(CMAKE_CXX_STANDARD 17) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_CXX_EXTENSIONS OFF) + +set(LIZARDBYTE_COMMON_IS_TOP_LEVEL OFF) +if(CMAKE_CURRENT_SOURCE_DIR STREQUAL CMAKE_SOURCE_DIR) + set(LIZARDBYTE_COMMON_IS_TOP_LEVEL ON) +endif() + +if(NOT CMAKE_BUILD_TYPE AND NOT CMAKE_CONFIGURATION_TYPES) + message(STATUS "Setting build type to 'Release' as none was specified.") + set(CMAKE_BUILD_TYPE + "Release" + CACHE STRING "Choose the type of build." FORCE) +endif() + +# +# Project optional configuration +# +option(BUILD_TESTS "Build tests" ${LIZARDBYTE_COMMON_IS_TOP_LEVEL}) +option(BUILD_DOCS "Build documentation" ${LIZARDBYTE_COMMON_IS_TOP_LEVEL}) +option(LIZARDBYTE_COMMON_BUILD_TEST_SUPPORT "Build GoogleTest support helpers" + ${BUILD_TESTS}) +option(LIZARDBYTE_COMMON_WARNINGS_AS_ERRORS + "Treat lizardbyte-common warnings as errors" + ${LIZARDBYTE_COMMON_IS_TOP_LEVEL}) + +set(CMAKE_COLOR_MAKEFILE ON) +set(CMAKE_EXPORT_COMPILE_COMMANDS ON) + +include(CMakePackageConfigHelpers) +include(GNUInstallDirs) + +# +# Additional setup for coverage +# https://gcovr.com/en/stable/guide/compiling.html#compiler-options +# +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME + AND BUILD_TESTS + AND NOT CMAKE_CXX_COMPILER_ID STREQUAL "MSVC") + set(CMAKE_CXX_FLAGS "-fprofile-arcs -ftest-coverage -ggdb -O0") + set(CMAKE_C_FLAGS "-fprofile-arcs -ftest-coverage -ggdb -O0") +endif() + +# +# Library code is located here When building tests this must be after the +# coverage flags are set +# +add_subdirectory(src) + +# +# Test support may be used by consuming projects +# +if(LIZARDBYTE_COMMON_BUILD_TEST_SUPPORT) + set(INSTALL_GTEST OFF) + set(INSTALL_GMOCK OFF) + if(WIN32) + set(gtest_force_shared_crt ON CACHE BOOL # cmake-lint: disable=C0103 + "Always use msvcrt.dll" FORCE) + endif() + + if(NOT TARGET gtest) + add_subdirectory(third-party/googletest third-party/googletest) + endif() + + add_subdirectory(tests/support) +endif() + +# +# Tests and docs are top-level only +# +if(CMAKE_PROJECT_NAME STREQUAL PROJECT_NAME) + if(BUILD_DOCS) + add_subdirectory(third-party/doxyconfig docs) + endif() + + if(BUILD_TESTS) + enable_testing() + add_subdirectory(tests) + endif() +endif() + +# +# Package config +# +configure_package_config_file( + "${CMAKE_CURRENT_SOURCE_DIR}/cmake/lizardbyte-common-config.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/lizardbyte-common-config.cmake" + INSTALL_DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/lizardbyte-common") + +write_basic_package_version_file( + "${CMAKE_CURRENT_BINARY_DIR}/lizardbyte-common-config-version.cmake" + VERSION ${PROJECT_VERSION} + COMPATIBILITY SameMajorVersion) + +install( + FILES "${CMAKE_CURRENT_BINARY_DIR}/lizardbyte-common-config.cmake" + "${CMAKE_CURRENT_BINARY_DIR}/lizardbyte-common-config-version.cmake" + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/lizardbyte-common") diff --git a/README.md b/README.md index f47de14..c84ea95 100644 --- a/README.md +++ b/README.md @@ -16,13 +16,16 @@ ## Overview -This repository contains shared helper scripts and repository-level tooling used across LizardByte projects. +This repository contains shared helper scripts, repository-level tooling, and C++ helpers used across LizardByte +projects. -The current tooling includes Python-managed helpers and reusable GitHub workflows: +The current tooling includes Python-managed helpers, reusable GitHub workflows, and a small C++ helper library: - `scripts/update_clang_format.py` runs `clang-format` across supported source directories. - `scripts/localize.py` updates gettext and Babel locale files. - `.github/workflows/localize.yml` runs the locale helper from GitHub Actions and opens localization update pull requests. +- `lizardbyte::common` provides shared C++ helpers, starting with environment variable manipulation. +- `lizardbyte::test_support` provides shared GoogleTest fixtures and test macros for LizardByte C++ projects. ## Python Tooling @@ -44,6 +47,50 @@ Run gettext extraction: uv run --locked --only-group locale lb-localize --extract ``` +## C++ Tooling + +Initialize submodules before configuring the C++ targets: + +```bash +git submodule update --init --recursive +``` + +Configure, build, and test the C++ helpers with CMake: + +```bash +cmake -DBUILD_DOCS=OFF -DBUILD_TESTS=ON -B build -S . +cmake --build build --parallel +ctest --test-dir build --output-on-failure +``` + +The C++ library exports the `lizardbyte::common` target and public headers under `lizardbyte/common/`. + +```cpp +#include + +std::string value; +if (lizardbyte::common::get_env("MY_ENV", value)) { + lizardbyte::common::append_env("MY_ENV", "suffix", ";"); +} +``` + +The optional test support target is available when `BUILD_TESTS=ON` or `LIZARDBYTE_COMMON_BUILD_TEST_SUPPORT=ON`. + +```cpp +#include + +TEST(MySuite, CapturesOutputUntilFailure) { + std::cout << "only printed when this test fails"; +} +``` + +Build the Doxygen documentation through the shared doxyconfig submodule: + +```bash +cmake -DBUILD_DOCS=ON -DBUILD_TESTS=OFF -B build/docs -S . +cmake --build build/docs --target docs +``` + ## Consuming Projects Projects with a `pyproject.toml` can use this repository as a local path dependency. For a submodule at @@ -82,6 +129,22 @@ lb-update-clang-format lb-localize --extract ``` +CMake projects can consume the C++ helpers from the same submodule: + +```cmake +add_subdirectory(third-party/lizardbyte-common) +target_link_libraries(my_target PRIVATE lizardbyte::common) +``` + +To consume the shared GoogleTest support helpers from a project that already builds tests, link the test binary to +`lizardbyte::test_support`: + +```cmake +set(LIZARDBYTE_COMMON_BUILD_TEST_SUPPORT ON) +add_subdirectory(third-party/lizardbyte-common) +target_link_libraries(my_test_binary PRIVATE lizardbyte::test_support) +``` + ## Workflows Reusable GitHub workflows live under `.github/workflows/`. @@ -119,3 +182,16 @@ Run the pytest suite: ```bash uv run --locked --only-group test-python pytest ``` + +Run the C++ GoogleTest suite: + +```bash +cmake -DBUILD_DOCS=OFF -DBUILD_TESTS=ON -B build -S . +cmake --build build --parallel +ctest --test-dir build --output-on-failure +``` + +
+ + [TOC] +
diff --git a/cmake/lizardbyte-common-config.cmake.in b/cmake/lizardbyte-common-config.cmake.in new file mode 100644 index 0000000..cfbc34d --- /dev/null +++ b/cmake/lizardbyte-common-config.cmake.in @@ -0,0 +1,3 @@ +@PACKAGE_INIT@ + +include("${CMAKE_CURRENT_LIST_DIR}/lizardbyte-common-targets.cmake") diff --git a/docs/Doxyfile b/docs/Doxyfile new file mode 100644 index 0000000..65c8f58 --- /dev/null +++ b/docs/Doxyfile @@ -0,0 +1,40 @@ +# This file describes the settings to be used by the documentation system +# doxygen (www.doxygen.org) for a project. +# +# All text after a double hash (##) is considered a comment and is placed in +# front of the TAG it is preceding. +# +# All text after a single hash (#) is considered a comment and will be ignored. +# The format is: +# TAG = value [value, ...] +# For lists, items can also be appended using: +# TAG += value [value, ...] +# Values that contain spaces should be placed between quotes (\" \"). +# +# Note: +# +# Use doxygen to compare the used configuration file with the template +# configuration file: +# doxygen -x [configFile] +# Use doxygen to compare the used configuration file with the template +# configuration file without replacing the environment variables or CMake type +# replacement variables: +# doxygen -x_noenv [configFile] + +# project metadata +DOCSET_BUNDLE_ID = dev.lizardbyte.lizardbyte-common +DOCSET_PUBLISHER_ID = dev.lizardbyte.lizardbyte-common.documentation +PROJECT_BRIEF = "Common helper scripts, repository tooling, and C++ helpers for LizardByte projects." +PROJECT_NAME = lizardbyte-common + +# project specific settings +DOT_GRAPH_MAX_NODES = 50 +INCLUDE_PATH = +WARN_IF_UNDOCUMENTED = YES + +# files and directories to process +USE_MDFILE_AS_MAINPAGE = ../README.md +INPUT = ../README.md \ + ../third-party/doxyconfig/docs/source_code.md \ + ../src/include \ + ../tests/support/include diff --git a/sonar-project.properties b/sonar-project.properties new file mode 100644 index 0000000..52dce32 --- /dev/null +++ b/sonar-project.properties @@ -0,0 +1,5 @@ +# Sonar project analysis properties overrides +sonar.projectKey=LizardByte_lizardbyte-common + +# support the oldest standard used in lizardbyte projects +sonar.cfamily.reportingCppStandardOverride=c++17 diff --git a/src/CMakeLists.txt b/src/CMakeLists.txt new file mode 100644 index 0000000..72ef932 --- /dev/null +++ b/src/CMakeLists.txt @@ -0,0 +1,40 @@ +add_library(lizardbyte_common STATIC) +add_library(lizardbyte::common ALIAS lizardbyte_common) + +target_sources(lizardbyte_common + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/common/env.cpp") + +target_include_directories( + lizardbyte_common PUBLIC $ + $) + +target_compile_features(lizardbyte_common PUBLIC cxx_std_17) +set_target_properties( + lizardbyte_common PROPERTIES EXPORT_NAME common OUTPUT_NAME lizardbyte-common) + +if(MSVC) + target_compile_options(lizardbyte_common PRIVATE /W4) + if(LIZARDBYTE_COMMON_WARNINGS_AS_ERRORS) + target_compile_options(lizardbyte_common PRIVATE /WX) + endif() +else() + target_compile_options(lizardbyte_common PRIVATE -Wall -Wextra -Wpedantic) + if(LIZARDBYTE_COMMON_WARNINGS_AS_ERRORS) + target_compile_options(lizardbyte_common PRIVATE -Werror) + endif() +endif() + +install( + TARGETS lizardbyte_common + EXPORT lizardbyte-common-targets + ARCHIVE DESTINATION "${CMAKE_INSTALL_LIBDIR}" + LIBRARY DESTINATION "${CMAKE_INSTALL_LIBDIR}" + RUNTIME DESTINATION "${CMAKE_INSTALL_BINDIR}") + +install(DIRECTORY "${PROJECT_SOURCE_DIR}/src/include/" + DESTINATION "${CMAKE_INSTALL_INCLUDEDIR}") + +install( + EXPORT lizardbyte-common-targets + NAMESPACE lizardbyte:: + DESTINATION "${CMAKE_INSTALL_LIBDIR}/cmake/lizardbyte-common") diff --git a/src/common/env.cpp b/src/common/env.cpp new file mode 100644 index 0000000..a6fe184 --- /dev/null +++ b/src/common/env.cpp @@ -0,0 +1,94 @@ +/** + * @file src/common/env.cpp + * @brief Environment variable helper implementation. + */ + +// standard includes +#include +#include + +// local includes +#include + +namespace lizardbyte::common { + namespace { + [[nodiscard]] bool is_valid_name(const std::string &name) { + return !name.empty() && name.find('=') == std::string::npos; + } + } // namespace + + bool get_env(const std::string &name, std::string &value) { + if (!is_valid_name(name)) { + return false; + } + +#if defined(_WIN32) && defined(_MSC_VER) + char *buffer {}; + std::size_t buffer_size {}; + if (_dupenv_s(&buffer, &buffer_size, name.c_str()) != 0 || buffer == nullptr) { + return false; + } + + value = buffer; + std::free(buffer); + return true; +#else + const auto *env_value = std::getenv(name.c_str()); + if (env_value == nullptr) { + return false; + } + + value = env_value; + return true; +#endif + } + + std::string get_env(const std::string &name) { + std::string value; + static_cast(get_env(name, value)); + return value; + } + + int set_env(const std::string &name, const std::string &value) { + if (!is_valid_name(name)) { + return EINVAL; + } + +#if defined(_WIN32) + return _putenv_s(name.c_str(), value.c_str()); +#else + return setenv(name.c_str(), value.c_str(), 1); +#endif + } + + int append_env(const std::string &name, const std::string &value, const std::string &separator) { + if (!is_valid_name(name)) { + return EINVAL; + } + + std::string old_value; + static_cast(get_env(name, old_value)); + + if (old_value.find(value) != std::string::npos) { + return 0; + } + + if (old_value.empty()) { + return set_env(name, value); + } + + return set_env(name, old_value + separator + value); + } + + int unset_env(const std::string &name) { + if (!is_valid_name(name)) { + return EINVAL; + } + +#if defined(_WIN32) + return _putenv_s(name.c_str(), ""); +#else + return unsetenv(name.c_str()); +#endif + } +} // namespace lizardbyte::common diff --git a/src/include/lizardbyte/common/env.h b/src/include/lizardbyte/common/env.h new file mode 100644 index 0000000..51c7fd6 --- /dev/null +++ b/src/include/lizardbyte/common/env.h @@ -0,0 +1,49 @@ +/** + * @file src/include/lizardbyte/common/env.h + * @brief Environment variable helper declarations. + */ +#pragma once + +// standard includes +#include + +namespace lizardbyte::common { + /** + * @brief Get an environment variable. + * @param name The name of the environment variable. + * @param value Reference to write the environment variable value into. + * @return true if value was updated, false if the environment variable did not exist. + */ + [[nodiscard]] bool get_env(const std::string &name, std::string &value); + + /** + * @brief Get an environment variable. + * @param name The name of the environment variable. + * @return Environment variable value, or an empty string if the variable does not exist. + */ + [[nodiscard]] std::string get_env(const std::string &name); + + /** + * @brief Set an environment variable. + * @param name The name of the environment variable. + * @param value The value to set the environment variable to. + * @return 0 on success, non-zero on failure. + */ + [[nodiscard]] int set_env(const std::string &name, const std::string &value); + + /** + * @brief Append a string to an environment variable if it does not already contain it. + * @param name The name of the environment variable. + * @param value The value to append to the environment variable. + * @param separator Optional separator for the new value if it is not the first one. + * @return 0 on success, non-zero on failure. + */ + [[nodiscard]] int append_env(const std::string &name, const std::string &value, const std::string &separator = ""); + + /** + * @brief Unset an environment variable. + * @param name The name of the environment variable. + * @return 0 on success, non-zero on failure. + */ + [[nodiscard]] int unset_env(const std::string &name); +} // namespace lizardbyte::common diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 0000000..8826faa --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,12 @@ +include(GoogleTest) + +set(TEST_BINARY test_lizardbyte_common) + +add_executable( + ${TEST_BINARY} "${CMAKE_CURRENT_SOURCE_DIR}/cpp/test_env.cpp" + "${CMAKE_CURRENT_SOURCE_DIR}/cpp/test_testing.cpp") + +target_link_libraries(${TEST_BINARY} PRIVATE gmock_main lizardbyte::common + lizardbyte::test_support) + +gtest_discover_tests(${TEST_BINARY}) diff --git a/tests/cpp/test_env.cpp b/tests/cpp/test_env.cpp new file mode 100644 index 0000000..bd73faa --- /dev/null +++ b/tests/cpp/test_env.cpp @@ -0,0 +1,133 @@ +/** + * @file tests/cpp/test_env.cpp + * @brief Unit tests for environment variable helpers. + */ + +// standard includes +#include +#include +#include +#include + +// local includes +#include +#include + +namespace { + constexpr auto test_env_name {"LIZARDBYTE_COMMON_TEST_ENV"}; + + class EnvGuard { + public: + explicit EnvGuard(std::string name): + name_ {std::move(name)}, + had_value_ {lizardbyte::common::get_env(name_, old_value_)} { + static_cast(lizardbyte::common::unset_env(name_)); + } + + ~EnvGuard() { + if (had_value_) { + static_cast(lizardbyte::common::set_env(name_, old_value_)); + } else { + static_cast(lizardbyte::common::unset_env(name_)); + } + } + + private: + std::string name_; + std::string old_value_; + bool had_value_; + }; +} // namespace + +TEST(EnvTest, GetEnvReturnsFalseForMissingVariable) { + EnvGuard guard {test_env_name}; + + std::string value {"unchanged"}; + + EXPECT_FALSE(lizardbyte::common::get_env(test_env_name, value)); + EXPECT_EQ(value, "unchanged"); + EXPECT_EQ(lizardbyte::common::get_env(test_env_name), ""); +} + +TEST(EnvTest, SetEnvUpdatesEnvironmentVariable) { + EnvGuard guard {test_env_name}; + + std::string value; + + EXPECT_EQ(lizardbyte::common::set_env(test_env_name, "alpha"), 0); + EXPECT_TRUE(lizardbyte::common::get_env(test_env_name, value)); + EXPECT_EQ(value, "alpha"); + EXPECT_EQ(lizardbyte::common::get_env(test_env_name), "alpha"); + EXPECT_STREQ(std::getenv(test_env_name), "alpha"); +} + +TEST(EnvTest, AppendEnvSetsMissingEnvironmentVariable) { + EnvGuard guard {test_env_name}; + + std::string value; + + EXPECT_EQ(lizardbyte::common::append_env(test_env_name, "alpha", ";"), 0); + EXPECT_TRUE(lizardbyte::common::get_env(test_env_name, value)); + EXPECT_EQ(value, "alpha"); +} + +TEST(EnvTest, AppendEnvAppendsWithSeparator) { + EnvGuard guard {test_env_name}; + + std::string value; + + EXPECT_EQ(lizardbyte::common::set_env(test_env_name, "alpha"), 0); + EXPECT_EQ(lizardbyte::common::append_env(test_env_name, "beta", ";"), 0); + EXPECT_TRUE(lizardbyte::common::get_env(test_env_name, value)); + EXPECT_EQ(value, "alpha;beta"); +} + +TEST(EnvTest, AppendEnvSkipsExistingValue) { + EnvGuard guard {test_env_name}; + + std::string value; + + EXPECT_EQ(lizardbyte::common::set_env(test_env_name, "alpha;beta"), 0); + EXPECT_EQ(lizardbyte::common::append_env(test_env_name, "beta", ";"), 0); + EXPECT_TRUE(lizardbyte::common::get_env(test_env_name, value)); + EXPECT_EQ(value, "alpha;beta"); +} + +TEST(EnvTest, AppendEnvMatchesSunshineCommaSeparatedUse) { + EnvGuard guard {test_env_name}; + + std::string value; + + EXPECT_EQ(lizardbyte::common::append_env(test_env_name, "video_encode", ","), 0); + EXPECT_EQ(lizardbyte::common::append_env(test_env_name, "rt", ","), 0); + EXPECT_EQ(lizardbyte::common::append_env(test_env_name, "video_encode", ","), 0); + EXPECT_TRUE(lizardbyte::common::get_env(test_env_name, value)); + EXPECT_EQ(value, "video_encode,rt"); +} + +TEST(EnvTest, UnsetEnvRemovesEnvironmentVariable) { + EnvGuard guard {test_env_name}; + + std::string value; + + EXPECT_EQ(lizardbyte::common::set_env(test_env_name, "alpha"), 0); + EXPECT_TRUE(lizardbyte::common::get_env(test_env_name, value)); + EXPECT_EQ(lizardbyte::common::unset_env(test_env_name), 0); + EXPECT_FALSE(lizardbyte::common::get_env(test_env_name, value)); +} + +TEST(EnvTest, InvalidEnvNamesAreRejected) { + std::string value {"unchanged"}; + + EXPECT_FALSE(lizardbyte::common::get_env("", value)); + EXPECT_FALSE(lizardbyte::common::get_env("INVALID=NAME", value)); + EXPECT_EQ(lizardbyte::common::get_env(""), ""); + EXPECT_EQ(lizardbyte::common::get_env("INVALID=NAME"), ""); + EXPECT_EQ(lizardbyte::common::set_env("", "value"), EINVAL); + EXPECT_EQ(lizardbyte::common::set_env("INVALID=NAME", "value"), EINVAL); + EXPECT_EQ(lizardbyte::common::append_env("", ""), EINVAL); + EXPECT_EQ(lizardbyte::common::append_env("INVALID=NAME", ""), EINVAL); + EXPECT_EQ(lizardbyte::common::unset_env(""), EINVAL); + EXPECT_EQ(lizardbyte::common::unset_env("INVALID=NAME"), EINVAL); + EXPECT_EQ(value, "unchanged"); +} diff --git a/tests/cpp/test_testing.cpp b/tests/cpp/test_testing.cpp new file mode 100644 index 0000000..b65e791 --- /dev/null +++ b/tests/cpp/test_testing.cpp @@ -0,0 +1,132 @@ +/** + * @file tests/cpp/test_testing.cpp + * @brief Unit tests for shared GoogleTest support helpers. + */ + +// standard includes +#include +#include +#include + +// local includes +#include +#include + +namespace { + class TestableBaseTest: public BaseTest { + public: + using BaseTest::cerrBuffer; + using BaseTest::coutBuffer; + using BaseTest::getArgWithMatchingPattern; + using BaseTest::isOutputSuppressed; + using BaseTest::isSystemTest; + using BaseTest::skipTest; + }; + + class EnvGuard { + public: + explicit EnvGuard(std::string name): + name_ {std::move(name)}, + had_value_ {lizardbyte::common::get_env(name_, old_value_)} { + static_cast(lizardbyte::common::unset_env(name_)); + } + + ~EnvGuard() { + if (had_value_) { + static_cast(lizardbyte::common::set_env(name_, old_value_)); + } else { + static_cast(lizardbyte::common::unset_env(name_)); + } + } + + private: + std::string name_; + std::string old_value_; + bool had_value_; + }; + + class SystemTestFixture: public BaseTest { + protected: + void SetUp() override { + skip_system_tests_guard_.emplace("SKIP_SYSTEM_TESTS"); + BaseTest::SetUp(); + } + + [[nodiscard]] bool isSystemTest() const override { + return true; + } + + private: + std::optional skip_system_tests_guard_; + }; + + class OutputVisibleFixture: public BaseTest { + protected: + [[nodiscard]] bool isOutputSuppressed() const override { + return false; + } + }; +} // namespace + +TEST(TestingSupportTest, DefaultTestMacroUsesBaseFixture) { + EXPECT_TRUE(isOutputSuppressed()); + std::cout << "captured cout"; + std::cerr << "captured cerr"; + EXPECT_EQ(coutBuffer().str(), "captured cout"); + EXPECT_EQ(cerrBuffer().str(), "captured cerr"); +} + +TEST(TestingSupportTest, ArgumentMatchingCanReturnFullArgument) { + EXPECT_TRUE(getArgWithMatchingPattern("--gtest_", false).has_value()); +} + +TEST(TestingSupportTest, ArgumentMatchingCanRemoveMatchedPrefix) { + const auto argument = getArgWithMatchingPattern("--gtest_", true); + + ASSERT_TRUE(argument.has_value()); + EXPECT_FALSE(argument->empty()); +} + +TEST(TestingSupportTest, DefaultBaseTestIsNotSystemTest) { + EXPECT_FALSE(isSystemTest()); + EXPECT_EQ(skipTest(), ""); +} + +TEST_F(OutputVisibleFixture, CanDisableOutputSuppression) { + EXPECT_FALSE(isOutputSuppressed()); +} + +TEST_F(SystemTestFixture, RespectsSkipSystemTestsEnvironment) { + EXPECT_EQ(skipTest(), ""); + EXPECT_EQ(lizardbyte::common::set_env("SKIP_SYSTEM_TESTS", "1"), 0); + EXPECT_NE(skipTest(), ""); +} + +TEST_F(TestableBaseTest, HelpersAreAvailableOnDerivedFixtures) { + EXPECT_TRUE(isOutputSuppressed()); + EXPECT_FALSE(isSystemTest()); +} + +TEST_F(LinuxTest, SkipsOrRunsLinuxFixture) { +#if defined(__linux__) + SUCCEED(); +#else + GTEST_FAIL() << "LinuxTest should skip before the test body on non-Linux platforms."; +#endif +} + +TEST_F(MacOSTest, SkipsOrRunsMacOSFixture) { +#if defined(__APPLE__) && defined(__MACH__) + SUCCEED(); +#else + GTEST_FAIL() << "MacOSTest should skip before the test body on non-macOS platforms."; +#endif +} + +TEST_F(WindowsTest, SkipsOrRunsWindowsFixture) { +#if defined(_WIN32) + SUCCEED(); +#else + GTEST_FAIL() << "WindowsTest should skip before the test body on non-Windows platforms."; +#endif +} diff --git a/tests/support/CMakeLists.txt b/tests/support/CMakeLists.txt new file mode 100644 index 0000000..1efd41c --- /dev/null +++ b/tests/support/CMakeLists.txt @@ -0,0 +1,29 @@ +add_library(lizardbyte_common_test_support STATIC) +add_library(lizardbyte::test_support ALIAS lizardbyte_common_test_support) + +target_sources(lizardbyte_common_test_support + PRIVATE "${CMAKE_CURRENT_SOURCE_DIR}/testing.cpp") + +target_include_directories( + lizardbyte_common_test_support + PUBLIC $) + +target_link_libraries(lizardbyte_common_test_support PUBLIC gtest + lizardbyte::common) + +target_compile_features(lizardbyte_common_test_support PUBLIC cxx_std_17) +set_target_properties(lizardbyte_common_test_support + PROPERTIES OUTPUT_NAME lizardbyte-common-test-support) + +if(MSVC) + target_compile_options(lizardbyte_common_test_support PRIVATE /W4) + if(LIZARDBYTE_COMMON_WARNINGS_AS_ERRORS) + target_compile_options(lizardbyte_common_test_support PRIVATE /WX) + endif() +else() + target_compile_options(lizardbyte_common_test_support PRIVATE -Wall -Wextra + -Wpedantic) + if(LIZARDBYTE_COMMON_WARNINGS_AS_ERRORS) + target_compile_options(lizardbyte_common_test_support PRIVATE -Werror) + endif() +endif() diff --git a/tests/support/include/lizardbyte/common/testing.h b/tests/support/include/lizardbyte/common/testing.h new file mode 100644 index 0000000..5719101 --- /dev/null +++ b/tests/support/include/lizardbyte/common/testing.h @@ -0,0 +1,172 @@ +/** + * @file tests/support/include/lizardbyte/common/testing.h + * @brief Shared GoogleTest support helpers. + */ +#pragma once + +// standard includes +#include +#include +#include +#include + +// lib includes +#include + +namespace lizardbyte::common::testing { + /** + * @brief Base class used by default for shared tests. + * + * ``cout`` and ``cerr`` are redirected during tests and printed when a test fails. + */ + class BaseTest: public ::testing::Test { + protected: + /** + * @brief Tear down the test base. + */ + ~BaseTest() override = default; + + /** + * @brief Set up the test. + */ + void SetUp() override; + + /** + * @brief Tear down the test. + */ + void TearDown() override; + + /** + * @brief Get available command line arguments. + * @return Command line args from GoogleTest. + */ + [[nodiscard]] virtual const std::vector &getArgs() const; + + /** + * @brief Get the command line argument that matches the pattern. + * @param pattern Pattern to look for. + * @param remove_match Specify if the matched pattern should be removed before returning the argument. + * @return Matching command line argument, or null optional if nothing matched. + */ + [[nodiscard]] virtual std::optional getArgWithMatchingPattern( + const std::string &pattern, + bool remove_match + ) const; + + /** + * @brief Check if test output should be printed only when the test fails. + * @return True if output is suppressed, false otherwise. + */ + [[nodiscard]] virtual bool isOutputSuppressed() const; + + /** + * @brief Check if the test interacts with system settings. + * @return True if it does, false otherwise. + * + * Set ``SKIP_SYSTEM_TESTS=1`` to skip tests that return true here. + */ + [[nodiscard]] virtual bool isSystemTest() const; + + /** + * @brief Skip the test by specifying the reason. + * @return A non-empty string if the test needs to be skipped, empty string otherwise. + */ + [[nodiscard]] virtual std::string skipTest() const; + + /** + * @brief Get captured cout. + * @return Captured cout stream. + */ + [[nodiscard]] std::stringstream &coutBuffer(); + + /** + * @brief Get captured cerr. + * @return Captured cerr stream. + */ + [[nodiscard]] std::stringstream &cerrBuffer(); + + private: + std::stringstream cout_buffer_; /**< Stores cout while output is suppressed. */ + std::stringstream cerr_buffer_; /**< Stores cerr while output is suppressed. */ + std::streambuf *cout_streambuf_ {nullptr}; /**< Original cout stream buffer. */ + std::streambuf *cerr_streambuf_ {nullptr}; /**< Original cerr stream buffer. */ + bool test_skipped_at_setup_ {false}; /**< True when SetUp skipped before redirection. */ + }; + + /** + * @brief Base class for Linux-only tests. + */ + class LinuxTest: public BaseTest { + protected: + /** + * @brief Set up the test. + */ + void SetUp() override; + + /** + * @brief Check that a Linux device node is readable and writable. + * @param path Device node path. + * @return GoogleTest assertion result. + */ + static ::testing::AssertionResult HasReadableWritableDeviceNode(const char *path); + }; + + /** + * @brief Base class for macOS-only tests. + */ + class MacOSTest: public BaseTest { + protected: + /** + * @brief Set up the test. + */ + void SetUp() override; + }; + + /** + * @brief Base class for Windows-only tests. + */ + class WindowsTest: public BaseTest { + protected: + /** + * @brief Set up the test. + */ + void SetUp() override; + }; +} // namespace lizardbyte::common::testing + +#if !defined(LIZARDBYTE_COMMON_TESTING_NO_GLOBAL_ALIASES) +/** + * @brief Global compatibility alias for the shared base test fixture. + */ +using BaseTest = ::lizardbyte::common::testing::BaseTest; + +/** + * @brief Global compatibility alias for the shared Linux-only test fixture. + */ +using LinuxTest = ::lizardbyte::common::testing::LinuxTest; + +/** + * @brief Global compatibility alias for the shared macOS-only test fixture. + */ +using MacOSTest = ::lizardbyte::common::testing::MacOSTest; + +/** + * @brief Global compatibility alias for the shared Windows-only test fixture. + */ +using WindowsTest = ::lizardbyte::common::testing::WindowsTest; +#endif + +#if !defined(LIZARDBYTE_COMMON_TESTING_KEEP_GTEST_TEST) + #undef TEST // NOSONAR(cpp:S959): Tests intentionally wrap TEST to use BaseTest. + + /** + * @brief Redefine TEST to automatically use the shared BaseTest fixture. + */ + #define TEST(test_case_name, test_name) \ + GTEST_TEST_( \ + test_case_name, \ + test_name, \ + ::lizardbyte::common::testing::BaseTest, \ + ::testing::internal::GetTypeId<::lizardbyte::common::testing::BaseTest>() \ + ) +#endif diff --git a/tests/support/testing.cpp b/tests/support/testing.cpp new file mode 100644 index 0000000..9c29597 --- /dev/null +++ b/tests/support/testing.cpp @@ -0,0 +1,151 @@ +/** + * @file tests/support/testing.cpp + * @brief Shared GoogleTest support helper definitions. + */ + +// standard includes +#include +#include +#include + +// platform includes +#if defined(__linux__) + #include +#endif + +// local includes +#include +#include + +namespace lizardbyte::common::testing { + void BaseTest::SetUp() { + cout_buffer_.str({}); + cout_buffer_.clear(); + cerr_buffer_.str({}); + cerr_buffer_.clear(); + + if (const auto skip_reason {skipTest()}; !skip_reason.empty()) { + test_skipped_at_setup_ = true; + GTEST_SKIP() << skip_reason; + } + + if (isOutputSuppressed()) { + cout_streambuf_ = std::cout.rdbuf(); + cerr_streambuf_ = std::cerr.rdbuf(); + std::cout.rdbuf(cout_buffer_.rdbuf()); + std::cerr.rdbuf(cerr_buffer_.rdbuf()); + } + } + + void BaseTest::TearDown() { + if (test_skipped_at_setup_) { + return; + } + + if (isOutputSuppressed()) { + if (cout_streambuf_ != nullptr) { + std::cout.rdbuf(cout_streambuf_); + cout_streambuf_ = nullptr; + } + + if (cerr_streambuf_ != nullptr) { + std::cerr.rdbuf(cerr_streambuf_); + cerr_streambuf_ = nullptr; + } + + const auto *test_info = ::testing::UnitTest::GetInstance()->current_test_info(); + if (test_info != nullptr && test_info->result()->Failed()) { + std::cout << std::endl + << "Test failed: " << test_info->name() << std::endl + << std::endl + << "Captured cout:" << std::endl + << cout_buffer_.str() << std::endl + << "Captured cerr:" << std::endl + << cerr_buffer_.str() << std::endl; + } + } + } + + const std::vector &BaseTest::getArgs() const { + static const auto args {::testing::internal::GetArgvs()}; + return args; + } + + std::optional BaseTest::getArgWithMatchingPattern( + const std::string &pattern, + const bool remove_match + ) const { + if (const auto &args {getArgs()}; !args.empty()) { + const std::regex re_pattern {pattern}; + + for (auto it {std::next(std::begin(args))}; it != std::end(args); ++it) { + if (std::smatch match; std::regex_search(*it, match, re_pattern)) { + return remove_match ? std::regex_replace(*it, re_pattern, "") : *it; + } + } + } + + return std::nullopt; + } + + bool BaseTest::isOutputSuppressed() const { + return true; + } + + bool BaseTest::isSystemTest() const { + return false; + } + + std::string BaseTest::skipTest() const { + if (isSystemTest() && get_env("SKIP_SYSTEM_TESTS") == "1") { + return "Skipping, this system test is disabled via SKIP_SYSTEM_TESTS=1 env."; + } + + return {}; + } + + std::stringstream &BaseTest::coutBuffer() { + return cout_buffer_; + } + + std::stringstream &BaseTest::cerrBuffer() { + return cerr_buffer_; + } + + void LinuxTest::SetUp() { +#if !defined(__linux__) + GTEST_SKIP() << "Skipping, this test is for Linux only."; +#else + BaseTest::SetUp(); +#endif + } + + ::testing::AssertionResult LinuxTest::HasReadableWritableDeviceNode(const char *path) { +#if defined(__linux__) + if (::access(path, R_OK | W_OK) == 0) { + return ::testing::AssertionSuccess(); + } + + return ::testing::AssertionFailure() << path << " must be readable and writable"; +#else + static_cast(path); + return ::testing::AssertionSuccess(); +#endif + } + + void MacOSTest::SetUp() { +#if !defined(__APPLE__) || !defined(__MACH__) + GTEST_SKIP() << "Skipping, this test is for macOS only."; +#else + BaseTest::SetUp(); +#endif + } + + void WindowsTest::SetUp() { +#if !defined(_WIN32) + GTEST_SKIP() << "Skipping, this test is for Windows only."; +#else + BaseTest::SetUp(); +#endif + } +} // namespace lizardbyte::common::testing diff --git a/third-party/doxyconfig b/third-party/doxyconfig new file mode 160000 index 0000000..e552f7c --- /dev/null +++ b/third-party/doxyconfig @@ -0,0 +1 @@ +Subproject commit e552f7c9f00cd0cf24764a37d0905935b3d2c188 diff --git a/third-party/googletest b/third-party/googletest new file mode 160000 index 0000000..52eb810 --- /dev/null +++ b/third-party/googletest @@ -0,0 +1 @@ +Subproject commit 52eb8108c5bdec04579160ae17225d66034bd723