diff --git a/.github/workflows/build-and-test-macos.yml b/.github/workflows/build-and-test-macos.yml new file mode 100644 index 0000000000..1d9cf0366d --- /dev/null +++ b/.github/workflows/build-and-test-macos.yml @@ -0,0 +1,184 @@ +name: Build and Test (macos) + +# Trigger the workflow on push or pull request +on: + push: + branches: + - master + pull_request: + types: [opened, reopened, synchronize, converted_to_draft, ready_for_review] + +jobs: + build: + strategy: + fail-fast: false + matrix: + host: [ + { + base: macos-12, + compiler: { cc: clang, cxx: clang++ }, + gcov: llvm-gcov, + python: ['3.8', '3.9', '3.10', '3.11', '3.12'], + vtk: '9.3.1' + }, + { + base: macos-13, + compiler: { cc: clang, cxx: clang++ }, + gcov: llvm-gcov, + python: ['3.8', '3.9', '3.10', '3.11', '3.12'], + vtk: '9.3.1' + }, + { + base: macos-14, + compiler: { cc: clang, cxx: clang++ }, + gcov: llvm-gcov, + python: ['3.8', '3.9', '3.10', '3.11', '3.12'], + vtk: '9.3.1' + } + ] + runs-on: ${{ matrix.host.base }} + name: vt-tv build and test (${{ matrix.host.base }}, ${{ matrix.host.compiler.cc }}, vtk-${{ matrix.host.vtk }}, py[${{ join(matrix.host.python, ', ') }}]) + env: + VTK_SRC_DIR: /opt/src/vtk + VTK_BUILD_DIR: /opt/build/vtk + CACHE_KEY: ${{ matrix.host.base }}-${{ matrix.host.compiler.cc }}-vtk-${{ matrix.host.vtk }} + VT_TV_BUILD_DIR: /opt/build/vt-tv + VT_TV_TESTS_ENABLED: "ON" + VT_TV_COVERAGE_ENABLED: ${{ matrix.host.compiler.gcov == '' && 'OFF' || 'ON' }} + VT_TV_OUTPUT_DIR: ${{ github.workspace }}/output + VT_TV_TESTS_OUTPUT_DIR: ${{ github.workspace }}/output/tests + VT_TV_ARTIFACTS_DIR: /tmp/artifacts + CONDA_PATH: /opt/conda + CC: ~ + CXX: ~ + GCOV: ~ + steps: + - uses: actions/checkout@v4 + + - name: Set folder permissions + run: | + sudo chown -R $(whoami) /opt + mkdir -p ${{ env.VTK_SRC_DIR }} ${{ env.VT_TV_BUILD_DIR }} + + - name: Set environment variables + run: | + echo "CC=$(which ${{ matrix.host.compiler.cc }})" >> $GITHUB_ENV + echo "CXX=$(which ${{ matrix.host.compiler.cxx }})" >> $GITHUB_ENV + echo "GCOV=$(which ${{ matrix.host.gcov }})" >> $GITHUB_ENV + + - name: Install dependencies + run: | + brew update && brew install coreutils lcov xquartz + + - name: Load cache (VTK, Miniconda3) + id: base-cache + uses: actions/cache@v4 + with: + path: | + ${{ env.VTK_SRC_DIR }} + ${{ env.VTK_BUILD_DIR }} + ${{ env.CONDA_PATH }} + ~/.zshrc + ~/.bash_profile + key: ${{ env.CACHE_KEY }} + save-always: true + + - name: Setup Conda + if: ${{steps.base-cache.outputs.cache-hit != 'true'}} + run: | + sudo CONDA_PATH=${{ env.CONDA_PATH }} bash ./ci/setup_conda.sh ${{ join(matrix.host.python) }} + + - name: Reload shell variables + run: | + if [ -f ~/.zshrc ]; then . ~/.zshrc; fi + if [ -f ~/.profile ]; then . ~/.profile; fi + if [ -f ~/.bashrc ]; then . ~/.bashrc; fi + which conda + conda --version + conda env list + + - name: Setup VTK ${{ matrix.host.vtk }} + if: ${{steps.base-cache.outputs.cache-hit != 'true'}} + run: | + VTK_DIR=${{ env.VTK_BUILD_DIR }} VTK_SRC_DIR=${{ env.VTK_SRC_DIR }} bash ./ci/setup_vtk.sh + + - name: Build + run: | + mkdir -p ${{ env.VT_TV_BUILD_DIR }} + + CC="${{ env.CC }}" \ + CXX="${{ env.CXX }}" \ + VTK_DIR="${{ env.VTK_BUILD_DIR }}" \ + VT_TV_BUILD_DIR="${{ env.VT_TV_BUILD_DIR }}" \ + VT_TV_CLEAN=OFF \ + VT_TV_TESTS_ENABLED=${{ env.VT_TV_TESTS_ENABLED }} \ + VT_TV_COVERAGE_ENABLED=${{ env.VT_TV_COVERAGE_ENABLED }} \ + GCOV="${{ env.GCOV }}" \ + VT_TV_PYTHON_BINDINGS_ENABLED=OFF \ + VT_TV_WERROR_ENABLED=ON \ + bash ./build.sh + + - name: Test + run: | + VTK_DIR=${{ env.VTK_BUILD_DIR }} \ + VT_TV_COVERAGE_ENABLED=${{ env.VT_TV_COVERAGE_ENABLED }} \ + VT_TV_OUTPUT_DIR="${{ env.VT_TV_OUTPUT_DIR }}" \ + bash ./ci/test.sh + + - name: Build Python package (${{ join(matrix.host.python, ', ') }}) + run: | + VTK_DIR=${{ env.VTK_BUILD_DIR }} bash ./ci/python_build.sh + + - name: Test Python bindings (${{ join(matrix.host.python) }}) + run: | + VTK_DIR=${{ env.VTK_BUILD_DIR }} bash ./ci/python_test.sh + + - name: Collect artifacts + run: | + mkdir -p ${{ env.VT_TV_ARTIFACTS_DIR }} + + # > go to output directory + pushd ${{ env.VT_TV_OUTPUT_DIR }} + + echo "> add junit test report artifact" + cp "junit-report.xml" ${{ env.VT_TV_ARTIFACTS_DIR }}/ || true + + if [[ "${{ env.VT_TV_COVERAGE_ENABLED }}" == "ON" ]]; then + echo "> add `coverage --list` file artifact" + lcov --list lcov_vt-tv_test_no_deps.info > ${{ env.VT_TV_ARTIFACTS_DIR }}/lcov-list-report.txt + + echo "> add total lines coverage file artifact (percentage of lines covered)" + # might be useful for generating later a badge in ci + LCOV_SUMMARY=$(lcov --summary lcov_vt-tv_test_no_deps.info) + LCOV_TOTAL_LINES_COV=$(echo $LCOV_SUMMARY | grep -E -o 'lines......: ([0-9.]+)*' | grep -o -E '[0-9]+.[0-9]+') + echo $LCOV_TOTAL_LINES_COV > lcov-lines-total.txt + cp lcov-lines-total.txt ${{ env.VT_TV_ARTIFACTS_DIR }}/ + fi + popd + + echo "> add tests output mesh files and png artifacts" + if [ -d "${{ env.VT_TV_TESTS_OUTPUT_DIR }}" ]; then + cp "${{ env.VT_TV_TESTS_OUTPUT_DIR }}/"*".vtp" ${{ env.VT_TV_ARTIFACTS_DIR }}/ || true + cp "${{ env.VT_TV_TESTS_OUTPUT_DIR }}/"*".png" ${{ env.VT_TV_ARTIFACTS_DIR }}/ || true + fi + + echo "> list of collected artifacts:" + pushd ${{ env.VT_TV_ARTIFACTS_DIR }} + find ${{ env.VT_TV_ARTIFACTS_DIR }} | while read line; do echo "- $line"; done + popd + + - name: Unit tests + if: ${{ env.VT_TV_TESTS_ENABLED == 'ON' }} + uses: phoenix-actions/test-reporting@v15 + with: + name: Tests report + path: ${{ env.VT_TV_ARTIFACTS_DIR }}/junit-report.xml + reporter: java-junit + output-to: step-summary + + - name: Upload artifacts + uses: actions/upload-artifact@v4 + if: always() + with: + name: vt-tv-artifacts-${{ env.CACHE_KEY }} + path: ${{ env.VT_TV_ARTIFACTS_DIR }} diff --git a/.github/workflows/build-and-test.yml b/.github/workflows/build-and-test-ubuntu.yml similarity index 59% rename from .github/workflows/build-and-test.yml rename to .github/workflows/build-and-test-ubuntu.yml index a0294d7683..8b2792cd55 100644 --- a/.github/workflows/build-and-test.yml +++ b/.github/workflows/build-and-test-ubuntu.yml @@ -1,4 +1,4 @@ -name: Build and Test +name: Build and Test (ubuntu) # Trigger the workflow on push or pull request on: @@ -14,17 +14,43 @@ jobs: strategy: fail-fast: false matrix: - image: - - ubuntu_22.04-gcc_11-vtk_9.2.2-py_3.8 - - ubuntu_22.04-clang_11-vtk_9.2.2-py_3.8 - - ubuntu_22.04-gcc_12-vtk_9.3.0-py_3.8 + host: [ + { + base: ubuntu-22.04, + compiler: { cc: gcc-11 }, + python: ['3.8', '3.9', '3.10', '3.11', '3.12'], + vtk: '9.2.2', + image: ubuntu_22.04-gcc_11-vtk_9.2.2-py_3.all + }, + { + base: ubuntu-22.04, + compiler: { cc: clang-11 }, + python: ['3.8', '3.9', '3.10', '3.11', '3.12'], + vtk: '9.2.2', + image: ubuntu_22.04-clang_11-vtk_9.2.2-py_3.all + }, + { + base: ubuntu-22.04, + compiler: { cc: gcc-12 }, + python: ['3.8', '3.9', '3.10', '3.11', '3.12'], + vtk: '9.3.0', + image: ubuntu_22.04-gcc_12-vtk_9.3.0-py_3.all + }, + { + base: ubuntu-24.04, + compiler: { cc: gcc-13 }, + python: ['3.8', '3.9', '3.10', '3.11', '3.12'], + vtk: '9.3.1', + image: ubuntu_24.04-gcc_13-vtk_9.3.1-py_3.all + } + ] env: OUTPUT_DIR: '/tmp/artifacts' - VT_TV_TESTS_ENABLED: 'ON' # Build & Test in all configurations - VT_TV_COVERAGE_ENABLED: ${{ matrix.image == 'ubuntu_22.04-gcc_12-vtk_9.3.0-py_3.8' && 'ON' || 'OFF' }} # Coverage only with main test image - VT_TV_PYTHON_BINDINGS_ENABLED: 'ON' + VT_TV_TESTS_ENABLED: 'ON' + VT_TV_COVERAGE_ENABLED: ${{ matrix.host.image == 'ubuntu_22.04-gcc_12-vtk_9.3.0-py_3.all' && 'ON' || 'OFF' }} # Coverage only with main test image DOCKER_REPOSITORY: lifflander1/vt - name: vt-tv build and test + DOCKER_TAG: ~ + name: vt-tv build and test (${{ matrix.host.base }}, ${{ matrix.host.compiler.cc }}, vtk-${{ matrix.host.vtk }}, py[${{ join(matrix.host.python, ', ') }}]) steps: - uses: actions/checkout@v4 @@ -51,11 +77,11 @@ jobs: uses: docker/build-push-action@v6 with: push: false - tags: ${{ env.DOCKER_TAG }} + # tags: ${{ env.DOCKER_TAG }} context: . - file: ./ci/docker/build-and-test.dockerfile + file: ./ci/docker/build-and-test-ubuntu.dockerfile build-args: | - BASE_IMAGE=${{ env.DOCKER_REPOSITORY }}:${{ matrix.image }} + BASE_IMAGE=${{ env.DOCKER_REPOSITORY }}:${{ matrix.host.image }} VT_TV_TESTS_ENABLED=${{ env.VT_TV_TESTS_ENABLED }} VT_TV_COVERAGE_ENABLED=${{ env.VT_TV_COVERAGE_ENABLED }} outputs: type=local,dest=${{ env.OUTPUT_DIR }} @@ -64,7 +90,7 @@ jobs: uses: actions/upload-artifact@v4 if: always() with: - name: vt-tv-Artifacts-${{ matrix.image }} + name: vt-tv-Artifacts-${{ matrix.host.image }} path: ${{ env.OUTPUT_DIR }} - name: Unit tests diff --git a/.github/workflows/pushbasedockerimage.yml b/.github/workflows/pushbasedockerimage.yml index 1b6d5be38f..74593c89b5 100644 --- a/.github/workflows/pushbasedockerimage.yml +++ b/.github/workflows/pushbasedockerimage.yml @@ -5,12 +5,13 @@ on: inputs: image: type: choice - description: The configuration to build as a combination of os, compiler, vtk and python versions + description: The configuration to build as a combination of os, compiler, vtk options: - - ubuntu_22.04-gcc_11-vtk_9.2.2-py_3.8 - - ubuntu_22.04-clang_11-vtk_9.2.2-py_3.8 - - ubuntu_22.04-gcc_12-vtk_9.3.0-py_3.8 - default: ubuntu_22.04-gcc_12-vtk_9.3.0-py_3.8 + - ubuntu_22.04-gcc_11-vtk_9.2.2-py_3.all + - ubuntu_22.04-clang_11-vtk_9.2.2-py_3.all + - ubuntu_22.04-gcc_12-vtk_9.3.0-py_3.all + - ubuntu_24.04-gcc_13-vtk_9.3.1-py_3.all + default: ubuntu_22.04-gcc_12-vtk_9.3.0-py_3.all jobs: push_to_registry: @@ -18,6 +19,13 @@ jobs: runs-on: ubuntu-latest env: DOCKER_REPOSITORY: lifflander1/vt + DOCKER_TAG: ~ + BASE_IMAGE: ~ + CXX: ~ + CC: ~ + GCOV: ~ + VTK_VERSION: ~ + PYTHON_VERSIONS: ~ steps: - name: Check out the repo uses: actions/checkout@v4 @@ -38,7 +46,11 @@ jobs: exit 1 fi echo "VTK_VERSION=${CONFIG[5]}" >> $GITHUB_ENV - echo "PYTHON_VERSION=${CONFIG[7]}" >> $GITHUB_ENV + if [[ "${CONFIG[7]}" == "3.all" ]]; then + echo "PYTHON_VERSIONS=3.8,3.9,3.10,3.11,3.12" >> $GITHUB_ENV + else + echo "PYTHON_VERSIONS=${CONFIG[7]}" >> $GITHUB_ENV + fi echo "DOCKER_TAG=${{ inputs.image }}" >> $GITHUB_ENV - name: Build configuration @@ -48,7 +60,7 @@ jobs: echo "CXX Compiler: $CXX" echo "GCOV: $GCOV" echo "VTK: $VTK_VERSION" - echo "Python: $PYTHON_VERSION" + echo "Python: $PYTHON_VERSIONS" echo "Docker tag: $DOCKER_TAG" - name: Log in to Docker Hub @@ -67,7 +79,7 @@ jobs: CXX=${{ env.CXX }} GCOV=${{ env.GCOV }} VTK_VERSION=${{ env.VTK_VERSION }} - PYTHON_VERSION=${{ env.PYTHON_VERSION }} - file: ci/docker/make-base.dockerfile + PYTHON_VERSIONS=${{ env.PYTHON_VERSIONS }} + file: ci/docker/base-ubuntu.dockerfile push: true tags: "${{ env.DOCKER_REPOSITORY }}:${{ env.DOCKER_TAG }}" diff --git a/CMakeLists.txt b/CMakeLists.txt index 54c4f20f00..8b90aad195 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,7 +45,7 @@ set(VT_TV_N_THREADS "2" CACHE STRING "Number of OpenMP threads to use") include(cmake/load_packages.cmake) -if(APPLE) +if(APPLE AND NOT CMAKE_CXX_COMPILER_ID MATCHES "AppleClang") add_compile_options(-ffat-lto-objects) endif() @@ -78,7 +78,7 @@ include(CTest) #adds option BUILD_TESTING (default ON) message(STATUS "VT_TV_COVERAGE_ENABLED: ${VT_TV_COVERAGE_ENABLED}") if (VT_TV_COVERAGE_ENABLED) add_compile_options(-fprofile-arcs -ftest-coverage -O0) - add_link_options(-lgcov --coverage) + add_link_options(--coverage) endif() message(STATUS "VT_TV_TESTS_ENABLED: ${VT_TV_TESTS_ENABLED}") diff --git a/README.md b/README.md index 7da168b7d6..3b0e82c2fc 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,5 @@ -[![Build and Test](https://github.com/DARMA-tasking/vt-tv/actions/workflows/build-and-test.yml/badge.svg)](https://github.com/DARMA-tasking/vt-tv/actions/workflows/build-and-test.yml) +[![Build and Test (Ubuntu)](https://github.com/DARMA-tasking/vt-tv/actions/workflows/build-and-test-ubuntu.yml/badge.svg)](https://github.com/DARMA-tasking/vt-tv/actions/workflows/build-and-test-ubuntu.yml) +[![Build and Test (MacOS)](https://github.com/DARMA-tasking/vt-tv/actions/workflows/build-and-test-macos.yml/badge.svg)](https://github.com/DARMA-tasking/vt-tv/actions/workflows/build-and-test-macos.yml) # tv => task visualizer diff --git a/bindings/python/tv.cc b/bindings/python/tv.cc index 3558fa631c..2d0580d567 100644 --- a/bindings/python/tv.cc +++ b/bindings/python/tv.cc @@ -41,7 +41,7 @@ void tvFromJson(const std::vector& input_json_per_rank_list, const // Throw an error if the output directory does not exist or is not absolute if (!std::filesystem::exists(output_path)) { - throw std::runtime_error("Visualization output directory does not exist."); + throw std::runtime_error(fmt::format("Visualization output directory does not exist at {}", output_dir)); } if (!output_path.is_absolute()) { throw std::runtime_error("Visualization output directory must be absolute."); @@ -124,7 +124,7 @@ void tvFromJson(const std::vector& input_json_per_rank_list, const ); render.generate(font_size, win_size); } catch (std::exception const& e) { - std::cout << "vt-tv: Error reading the configuration file: " << e.what() << std::endl; + throw std::runtime_error(fmt::format("vt-tv: Error reading the configuration file: {}", e.what())); } fmt::print("vt-tv: Done.\n"); diff --git a/build.sh b/build.sh index b1845978f2..07ea3266c0 100755 --- a/build.sh +++ b/build.sh @@ -5,7 +5,7 @@ set -e -CURRENT_DIR="$(dirname -- "$(realpath -- "$0")")" # Current directory +CURRENT_DIR="$(dirname -- "$(realpath -- "$0")")" PARENT_DIR="$(dirname "$CURRENT_DIR")" @@ -116,7 +116,7 @@ EOF while getopts btch-: OPT; do # allow -b -t -c -h, and --long_attr=value" # support long options: https://stackoverflow.com/a/28466267/519360 - if [ "$OPT" = "-" ]; then # long option: reformulate OPT and OPTARG + if [ "$OPT" == "-" ]; then # long option: reformulate OPT and OPTARG OPT="${OPTARG%%=*}" # extract long option name OPTARG="${OPTARG#"$OPT"}" # extract long option argument (may be empty) OPTARG="${OPTARG#=}" # if long option argument, remove assigning `=` @@ -164,6 +164,7 @@ echo CC=$CC echo CXX=$CXX echo GCOV=$GCOV echo VTK_DIR=$VTK_DIR +echo DISPLAY=$DISPLAY # Build if [[ "${VT_TV_BUILD}" == "ON" ]]; then @@ -205,7 +206,7 @@ if [[ "${VT_TV_BUILD}" == "ON" ]]; then fi # End build # Run tests -if [[ "$VT_TV_RUN_TESTS" == "ON" ]]; then +if [ "$VT_TV_RUN_TESTS" == "ON" ]; then mkdir -p "$VT_TV_OUTPUT_DIR" pushd $VT_TV_OUTPUT_DIR # Tests @@ -224,14 +225,13 @@ if [[ "$VT_TV_RUN_TESTS" == "ON" ]]; then gtest_cmd="\"$VT_TV_BUILD_DIR/tests/unit/AllTests\" $GTEST_OPTIONS" echo "Run GTest..." eval "$gtest_cmd" || true - echo "Tests done." popd fi # Coverage -if [[ "$VT_TV_COVERAGE_ENABLED" == "ON" ]]; then +if [ "$VT_TV_COVERAGE_ENABLED" == "ON" ]; then mkdir -p "$VT_TV_OUTPUT_DIR" pushd $VT_TV_OUTPUT_DIR # base coverage files diff --git a/ci/docker/README.md b/ci/docker/README.md index a2eff9cc15..07bad7fbc6 100644 --- a/ci/docker/README.md +++ b/ci/docker/README.md @@ -4,7 +4,7 @@ To build image locally here is an example call using available build arguments: For the base image ```shell -docker build -t vttv:latest --build-arg BASE_IMAGE="ubuntu:24.04" --build-arg GCOV=gcov-14 --build-arg BASE_IMAGE=ubuntu:24.04 --build-arg CC=gcc-14 --build-arg CXX=g++-14 --build-arg VTK=9.3.1 -f make-base.dockerfile . +docker build -t vttv:latest --build-arg BASE_IMAGE="ubuntu:24.04" --build-arg GCOV=gcov-14 --build-arg BASE_IMAGE=ubuntu:24.04 --build-arg CC=gcc-14 --build-arg CXX=g++-14 --build-arg VTK=9.3.1 -f base-ubuntu.dockerfile . ``` Then for the build & test image ```shell diff --git a/ci/docker/base-ubuntu.dockerfile b/ci/docker/base-ubuntu.dockerfile new file mode 100644 index 0000000000..eb0e775df2 --- /dev/null +++ b/ci/docker/base-ubuntu.dockerfile @@ -0,0 +1,78 @@ +# Docker instructions to build an image with some arguments to specify compilers, python and VTK versions. +# @see .github/workflows/pushbasedockerimage.yml + +ARG BASE_IMAGE=ubuntu:22.04 + +# Base image & requirements +FROM ${BASE_IMAGE} AS base + +# Arguments +ARG VTK_VERSION=9.2.2 +ARG PYTHON_VERSIONS=3.8,3.9,3.10,3.11,3.12 +ARG CC=gcc-11 +ARG CXX=g++-11 +ARG GCOV=gcov-11 + +# Copy setup scripts +RUN mkdir -p /opt/scripts +COPY ci/setup_mesa.sh /opt/scripts/setup_mesa.sh +COPY ci/setup_conda.sh /opt/scripts/setup_conda.sh +COPY ci/setup_vtk.sh /opt/scripts/setup_vtk.sh + +ENV DEBIAN_FRONTEND=noninteractive + +# Setup common tools and compiler +RUN apt-get update -y -q && \ + apt-get install -y -q --no-install-recommends \ + ${CC} \ + ${CXX} \ + git \ + xz-utils \ + bzip2 \ + zip \ + gpg \ + wget \ + gpgconf \ + software-properties-common \ + libsigsegv2 \ + libsigsegv-dev \ + pkg-config \ + zlib1g \ + zlib1g-dev \ + m4 \ + gfortran-11 \ + make \ + cmake-data \ + cmake \ + pkg-config \ + libncurses5-dev \ + m4 \ + perl \ + curl \ + xvfb \ + lcov + +# Setup MESA (opengl) +RUN bash /opt/scripts/setup_mesa.sh +RUN xvfb-run bash -c "glxinfo | grep 'OpenGL version'" + +# Environment variables (conda path, python environments, compiler path, vtk) +ENV CC=/usr/bin/$CC +ENV CXX=/usr/bin/$CXX +ENV GCOV=/usr/bin/$GCOV +ENV CONDA_PATH=/opt/conda +ENV VTK_DIR=/opt/build/vtk + +# Setup conda with python environments +RUN bash /opt/scripts/setup_conda.sh ${PYTHON_VERSIONS} + +# Setup VTK +RUN VTK_VERSION=${VTK_VERSION} \ + VTK_DIR=${VTK_DIR} \ + VTK_SRC_DIR=/opt/src/vtk \ + bash /opt/scripts/setup_vtk.sh + +# Clean apt +RUN apt-get clean && rm -rf /var/lib/apt/lists/* + +RUN echo "Base creation success" diff --git a/ci/docker/build-and-test.dockerfile b/ci/docker/build-and-test-ubuntu.dockerfile similarity index 60% rename from ci/docker/build-and-test.dockerfile rename to ci/docker/build-and-test-ubuntu.dockerfile index 1563041c01..9a0e008a96 100644 --- a/ci/docker/build-and-test.dockerfile +++ b/ci/docker/build-and-test-ubuntu.dockerfile @@ -1,14 +1,12 @@ ARG BASE_IMAGE=lifflander1/vt:ubuntu_22.04-gcc_11-vtk_9.2.2-py_3.8 ARG VT_TV_TESTS_ENABLED=OFF ARG VT_TV_COVERAGE_ENABLED=OFF -ARG VT_TV_PYTHON_BINDINGS_ENABLED=ON +ARG VT_TV_TEST_PYTHON_BINDINGS=OFF FROM ${BASE_IMAGE} AS base -# setup requirements for rendering tests (xvfb) + coverage report (lcov) -RUN apt-get update && apt-get install -y \ - xvfb \ - lcov +ENV CONDA_PATH=/opt/conda +ENV PATH=$PATH:$CONDA_PATH/bin COPY . /opt/src/vt-tv RUN mkdir -p /opt/build/vt-tv @@ -23,15 +21,16 @@ RUN VT_TV_COVERAGE_ENABLED=$VT_TV_COVERAGE_ENABLED bash /opt/src/vt-tv/ci/build. FROM build AS test-cpp ARG VT_TV_COVERAGE_ENABLED=OFF ARG VT_TV_TESTS_ENABLED=OFF -RUN VT_TV_COVERAGE_ENABLED=$VT_TV_COVERAGE_ENABLED bash /opt/src/vt-tv/ci/test_cpp.sh +RUN VT_TV_COVERAGE_ENABLED=$VT_TV_COVERAGE_ENABLED bash /opt/src/vt-tv/ci/test.sh # Python tests (Builds VT-TV with Python bindings & test python package) -FROM base AS test-python -ARG VT_TV_PYTHON_BINDINGS_ENABLED=OFF -RUN if [[ VT_TV_PYTHON_BINDINGS_ENABLED == "ON" ]]; then \n \ - bash /opt/src/vt-tv/ci/test_python.sh \n \ - fi +FROM test-cpp AS test-python +# Create vizualization output directory (required) +RUN mkdir -p /opt/src/vt-tv/output/python_tests +RUN VTK_DIR=/opt/build/vtk bash /opt/src/vt-tv/ci/python_build.sh +RUN VTK_DIR=/opt/build/vtk bash /opt/src/vt-tv/ci/python_test.sh # Artifacts FROM scratch AS artifacts COPY --from=test-cpp /tmp/artifacts /tmp/artifacts +COPY --from=test-python /opt/src/vt-tv/output/python_tests /tmp/python-artifacts diff --git a/ci/docker/make-base.dockerfile b/ci/docker/make-base.dockerfile deleted file mode 100644 index ee811cc5c3..0000000000 --- a/ci/docker/make-base.dockerfile +++ /dev/null @@ -1,102 +0,0 @@ -# Docker instructions to build an image with some arguments to specify compilers, python and VTK versions. -# @see .github/workflows/pushbasedockerimage.yml - -ARG BASE_IMAGE=ubuntu:22.04 - -# Base image & requirements -FROM ${BASE_IMAGE} AS base - -# Arguments -ARG VTK_VERSION=9.2.2 -ARG PYTHON_VERSION=3.8 -ARG CC=gcc-11 -ARG CXX=g++-11 -ARG GCOV=gcov-11 - -ENV DEBIAN_FRONTEND=noninteractive -RUN apt-get update -y -q && \ - apt-get install -y -q --no-install-recommends \ - ${CC} \ - ${CXX} \ - git \ - xz-utils \ - bzip2 \ - zip \ - gpg \ - wget \ - gpgconf \ - software-properties-common \ - libsigsegv2 \ - libsigsegv-dev \ - pkg-config \ - zlib1g \ - zlib1g-dev \ - m4 \ - gfortran-11 \ - make \ - cmake-data \ - cmake \ - pkg-config \ - libncurses5-dev \ - m4 \ - libgl1-mesa-dev \ - libglu1-mesa-dev \ - mesa-common-dev \ - libosmesa6-dev \ - perl \ - curl \ - xvfb \ - lcov \ - && \ - apt-get clean && \ - rm -rf /var/lib/apt/lists/* - -# Share environment variables for use in images based on this. -ENV CC=/usr/bin/$CC -ENV CXX=/usr/bin/$CXX -ENV GCOV=/usr/bin/$GCOV -ENV VTK_DIR=/opt/build/vtk - -# Setup python 3.8 with conda - -# Download and install Miniconda -RUN curl -LO https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh && \ - bash Miniconda3-latest-Linux-x86_64.sh -b -p /opt/conda && \ - rm Miniconda3-latest-Linux-x86_64.sh - -# Update PATH so that conda and the installed packages are usable -ENV PATH="/opt/conda/bin:${PATH}" - -# Create a new environment and install necessary packages -RUN conda create -y -n deves python=${PYTHON_VERSION} && \ - echo "source activate deves" > ~/.bashrc && \ - /bin/bash -c ". /opt/conda/etc/profile.d/conda.sh && conda activate deves && pip install nanobind" - -# Set the environment to deves on container run -ENV CONDA_DEFAULT_ENV=deves -ENV CONDA_PREFIX=/opt/conda/envs/$CONDA_DEFAULT_ENV -ENV PATH=$PATH:$CONDA_PREFIX/bin -ENV CONDA_AUTO_UPDATE_CONDA=false - -# Clone VTK source -RUN mkdir -p /opt/src/vtk -RUN git clone --recursive --branch v${VTK_VERSION} https://gitlab.kitware.com/vtk/vtk.git /opt/src/vtk - -# Build VTK -RUN mkdir -p ${VTK_DIR} -WORKDIR ${VTK_DIR} -RUN cmake \ - -DCMAKE_BUILD_TYPE:STRING=Release \ - -DBUILD_TESTING:BOOL=OFF \ - -DVTK_OPENGL_HAS_OSMESA:BOOL=ON \ - -DVTK_DEFAULT_RENDER_WINDOW_OFFSCREEN:BOOL=ON \ - -DVTK_USE_X:BOOL=OFF \ - -DVTK_USE_WIN32_OPENGL:BOOL=OFF \ - -DVTK_USE_COCOA:BOOL=OFF \ - -DVTK_USE_SDL2:BOOL=OFF \ - -DVTK_Group_Rendering:BOOL=OFF \ - -DBUILD_SHARED_LIBS:BOOL=ON \ - -S /opt/src/vtk -B ${VTK_DIR} -RUN cmake --build ${VTK_DIR} -j$(nproc) - -RUN echo "Base creation success" diff --git a/ci/python_build.sh b/ci/python_build.sh new file mode 100644 index 0000000000..6c3fddb50a --- /dev/null +++ b/ci/python_build.sh @@ -0,0 +1,28 @@ +#!/bin/bash + +# This script builds and install vt-tv as a pip package + +set -ex + +CURRENT_DIR="$(dirname -- "$(realpath -- "$0")")" +PARENT_DIR="$(dirname "$CURRENT_DIR")" +CONDA_PATH=${CONDA_PATH:-"/opt/conda"} +VTK_DIR="${VTK_DIR:-$PARENT_DIR/vtk/build}" + +VT_TV_SRC_DIR=${VT_TV_SRC_DIR:-$PARENT_DIR} + +for env in $(conda env list | grep ^py | perl -lane 'print $F[-1]' | xargs ls -lrt1d | perl -lane 'print $F[-1]' | sed -r 's/^.*\/(.*)$/\1/'); do + echo "::group::Build Python Bindings (${python_version})" + + # Activate conda environment + . $CONDA_PATH/etc/profile.d/conda.sh && conda activate $env + + # Build VT-TV python package + pip install PyYAML + pip install $VT_TV_SRC_DIR + + # Deactivate conda environment + conda deactivate + + echo "::endgroup::" +done \ No newline at end of file diff --git a/ci/python_test.sh b/ci/python_test.sh new file mode 100644 index 0000000000..b2a294363d --- /dev/null +++ b/ci/python_test.sh @@ -0,0 +1,41 @@ +#!/bin/bash + +# This script tests vt-tv pip package + +set -ex + +CURRENT_DIR="$(dirname -- "$(realpath -- "$0")")" +PARENT_DIR="$(dirname "$CURRENT_DIR")" + +VT_TV_SRC_DIR=${VT_TV_SRC_DIR:-$PARENT_DIR} +VT_TV_OUTPUT_DIR=${VT_TV_OUTPUT_DIR:-"$VT_TV_SRC_DIR/output"} + +pushd $VT_TV_SRC_DIR + +# Create vizualization output directory (required). +mkdir -p $VT_TV_OUTPUT_DIR/python_tests + +for env in $(conda env list | grep ^py | perl -lane 'print $F[-1]' | xargs ls -lrt1d | perl -lane 'print $F[-1]' | sed -r 's/^.*\/(.*)$/\1/'); do + # Clear vizualization output directory + rm -rf $VT_TV_OUTPUT_DIR/python_tests/* + + echo "::group::Test Python Bindings (${python_version})" + + # Activate conda environment + . $CONDA_PATH/etc/profile.d/conda.sh && conda activate $env + + # Run test + if [[ $(uname -a) != *"Darwin"* ]]; then + # Start virtual display (Linux) + xvfb-run python $VT_TV_SRC_DIR/tests/test_bindings.py + else + python $VT_TV_SRC_DIR/tests/test_bindings.py + fi + + # Deactivate conda environment + conda deactivate + + echo "::endgroup::" +done + +popd \ No newline at end of file diff --git a/ci/setup_conda.sh b/ci/setup_conda.sh new file mode 100644 index 0000000000..9d89c5c01f --- /dev/null +++ b/ci/setup_conda.sh @@ -0,0 +1,47 @@ +#!/bin/bash + +# This script installs Conda and setup conda environments on the host machine for the given python versions +# Example: `setup_conda.sh 3.8,3.9,3.10,3.11,3.12` will +# 1. Setup conda +# 2. Create conda environments py3.8, py3.9, py3.10, py3.11, py3.12 (with python version and nanobind package) + +CONDA_PATH=${CONDA_PATH:-"/opt/conda"} +PYTHON_VERSIONS=${1:-"3.8,3.9,3.10,3.11,3.12"} + +echo "::group::Install conda" +mkdir -p ~/miniconda3 +if [[ $(uname -a) == *"Darwin"* ]]; then + if [[ $(arch) == 'arm64' ]]; then + curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-arm64.sh -o ~/miniconda.sh + else + curl https://repo.anaconda.com/miniconda/Miniconda3-latest-MacOSX-x86_64.sh -o ~/miniconda.sh + fi +else + curl https://repo.anaconda.com/miniconda/Miniconda3-latest-Linux-x86_64.sh -o ~/miniconda.sh +fi +bash ~/miniconda.sh -b -u -p $CONDA_PATH +rm -rf ~/miniconda.sh + +$CONDA_PATH/bin/conda init bash +$CONDA_PATH/bin/conda init zsh +if [ -f ~/.zshrc ]; then . ~/.zshrc; fi +if [ -f ~/.profile ]; then . ~/.profile; fi +if [ -f ~/.bashrc ]; then . ~/.bashrc; fi + +echo "Conda path: $(which conda)" +echo "Conda version: $(conda --version)" + +echo "::endgroup::" + +versions=(`echo $PYTHON_VERSIONS | sed 's/,/\n/g'`) +for python_version in "${versions[@]}" +do + echo "::group::Create conda environment (py${python_version})" + conda create -y -n py${python_version} python=${python_version} + + . $CONDA_PATH/etc/profile.d/conda.sh && conda activate py${python_version} + echo "Python version: $(python --version)" + pip install nanobind + conda deactivate + echo "::endgroup::" +done diff --git a/ci/setup_mesa.sh b/ci/setup_mesa.sh new file mode 100644 index 0000000000..af73f81a94 --- /dev/null +++ b/ci/setup_mesa.sh @@ -0,0 +1,20 @@ +#!/bin/bash + +# This script installs Mesa libraries and utilities + + +# FIX MESA driver (Ubuntu 24.04). +# Error: MESA: error: ZINK: vkCreateInstance failed (VK_ERROR_INCOMPATIBLE_DRIVER) +. /etc/lsb-release +if [ "$DISTRIB_RELEASE" == "24.04" ]; then + echo "FIX: Using latest MESA drivers (dev) for Ubuntu 24.04 to fix MESA errors !" + add-apt-repository ppa:oibaf/graphics-drivers -y + apt-get update +fi + +apt-get install -y -q --no-install-recommends \ + libgl1-mesa-dev \ + libglu1-mesa-dev \ + mesa-common-dev \ + libosmesa6-dev \ + mesa-utils diff --git a/ci/setup_vtk.sh b/ci/setup_vtk.sh new file mode 100755 index 0000000000..f19edfcb39 --- /dev/null +++ b/ci/setup_vtk.sh @@ -0,0 +1,27 @@ +#!/bin/bash + +# This script builds VTK (required by vt-tv) + +set -ex + +VTK_SRC_DIR=${VTK_SRC_DIR:-"/opt/src/vtk"} +VTK_DIR=${VTK_DIR:-"/opt/build/vtk"} +VTK_VERSION=${VTK_VERSION:-"9.3.1"} + +echo "Setup VTK $VTK_VERSION from source..." +git clone --recursive --branch v${VTK_VERSION} https://gitlab.kitware.com/vtk/vtk.git ${VTK_SRC_DIR} + +mkdir -p $VTK_DIR +pushd $VTK_DIR + +cmake \ + -DCMAKE_BUILD_TYPE:STRING=Release \ + -DBUILD_TESTING:BOOL=OFF \ + -DBUILD_SHARED_LIBS:BOOL=ON \ + -S "$VTK_SRC_DIR" -B "$VTK_DIR" +cmake --build "$VTK_DIR" -j$(nproc) + +echo "VTK build success" +popd + +echo "VTK $VTK_VERSION has been installed successfully." diff --git a/ci/test_cpp.sh b/ci/test.sh similarity index 67% rename from ci/test_cpp.sh rename to ci/test.sh index a441dfc9d2..23e626b0ea 100644 --- a/ci/test_cpp.sh +++ b/ci/test.sh @@ -2,19 +2,30 @@ set -ex -VT_TV_OUTPUT_DIR=/var/vt-tv/output -VT_TV_TESTS_OUTPUT_DIR=/opt/src/vt-tv/output/tests +CURRENT_DIR="$(dirname -- "$(realpath -- "$0")")" +PARENT_DIR="$(dirname "$CURRENT_DIR")" -# call build script with options to only run tests without building -# (in CI the docker image define the build and the test stages separately). -bash -c "VTK_DIR=/opt/build/vtk \ +VTK_DIR=${VTK_DIR:-"/opt/build/vtk"} + +VT_TV_SRC_DIR=${VT_TV_SRC_DIR:-$PARENT_DIR} +VT_TV_BUILD_DIR=${VT_TV_BUILD_DIR:-"/opt/build/vt-tv"} +VT_TV_OUTPUT_DIR=${VT_TV_OUTPUT_DIR:-"$VT_TV_SRC_DIR/output"} +VT_TV_TESTS_OUTPUT_DIR=${VT_TV_TESTS_OUTPUT_DIR:-"$VT_TV_OUTPUT_DIR/tests"} + +VT_TV_TEST_CMD="VTK_DIR=/opt/build/vtk \ VT_TV_BUILD=OFF \ - VT_TV_BUILD_DIR=/opt/build/vt-tv \ + VT_TV_BUILD_DIR=${VT_TV_BUILD_DIR} \ VT_TV_COVERAGE_ENABLED=${VT_TV_COVERAGE_ENABLED:-OFF} \ VT_TV_OUTPUT_DIR=$VT_TV_OUTPUT_DIR \ VT_TV_RUN_TESTS=ON \ - VT_TV_XVFB_ENABLED=ON \ - /opt/src/vt-tv/build.sh" + $VT_TV_SRC_DIR/build.sh" + +# Run tests +if [[ $(uname -a) != *"Darwin"* ]]; then + xvfb-run bash -c "$VT_TV_TEST_CMD" +else + bash -c "$VT_TV_TEST_CMD" +fi # Add artifacts VT_TV_ARTIFACTS_DIR="/tmp/artifacts" diff --git a/ci/test_python.sh b/ci/test_python.sh deleted file mode 100644 index a0efc19e6d..0000000000 --- a/ci/test_python.sh +++ /dev/null @@ -1,29 +0,0 @@ -#!/bin/bash - -# This script is running tests -# > setup as pip package (internally build VT-TV with Python binding) -# > run tests - - -set -ex - -export DISPLAY=:99.0 - -# Activate conda environment -. /opt/conda/etc/profile.d/conda.sh && conda activate deves - -# Build -pip install PyYAML -pip install /opt/src/vt-tv - -# Start custom display with X virtual frame buffer -Xvfb :99 -screen 0 1024x768x24 -nolisten tcp > /dev/null 2>&1 & -sleep 1s - -# Test (needs display) -python /opt/src/vt-tv/tests/test_bindings.py - -# Clean and restore regular display -pkill Xvfb -rm -rf /tmp/.X11-unix/X99 -export DISPLAY=:0 diff --git a/cmake/load_nanobind_package.cmake b/cmake/load_nanobind_package.cmake index 2f0214e9df..29b993ed4a 100644 --- a/cmake/load_nanobind_package.cmake +++ b/cmake/load_nanobind_package.cmake @@ -1,8 +1,8 @@ find_package(Python COMPONENTS Interpreter Development.Module REQUIRED) if(NOT (${Python_VERSION_MAJOR} EQUAL 3 AND - (${Python_VERSION_MINOR} GREATER_EQUAL 8 AND ${Python_VERSION_MINOR} LESS_EQUAL 11))) - message(FATAL_ERROR "With Python bindings enabled, vt-tv requires Python version 3.8 or 3.9.") + (${Python_VERSION_MINOR} GREATER_EQUAL 8))) + message(FATAL_ERROR "With Python bindings enabled, vt-tv requires Python version 3.8 or later.") endif() diff --git a/tests/test_bindings.py b/tests/test_bindings.py index d3fffcf0df..45fbc9ce45 100644 --- a/tests/test_bindings.py +++ b/tests/test_bindings.py @@ -1,33 +1,38 @@ +"""This module calls vttv module to test that vttv bindings work as expected""" import json +import os + import yaml import vttv -import sys -import os # source dir is the directory a level above this file source_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) -with open(f'{source_dir}/tests/test_bindings_conf.yaml', 'r') as stream: - try: - params = yaml.safe_load(stream) - except yaml.YAMLError as exc: - print(exc) +with open(f'{source_dir}/tests/test_bindings_conf.yaml', 'r', encoding='utf-8') as stream: + try: + params = yaml.safe_load(stream) + except yaml.YAMLError as exc: + print(exc) + exit(1) # make output_visualization_dir directory parameter absolute -params["visualization"]["output_visualization_dir"] = os.path.abspath(params["visualization"]["output_visualization_dir"]) +if not os.path.isabs(params["visualization"]["output_visualization_dir"]): + params["visualization"]["output_visualization_dir"] = source_dir + \ + "/" + params["visualization"]["output_visualization_dir"] params_serialized = yaml.dump(params["visualization"]) -n_ranks = params["visualization"]["x_ranks"] * params["visualization"]["y_ranks"] * params["visualization"]["z_ranks"] +n_ranks = params["visualization"]["x_ranks"] * \ + params["visualization"]["y_ranks"] * params["visualization"]["z_ranks"] rank_data = [] for rank in range(n_ranks): - with open(f'{source_dir}/data/lb_test_data/data.{rank}.json', 'r') as f: - data = json.load(f) + with open(f'{source_dir}/data/lb_test_data/data.{rank}.json', 'r', encoding='utf-8') as f: + data = json.load(f) - data_serialized = json.dumps(data) + data_serialized = json.dumps(data) - rank_data.append((data_serialized)) + rank_data.append((data_serialized)) vttv.tvFromJson(rank_data, params_serialized, n_ranks) diff --git a/tests/test_bindings_conf.yaml b/tests/test_bindings_conf.yaml index c51d26f7d4..a333186b07 100644 --- a/tests/test_bindings_conf.yaml +++ b/tests/test_bindings_conf.yaml @@ -7,5 +7,5 @@ visualization: object_qoi: load save_meshes: true force_continuous_object_qoi: true - output_visualization_dir: /opt/build/vt-tv/test_output + output_visualization_dir: output/python_tests output_visualization_file_stem: output_file diff --git a/tests/test_image.sh b/tests/test_image.sh index 2c6a016ac5..174bd3160f 100755 --- a/tests/test_image.sh +++ b/tests/test_image.sh @@ -14,6 +14,11 @@ if [ ! -f "$ACTUAL" ]; then exit 1 fi +if [ ! -f "$EXPECTED" ]; then + echo "Image not found at "$EXPECTED + exit 1 +fi + pip install imgcompare --quiet 2>/dev/null DIFF=$(printf "%.2f" $(python -c 'import imgcompare; print(imgcompare.image_diff_percent("'$ACTUAL'", "'$EXPECTED'"));')) diff --git a/tests/unit/api/test_info.cc b/tests/unit/api/test_info.cc index 3b68ee0425..494aa69556 100644 --- a/tests/unit/api/test_info.cc +++ b/tests/unit/api/test_info.cc @@ -179,7 +179,7 @@ INSTANTIATE_TEST_SUITE_P( TEST_F(InfoTest, test_add_info) { Info info = Info(); - std::vector idx; + std::vector idx; // Create object info and add to a map ObjectInfo o_info = ObjectInfo(0, 0, true, idx); diff --git a/tests/unit/api/test_object_info.cc b/tests/unit/api/test_object_info.cc index 2a23c782af..7e02c777e7 100644 --- a/tests/unit/api/test_object_info.cc +++ b/tests/unit/api/test_object_info.cc @@ -56,13 +56,13 @@ struct ObjectInfoTest : public ::testing::Test { 6, // id 2, // home false, // migratable - std::vector({0, 1, 2})); + std::vector({0, 1, 2})); ObjectInfo object_1 = ObjectInfo( 7, // id 1, // home true, // migratable - std::vector({3, 5, 6})); + std::vector({3, 5, 6})); }; /** diff --git a/tests/unit/generator.h b/tests/unit/generator.h index c8dfded63e..09bea5e706 100644 --- a/tests/unit/generator.h +++ b/tests/unit/generator.h @@ -128,7 +128,7 @@ struct Generator { const std::unordered_map object_work_map, bool migratable = true) { auto object_info_map = std::unordered_map(); - std::vector idx; + std::vector idx; for (auto& it : object_work_map) { ObjectInfo object_info = ObjectInfo(it.first, 0, migratable, idx); object_info_map.insert(std::make_pair(it.first, object_info)); diff --git a/tests/unit/render/test_render.cc b/tests/unit/render/test_render.cc index 5dd5679f23..6aa2cd8ed8 100644 --- a/tests/unit/render/test_render.cc +++ b/tests/unit/render/test_render.cc @@ -155,9 +155,9 @@ class RenderTest : public ::testing::TestWithParam { void assertPolyEquals(vtkPolyData* actual, vtkPolyData* expected) { // fmt::print("Actual vtkPolyData:\n"); - printVtkPolyData(actual); + // printVtkPolyData(actual); // fmt::print("Expected vtkPolyData:\n"); - printVtkPolyData(expected); + // printVtkPolyData(expected); // Assertions required to test vt-tv meshaes // Number of point data should be ranks @@ -258,12 +258,16 @@ TEST_P(RenderTest, test_render_from_config_with_png) { std::vector cmd_vars = { fmt::format("ACTUAL={}", png_file), fmt::format("EXPECTED={}", expected_png_file), - "TOLERANCE=0.01", + "TOLERANCE=0.1", }; auto cmd = fmt::format( - "{} {}/tests/test_image.sh", fmt::join(cmd_vars, " "), SRC_DIR); + "{} {}/tests/test_image.sh", + fmt::join(cmd_vars, " "), + SRC_DIR + ); + cout << cmd << endl; const auto [status, output] = Util::exec(cmd.c_str()); - cout << output; + cout << output << endl; ASSERT_EQ(status, EXIT_SUCCESS) << output; } else { ADD_FAILURE() << "Cannot test png file (not generated)"; diff --git a/tests/unit/util.h b/tests/unit/util.h index eda43dbf91..db479a0591 100644 --- a/tests/unit/util.h +++ b/tests/unit/util.h @@ -45,59 +45,67 @@ #define INCLUDED_VT_TV_TESTS_UNIT_UTIL_H // common includes for any tests -#include +#include #include #include #include -#include -#include #include +#include +#include #include +#include #include -#include #include +#include namespace vt::tv::tests::unit { /** * Utility methods */ -struct Util { +class Util { public: /** - * \brief Execute a command on the underlying system and returns exit code and output - * \throws {@link std::runtime_error} if an error occurs while opening the process - */ + * \brief Execute a command on the underlying system and returns exit code and + * output \throws {@link std::runtime_error} if an error occurs while opening + * the process + */ static std::tuple exec(const char* cmd) { std::array buffer; - std::string output; - int status = 100; + std::string result; - FILE* pipe = popen(cmd, "r"); - if (!pipe) + // Open the pipe + std::unique_ptr pipe(popen(cmd, "r"), pclose); + if (!pipe) { throw std::runtime_error("popen() failed!"); - try { - while (fgets(buffer.data(), static_cast(buffer.size()), pipe) != - nullptr) { - output += buffer.data(); - } - } catch (...) { - status = WEXITSTATUS(pclose(pipe)); - throw; } - status = WEXITSTATUS(pclose(pipe)); - return std::make_tuple(status, output); + // Read the output + while (fgets(buffer.data(), buffer.size(), pipe.get()) != nullptr) { + result += buffer.data(); + } + + // Close the pipe and get the exit status + int status = pclose(pipe.release()); + int exit_status = 0; + if (WIFEXITED(status)) { + exit_status = WEXITSTATUS(status); + } else { + throw std::runtime_error("Command did not terminate normally."); + } + + return std::make_tuple(exit_status, result); } /** - * \brief Resolves a directory absolute path. - * \param[in] base_path Prepends "{base_path}/" to the path if path is relative - * \param[in] path The path as either a relative or an absolute path - * \param[in] add_trailing_sep Appends a trailing "/" char at the end of the path if not exist - */ + * \brief Resolves a directory absolute path. + * \param[in] base_path Prepends "{base_path}/" to the path if path is + * relative \param[in] path The path as either a relative or an absolute path + * \param[in] add_trailing_sep Appends a trailing "/" char at the end of the + * path if not exist + */ static std::string resolveDir( std::string base_path, std::string path, bool add_trailing_sep = false) { std::filesystem::path abs_path(path); @@ -118,8 +126,8 @@ struct Util { } /** - * \brief Reads file content and returns it as a string - */ + * \brief Reads file content and returns it as a string + */ static std::string getFileContent(std::string filename) { std::ifstream ifs(filename); std::string content( @@ -130,8 +138,8 @@ struct Util { } /** - * \brief Formats a text with suport of null values - */ + * \brief Formats a text with suport of null values + */ static std::string formatNullable(const char* data) { if (data == nullptr) { return "";