diff --git a/.github/actions/cpp-bazel/pre-merge/action.yml b/.github/actions/cpp-bazel/pre-merge/action.yml index 0cd50e62f0..f46f0f3b50 100644 --- a/.github/actions/cpp-bazel/pre-merge/action.yml +++ b/.github/actions/cpp-bazel/pre-merge/action.yml @@ -63,7 +63,7 @@ runs: shell: bash run: | cd foreign/cpp - cargo clippy --manifest-path Cargo.toml --all-targets --all-features -- -D warnings + cargo clippy --all-targets --all-features -- -D warnings - name: Build if: inputs.task == 'build' @@ -72,18 +72,24 @@ runs: cd foreign/cpp bazel build --config=ci //:iggy-cpp + - name: Install cargo-llvm-cov + if: inputs.task == 'test' + uses: taiki-e/install-action@v2.87.3 + with: + tool: cargo-llvm-cov + - name: Setup server for tests if: inputs.task == 'test' uses: ./.github/actions/utils/server-start with: cargo-bin: iggy-server - - name: Test + - name: Test with coverage if: inputs.task == 'test' shell: bash run: | cd foreign/cpp - bazel test --config=ci //... + ../../scripts/ci/cpp-coverage.sh - name: Stop server after tests if: always() && inputs.task == 'test' diff --git a/.github/workflows/_test.yml b/.github/workflows/_test.yml index 82a70444ba..8d0594c679 100644 --- a/.github/workflows/_test.yml +++ b/.github/workflows/_test.yml @@ -231,6 +231,18 @@ jobs: with: task: ${{ inputs.task }} + - name: Upload C++ coverage to Codecov + if: inputs.component == 'sdk-cpp' && inputs.task == 'test' + uses: codecov/codecov-action@v7.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: reports/cpp-coverage.lcov + disable_search: true + flags: cpp + fail_ci_if_error: false + verbose: true + override_pr: ${{ github.event.pull_request.number }} + - name: Upload Java coverage to Codecov if: inputs.component == 'sdk-java' && inputs.task == 'test' uses: codecov/codecov-action@v7.0.0 diff --git a/.github/workflows/coverage-baseline.yml b/.github/workflows/coverage-baseline.yml index 42a90fccf3..2e7b2a805b 100644 --- a/.github/workflows/coverage-baseline.yml +++ b/.github/workflows/coverage-baseline.yml @@ -15,8 +15,8 @@ # specific language governing permissions and limitations # under the License. -# Coverage baseline for all 7 languages (Rust, Java, C#, Python, PHP, -# Node, Go). Runs on push to master so Codecov has data on master SHAs for +# Coverage baseline for all 8 languages (Rust, Java, C#, Python, PHP, +# Node, Go, C++). Runs on push to master so Codecov has data on master SHAs for # carryforward on PR builds where only a subset of SDKs is tested. # # Jobs are path-gated: a language job runs only when the paths feeding its @@ -61,6 +61,7 @@ jobs: php: ${{ steps.affected.outputs.php }} node: ${{ steps.affected.outputs.node }} go: ${{ steps.affected.outputs.go }} + cpp: ${{ steps.affected.outputs.cpp }} steps: - uses: actions/checkout@v7.0.1 with: @@ -581,6 +582,56 @@ jobs: flags: go fail_ci_if_error: false + cpp-coverage: + name: C++ coverage baseline + needs: detect + if: needs.detect.outputs.cpp == 'true' + runs-on: ubuntu-latest + timeout-minutes: 45 + steps: + - uses: actions/checkout@v7.0.1 + + - name: Setup Bazel with cache + uses: ./.github/actions/utils/setup-cpp-with-cache + + - name: Setup Rust with cache + uses: ./.github/actions/utils/setup-rust-with-cache + with: + save-cache: "false" + + - name: Install cargo-llvm-cov + uses: taiki-e/install-action@v2.87.3 + with: + tool: cargo-llvm-cov + + # The e2e suite needs a live server; unit tests run standalone. + - name: Start Iggy server + id: iggy + uses: ./.github/actions/utils/server-start + with: + cargo-bin: iggy-server + + - name: Run tests and generate coverage + run: | + cd foreign/cpp + ../../scripts/ci/cpp-coverage.sh + + - name: Stop Iggy server + if: always() + uses: ./.github/actions/utils/server-stop + with: + pid-file: ${{ steps.iggy.outputs.pid_file }} + log-file: ${{ steps.iggy.outputs.log_file }} + + - name: Upload to Codecov + uses: codecov/codecov-action@v7.0.0 + with: + token: ${{ secrets.CODECOV_TOKEN }} + files: reports/cpp-coverage.lcov + disable_search: true + flags: cpp + fail_ci_if_error: false + warm-cache-macos: name: Warm macOS cache needs: detect diff --git a/.github/workflows/pre-merge.yml b/.github/workflows/pre-merge.yml index aae32a00c6..ef9527e6d2 100644 --- a/.github/workflows/pre-merge.yml +++ b/.github/workflows/pre-merge.yml @@ -165,6 +165,8 @@ jobs: with: component: ${{ matrix.component }} task: ${{ matrix.task }} + secrets: + CODECOV_TOKEN: ${{ secrets.CODECOV_TOKEN }} # Other components test-other: diff --git a/codecov.yml b/codecov.yml index 7c71b59c96..9557471df1 100644 --- a/codecov.yml +++ b/codecov.yml @@ -71,6 +71,9 @@ flag_management: - name: go paths: - foreign/go/ + - name: cpp + paths: + - foreign/cpp/ component_management: individual_components: @@ -102,6 +105,10 @@ component_management: name: "Go SDK" paths: - foreign/go/ + - component_id: cpp + name: "C++ SDK" + paths: + - foreign/cpp/ comment: layout: "header, diff, components, files" diff --git a/foreign/cpp/BUILD.bazel b/foreign/cpp/BUILD.bazel index 286cce09e6..fcc784b6e9 100644 --- a/foreign/cpp/BUILD.bazel +++ b/foreign/cpp/BUILD.bazel @@ -17,14 +17,21 @@ load("@rules_cc//cc:defs.bzl", "cc_binary", "cc_import", "cc_library", "cc_test") +config_setting( + name = "coverage_enabled", + values = {"define": "iggy_cpp_coverage=1"}, +) + genrule( name = "cargo_build", srcs = glob(["src/**/*.rs"]) + [ "Cargo.toml", "build.rs", + "@iggy_source//:cpp_rust_dependencies", ], outs = [ "libiggy_cpp.a", + "libprofiler_builtins.a", "include/lib.rs.h", "cxxbridge/rust/cxx.h", "cxxbridge/sources/lib.rs.cc", @@ -37,6 +44,7 @@ genrule( RUSTC="$$EXECROOT/$(execpath @rs_host_tools//:rustc)" CARGO_TARGET_DIR="$$EXECROOT/$(@D)/cargo_target" OUT_LIB="$$EXECROOT/$(location libiggy_cpp.a)" + PROFILER_RT_OUT="$$EXECROOT/$(location libprofiler_builtins.a)" OUT_RS="$$EXECROOT/$(location include/lib.rs.h)" OUT_CXX="$$EXECROOT/$(location cxxbridge/rust/cxx.h)" OUT_CC="$$EXECROOT/$(location cxxbridge/sources/lib.rs.cc)" @@ -55,10 +63,25 @@ genrule( env -u PWD \ CARGO_TARGET_DIR="$$CARGO_TARGET_DIR" \ RUSTC="$$RUSTC" \ - "$$CARGO" build $$FLAGS + "$$CARGO" build --quiet $$FLAGS cp "$$CARGO_TARGET_DIR/$$PROFILE/libiggy_cpp.a" "$$OUT_LIB" + RT_SYSROOT="$$("$$RUSTC" --print sysroot)" + RT_HOST="$$("$$RUSTC" -vV | sed -n 's/^host: //p')" + RT_RLIB="$$(find "$$RT_SYSROOT/lib/rustlib/$$RT_HOST/lib" -maxdepth 1 -name 'libprofiler_builtins-*.rlib' -print -quit)" + if [ -z "$$RT_RLIB" ]; then + echo "ERROR: Failed to locate libprofiler_builtins rlib under $$RT_SYSROOT" >&2 + exit 1 + fi + cp "$$RT_RLIB" "$$PROFILER_RT_OUT" + for member in $$(ar t "$$PROFILER_RT_OUT"); do + case "$$member" in + *.o) ;; + *) ar d "$$PROFILER_RT_OUT" "$$member" ;; + esac + done + RS_HDR="$$(find "$$CARGO_TARGET_DIR/$$PROFILE/build" -name lib.rs.h -print -quit)" if [ -z "$$RS_HDR" ]; then echo "ERROR: Failed to locate generated lib.rs.h under $$CARGO_TARGET_DIR/$$PROFILE/build" >&2 @@ -93,6 +116,12 @@ cc_import( static_library = ":libiggy_cpp.a", ) +cc_import( + name = "profiler_rt", + static_library = ":libprofiler_builtins.a", + alwayslink = True, +) + cc_library( name = "iggy-cpp", srcs = glob( @@ -139,7 +168,10 @@ cc_library( visibility = ["//visibility:public"], deps = [ ":iggy_cpp_static", - ], + ] + select({ + ":coverage_enabled": [":profiler_rt"], + "//conditions:default": [], + }), ) cc_binary( diff --git a/foreign/cpp/MODULE.bazel b/foreign/cpp/MODULE.bazel index 3401589b32..c53f23adb5 100644 --- a/foreign/cpp/MODULE.bazel +++ b/foreign/cpp/MODULE.bazel @@ -26,6 +26,23 @@ bazel_dep(name = "googletest", version = "1.18.0.bcr.1") bazel_dep(name = "cucumber-cpp", version = "0.8.0.bcr.1") bazel_dep(name = "rules_rust", version = "0.74.0") +iggy_source = use_repo_rule("@bazel_tools//tools/build_defs/repo:local.bzl", "new_local_repository") +iggy_source( + name = "iggy_source", + build_file_content = """ +filegroup( + name = "cpp_rust_dependencies", + srcs = glob([ + "core/binary_protocol/**", + "core/common/**", + "core/sdk/**", + ]) + ["Cargo.toml"], + visibility = ["//visibility:public"], +) +""", + path = "../..", +) + rust_host_tools = use_extension("@rules_rust//rust:extensions.bzl", "rust_host_tools") rust_host_tools.host_tools( name = "rs_host_tools", diff --git a/scripts/ci/coverage-baseline-affected.sh b/scripts/ci/coverage-baseline-affected.sh index 930a6c2db2..b3c7b84927 100755 --- a/scripts/ci/coverage-baseline-affected.sh +++ b/scripts/ci/coverage-baseline-affected.sh @@ -33,7 +33,7 @@ set -euo pipefail BASE="${1:-}" HEAD="${2:-}" ZERO="0000000000000000000000000000000000000000" -GATES=(rust java csharp python php node go) +GATES=(rust java csharp python php node go cpp) emit_all() { local gate @@ -82,6 +82,7 @@ for gate in "${GATES[@]}"; do node) paths=(foreign/node) ;; # The go job also runs the bdd/go suite with foreign/go in -coverpkg. go) paths=(foreign/go bdd/go) ;; + cpp) paths=(foreign/cpp) ;; *) echo "coverage-gate: no pathspecs defined for gate '$gate'" >&2 exit 1 diff --git a/scripts/ci/cpp-coverage.sh b/scripts/ci/cpp-coverage.sh new file mode 100755 index 0000000000..7d193898f0 --- /dev/null +++ b/scripts/ci/cpp-coverage.sh @@ -0,0 +1,81 @@ +#!/usr/bin/env bash +# Licensed to the Apache Software Foundation (ASF) under one +# or more contributor license agreements. See the NOTICE file +# distributed with this work for additional information +# regarding copyright ownership. The ASF licenses this file +# to you under the Apache License, Version 2.0 (the +# "License"); you may not use this file except in compliance +# with the License. You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, +# software distributed under the License is distributed on an +# "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY +# KIND, either express or implied. See the License for the +# specific language governing permissions and limitations +# under the License. + +set -euo pipefail + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$SCRIPT_DIR/../.." && pwd)" +CPP_ROOT="$REPO_ROOT/foreign/cpp" + +cd "$CPP_ROOT" + +OUTPUT="$REPO_ROOT/reports/cpp-coverage.lcov" +COVERAGE_DIR="$CPP_ROOT/target/cpp-coverage" +BUILD_PROFRAW_DIR="$COVERAGE_DIR/build" +PROFDATA="$COVERAGE_DIR/shim.profdata" +CPP_RAW="$CPP_ROOT/bazel-out/_coverage/_coverage_report.dat" + +mkdir -p "$COVERAGE_DIR" "$BUILD_PROFRAW_DIR" "$(dirname "$OUTPUT")" +rm -f "$COVERAGE_DIR"/*.profraw "$BUILD_PROFRAW_DIR"/*.profraw "$PROFDATA" "$OUTPUT" + +if ! LLVM_COV_ENV="$(cargo llvm-cov show-env --no-rustc-wrapper --sh)"; then + echo "cpp-coverage: 'cargo llvm-cov show-env' failed" >&2 + exit 1 +fi +eval "$LLVM_COV_ENV" +export LLVM_PROFILE_FILE="$COVERAGE_DIR/%p-%m.profraw" + +LLVM_BIN="$(rustc --print target-libdir)/../bin" +LLVM_COV="$LLVM_BIN/llvm-cov" +LLVM_PROFDATA="$LLVM_BIN/llvm-profdata" + +bazel coverage \ + --config=debug \ + --lockfile_mode=error \ + --combined_report=lcov \ + '--instrumentation_filter=//:iggy-cpp' \ + --strategy=TestRunner=standalone \ + --define=iggy_cpp_coverage=1 \ + --action_env=RUSTFLAGS \ + "--action_env=LLVM_PROFILE_FILE=$BUILD_PROFRAW_DIR/%p-%m.profraw" \ + "--run_under=/usr/bin/env LLVM_PROFILE_FILE=$LLVM_PROFILE_FILE" \ + //:unit //:e2e + +"$LLVM_PROFDATA" merge -sparse \ + "$COVERAGE_DIR"/*.profraw \ + -o "$PROFDATA" + +sed 's|^SF:|SF:foreign/cpp/|' "$CPP_RAW" >"$OUTPUT" + +"$LLVM_COV" export \ + --object bazel-bin/unit \ + --object bazel-bin/e2e \ + -instr-profile="$PROFDATA" \ + -format=lcov \ + -ignore-filename-regex='(\.cargo/|/rustc/|/core/|cxxbridge|/registry/)' | + awk ' + /^SF:/ { + include = /foreign\/cpp\/src\/[^/]+\.rs$/ + if (include) { + sub(/^SF:.*foreign\/cpp\//, "SF:foreign/cpp/") + } + } + include + ' >>"$OUTPUT" + +"$SCRIPT_DIR/validate-lcov.sh" "$OUTPUT"