diff --git a/.coveragerc b/.coveragerc index 409fde55643..4d44894d4f5 100644 --- a/.coveragerc +++ b/.coveragerc @@ -5,7 +5,7 @@ concurrency = multiprocessing,thread parallel = True data_file = ${INITIAL_PWD-.}/.coverage omit = - ${INITIAL_PWD-.}/testreport + **/testreport/* ${INITIAL_PWD-.}/.github/* ${INITIAL_PWD-.}/bin.*/* ${INITIAL_PWD-.}/dist.*/* diff --git a/.github/workflows/macos.yml b/.github/workflows/macos.yml index e9e693e57c3..88b8cae9dcb 100644 --- a/.github/workflows/macos.yml +++ b/.github/workflows/macos.yml @@ -89,6 +89,17 @@ jobs: if: ${{ !cancelled() }} shell: micromamba-shell {0} run: source ./.github/workflows/print_versions.sh + - name: Place a usercustomize.py file enabling subprocess code coverage + shell: bash -el {0} + run: | + mkdir -p $(python -m site --user-site) + printf "import coverage\ncoverage.process_startup()" \ + > "$(python -m site --user-site)/usercustomize.py" + - name: Set COVERAGE_PROCESS_START env variable + shell: bash -el {0} + run: | + echo "COVERAGE_PROCESS_START=${PWD}/.coveragerc" >> $GITHUB_ENV + echo "COVERAGE_RCFILE=${PWD}/.coveragerc" >> $GITHUB_ENV - name: Run pytest with multiple workers in parallel shell: micromamba-shell {0} @@ -135,15 +146,65 @@ jobs: - name: Run gunittest tests shell: micromamba-shell {0} - run: .github/workflows/test_thorough.sh --config .github/workflows/macos_gunittest.cfg + run: | + export INITIAL_GISBASE="$(grass --config path)" + export INITIAL_PWD="${PWD}" + grass --tmp-project XY --exec \ + g.download.location url=${{ env.SAMPLE_DATA_URL }} path=$HOME + grass --tmp-project XY --exec \ + ${PYTHON} -m grass.gunittest.main \ + --grassdata $HOME --location nc_spm_full_v2alpha2 --location-type nc \ + --min-success 100 --config .github/workflows/macos_gunittest.cfg env: SAMPLE_DATA_URL: "file://${{ github.workspace }}/sample-data/\ nc_spm_full_v2alpha2.tar.gz" - + PYTHON: coverage run + - name: Fix non-standard installed script paths in coverage data + shell: bash -el {0} + run: | + export PYTHONPATH=`grass --config python_path`:$PYTHONPATH + # export LD_LIBRARY_PATH=$(grass --config path)/lib:$LD_LIBRARY_PATH + export INITIAL_GISBASE="$(grass --config path)" + export INITIAL_PWD="${PWD}" + coverage combine + python utils/coverage_mapper.py + coverage combine + - name: Show python coverage report summary + shell: bash -el {0} + run: | + export PYTHONPATH=`grass --config python_path`:$PYTHONPATH + # export LD_LIBRARY_PATH=$(grass --config path)/lib:$LD_LIBRARY_PATH + export INITIAL_GISBASE="$(grass --config path)" + export INITIAL_PWD="${PWD}" + coverage report + coverage json + - name: Generate HTML coverage report + shell: bash -el {0} + run: | + export PYTHONPATH=`grass --config python_path`:$PYTHONPATH + # export LD_LIBRARY_PATH=$(grass --config path)/lib:$LD_LIBRARY_PATH + export INITIAL_GISBASE="$(grass --config path)" + export INITIAL_PWD="${PWD}" + coverage html - name: Make HTML test report available if: ${{ !cancelled() }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: testreport-macOS + name: testreport-${{ runner.os }} path: testreport retention-days: 3 + - name: Make python-only code coverage test report available + if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: >- + python-codecoverage-report-${{ 'macos-14' }} + path: coverage_html_report + retention-days: 1 + - name: Upload coverage reports to Codecov + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + with: + verbose: true + flags: gunittest-${{ 'macos-14' }} + name: gunittest-${{ 'macos-14' }} + token: ${{ secrets.CODECOV_TOKEN }} diff --git a/.github/workflows/test_thorough.sh b/.github/workflows/test_thorough.sh index 57c545133df..cdc4bdfdc1d 100755 --- a/.github/workflows/test_thorough.sh +++ b/.github/workflows/test_thorough.sh @@ -5,10 +5,16 @@ set -e SAMPLE_DATA_URL=${SAMPLE_DATA_URL:-"https://grass.osgeo.org/sampledata/north_carolina/nc_spm_full_v2alpha2.tar.gz"} +if [[ -z "${PYTHON}" ]]; then + PYTHON="python3" +else + PYTHON="${PYTHON}" +fi + grass --tmp-project XY --exec \ g.download.project url=$SAMPLE_DATA_URL path=$HOME grass --tmp-project XY --exec \ - python3 -m grass.gunittest.main \ + ${PYTHON} -m grass.gunittest.main \ --grassdata $HOME --location nc_spm_full_v2alpha2 --location-type nc \ --min-success 100 $@ diff --git a/.github/workflows/ubuntu.yml b/.github/workflows/ubuntu.yml index ec109386ad0..fc5830b4d1a 100644 --- a/.github/workflows/ubuntu.yml +++ b/.github/workflows/ubuntu.yml @@ -44,6 +44,7 @@ jobs: # newlines with spaces, and strip newline at end. # See https://yaml-multiline.info/ - temporal + - db - >- db display doc docker general gui imagery lib misc ps python raster raster3d scripts @@ -76,14 +77,14 @@ jobs: echo "${delete[@]}" for target in "${delete[@]}"; do for i in "${!array[@]}"; do - if [[ ${array[i]} = $target ]]; then + if [[ ${array[i]} = "$target" ]]; then unset 'array[i]' fi done done unset new_array for i in "${!array[@]}"; do - new_array+=( "${array[i]}" ) + new_array+=( "${array[i]}" ) done echo "Excluded folders:" echo "${new_array[@]}" @@ -91,6 +92,26 @@ jobs: echo "Exclusion string to add to gunittest config" echo "${extra_exclude}" echo "extra-exclude=${extra_exclude}" >> "${GITHUB_OUTPUT}" + echo "Inclusion string for tags" + printf -v extra_include_tag ' %s' "${delete[@]}" + extra_include_tag="${extra_include_tag:1}" # trim extra first space + extra_include_tag=${extra_include_tag// /-} # replace spaces by hyphens + echo "${extra_include_tag}" + echo "extra-include-tag=${extra_include_tag}" >> "${GITHUB_OUTPUT}" + echo "Truncated string for codecov upload" + max_flag_len=45 + max_flag_prefix_len=33 # gunittest-ubuntu-22.04_without_x- + max_flag_suffix_len=$((max_flag_len - max_flag_prefix_len)) + extra_include_tag_len=${#extra_include_tag} # string's length + extra_include_tag_len_len=${#extra_include_tag_len} # length of string's length + if [[ "${extra_include_tag_len}" -gt "${max_flag_suffix_len}" ]]; then + # Extra include too long + extra_include_tag_trunc="${extra_include_tag:0:$max_flag_suffix_len-1-$extra_include_tag_len_len}.${extra_include_tag_len}" + else + extra_include_tag_trunc="${extra_include_tag}" + fi + echo "${extra_include_tag_trunc}" + echo "extra-include-tag-trunc=${extra_include_tag_trunc}" >> "${GITHUB_OUTPUT}" env: DELETE_ARRAY: ${{ matrix.extra-include }} @@ -107,6 +128,9 @@ jobs: sudo apt-get install -y wget git gawk findutils xargs -a <(awk '! /^ *(#|$)/' ".github/workflows/apt.txt") -r -- \ sudo apt-get install -y --no-install-recommends --no-install-suggests + - name: Install python coverage tools + run: pip install coverage slipcover + - run: python -m slipcover --help - name: Create installation directory run: | @@ -139,13 +163,35 @@ jobs: run: | echo "$HOME/install/bin" >> $GITHUB_PATH + - name: Replace scripts with symbolic links to have files with .py extensions + if: ${{ false }} + run: | + cd "$(grass --config path)" + cp -R scripts/ scrips_orig/ + # Output first lines of files + head $(grass --config path)/scripts/* -n1 -q + # Rename files to have .py extension + ls | xargs -I fileName mv fileName fileName.py + # Copy with symbolic link + for old in scripts/*.py + do new=`echo $old | sed -e's/.py//'` + cp --symbolic-link "$old" "$new" + done + ls -la + - name: Print installed versions - if: always() + if: ${{ !cancelled() }} run: .github/workflows/print_versions.sh - name: Test executing of the grass command run: .github/workflows/test_simple.sh + # - name: Place a usercustomize.py file enabling subprocess code coverage + # run: | + # mkdir -p $(python -m site --user-site) + # printf "import coverage\ncoverage.process_startup()" \ + # > "$(python -m site --user-site)/usercustomize.py" + - name: Cache GRASS Sample Dataset id: cached-data uses: actions/cache@5a3ec84eff668545956fd18022155c47e93e2684 # v4.2.3 @@ -163,19 +209,88 @@ jobs: SAMPLE_DATA: "https://grass.osgeo.org/sampledata/north_carolina/\ nc_spm_full_v2alpha2.tar.gz" + # - name: Set COVERAGE_PROCESS_START env variable + # run: | + # echo "COVERAGE_PROCESS_START=${PWD}/.coveragerc" >> $GITHUB_ENV + # echo "COVERAGE_RCFILE=${PWD}/.coveragerc" >> $GITHUB_ENV + - name: Run tests - run: .github/workflows/test_thorough.sh --config .gunittest.extra.cfg + run: | + export INITIAL_GISBASE="$(grass --config path)" + export INITIAL_PWD="${PWD}" + #export PYTHON="${PYTHON} --source ${INITIAL_PWD}/ --source ${INITIAL_GISBASE}/" + #export PYTHON="${PYTHON} --source ${INITIAL_GISBASE}/scripts" + echo "$PYTHON" + export PYTHON="${PYTHON} --source .,${INITIAL_PWD}/,${INITIAL_GISBASE}/,${INITIAL_GISBASE}/scripts" + echo "$PYTHON" + .github/workflows/test_thorough.sh --config .gunittest.extra.cfg env: + PYTHON: >- + python -m slipcover + --json + --out ${{ github.workspace }}/slipcover.json + #--source ${{ github.workspace }} SAMPLE_DATA_URL: "file://${{ github.workspace }}/sample-data/\ nc_spm_full_v2alpha2.tar.gz" + - run: cat slipcover.json + - run: jq . slipcover.json + - name: Convert slipcover's output to coverage.py data file + run: python utils/slipcover_to_coverage.py slipcover.json .coverage.slipcover + - run: ls -la + - name: Fix non-standard installed script paths in coverage data + run: | + export PYTHONPATH=`grass --config python_path`:$PYTHONPATH + export LD_LIBRARY_PATH=$(grass --config path)/lib:$LD_LIBRARY_PATH + export INITIAL_GISBASE="$(grass --config path)" + export INITIAL_PWD="${PWD}" + coverage combine + python utils/coverage_mapper.py + coverage combine + + - name: Show python coverage report summary + run: | + export PYTHONPATH=`grass --config python_path`:$PYTHONPATH + export LD_LIBRARY_PATH=$(grass --config path)/lib:$LD_LIBRARY_PATH + export INITIAL_GISBASE="$(grass --config path)" + export INITIAL_PWD="${PWD}" + coverage report + continue-on-error: true + - name: Generate HTML coverage report + run: | + export PYTHONPATH=`grass --config python_path`:$PYTHONPATH + export LD_LIBRARY_PATH=$(grass --config path)/lib:$LD_LIBRARY_PATH + export INITIAL_GISBASE="$(grass --config path)" + export INITIAL_PWD="${PWD}" + coverage html + continue-on-error: true - name: Make HTML test report available - if: ${{ always() }} + if: ${{ !cancelled() }} uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 with: - name: testreport-${{ matrix.os }}-${{ matrix.config }}-${{ matrix.extra-include }} + name: >- + testreport-${{ matrix.os }}-${{ matrix.config }}-${{ + '' }}${{ steps.get-exclude.outputs.extra-include-tag }} path: testreport retention-days: 3 + - name: Make python-only code coverage test report available + if: ${{ !cancelled() }} + uses: actions/upload-artifact@ea165f8d65b6e75b540449e92b4886f43607fa02 # v4.6.2 + with: + name: >- + python-codecoverage-report-${{ matrix.os }}-${{ matrix.config }}-${{ + '' }}${{ steps.get-exclude.outputs.extra-include-tag }} + path: coverage_html_report + retention-days: 1 + + - name: Upload coverage reports to Codecov + if: ${{ !cancelled() }} + uses: codecov/codecov-action@18283e04ce6e62d37312384ff67231eb8fd56d24 # v5.4.3 + with: + verbose: true + flags: gunittest-${{ matrix.config }}-${{ steps.get-exclude.outputs.extra-include-tag-trunc }} + name: gunittest-${{ matrix.config }}-${{ steps.get-exclude.outputs.extra-include-tag-trunc }} + token: ${{ secrets.CODECOV_TOKEN }} build-and-test-success: name: Build & Test Result diff --git a/macos/files/conda-requirements-dev-arm64.txt b/macos/files/conda-requirements-dev-arm64.txt index 496dfd6f0c4..4fa79596c23 100644 --- a/macos/files/conda-requirements-dev-arm64.txt +++ b/macos/files/conda-requirements-dev-arm64.txt @@ -3,6 +3,7 @@ cairo clangxx_osx-arm64 clang_osx-arm64 cmake +coverage expat fftw flex diff --git a/utils/slipcover_to_coverage.py b/utils/slipcover_to_coverage.py new file mode 100644 index 00000000000..d34f474f89b --- /dev/null +++ b/utils/slipcover_to_coverage.py @@ -0,0 +1,28 @@ +from coverage import CoverageData +import json +from pathlib import Path +import sys + + +def convert_slipcover_json_to_coverage(input_path, output_path): + with Path(input_path).open("r") as input_fp: + input_json = json.load(input_fp) + data_file = CoverageData(output_path) + data_file.read() + + line_data = { + str(Path().joinpath(file).resolve()): data["executed_lines"] + for file, data in input_json["files"].items() + } + data_file.add_lines(line_data) + data_file.write() + + +def main(): + infile = sys.argv[1] + outfile = sys.argv[2] + convert_slipcover_json_to_coverage(infile, outfile) + + +if __name__ == "__main__": + main()