diff --git a/.github/workflows/cmake.yml b/.github/workflows/cmake.yml index aff291dd9d8..5ea7c4ce135 100644 --- a/.github/workflows/cmake.yml +++ b/.github/workflows/cmake.yml @@ -27,6 +27,13 @@ jobs: runs-on: ubuntu-22.04 env: CMakeVersion: "3.22.0" + CC: clang-${{ matrix.llvm-version }} + CXX: clang++-${{ matrix.llvm-version }} + ASAN_OPTIONS: detect_leaks=0 + + strategy: + matrix: + llvm-version: [19] steps: - name: Checkout GRASS uses: actions/checkout@11bd71901bbe5b1630ceea73d27597364c9af683 # v4.2.2 @@ -41,10 +48,19 @@ jobs: echo "$GITHUB_WORKSPACE/cmake-${{ env.CMakeVersion }}-${arch}/bin" >> $GITHUB_PATH - run: | cmake --version + - name: Apt LLVM + run: | + wget https://apt.llvm.org/llvm.sh + chmod +x llvm.sh + sudo ./llvm.sh ${{ matrix.llvm-version }} + sudo apt-get update - name: Install dependencies run: | sudo apt-get update -y sudo apt-get install -y wget git gawk findutils ninja-build libpq-dev \ + clang-${{ matrix.llvm-version }} clang-tools-${{ matrix.llvm-version }} \ + libomp-${{ matrix.llvm-version }}-dev lld-${{ matrix.llvm-version }} \ + llvm-${{ matrix.llvm-version }} \ gettext unixodbc-dev libnetcdf-dev xargs -a <(awk '! /^ *(#|$)/' ".github/workflows/apt.txt") -r -- \ sudo apt-get install -y --no-install-recommends --no-install-suggests @@ -61,7 +77,9 @@ jobs: run: | cmake ${CMAKE_OPTIONS} -S $GITHUB_WORKSPACE -B $GITHUB_WORKSPACE/build -G Ninja \ -DCMAKE_INSTALL_PREFIX=$HOME/install -DWITH_NLS=ON -DWITH_GUI=OFF -DWITH_DOCS=OFF \ - -DWITH_READLINE=ON -DWITH_ODBC=ON -DWITH_NETCDF=ON -DWITH_BZLIB=ON + -DWITH_READLINE=ON -DWITH_ODBC=ON -DWITH_NETCDF=ON -DWITH_BZLIB=ON \ + -DBUILD_SHARED_LIBS=OFF \ + -DCMAKE_C_COMPILER=clang-${{ matrix.llvm-version }} -DCMAKE_CXX_COMPILER=clang++-${{ matrix.llvm-version }} - name: Print CMakeCache.txt shell: bash -el {0} run: | @@ -75,6 +93,8 @@ jobs: - name: Add the bin directory to PATH run: | echo "$HOME/install/bin" >> $GITHUB_PATH + - name: Set profile file name env var + run: echo "LLVM_PROFILE_FILE=${{ github.workspace }}/grass-prof-%p-%4m.profraw" >> $GITHUB_ENV - name: Print installed versions if: always() run: .github/workflows/print_versions.sh @@ -89,3 +109,46 @@ jobs: name: testreport-CMake path: testreport retention-days: 3 + - name: Use llvm's coverage script + if: ${{ !cancelled() }} + run: | + WORKSPACE=$GITHUB_WORKSPACE + + BUILD_DIR=$(pwd)/dist* + FAST_BUILD_DIR=$(pwd)/clang-build + REPORT_DIR=$(pwd)/coverage-reports + HOST= + LLVM_PROFDATA=llvm-profdata-${{ matrix.llvm-version }} + LLVM_COV=llvm-cov-${{ matrix.llvm-version }} + ARTIFACT_PREP_SCRIPT=$WORKSPACE/utils/prepare-code-coverage-artifact.py + + PROFILES_DIR=$WORKSPACE + + COV_BINARIES=$(find ./build/output/ -type f \( -executable -o -name "*.dylib" -o -name "*.so*" \) \ + -exec file {} + | grep -E 'Mach-O|ELF' | cut -d':' -f1) + + rm -rf $REPORT_DIR + mkdir -p $REPORT_DIR + python $ARTIFACT_PREP_SCRIPT $LLVM_PROFDATA $LLVM_COV \ + $PROFILES_DIR $REPORT_DIR $COV_BINARIES \ + --unified-report \ + --lcov=grass.llvm-source.coverage.lcov + - name: Make profile data available + if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: >- + profile-data-${{ 'cmake' }} + path: | + *.prof* + *coverage* + llvm_coverage*/** + retention-days: 1 + - name: Upload coverage reports to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + with: + verbose: true + flags: linux-cmake + name: linux-cmake + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e9e693e57c3..1ba916cb163 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -55,6 +55,7 @@ jobs: - name: Prepare Conda environment file env: test_dependencies: | + findutils ipython pytest pytest-github-actions-annotate-failures @@ -73,6 +74,8 @@ jobs: # Persist on the same period (date). cache-environment-key: environment-${{ steps.date.outputs.date }} + - name: Set profile file name env var + run: echo "LLVM_PROFILE_FILE=${{ github.workspace }}/grass-prof-%p-%4m.profraw" >> $GITHUB_ENV - name: Environment info shell: bash -el {0} run: | @@ -89,6 +92,9 @@ jobs: if: ${{ !cancelled() }} shell: micromamba-shell {0} run: source ./.github/workflows/print_versions.sh + - name: Print config + shell: micromamba-shell {0} + run: grass --config - name: Run pytest with multiple workers in parallel shell: micromamba-shell {0} @@ -147,3 +153,61 @@ jobs: name: testreport-macOS path: testreport retention-days: 3 + # - name: Merge and index profile data + # shell: micromamba-shell {0} + # run: | + # llvm-profdata merge -sparse *.profraw -o merged.profdata + # rm *.profraw + # - name: Export coverage report with llvm-cov export (all, json) + # shell: micromamba-shell {0} + # continue-on-error: true + # run: | + # COV_BINARIES=$(find . -type f \( -executable -o -name "*.dylib" \) \ + # -exec file {} + | grep -E 'Mach-O|ELF' | cut -d':' -f1) + # llvm-cov export -format=text -instr-profile=merged.profdata \ + # /Users/runner/install/grass85/lib/libgrass_gis.8.5.dylib \ + # | tee coverage.all1.json + - name: Use llvm's coverage script + if: ${{ !cancelled() }} + shell: micromamba-shell {0} + run: | + WORKSPACE=$GITHUB_WORKSPACE + + BUILD_DIR=$(pwd)/dist* + FAST_BUILD_DIR=$(pwd)/clang-build + REPORT_DIR=$(pwd)/coverage-reports + HOST= + LLVM_PROFDATA=llvm-profdata + LLVM_COV=llvm-cov + ARTIFACT_PREP_SCRIPT=$WORKSPACE/utils/prepare-code-coverage-artifact.py + + PROFILES_DIR=$WORKSPACE + + COV_BINARIES=$(find . -type f \( -executable -o -name "*.dylib" \) \ + -exec file {} + | grep -E 'Mach-O|ELF' | cut -d':' -f1) + + rm -rf $REPORT_DIR + mkdir -p $REPORT_DIR + python $ARTIFACT_PREP_SCRIPT $LLVM_PROFDATA $LLVM_COV \ + $PROFILES_DIR $REPORT_DIR $COV_BINARIES \ + --unified-report \ + --lcov=grass.llvm-source.coverage.lcov + - name: Make profile data available + if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: >- + profile-data-${{ 'macos-14' }} + path: | + *.prof* + *coverage* + llvm_coverage*/** + retention-days: 1 + - name: Upload coverage reports to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + with: + verbose: true + flags: pytest-macos-14 + name: pytest-macos-14 + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/macos_install.sh b/.github/workflows/macos_install.sh index 4f340c73f9e..bdb19d5c6d4 100755 --- a/.github/workflows/macos_install.sh +++ b/.github/workflows/macos_install.sh @@ -59,14 +59,20 @@ CONFIGURE_FLAGS="\ --without-x \ " +export EXTRA_COV_FLAGS="-fprofile-instr-generate -fcoverage-mapping" export CFLAGS="-O2 -pipe -ffp-contract=off -arch ${CONDA_ARCH} -DGL_SILENCE_DEPRECATION -Wall -Wextra -Wpedantic -Wvla" export CXXFLAGS="-O2 -pipe -ffp-contract=off -stdlib=libc++ -arch ${CONDA_ARCH} -Wall -Wextra -Wpedantic" export CPPFLAGS="-isystem${CONDA_PREFIX}/include" -./configure $CONFIGURE_FLAGS +export CFLAGS="$EXTRA_COV_FLAGS $CFLAGS" +export CXXFLAGS="$EXTRA_COV_FLAGS $CXXFLAGS" + +LDFLAGS="-fprofile-instr-generate $LDFLAGS" ./configure $CONFIGURE_FLAGS EXEMPT="" -make -j$(sysctl -n hw.ncpu) CFLAGS="$CFLAGS -Werror $EXEMPT" \ +# make -j$(sysctl -n hw.ncpu) CFLAGS="$CFLAGS -Werror $EXEMPT" \ +# CXXFLAGS="$CXXFLAGS -Werror $EXEMPT" +make -j1 CFLAGS="$CFLAGS -Werror $EXEMPT" \ CXXFLAGS="$CXXFLAGS -Werror $EXEMPT" make install diff --git a/.gunittest.cfg b/.gunittest.cfg index bb5636cbb51..0419736f180 100644 --- a/.gunittest.cfg +++ b/.gunittest.cfg @@ -25,6 +25,9 @@ exclude = vector/v.in.pdal/testsuite/test_v_in_pdal_basic.py vector/v.in.pdal/testsuite/test_v_in_pdal_filter.py vector/v.out.lidar/testsuite/test_v_out_lidar.py + temporal/.* + *.sh + raster/r.s* # Maximum time for execution of one test file (not a test function) # after which test is terminated (which may not terminate child processes diff --git a/CMakeLists.txt b/CMakeLists.txt index 255b94dfc99..ab42d05eeee 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -45,6 +45,9 @@ if(NOT CMAKE_C_STANDARD) set(CMAKE_C_STANDARD 11) set(CMAKE_C_STANDARD_REQUIRED ON) endif() +SET(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -g -fprofile-instr-generate -fcoverage-mapping -fno-omit-frame-pointer -fsanitize=address") +SET(CMAKE_C_FLAGS "${CMAKE_C_FLAGS} -g -fprofile-instr-generate -fcoverage-mapping -fno-omit-frame-pointer -fsanitize=address") +SET(CMAKE_EXE_LINKER_FLAGS "${CMAKE_EXE_LINKER_FLAGS} -fprofile-instr-generate -fno-omit-frame-pointer -fsanitize=address") set(x11_default_option_enabled ON) set(nls_default_option_enabled ON) diff --git a/imagery/i.smap/model.c b/imagery/i.smap/model.c index dd567cfe5bb..4dd3ab5f761 100644 --- a/imagery/i.smap/model.c +++ b/imagery/i.smap/model.c @@ -141,7 +141,7 @@ void extract(DCELL ***img, /* multispectral image, img[band][i][j] */ ll[i][j][m] = subll[0]; } /* compute mixture likelihood */ - else { + else if (C->nsubclasses > 1) { /* find the most likely subclass */ for (k = 0; k < C->nsubclasses; k++) { if (k == 0) @@ -157,6 +157,9 @@ void extract(DCELL ***img, /* multispectral image, img[band][i][j] */ ll[i][j][m] = log(subsum) + maxlike; } + else { + ll[i][j][m] = 0.0; + } } } } diff --git a/utils/prepare-code-coverage-artifact.py b/utils/prepare-code-coverage-artifact.py new file mode 100644 index 00000000000..5edc4538133 --- /dev/null +++ b/utils/prepare-code-coverage-artifact.py @@ -0,0 +1,266 @@ +#!/usr/bin/env python + + +"""Prepare a code coverage artifact. + +- Collate raw profiles into one indexed profile. +- Generate html reports for the given binaries. + +Caution: The positional arguments to this script must be specified before any +optional arguments, such as --restrict. +""" + +import argparse +import glob +import os +import subprocess +import sys +from pathlib import Path + + +def merge_raw_profiles(host_llvm_profdata, profile_data_dir, preserve_profiles): + print(":: Merging raw profiles...", end="") + sys.stdout.flush() + raw_profiles = glob.glob(os.path.join(profile_data_dir, "*.profraw")) + manifest_path = os.path.join(profile_data_dir, "profiles.manifest") + profdata_path = os.path.join(profile_data_dir, "Coverage.profdata") + Path(manifest_path).write_text("\n".join(raw_profiles), encoding="utf-8") + subprocess.check_call( + [ + host_llvm_profdata, + "merge", + "-sparse", + "-f", + manifest_path, + "-o", + profdata_path, + ] + ) + if not preserve_profiles: + for raw_profile in raw_profiles: + os.remove(raw_profile) + os.remove(manifest_path) + print("Done!") + return profdata_path + + +def prepare_export_file( + host_llvm_cov, + profile, + report_file, + binaries, + restricted_dirs, + compilation_dir, + export_format, +): + export_format_label = export_format + if export_format == "text": + export_format_label = "json" + print(f":: Preparing {export_format_label} report for {binaries}...", end="") + sys.stdout.flush() + objects = [] + for i, binary in enumerate(binaries): + if i == 0: + objects.append(binary) + else: + objects.extend(("-object", binary)) + invocation = ( + [host_llvm_cov, "export"] + + objects + + [ + "-format", + export_format, + "-instr-profile", + profile, + "-skip-functions", + ] + + restricted_dirs + ) + if compilation_dir: + invocation += ["-compilation-dir=" + compilation_dir] + with Path(report_file).open("w", encoding="utf-8") as summary: + subprocess.check_call( + invocation, + stdout=summary, + ) + print("Done!") + + +def prepare_html_report( + host_llvm_cov, profile, report_dir, binaries, restricted_dirs, compilation_dir +): + print(f":: Preparing html report for {binaries}...", end="") + sys.stdout.flush() + objects = [] + for i, binary in enumerate(binaries): + if i == 0: + objects.append(binary) + else: + objects.extend(("-object", binary)) + invocation = ( + [host_llvm_cov, "show"] + + objects + + [ + "-format", + "html", + "-instr-profile", + profile, + "-o", + report_dir, + "-show-line-counts-or-regions", + "-dump", + "-show-directory-coverage", + "-Xdemangler", + "c++filt", + "-Xdemangler", + "-n", + ] + + restricted_dirs + ) + if compilation_dir: + invocation += ["-compilation-dir=" + compilation_dir] + subprocess.check_call(invocation) + with Path(report_dir, "summary.txt").open("wb") as summary: + subprocess.check_call( + [host_llvm_cov, "report"] + + objects + + ["-instr-profile", profile] + + restricted_dirs, + stdout=summary, + ) + print("Done!") + + +def prepare_html_reports( + host_llvm_cov, + profdata_path, + report_dir, + binaries, + unified_report, + restricted_dirs, + compilation_dir, +): + if unified_report: + prepare_html_report( + host_llvm_cov, + profdata_path, + report_dir, + binaries, + restricted_dirs, + compilation_dir, + ) + else: + for binary in binaries: + binary_report_dir = os.path.join(report_dir, os.path.basename(binary)) + prepare_html_report( + host_llvm_cov, + profdata_path, + binary_report_dir, + [binary], + restricted_dirs, + compilation_dir, + ) + + +if __name__ == "__main__": + parser = argparse.ArgumentParser(description=__doc__) + parser.add_argument("host_llvm_profdata", help="Path to llvm-profdata") + parser.add_argument("host_llvm_cov", help="Path to llvm-cov") + parser.add_argument( + "profile_data_dir", help="Path to the directory containing the raw profiles" + ) + parser.add_argument( + "report_dir", help="Path to the output directory for html reports" + ) + parser.add_argument( + "binaries", + metavar="B", + type=str, + nargs="*", + help="Path to an instrumented binary", + ) + parser.add_argument( + "--only-merge", + action="store_true", + help="Only merge raw profiles together, skip report generation", + ) + parser.add_argument( + "--preserve-profiles", help="Do not delete raw profiles", action="store_true" + ) + parser.add_argument( + "--use-existing-profdata", help="Specify an existing indexed profile to use" + ) + parser.add_argument( + "--unified-report", + action="store_true", + help="Emit a unified report for all binaries", + ) + parser.add_argument( + "--json", help="If specified, export the coverage data to this json file" + ) + parser.add_argument( + "--lcov", help="If specified, export the coverage data to this lcov file" + ) + parser.add_argument( + "--restrict", + metavar="R", + type=str, + nargs="*", + default=[], + help="Restrict the reporting to the given source paths" + " (must be specified after all other positional arguments)", + ) + parser.add_argument( + "-C", + "--compilation-dir", + type=str, + default="", + help="The compilation directory of the binary", + ) + args = parser.parse_args() + + if args.use_existing_profdata and args.only_merge: + print("--use-existing-profdata and --only-merge are incompatible") + sys.exit(1) + + if args.use_existing_profdata: + profdata_path = args.use_existing_profdata + else: + profdata_path = merge_raw_profiles( + args.host_llvm_profdata, args.profile_data_dir, args.preserve_profiles + ) + + if not len(args.binaries): + print("No binaries specified, no work to do!") + sys.exit(1) + + if not args.only_merge: + prepare_html_reports( + args.host_llvm_cov, + profdata_path, + args.report_dir, + args.binaries, + args.unified_report, + args.restrict, + args.compilation_dir, + ) + if args.json: + prepare_export_file( + args.host_llvm_cov, + profdata_path, + args.json, + args.binaries, + args.restrict, + args.compilation_dir, + export_format="text", + ) + if args.lcov: + prepare_export_file( + args.host_llvm_cov, + profdata_path, + args.lcov, + args.binaries, + args.restrict, + args.compilation_dir, + export_format="lcov", + )