diff --git a/.github/contract-tests/pom.xml b/.github/contract-tests/pom.xml new file mode 100644 index 000000000..d70d903a7 --- /dev/null +++ b/.github/contract-tests/pom.xml @@ -0,0 +1,74 @@ + + + + 4.0.0 + + org.gephi + gephi-plugins-contract-tests + 1.0.0 + jar + + + UTF-8 + 1.2.0 + + + + + + + org.gephi + gephi-plugin-parent + ${gephi.version} + pom + import + + + + + + + org.gephi + gephi-plugins-tests + ${gephi-plugins-tests.version} + test-jar + test + + + ${plugin.groupId} + ${plugin.artifactId} + ${plugin.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + 3.6.1 + + + -Djava.awt.headless=true --add-opens=java.base/java.net=ALL-UNNAMED --add-opens=java.base/java.nio=ALL-UNNAMED --add-opens=java.desktop/javax.swing=ALL-UNNAMED --add-exports=java.desktop/sun.awt=ALL-UNNAMED + + + ${plugin.artifactId}-${plugin.version}.jar + + ${project.build.directory}/contract-warnings.tsv + + + org.gephi:gephi-plugins-tests + + + + + + diff --git a/.github/contract-tests/run.sh b/.github/contract-tests/run.sh new file mode 100755 index 000000000..2adc2836b --- /dev/null +++ b/.github/contract-tests/run.sh @@ -0,0 +1,179 @@ +#!/usr/bin/env bash +# Runs gephi-plugins-tests' SPI contract tests (see pom.xml in this folder) against this +# checkout's plugin modules, one Maven run per NBM module so each failure names a single plugin. +# +# Usage: .github/contract-tests/run.sh [modules/ ...] +# +# With no arguments, every module in the root pom.xml's list is tested. Otherwise only +# NBM modules under the given folders are; a folder outside the reactor (e.g. a disabled plugin) +# is skipped with a warning. Checks that are only advisory (e.g. a layout adding nodes) don't fail +# the run: they are reported as warning annotations, and in the job summary when running on GitHub +# Actions. Modules must be installed in the local Maven repository first +# (mvn install): a module that is missing there, or was rebuilt since, fails instead of testing a +# stale jar. +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/../.." && pwd)" +HARNESS_DIR="$ROOT/.github/contract-tests" +GEPHI_VERSION=$(sed -nE 's#.*(.*).*#\1#p' "$ROOT/pom.xml" | head -1) +if [ -z "$GEPHI_VERSION" ]; then + echo "::error::Could not read from $ROOT/pom.xml" + exit 1 +fi + +# One "packaging|groupId|artifactId|version|basedir|built jar|local repository" line per project +if ! PROJECTS=$(cd "$ROOT" && mvn -B -q -ntp org.codehaus.mojo:exec-maven-plugin:3.5.0:exec \ + -Dexec.executable=echo \ + '-Dexec.args=${project.packaging}|${project.groupId}|${project.artifactId}|${project.version}|${project.basedir}|${project.build.directory}/${project.build.finalName}.jar|${settings.localRepository}'); then + echo "$PROJECTS" + echo "::error::Could not list the reactor's modules, see the Maven output above" + exit 1 +fi + +FOLDERS=() +for arg in "$@"; do + folder="${arg#"$ROOT"/}" + folder="${folder#./}" + FOLDERS+=("${folder%/}") +done + +in_folder() { + case "$1/" in + */"$2"/*) return 0 ;; + esac + return 1 +} + +TARGETS=() +while IFS='|' read -r packaging group artifact version basedir jar repo; do + [ "$packaging" = nbm ] || continue + if [ ${#FOLDERS[@]} -eq 0 ]; then + TARGETS+=("$group|$artifact|$version|$jar|$repo") + continue + fi + for folder in "${FOLDERS[@]}"; do + if in_folder "$basedir" "$folder"; then + TARGETS+=("$group|$artifact|$version|$jar|$repo") + break + fi + done +done <<< "$PROJECTS" + +for folder in ${FOLDERS[@]+"${FOLDERS[@]}"}; do + matched=false + while IFS='|' read -r packaging _ _ _ basedir _; do + if [ "$packaging" = nbm ] && in_folder "$basedir" "$folder"; then + matched=true + break + fi + done <<< "$PROJECTS" + if [ "$matched" = false ]; then + echo "::warning::$folder has no plugin module in the root pom.xml's list; skipping" + fi +done + +if [ ${#TARGETS[@]} -eq 0 ]; then + echo "No plugin modules to test." + exit 0 +fi + +# Advisory test warnings, one "plugincheckmessage" entry each +WARNINGS_FILE="$HARNESS_DIR/target/contract-warnings.tsv" +WARNINGS=() + +# Workflow command data can't contain %, the warnings are single lines already +annotation_escape() { + printf '%s' "$1" | sed -e 's/%/%25/g' +} + +# Plugin names and messages come from the plugin's code, so don't let them format the summary +markdown_escape() { + printf '%s' "$1" | sed -e 's/&/\&/g' -e 's//\>/g' -e 's/|/\\|/g' -e 's/`/\\`/g' +} + +collect_warnings() { + [ -f "$WARNINGS_FILE" ] || return 0 + local check message + while IFS=$'\t' read -r check message; do + WARNINGS+=("$1"$'\t'"$check"$'\t'"$message") + echo "::warning title=SPI contract test warning::$(annotation_escape "$1: $check: $message")" + done < "$WARNINGS_FILE" +} + +# The jars of the modules that depend on the one being tested, installed in the local repository. A plugin split in +# modules keeps parts of it (e.g. its user interface) in modules that depend on the main one, and the tests look +# those up too. +dependent_jars() { + local packaging group artifact version basedir repo jar + while IFS='|' read -r packaging group artifact version basedir _ repo; do + [ "$packaging" = nbm ] || continue + [ "$artifact" != "$1" ] || continue + grep -q "$1" "$basedir/pom.xml" || continue + jar="$repo/${group//.//}/$artifact/$version/$artifact-$version.jar" + [ -f "$jar" ] && echo "$jar" + done <<< "$PROJECTS" + return 0 +} + +PASSED=() +FAILED=() +for target in "${TARGETS[@]}"; do + IFS='|' read -r group artifact version jar repo <<< "$target" + gav="$group:$artifact:$version" + installed="$repo/${group//.//}/$artifact/$version/$artifact-$version.jar" + if [ ! -f "$installed" ]; then + echo "::error::$gav isn't installed in $repo, run mvn install first" + FAILED+=("$gav") + continue + fi + if [ -f "$jar" ] && ! cmp -s "$jar" "$installed"; then + echo "::error::$gav was rebuilt since it was installed, run mvn install again" + FAILED+=("$gav") + continue + fi + + EXTRA_ARGS=() + EXTRA_JARS=$(dependent_jars "$artifact" | paste -sd, -) + if [ -n "$EXTRA_JARS" ]; then + EXTRA_ARGS=(-Dmaven.test.additionalClasspath="$EXTRA_JARS") + fi + + rm -rf "$HARNESS_DIR/target" + echo "::group::Contract tests: $gav" + if mvn -B -ntp -f "$HARNESS_DIR/pom.xml" test -Dgephi.version="$GEPHI_VERSION" \ + -Dplugin.groupId="$group" -Dplugin.artifactId="$artifact" -Dplugin.version="$version" \ + ${EXTRA_ARGS[@]+"${EXTRA_ARGS[@]}"}; then + echo "::endgroup::" + else + echo "::endgroup::" + echo "::error::SPI contract tests failed for $gav" + collect_warnings "$gav" + FAILED+=("$gav") + continue + fi + collect_warnings "$gav" + PASSED+=("$gav") +done + +if [ ${#WARNINGS[@]} -gt 0 ] && [ -n "${GITHUB_STEP_SUMMARY:-}" ]; then + { + echo "### SPI contract test warnings" + echo + echo "These checks don't fail the build, but are worth a look in review." + echo + echo "| Plugin | Check | Warning |" + echo "| --- | --- | --- |" + for warning in "${WARNINGS[@]}"; do + IFS=$'\t' read -r gav check message <<< "$warning" + echo "| $(markdown_escape "$gav") | $(markdown_escape "$check") | $(markdown_escape "$message") |" + done + } >> "$GITHUB_STEP_SUMMARY" +fi + +if [ ${#PASSED[@]} -gt 0 ]; then + echo "SPI contract tests passed for: ${PASSED[*]}" +fi +if [ ${#FAILED[@]} -gt 0 ]; then + echo "SPI contract tests failed for: ${FAILED[*]}" + exit 1 +fi diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml index f1f4f852a..84f721c52 100644 --- a/.github/workflows/build.yml +++ b/.github/workflows/build.yml @@ -24,4 +24,7 @@ jobs: echo "No plugin modules present under modules/; skipping build/validate (see test-generation workflow for scaffold validation)." exit 0 fi - mvn -B package --file pom.xml \ No newline at end of file + mvn -B install --file pom.xml + - name: Run SPI contract tests + if: hashFiles('modules/*/pom.xml') != '' + run: .github/contract-tests/run.sh diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index 06615863c..a120762d2 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -10,10 +10,32 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v6 + with: + # The PR's merge commit and its first parent (the base), to diff against + fetch-depth: 2 - name: Set up JDK 17 uses: actions/setup-java@v5 with: java-version: '17' distribution: 'temurin' - name: Build project with Maven - run: mvn -B package --file pom.xml \ No newline at end of file + run: mvn -B install --file pom.xml + - name: Run SPI contract tests on changed plugins + run: | + CHANGED_FILES=$(git -c core.quotePath=false diff --name-only HEAD^1 HEAD) + # A root pom.xml change (gephi.version bump, re-enabled , ...) or a harness change + # can affect every plugin, so test them all + if grep -qxE 'pom\.xml|\.github/contract-tests/.*' <<< "$CHANGED_FILES"; then + exec .github/contract-tests/run.sh + fi + FOLDERS=() + while IFS= read -r folder; do + if [ -d "$folder" ]; then + FOLDERS+=("$folder") + fi + done < <(grep '^modules/' <<< "$CHANGED_FILES" | cut -d/ -f1-2 | sort -u) + if [ ${#FOLDERS[@]} -eq 0 ]; then + echo "No plugin modules changed; skipping." + exit 0 + fi + .github/contract-tests/run.sh "${FOLDERS[@]}" diff --git a/.github/workflows/test-generation.yml b/.github/workflows/test-generation.yml index c1794c069..b3b20de0f 100644 --- a/.github/workflows/test-generation.yml +++ b/.github/workflows/test-generation.yml @@ -49,7 +49,23 @@ jobs: grep -q 'modules/CiIntegrationTestPlugin' pom.xml - name: Build, validate and generate the autoupdate site - run: mvn -B package -P release --file pom.xml + run: mvn -B install -P release --file pom.xml + + # The fixture implements no SPI, so this runs no test cases: it checks the contract-test + # harness still resolves gephi-plugins-tests and gephi-plugin-parent for this gephi.version. + - name: Run SPI contract tests against the fixture + run: .github/contract-tests/run.sh modules/CiIntegrationTestPlugin + + # Positive control: core Gephi's layout-plugin implements a covered SPI, so the harness must + # run actual test cases against it. Guards against the harness silently testing nothing. + - name: Check the contract-test harness runs test cases + run: | + set -e + GEPHI_VERSION=$(sed -nE 's/.*(.*)<\/gephi\.version>.*/\1/p' pom.xml | head -1) + rm -rf .github/contract-tests/target + mvn -B -f .github/contract-tests/pom.xml test -Dgephi.version="$GEPHI_VERSION" \ + -Dplugin.groupId=org.gephi -Dplugin.artifactId=layout-plugin -Dplugin.version="$GEPHI_VERSION" + grep -ho 'tests="[0-9]*"' .github/contract-tests/target/surefire-reports/TEST-*.xml | grep -qv 'tests="0"' - name: Assert the generation process produced a working autoupdate site run: | diff --git a/AGENTS.md b/AGENTS.md index 1082a1c96..616df2c2c 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -24,6 +24,9 @@ Requires JDK 17 and Maven. - Build and validate every plugin currently listed in `pom.xml`: `mvn clean package` - Build/test a single plugin module: `mvn -pl modules/ clean package` - Run a single module's unit tests only: `mvn -pl modules/ test` +- Run the SPI contract tests CI runs (from `gephi-plugins-tests`) on a module, after `mvn install`: + `.github/contract-tests/run.sh modules/`. A failing check fails the run; advisory checks + (e.g. a layout adding nodes) only print `::warning::` lines and, in CI, a job summary table. - Run Gephi with the module(s) installed (build first): `mvn org.gephi:gephi-maven-plugin:run` - IDE run/debug configs are defined in `nbactions.xml`; the IntelliJ debug setup (remote debugger + `-Drun.params.debug` VM option) is documented in `README.md`. diff --git a/ARCHITECTURE.md b/ARCHITECTURE.md index 605edae87..74020a9d5 100644 --- a/ARCHITECTURE.md +++ b/ARCHITECTURE.md @@ -38,9 +38,21 @@ different purposes on three different branches. Confusion here (e.g. "why is ``. ## How Gephi can be extended @@ -144,7 +156,8 @@ everything above. Its goals, bound in this repo's root `pom.xml` or invoked dire ## Root files ```text -.github/workflows/ # build.yml (feature branches), test-generation.yml (master), release-pom.yml (parent-pom) +.github/workflows/ # build.yml (feature branches), pr.yml (PRs to master-forge), test-generation.yml (master), release-pom.yml (parent-pom) +.github/contract-tests/ # CI harness running gephi-plugins-tests' SPI contract tests against plugin modules modules/ # one folder per plugin/suite; empty list on master until you add one plugins/ # static assets (images) used by the gephi.org plugin listing, not source code pom.xml # reactor POM for this checkout's plugin modules — NOT the plugin parent POM diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 17d4c09ec..6ebd82d28 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -337,6 +337,11 @@ See "Gephi platform conventions" above for the reasoning; on review, check that using the `org-netbeans-modules-nbjunit` dependency per `README.md`'s testing section. - Tests are not disabled or skipped (`@Ignore`, `-DskipTests` baked into the module's own `pom.xml`) to force a green build. +- The PR's "Run SPI contract tests on changed plugins" CI step passes. It runs + `gephi-plugins-tests`' contract tests for every SPI implementation each changed module registers; + locally, `mvn install` then `.github/contract-tests/run.sh modules/`. The step runs the + PR's own copy of `.github/`, so a pass only counts if the PR leaves `.github/` untouched (see + "Build actually passes" above). ### PR completeness