diff --git a/.github/workflows/pd-store-ci.yml b/.github/workflows/pd-store-ci.yml index 6f670e1cb9..2ad45bd648 100644 --- a/.github/workflows/pd-store-ci.yml +++ b/.github/workflows/pd-store-ci.yml @@ -33,6 +33,9 @@ jobs: with: fetch-depth: 5 + - name: Run JaCoCo report validator tests + run: hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh + - name: Use staged maven repo settings run: | cp $HOME/.m2/settings.xml /tmp/settings.xml || true @@ -65,7 +68,8 @@ jobs: USE_STAGE: 'false' # Whether to include the stage repository. # TODO: remove outdated env TRAVIS_DIR: hugegraph-server/hugegraph-dist/src/assembly/travis - REPORT_DIR: target/site/jacoco + REPORT_FILE: hugegraph-pd/hg-pd-test/target/site/jacoco/jacoco.xml + TEST_REPORT_DIR: hugegraph-pd/hg-pd-test/target/surefire-reports steps: - name: Install JDK 11 @@ -92,20 +96,26 @@ jobs: cp $HOME/.m2/settings.xml /tmp/settings.xml mv -vf .github/configs/settings.xml $HOME/.m2/settings.xml - - name: Run common test + - name: Package + # todo remove --fail-at-end after test run: | - mvn test -pl hugegraph-pd/hg-pd-test -am -P pd-common-test + mvn clean package -U -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -ntp --fail-at-end - - name: Run core test + - name: Check source formatting run: | - mvn test -pl hugegraph-pd/hg-pd-test -am -P pd-core-test + mvn editorconfig:check -pl hugegraph-pd/hg-pd-test -am -ntp - # The above tests do not require starting a PD instance. + # These tests do not require starting a PD instance. Run them after the + # clean package so their JaCoCo data survives until the final report. + - name: Run common test + run: | + mvn test -pl hugegraph-pd/hg-pd-test -am \ + -P pd-common-test -Djacoco.sessionId=pd-common-test - - name: Package - # todo remove --fail-at-end after test + - name: Run core test run: | - mvn clean package -U -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -ntp --fail-at-end + mvn test -pl hugegraph-pd/hg-pd-test -am \ + -P pd-core-test -Djacoco.sessionId=pd-core-test - name: Check startup test prerequisites (PD) id: pd-preflight @@ -139,16 +149,44 @@ jobs: - name: Run client test run: | - mvn test -pl hugegraph-pd/hg-pd-test -am -P pd-client-test + mvn test -pl hugegraph-pd/hg-pd-test -am \ + -P pd-client-test -Djacoco.sessionId=pd-client-test - name: Run rest test run: | - mvn test -pl hugegraph-pd/hg-pd-test -am -P pd-rest-test + mvn test -pl hugegraph-pd/hg-pd-test -am \ + -P pd-rest-test -Djacoco.sessionId=pd-rest-test + + - name: Generate aggregate coverage report + run: | + mvn verify -pl hugegraph-pd/hg-pd-test -am -P jacoco \ + -DskipTests -Deditorconfig.skip=true -ntp + + - name: Validate aggregate coverage report + run: | + $TRAVIS_DIR/check-jacoco-report.sh \ + --require-test-report \ + "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.pd.common.CommonSuiteTest.xml" \ + --require-test-report \ + "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.pd.core.PDCoreSuiteTest.xml" \ + --require-test-report \ + "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.pd.client.PDClientSuiteTest.xml" \ + --require-test-report \ + "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.pd.rest.PDRestSuiteTest.xml" \ + --require-covered-group hg-pd-common \ + --require-covered-group hg-pd-client \ + --require-covered-group hg-pd-core \ + --require-session pd-common-test \ + --require-session pd-core-test \ + --require-session pd-client-test \ + --require-session pd-rest-test \ + "$REPORT_FILE" \ + hg-pd-grpc hg-pd-common hg-pd-client hg-pd-core hg-pd-service hg-pd-dist - name: Upload coverage to Codecov uses: codecov/codecov-action@v3.0.0 with: - file: ${{ env.REPORT_DIR }}/*.xml + files: ${{ env.REPORT_FILE }} store: needs: struct @@ -157,7 +195,8 @@ jobs: USE_STAGE: 'false' # Whether to include the stage repository. # TODO: remove outdated env TRAVIS_DIR: hugegraph-server/hugegraph-dist/src/assembly/travis - REPORT_DIR: target/site/jacoco + REPORT_FILE: hugegraph-store/hg-store-test/target/site/jacoco/jacoco.xml + TEST_REPORT_DIR: hugegraph-store/hg-store-test/target/surefire-reports steps: - name: Install JDK 11 @@ -189,6 +228,10 @@ jobs: run: | mvn clean package -U -Dmaven.javadoc.skip=true -Dmaven.test.skip=true -ntp --fail-at-end + - name: Check source formatting + run: | + mvn editorconfig:check -pl hugegraph-store/hg-store-test -am -ntp + - name: Check startup test prerequisites (Store) id: store-preflight run: | @@ -228,32 +271,54 @@ jobs: - name: Run common test run: | - mvn test -pl hugegraph-store/hg-store-test -am -P store-common-test + mvn test -pl hugegraph-store/hg-store-test -am \ + -P store-common-test -Djacoco.sessionId=store-common-test - name: Run client test run: | - mvn test -pl hugegraph-store/hg-store-test -am -P store-client-test - - - name: Run core test - run: | - mvn test -pl hugegraph-store/hg-store-test -am -P store-core-test + mvn test -pl hugegraph-store/hg-store-test -am \ + -P store-client-test -Djacoco.sessionId=store-client-test - name: Run rocksdb test run: | - mvn test -pl hugegraph-store/hg-store-test -am -P store-rocksdb-test - - - name: Run server test - run: | - mvn test -pl hugegraph-store/hg-store-test -am -P store-server-test + mvn test -pl hugegraph-store/hg-store-test -am \ + -P store-rocksdb-test -Djacoco.sessionId=store-rocksdb-test - name: Run raft-core test run: | - mvn test -pl hugegraph-store/hg-store-test -am -P store-raftcore-test + mvn test -pl hugegraph-store/hg-store-test -am \ + -P store-raftcore-test -Djacoco.sessionId=store-raftcore-test + + - name: Generate aggregate coverage report + run: | + mvn verify -pl hugegraph-store/hg-store-test -am -P jacoco \ + -DskipTests -Deditorconfig.skip=true -ntp + + - name: Validate aggregate coverage report + run: | + $TRAVIS_DIR/check-jacoco-report.sh \ + --require-test-report \ + "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.store.common.CommonSuiteTest.xml" \ + --require-test-report \ + "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.store.client.ClientSuiteTest.xml" \ + --require-test-report \ + "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.store.rocksdb.RocksDbSuiteTest.xml" \ + --require-test-report \ + "$TEST_REPORT_DIR/TEST-org.apache.hugegraph.store.raftcore.RaftSuiteTest.xml" \ + --require-covered-group hg-store-common \ + --require-covered-group hg-store-client \ + --require-covered-group hg-store-rocksdb \ + --require-session store-common-test \ + --require-session store-client-test \ + --require-session store-rocksdb-test \ + --require-session store-raftcore-test \ + "$REPORT_FILE" \ + hg-store-grpc hg-store-common hg-store-client hg-store-rocksdb - name: Upload coverage to Codecov uses: codecov/codecov-action@v3.0.0 with: - file: ${{ env.REPORT_DIR }}/*.xml + files: ${{ env.REPORT_FILE }} hstore: needs: struct diff --git a/hugegraph-pd/hg-pd-test/pom.xml b/hugegraph-pd/hg-pd-test/pom.xml index 44d5d084ca..72cdef81aa 100644 --- a/hugegraph-pd/hg-pd-test/pom.xml +++ b/hugegraph-pd/hg-pd-test/pom.xml @@ -46,18 +46,19 @@ org.jacoco jacoco-maven-plugin - 0.8.4 - - - **/grpc/**.* - **/config/**.* - - + 0.8.8 + coverage-report + verify - prepare-agent + report-aggregate + + + ${project.basedir}/target/site/jacoco + + @@ -258,25 +259,7 @@ org.jacoco jacoco-maven-plugin - 0.8.4 - - - pre-test - - prepare-agent - - - - post-test - test - - report-aggregate - - - ${basedir}/target/site/jacoco - - - + 0.8.8 org/apache/hugegraph/pd/rest/*.class diff --git a/hugegraph-pd/pom.xml b/hugegraph-pd/pom.xml index ceb8af33b2..83b39c8377 100644 --- a/hugegraph-pd/pom.xml +++ b/hugegraph-pd/pom.xml @@ -74,8 +74,9 @@ org.jacoco jacoco-maven-plugin - 0.8.4 + 0.8.8 + true **/grpc/**.* **/config/**.* diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/check-jacoco-report.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/check-jacoco-report.sh new file mode 100755 index 0000000000..a0d674f157 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/check-jacoco-report.sh @@ -0,0 +1,210 @@ +#!/bin/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 -uo pipefail + +REQUIRED_SESSIONS=() +REQUIRED_TEST_REPORTS=() +REQUIRED_COVERED_GROUPS=() +while (( $# > 0 )); do + case "${1}" in + --require-session) + if (( $# < 2 )) || [[ -z "${2:-}" || "${2}" == --* ]]; then + echo "ERROR: --require-session requires a non-empty value" >&2 + exit 1 + fi + REQUIRED_SESSIONS+=("${2}") + shift 2 + ;; + --require-test-report) + if (( $# < 2 )) || [[ -z "${2:-}" || "${2}" == --* ]]; then + echo "ERROR: --require-test-report requires a non-empty value" >&2 + exit 1 + fi + REQUIRED_TEST_REPORTS+=("${2}") + shift 2 + ;; + --require-covered-group) + if (( $# < 2 )) || [[ -z "${2:-}" || "${2}" == --* ]]; then + echo "ERROR: --require-covered-group requires a non-empty value" >&2 + exit 1 + fi + REQUIRED_COVERED_GROUPS+=("${2}") + shift 2 + ;; + --*) + echo "ERROR: unknown option: ${1}" >&2 + exit 1 + ;; + *) + break + ;; + esac +done + +if (( ${#REQUIRED_SESSIONS[@]} == 0 )); then + echo "ERROR: at least one --require-session is required" >&2 + exit 1 +fi + +if (( ${#REQUIRED_TEST_REPORTS[@]} == 0 )); then + echo "ERROR: at least one --require-test-report is required" >&2 + exit 1 +fi + +REPORT_FILE="${1:-}" +if (( $# > 0 )); then + shift +fi + +if [[ -z "${REPORT_FILE}" || ! -s "${REPORT_FILE}" ]]; then + echo "ERROR: JaCoCo report not found or empty: ${REPORT_FILE:-}" >&2 + exit 1 +fi + +if (( $# == 0 )); then + echo "ERROR: at least one expected module is required" >&2 + exit 1 +fi + +validate_test_report() { + local test_report="${1}" + + if [[ ! -s "${test_report}" ]]; then + echo "ERROR: Surefire report not found or empty: ${test_report}" >&2 + return 1 + fi + + local test_counts + if ! test_counts=$(python3 - "${test_report}" <<'PY' +import sys +import xml.etree.ElementTree as ET + +root = ET.parse(sys.argv[1]).getroot() +if root.tag.rsplit("}", 1)[-1] != "testsuite" or "tests" not in root.attrib: + raise ValueError("not a Surefire testsuite report") +test_count = int(root.attrib["tests"]) +skipped_count = int(root.attrib.get("skipped", "0")) +if test_count < 0: + raise ValueError("negative Surefire test count") +if skipped_count < 0 or skipped_count > test_count: + raise ValueError("invalid Surefire skipped count") +print(test_count, test_count - skipped_count) +PY + ); then + echo "ERROR: unable to parse Surefire report: ${test_report}" >&2 + return 1 + fi + local test_count + local executed_count + read -r test_count executed_count <<< "${test_counts}" + if (( test_count <= 0 )); then + echo "ERROR: Surefire report has no tests: ${test_report}" >&2 + return 1 + fi + if (( executed_count <= 0 )); then + echo "ERROR: Surefire report has no executed tests: ${test_report}" >&2 + return 1 + fi +} + +for test_report in "${REQUIRED_TEST_REPORTS[@]}"; do + validate_test_report "${test_report}" || exit 1 +done + +python3 - "${REPORT_FILE}" "${REQUIRED_SESSIONS[@]}" -- \ + ${REQUIRED_COVERED_GROUPS[@]+"${REQUIRED_COVERED_GROUPS[@]}"} \ + -- "$@" <<'PY' || exit 1 +import sys +import xml.etree.ElementTree as ET + +report_file = sys.argv[1] +session_separator = sys.argv.index("--", 2) +group_separator = sys.argv.index("--", session_separator + 1) +required_sessions = sys.argv[2:session_separator] +required_covered_groups = sys.argv[session_separator + 1:group_separator] +required_modules = sys.argv[group_separator + 1:] + + +def fail(message): + print(f"ERROR: {message}", file=sys.stderr) + sys.exit(1) + + +def local_name(tag): + return tag.rsplit("}", 1)[-1] + + +try: + root = ET.parse(report_file).getroot() +except (OSError, ET.ParseError) as error: + fail(f"unable to parse JaCoCo report: {report_file}: {error}") + +if local_name(root.tag) != "report": + fail(f"unable to parse JaCoCo report: {report_file}: expected report root") + +children = list(root) +instruction_counters = [ + element for element in children + if local_name(element.tag) == "counter" and + element.attrib.get("type") == "INSTRUCTION" +] +try: + has_coverage = any(int(counter.attrib.get("covered", "0")) > 0 + for counter in instruction_counters) +except ValueError as error: + fail(f"unable to parse JaCoCo report: {report_file}: {error}") +if not has_coverage: + fail(f"JaCoCo report has no covered instructions: {report_file}") + +session_ids = { + element.attrib.get("id") for element in children + if local_name(element.tag) == "sessioninfo" +} +for session in required_sessions: + if session not in session_ids: + fail(f"missing JaCoCo session '{session}' in {report_file}") + +groups_by_name = { + element.attrib.get("name"): element for element in children + if local_name(element.tag) == "group" +} +for module in required_modules: + if module not in groups_by_name: + fail(f"missing JaCoCo group '{module}' in {report_file}") + +for group_name in required_covered_groups: + group = groups_by_name.get(group_name) + if group is None: + fail(f"missing JaCoCo group '{group_name}' in {report_file}") + counters = [ + element for element in list(group) + if local_name(element.tag) == "counter" and + element.attrib.get("type") == "INSTRUCTION" + ] + try: + has_coverage = any(int(counter.attrib.get("covered", "0")) > 0 + for counter in counters) + except ValueError as error: + fail(f"unable to parse JaCoCo report: {report_file}: {error}") + if not has_coverage: + fail(f"JaCoCo group '{group_name}' has no covered instructions: " + f"{report_file}") +PY + +echo "JaCoCo report ${REPORT_FILE} contains all expected modules" diff --git a/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh new file mode 100755 index 0000000000..bdb09ba162 --- /dev/null +++ b/hugegraph-server/hugegraph-dist/src/assembly/travis/test-check-jacoco-report.sh @@ -0,0 +1,522 @@ +#!/bin/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 -uo pipefail + +SCRIPT_DIR=$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd) +REPO_ROOT=$(cd "${SCRIPT_DIR}/../../../../.." && pwd) +VALIDATOR="${SCRIPT_DIR}/check-jacoco-report.sh" +TMP_DIR=$(mktemp -d "${TMPDIR:-/tmp}/jacoco-report-test.XXXXXX") +CASE_OUTPUT="" +CASE_RC=0 + +trap 'rm -rf "${TMP_DIR}"' EXIT + +fail() { + echo "FAIL: $1" >&2 + [[ -z "${CASE_OUTPUT}" ]] || printf '%s\n' "${CASE_OUTPUT}" >&2 + exit 1 +} + +run_case() { + CASE_OUTPUT=$("${VALIDATOR}" "$@" 2>&1) + CASE_RC=$? +} + +run_report_case() { + run_case --require-test-report "${TMP_DIR}/tests.xml" "$@" +} + +run_case_with_timeout() { + CASE_OUTPUT=$(python3 - "${VALIDATOR}" "$@" <<'PY' +import os +import signal +import subprocess +import sys + +process = subprocess.Popen( + sys.argv[1:], + stdout=subprocess.PIPE, + stderr=subprocess.STDOUT, + universal_newlines=True, + start_new_session=True, +) +try: + output, _ = process.communicate(timeout=2) +except subprocess.TimeoutExpired: + try: + os.killpg(process.pid, signal.SIGTERM) + except ProcessLookupError: + pass + try: + output, _ = process.communicate(timeout=1) + except subprocess.TimeoutExpired: + try: + os.killpg(process.pid, signal.SIGKILL) + except ProcessLookupError: + pass + output, _ = process.communicate() + sys.stdout.write(output) + sys.exit(124) + +sys.stdout.write(output) +sys.exit(process.returncode) +PY + ) + CASE_RC=$? +} + +assert_success() { + [[ "${CASE_RC}" -eq 0 ]] || fail "$1 returned ${CASE_RC}" +} + +assert_failure() { + [[ "${CASE_RC}" -ne 0 ]] || fail "$1 unexpectedly succeeded" +} + +assert_output() { + [[ "${CASE_OUTPUT}" == *"$1"* ]] || fail "missing output '$1'" +} + +if [[ ! -x "${VALIDATOR}" ]]; then + fail "validator not found or not executable at ${VALIDATOR}" +fi + +cat > "${TMP_DIR}/tests.xml" <<'EOF' + + +EOF + +cat > "${TMP_DIR}/zero-tests.xml" <<'EOF' + + +EOF + +cat > "${TMP_DIR}/all-skipped.xml" <<'EOF' + + +EOF + +cat > "${TMP_DIR}/not-surefire.xml" <<'EOF' + + +EOF + +cat > "${TMP_DIR}/hanging-validator.sh" <<'EOF' +#!/bin/bash +sleep 30 >/dev/null 2>&1 & +child_pid=$! +printf '%s\n' "${child_pid}" > "${1}" +wait "${child_pid}" +EOF +chmod +x "${TMP_DIR}/hanging-validator.sh" + +echo "JaCoCo report validator tests" + +# Simulate stock macOS, where GNU timeout is not installed by default. +timeout() { + return 127 +} +run_case_with_timeout --require-session +unset -f timeout +[[ "${CASE_RC}" -ne 124 ]] || fail "missing session value timed out" +assert_failure "missing session value" +assert_output "--require-session requires a non-empty value" + +REAL_VALIDATOR="${VALIDATOR}" +VALIDATOR="${TMP_DIR}/hanging-validator.sh" +run_case_with_timeout "${TMP_DIR}/hanging-child.pid" +VALIDATOR="${REAL_VALIDATOR}" +[[ "${CASE_RC}" -eq 124 ]] || fail "hanging validator returned ${CASE_RC}" +child_pid=$(cat "${TMP_DIR}/hanging-child.pid") +if ! python3 - "${child_pid}" <<'PY' +import os +import sys +import time + +pid = int(sys.argv[1]) +for _ in range(20): + try: + os.kill(pid, 0) + except ProcessLookupError: + sys.exit(0) + time.sleep(0.05) +sys.exit(1) +PY +then + kill "${child_pid}" 2>/dev/null || true + fail "timed-out validator left child process ${child_pid} running" +fi + +run_case --require-session "" +assert_failure "empty session value" +assert_output "--require-session requires a non-empty value" + +run_case --require-test-report +assert_failure "missing test report value" +assert_output "--require-test-report requires a non-empty value" + +run_case --require-covered-group +assert_failure "missing covered group value" +assert_output "--require-covered-group requires a non-empty value" + +run_case --require-covered-group "" +assert_failure "empty covered group value" +assert_output "--require-covered-group requires a non-empty value" + +run_case --require-suite-report +assert_failure "removed suite report option" +assert_output "unknown option: --require-suite-report" + +run_report_case --require-session suite-a "${TMP_DIR}/missing.xml" hg-pd-client +assert_failure "missing report" +assert_output "not found or empty" + +touch "${TMP_DIR}/empty.xml" +run_report_case --require-session suite-a "${TMP_DIR}/empty.xml" hg-pd-client +assert_failure "empty report" +assert_output "not found or empty" + +cat > "${TMP_DIR}/valid.xml" <<'EOF' + + + + + + + + +EOF + +cat > "${TMP_DIR}/comment-only.xml" <<'EOF' + + + + + + + + +EOF + +cat > "${TMP_DIR}/reordered.xml" <<'EOF' + + + + + + + + +EOF + +cat > "${TMP_DIR}/truncated.xml" <<'EOF' + + + + + + + +EOF + +cat > "${TMP_DIR}/partial-group-coverage.xml" <<'EOF' + + + + + + + + + + + + +EOF + +run_report_case --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/valid.xml" hg-pd-client hg-pd-core +assert_success "complete report" +assert_output "contains all expected modules" + +run_report_case --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/truncated.xml" hg-pd-client hg-pd-core +assert_failure "truncated JaCoCo report" +assert_output "unable to parse JaCoCo report" + +run_report_case --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/reordered.xml" hg-pd-client hg-pd-core +assert_success "report with reordered XML attributes" + +run_report_case --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/comment-only.xml" hg-pd-client hg-pd-core +assert_failure "report with evidence only in XML comments" +assert_output "has no covered instructions" + +run_report_case --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/partial-group-coverage.xml" \ + hg-pd-core hg-pd-service +assert_success "presence-only groups with partial coverage" + +run_report_case --require-covered-group hg-pd-service \ + --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/partial-group-coverage.xml" \ + hg-pd-core hg-pd-service +assert_success "required group with covered instructions" + +run_report_case --require-covered-group hg-pd-core \ + --require-covered-group hg-pd-service \ + --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/partial-group-coverage.xml" \ + hg-pd-core hg-pd-service +assert_failure "required group without covered instructions" +assert_output "JaCoCo group 'hg-pd-core' has no covered instructions" + +run_case --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/valid.xml" hg-pd-client hg-pd-core +assert_failure "report without required test reports" +assert_output "at least one --require-test-report is required" + +run_case --require-test-report "${TMP_DIR}/missing-tests.xml" \ + --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/valid.xml" hg-pd-client hg-pd-core +assert_failure "missing required test report" +assert_output "Surefire report not found or empty" + +run_case --require-test-report "${TMP_DIR}/zero-tests.xml" \ + --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/valid.xml" hg-pd-client hg-pd-core +assert_failure "required test report without tests" +assert_output "Surefire report has no tests" + +run_case --require-test-report "${TMP_DIR}/all-skipped.xml" \ + --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/valid.xml" hg-pd-client hg-pd-core +assert_failure "required all-skipped test report" +assert_output "Surefire report has no executed tests" + +run_case --require-test-report "${TMP_DIR}/not-surefire.xml" \ + --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/valid.xml" hg-pd-client hg-pd-core +assert_failure "required test report with invalid root" +assert_output "unable to parse Surefire report" + +run_report_case --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/valid.xml" +assert_failure "report without expected modules" +assert_output "at least one expected module is required" + +run_report_case --require-session suite-a --require-session suite-c \ + "${TMP_DIR}/valid.xml" hg-pd-client hg-pd-core +assert_failure "report missing a required session" +assert_output "missing JaCoCo session 'suite-c'" + +sed 's/covered="3"/covered="0"/' "${TMP_DIR}/valid.xml" > "${TMP_DIR}/uncovered.xml" +run_report_case --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/uncovered.xml" hg-pd-client hg-pd-core +assert_failure "report without covered instructions" +assert_output "has no covered instructions" + +run_report_case --require-session suite-a --require-session suite-b \ + "${TMP_DIR}/valid.xml" hg-pd-client hg-pd-service +assert_failure "report missing an expected module" +assert_output "missing JaCoCo group 'hg-pd-service'" + +python3 - "${REPO_ROOT}" <<'PY' || fail "aggregation configuration contract failed" +from pathlib import Path +import re +import sys +import xml.etree.ElementTree as ET + +ROOT = Path(sys.argv[1]) +NS = "{http://maven.apache.org/POM/4.0.0}" + + +def child_text(element, name): + child = element.find(NS + name) + return "" if child is None or child.text is None else child.text.strip() + + +def jacoco_plugin(container): + plugins = container.find(NS + "plugins") + assert plugins is not None + for plugin in plugins.findall(NS + "plugin"): + if child_text(plugin, "artifactId") == "jacoco-maven-plugin": + return plugin + raise AssertionError("JaCoCo plugin is missing") + + +def goals(plugin): + return [goal.text.strip() for goal in plugin.findall( + ".//" + NS + "goal") if goal.text] + + +def check_module(module, test_module): + parent = ET.parse(ROOT / module / "pom.xml").getroot() + parent_plugin = jacoco_plugin(parent.find(NS + "build")) + assert child_text(parent_plugin, "version") == "0.8.8" + assert child_text(parent_plugin.find(NS + "configuration"), "append") == "true" + + test = ET.parse(ROOT / module / test_module / "pom.xml").getroot() + default_plugin = jacoco_plugin(test.find(NS + "build")) + assert child_text(default_plugin, "version") == "0.8.8" + assert "report-aggregate" not in goals(default_plugin) + + profile = None + for candidate in test.findall(".//" + NS + "profile"): + if child_text(candidate, "id") == "jacoco": + profile = candidate + break + assert profile is not None + profile_plugin = jacoco_plugin(profile.find(NS + "build")) + assert child_text(profile_plugin, "version") == "0.8.8" + executions = profile_plugin.findall(".//" + NS + "execution") + aggregates = [execution for execution in executions + if "report-aggregate" in goals(execution)] + assert len(aggregates) == 1 + assert child_text(aggregates[0], "phase") == "verify" + + +check_module("hugegraph-pd", "hg-pd-test") +check_module("hugegraph-store", "hg-store-test") + +store_test = ET.parse(ROOT / "hugegraph-store/hg-store-test/pom.xml").getroot() +dependencies = store_test.find(NS + "dependencies") +assert dependencies is not None +assert not any(child_text(dep, "artifactId") == "hg-store-rocksdb" + for dep in dependencies.findall(NS + "dependency")) +store_jacoco = next(profile for profile in store_test.findall( + ".//" + NS + "profile") if child_text(profile, "id") == "jacoco") +profile_dependencies = store_jacoco.find(NS + "dependencies") +assert profile_dependencies is not None +assert any(child_text(dep, "artifactId") == "hg-store-rocksdb" + for dep in profile_dependencies.findall(NS + "dependency")) + +workflow = (ROOT / ".github/workflows/pd-store-ci.yml").read_text() +pd_job = workflow.split("\n pd:\n", 1)[1].split("\n store:\n", 1)[0] +store_job = workflow.split("\n store:\n", 1)[1].split("\n hstore:\n", 1)[0] + + +def assert_order(job, commands): + positions = [job.index(command) for command in commands] + assert positions == sorted(positions) + + +def validation_command(job): + return job.split("$TRAVIS_DIR/check-jacoco-report.sh", 1)[1].split( + "- name: Upload coverage", 1)[0] + + +def reports_for_option(job, option): + pattern = re.escape(option) + ( + r'\s+\\?\s*"\$TEST_REPORT_DIR/' + r'(TEST-[A-Za-z0-9_.]+SuiteTest[.]xml)"' + ) + return set(re.findall(pattern, validation_command(job))) + + +def values_for_option(job, option): + pattern = re.escape(option) + r"\s+([A-Za-z0-9_-]+)" + return set(re.findall(pattern, validation_command(job))) + + +def required_modules(job): + command = validation_command(job).split('"$REPORT_FILE"', 1)[1] + return set(re.findall(r"\bhg-(?:pd|store)-[a-z0-9-]+\b", command)) + + +def selected_profiles(job, prefix): + return set(re.findall(r"-P (" + prefix + r"-[a-z0-9-]+-test)\b", job)) + + +assert_order(pd_job, [ + "mvn clean package", + "mvn editorconfig:check -pl hugegraph-pd/hg-pd-test -am -ntp", + "-P pd-common-test -Djacoco.sessionId=pd-common-test", + "-P pd-core-test -Djacoco.sessionId=pd-core-test", + "-P pd-client-test -Djacoco.sessionId=pd-client-test", + "-P pd-rest-test -Djacoco.sessionId=pd-rest-test", + "mvn verify", "--require-session pd-common-test", + "--require-session pd-core-test", "--require-session pd-client-test", + "--require-session pd-rest-test", "codecov/codecov-action", +]) +assert pd_job.count("mvn clean") == 1 +assert "hugegraph-pd/hg-pd-test/target/site/jacoco/jacoco.xml" in pd_job +assert "files: ${{ env.REPORT_FILE }}" in pd_job +assert "\n directory:" not in pd_job +assert "mvn verify -pl hugegraph-pd/hg-pd-test -am -P jacoco \\ " \ + "-DskipTests -Deditorconfig.skip=true -ntp" in " ".join(pd_job.split()) +assert selected_profiles(pd_job, "pd") == { + "pd-common-test", "pd-core-test", "pd-client-test", "pd-rest-test", +} +assert reports_for_option(pd_job, "--require-test-report") == { + "TEST-org.apache.hugegraph.pd.common.CommonSuiteTest.xml", + "TEST-org.apache.hugegraph.pd.core.PDCoreSuiteTest.xml", + "TEST-org.apache.hugegraph.pd.client.PDClientSuiteTest.xml", + "TEST-org.apache.hugegraph.pd.rest.PDRestSuiteTest.xml", +} +assert not reports_for_option(pd_job, "--require-suite-report") +assert values_for_option(pd_job, "--require-covered-group") == { + "hg-pd-common", "hg-pd-client", "hg-pd-core", +} +assert required_modules(pd_job) == { + "hg-pd-grpc", "hg-pd-common", "hg-pd-client", "hg-pd-core", + "hg-pd-service", "hg-pd-dist", +} + +assert_order(store_job, [ + "mvn clean package", + "mvn editorconfig:check -pl hugegraph-store/hg-store-test -am -ntp", + "-P store-common-test -Djacoco.sessionId=store-common-test", + "-P store-client-test -Djacoco.sessionId=store-client-test", + "-P store-rocksdb-test -Djacoco.sessionId=store-rocksdb-test", + "-P store-raftcore-test -Djacoco.sessionId=store-raftcore-test", + "mvn verify", "--require-session store-common-test", + "--require-session store-client-test", "--require-session store-rocksdb-test", + "--require-session store-raftcore-test", "codecov/codecov-action", +]) +assert store_job.count("mvn clean") == 1 +assert "hugegraph-store/hg-store-test/target/site/jacoco/jacoco.xml" in store_job +assert "files: ${{ env.REPORT_FILE }}" in store_job +assert "\n directory:" not in store_job +assert "mvn verify -pl hugegraph-store/hg-store-test -am -P jacoco \\ " \ + "-DskipTests -Deditorconfig.skip=true -ntp" in " ".join(store_job.split()) +assert selected_profiles(store_job, "store") == { + "store-common-test", "store-client-test", "store-rocksdb-test", + "store-raftcore-test", +} +assert reports_for_option(store_job, "--require-test-report") == { + "TEST-org.apache.hugegraph.store.common.CommonSuiteTest.xml", + "TEST-org.apache.hugegraph.store.client.ClientSuiteTest.xml", + "TEST-org.apache.hugegraph.store.rocksdb.RocksDbSuiteTest.xml", + "TEST-org.apache.hugegraph.store.raftcore.RaftSuiteTest.xml", +} +assert not reports_for_option(store_job, "--require-suite-report") +assert values_for_option(store_job, "--require-covered-group") == { + "hg-store-common", "hg-store-client", "hg-store-rocksdb", +} +assert required_modules(store_job) == { + "hg-store-grpc", "hg-store-common", "hg-store-client", + "hg-store-rocksdb", +} + +print("PASS: JaCoCo aggregation configuration contract") +PY + +echo "PASS: JaCoCo report validator contract" diff --git a/hugegraph-store/hg-store-test/pom.xml b/hugegraph-store/hg-store-test/pom.xml index 36308f449d..8b7f10dd69 100644 --- a/hugegraph-store/hg-store-test/pom.xml +++ b/hugegraph-store/hg-store-test/pom.xml @@ -39,22 +39,30 @@ false + + + org.apache.hugegraph + hg-store-rocksdb + ${revision} + + org.jacoco jacoco-maven-plugin - 0.8.4 + 0.8.8 - default + coverage-report verify report-aggregate - ${project.basedir}/../target/site/jacoco + + ${project.basedir}/target/site/jacoco @@ -295,26 +303,7 @@ org.jacoco jacoco-maven-plugin - 0.8.4 - - - pre-test - - - prepare-agent - - - - post-test - test - - report-aggregate - - - ${basedir}/target/site/jacoco - - - + 0.8.8 **/grpc/**/* diff --git a/hugegraph-store/pom.xml b/hugegraph-store/pom.xml index 9ff1e933e5..a8212b70db 100644 --- a/hugegraph-store/pom.xml +++ b/hugegraph-store/pom.xml @@ -98,7 +98,10 @@ org.jacoco jacoco-maven-plugin - 0.8.4 + 0.8.8 + + true +