Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
74 changes: 74 additions & 0 deletions .github/contract-tests/pom.xml
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
<?xml version="1.0" encoding="UTF-8"?>
<!-- CI harness running gephi-plugins-tests' SPI contract tests against a single plugin module
already installed in the local Maven repository. Driven by run.sh in
this folder, which passes gephi.version (from the root pom.xml) and the plugin's coordinates.
Plugins don't need to declare anything for this: neither their pom.xml nor
gephi-plugin-parent reference gephi-plugins-tests. -->
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>

<groupId>org.gephi</groupId>
<artifactId>gephi-plugins-contract-tests</artifactId>
<version>1.0.0</version>
<packaging>jar</packaging>

<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<gephi-plugins-tests.version>1.2.0</gephi-plugins-tests.version>
</properties>

<!-- Align the plugin's Gephi/NetBeans dependencies on the current gephi.version, so plugins
built against an older gephi-plugin-parent are tested against the Gephi release they'll
run in -->
<dependencyManagement>
<dependencies>
<dependency>
<groupId>org.gephi</groupId>
<artifactId>gephi-plugin-parent</artifactId>
<version>${gephi.version}</version>
<type>pom</type>
<scope>import</scope>
</dependency>
</dependencies>
</dependencyManagement>

<dependencies>
<dependency>
<groupId>org.gephi</groupId>
<artifactId>gephi-plugins-tests</artifactId>
<version>${gephi-plugins-tests.version}</version>
<type>test-jar</type>
<scope>test</scope>
</dependency>
<dependency>
<groupId>${plugin.groupId}</groupId>
<artifactId>${plugin.artifactId}</artifactId>
<version>${plugin.version}</version>
<scope>test</scope>
</dependency>
</dependencies>

<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.6.1</version>
<configuration>
<!-- Headless, with the same module access Gephi's launcher grants (gephi.conf) -->
<argLine>-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</argLine>
<systemPropertyVariables>
<!-- Only test implementations from the plugin's own jar, not the ones its
dependencies (e.g. core layout-plugin) register -->
<gephi.plugins.tests.jar>${plugin.artifactId}-${plugin.version}.jar</gephi.plugins.tests.jar>
<!-- Where the tests write their warnings (checks that don't fail the build), read by run.sh -->
<gephi.plugins.tests.warnings>${project.build.directory}/contract-warnings.tsv</gephi.plugins.tests.warnings>
</systemPropertyVariables>
<dependenciesToScan>
<dependency>org.gephi:gephi-plugins-tests</dependency>
</dependenciesToScan>
</configuration>
</plugin>
</plugins>
</build>
</project>
179 changes: 179 additions & 0 deletions .github/contract-tests/run.sh
Original file line number Diff line number Diff line change
@@ -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/<Folder> ...]
#
# With no arguments, every module in the root pom.xml's <modules> 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#.*<gephi\.version>(.*)</gephi\.version>.*#\1#p' "$ROOT/pom.xml" | head -1)
if [ -z "$GEPHI_VERSION" ]; then
echo "::error::Could not read <gephi.version> 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 <modules> list; skipping"
fi
done

if [ ${#TARGETS[@]} -eq 0 ]; then
echo "No plugin modules to test."
exit 0
fi

# Advisory test warnings, one "plugin<TAB>check<TAB>message" 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/&/\&amp;/g' -e 's/</\&lt;/g' -e 's/>/\&gt;/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 "<artifactId>$1</artifactId>" "$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
5 changes: 4 additions & 1 deletion .github/workflows/build.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
mvn -B install --file pom.xml
- name: Run SPI contract tests
if: hashFiles('modules/*/pom.xml') != ''
run: .github/contract-tests/run.sh
24 changes: 23 additions & 1 deletion .github/workflows/pr.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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
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 <module>, ...) 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[@]}"
18 changes: 17 additions & 1 deletion .github/workflows/test-generation.yml
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,23 @@ jobs:
grep -q '<module>modules/CiIntegrationTestPlugin</module>' 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>(.*)<\/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: |
Expand Down
3 changes: 3 additions & 0 deletions AGENTS.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<ModuleName> clean package`
- Run a single module's unit tests only: `mvn -pl modules/<ModuleName> test`
- Run the SPI contract tests CI runs (from `gephi-plugins-tests`) on a module, after `mvn install`:
`.github/contract-tests/run.sh modules/<ModuleName>`. 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`.
Expand Down
19 changes: 16 additions & 3 deletions ARCHITECTURE.md
Original file line number Diff line number Diff line change
Expand Up @@ -38,9 +38,21 @@ different purposes on three different branches. Confusion here (e.g. "why is `<m
`build.yml` (the main CI workflow) runs on every branch push *except* `master-forge`, `master`,
`parent-pom`, and `gh-pages` — i.e. on the feature/topic branches contributors actually push to.
`test-generation.yml` runs on `master` and is an integration test for the scaffold itself: it runs
`generate`, `package`, and the `release` profile's `build-metadata`/`create-autoupdate` goals against
`generate`, `install`, and the `release` profile's `build-metadata`/`create-autoupdate` goals against
a throwaway fixture plugin, so a `gephi-maven-plugin` version bump in `pom.xml` is caught before real
plugin repos pick it up.
plugin repos pick it up. It also checks the SPI contract-test harness (below) still resolves and
runs test cases for the current `gephi.version`. `pr.yml` builds PRs targeting `master-forge`. All workflow files live on
`master` and reach `master-forge` through the regular `master` merge.

`build.yml` and `pr.yml` also run SPI contract tests from
[`gephi-plugins-tests`](https://github.com/gephi/gephi-plugins-tests) against each plugin. These
check that every SPI implementation a plugin registers (found through `Lookup`, ignoring the ones
its dependencies register) behaves as Gephi expects, for each SPI the library covers. `build.yml`
runs them on every module of the branch. `pr.yml` runs them on the modules the PR changes, or on
every module when it changes the root `pom.xml` or the harness. The harness lives in `.github/contract-tests/`, so
neither plugins nor `gephi-plugin-parent` declare anything for it, and plugins built against an
older `gephi-plugin-parent` are tested against the current `gephi.version`. To run it locally:
`mvn install`, then `.github/contract-tests/run.sh modules/<Folder>`.

## How Gephi can be extended

Expand Down Expand Up @@ -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 <modules> 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
Expand Down
5 changes: 5 additions & 0 deletions CONTRIBUTING.md
Original file line number Diff line number Diff line change
Expand Up @@ -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/<Folder>`. 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

Expand Down
Loading