Skip to content
Open
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
8 changes: 8 additions & 0 deletions .dockerignore
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@
**/gradle-app.setting
!**/gradle-wrapper.jar

##############################
## install4j
##############################
**/*.install4j~

##############################
## IntelliJ
##############################
Expand Down Expand Up @@ -118,6 +123,9 @@ exports
MapUtilTestDB
testconf

ci/test-results
ci/coverage

client/logs
client/classes
client/dist
Expand Down
58 changes: 58 additions & 0 deletions .github/workflows/build.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,7 @@ on:
# miss each other's build cache.
env:
GRADLE_BUILD_ARGS: ${{ github.ref != 'refs/heads/main' && '-PdisableSigning=true -Pcoverage=true' || '' }}
COVERAGE: ${{ github.ref != 'refs/heads/main' }}

jobs:
build:
Expand Down Expand Up @@ -56,6 +57,13 @@ jobs:
name: oie-build
path: openintegrationengine.tar.gz

- name: Upload unit coverage
if: env.COVERAGE == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: coverage-unit
path: docker-build-output/app/build/coverage

- name: Stage Test Results
if: (!cancelled())
run: |
Expand Down Expand Up @@ -91,13 +99,28 @@ jobs:
- name: Expose GitHub Actions runtime for buildx cache
uses: crazy-max/ghaction-github-runtime@04d248b84655b509d8c44dc1d6f990c879747487 # v4.0.0

- name: Download JaCoCo agent
if: env.COVERAGE == 'true'
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: coverage-unit
path: coverage-unit

# Read-only: the build job populates this cache, and concurrent writers
# from the matrix would just fight over the same scope.
- name: Run integration tests
env:
EXTRA_BUILDX_ARGS: --cache-from type=gha,scope=oie-build
JACOCO_AGENT: ${{ env.COVERAGE == 'true' && 'coverage-unit/jacocoagent.jar' || '' }}
run: ci/runtests.sh ${{ matrix.configuration }}

- name: Upload smoke-test coverage
if: (!cancelled()) && env.COVERAGE == 'true'
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: coverage-smoke-${{ matrix.configuration }}
path: ci/coverage/*.exec

- name: Upload Test Results
if: (!cancelled())
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
Expand All @@ -123,3 +146,38 @@ jobs:
name: Test Results
pattern: Test Results (*)
delete-merged: true

coverage:
needs: [build, integration-test]
if: (!cancelled()) && needs.build.result == 'success' && github.ref != 'refs/heads/main'
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@3d3c42e5aac5ba805825da76410c181273ba90b1 # v7.0.1

- name: Download build
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
name: oie-build
path: artifacts

- name: Download coverage data
uses: actions/download-artifact@9000827ccba6bdab643e8b6fd33ac0654aef8333 # v8.0.2
with:
pattern: coverage-*
path: artifacts

# Lay the artifacts out as a local build would leave them.
- name: Merge unit and smoke-test coverage
run: |
mkdir -p server build ci/coverage
tar xzf artifacts/openintegrationengine.tar.gz -C server --transform 's|^openintegrationengine/|setup/|'
cp -r artifacts/coverage-unit build/coverage
cp artifacts/coverage-smoke-*/*.exec ci/coverage/
ci/coverage-report.sh

- name: Upload coverage report
uses: actions/upload-artifact@cf430e030ddbb5b0abf93d22962f4752f3646cd9 # v7.0.2
with:
name: Coverage Report
path: build/coverage/report
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -122,6 +122,7 @@ donkey/donkey-test

# /ci
/ci/test-results/
/ci/coverage/

# /client/
/client/logs
Expand Down
5 changes: 4 additions & 1 deletion Dockerfile
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,8 @@ RUN mkdir -p /home/ubuntu/.gradle
RUN --mount=type=cache,target=/home/ubuntu/.gradle/caches,sharing=locked,uid=1000,gid=1000 \
--mount=type=cache,target=/home/ubuntu/.gradle/wrapper,sharing=locked,uid=1000,gid=1000 \
source "$HOME/.sdkman/bin/sdkman-init.sh" \
&& ./gradlew --no-daemon build :smoketest:installHarnessDist ${GRADLE_BUILD_ARGS}
&& ./gradlew --no-daemon build :smoketest:installHarnessDist stageCoverage ${GRADLE_BUILD_ARGS} \
&& mkdir -p build/coverage

# Stage 1c: Present artifacts for export if not running within docker
FROM scratch AS build-output-export
Expand All @@ -51,6 +52,8 @@ COPY --from=builder /app/client/build/test-results /app/client/build/test-result
COPY --from=builder /app/command/build/test-results /app/command/build/test-results/
COPY --from=builder /app/donkey/build/test-results /app/donkey/build/test-results/
COPY --from=builder /app/server/build/test-results /app/server/build/test-results/
# empty unless built with -Pcoverage=true
COPY --from=builder /app/build/coverage /app/build/coverage/

# Stage 1d: Smoke Test Harness
FROM eclipse-temurin:21.0.9_10-jre-noble AS smoketest-harness
Expand Down
51 changes: 50 additions & 1 deletion build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,25 @@ plugins {
// vulnerability scanning: ./gradlew dependencyCheckAggregate
// (replaces the old standalone dependency-check.xml Ant entry point)
alias(libs.plugins.owasp.dependencycheck)
// static analysis: ./gradlew sonar -Dsonar.host.url=... -Dsonar.token=...
alias(libs.plugins.sonarqube)
}

sonar {
properties {
property 'sonar.projectKey', 'oie'
property 'sonar.projectName', 'Open Integration Engine'
}
}

allprojects {
sonar {
properties {
// unit and smoke-test coverage merged by ci/coverage-report.sh;
// set on every module, or JaCoCo's per-module default wins
property 'sonar.coverage.jacoco.xmlReportPaths', rootProject.file('build/coverage/report/coverage.xml').path
}
}
}

dependencyCheck {
Expand Down Expand Up @@ -275,7 +294,7 @@ subprojects {

if (rootProject.coverageEnabled) {
apply plugin: 'jacoco'
jacoco { toolVersion = '0.8.8' }
jacoco { toolVersion = libs.versions.jacoco.get() }
tasks.withType(Test).configureEach { finalizedBy tasks.named('jacocoTestReport') }
tasks.named('jacocoTestReport') {
reports.xml.required = true
Expand Down Expand Up @@ -330,6 +349,36 @@ tasks.register('test') {
dependsOn ':donkey:test', ':server:test', ':client:test', ':command:test'
}

// Collects what CI needs to merge unit and smoke-test coverage: the unit
// test execution data, the agent for the smoke-test server and the CLI
// that merges and reports. See ci/README.md.
def coverageTools = configurations.create('coverageTools') { transitive = false }
dependencies {
add('coverageTools', variantOf(libs.jacoco.agent) { classifier('runtime') })
add('coverageTools', variantOf(libs.jacoco.cli) { classifier('nodeps') })
}
repositories {
mavenCentral()
}

tasks.register('stageCoverage', Sync) {
group = 'verification'
description = 'Stages unit-test JaCoCo data and the JaCoCo agent and CLI into build/coverage.'
onlyIf { rootProject.coverageEnabled }
mustRunAfter ':donkey:test', ':server:test', ':client:test', ':command:test'
into layout.buildDirectory.dir('coverage')
from(coverageTools) {
rename { it.startsWith('org.jacoco.agent') ? 'jacocoagent.jar' : 'jacococli.jar' }
}
['donkey', 'server', 'client', 'command'].each { name ->
from(project(":${name}").layout.buildDirectory.dir('jacoco')) {
include '*.exec'
into 'unit'
rename { "${name}-${it}" }
}
}
}

tasks.register('clean') {
group = 'build'
description = 'Cleans every module.'
Expand Down
22 changes: 22 additions & 0 deletions ci/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -140,3 +140,25 @@ started yourself logged a generated password on its first boot:

JUnit XML results are written to `ci/test-results/<configuration>/` when using
`ci/runtests.sh`.

## Coverage

CI merges the unit-test and smoke-test coverage into one JaCoCo report (the
`Coverage Report` artifact). To produce the same report locally, build with
coverage, run the smoke tests with the staged agent, then merge:

```sh
./gradlew build stageCoverage -Pcoverage=true
JACOCO_AGENT=build/coverage/jacocoagent.jar ci/runtests.sh alpine-temurin21-derby
ci/coverage-report.sh
```

Each configuration writes `ci/coverage/<configuration>.exec` when its server stops.
The merged report lands in `build/coverage/report`, where `./gradlew sonar` reads it:

```sh
./gradlew sonar -Dsonar.host.url=... -Dsonar.token=...
```

Override the project key with `-Dsonar.projectKey=...`; SonarCloud also needs
`-Dsonar.organization=...`.
50 changes: 50 additions & 0 deletions ci/coverage-report.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
#!/usr/bin/env bash
# Merges unit-test and smoke-test JaCoCo data into build/coverage/report,
# which `./gradlew sonar` picks up. Expects, from the same commit:
# ./gradlew build stageCoverage -Pcoverage=true (server/setup, build/coverage)
# JACOCO_AGENT=build/coverage/jacocoagent.jar ci/runtests.sh <configuration>
# (ci/coverage/*.exec)
set -euo pipefail

cd "$(dirname "${BASH_SOURCE[0]}")/.."
cli=build/coverage/jacococli.jar
dist=server/setup
output=build/coverage/report
mkdir -p "$output"

shopt -s nullglob
unit=(build/coverage/unit/*.exec)
smoke=(ci/coverage/*.exec)
echo "Merging ${#unit[@]} unit-test and ${#smoke[@]} smoke-test data files"
if [[ ${#unit[@]} -eq 0 || ${#smoke[@]} -eq 0 ]]; then
echo "Missing unit-test or smoke-test coverage data" >&2
exit 1
fi
java -jar "$cli" merge "${unit[@]}" "${smoke[@]}" --destfile "$output/merged.exec"

# Only our own jars: third-party jars would add their classes to the report,
# and some of them hold originals of classes we patch in mirth-server.jar.
classfiles=()
for jar in "$dist"/mirth-*-launcher.jar \
"$dist"/server-lib/mirth-*.jar \
"$dist"/server-lib/donkey/donkey-*.jar \
"$dist"/client-lib/mirth-client.jar \
"$dist"/cli-lib/mirth-cli.jar \
"$dist"/extensions/*/*.jar; do
classfiles+=(--classfiles "$jar")
done
if [[ ${#classfiles[@]} -eq 0 ]]; then
echo "No jars found in the distribution" >&2
exit 1
fi

sourcefiles=()
for dir in donkey server client command; do
sourcefiles+=(--sourcefiles "$dir/src/main/java")
done

java -jar "$cli" report "$output/merged.exec" \
"${classfiles[@]}" "${sourcefiles[@]}" \
--name "Open Integration Engine (unit + smoke tests)" \
--xml "$output/coverage.xml" \
--html "$output/html"
12 changes: 12 additions & 0 deletions ci/coverage.compose.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
# Overlay that records JaCoCo coverage of the server under test. Added by
# ci/run-configuration.sh when JACOCO_AGENT points at a jacocoagent.jar.
services:
oie:
environment:
# configure-from-env splits VMOPTIONS on commas, so the agent gets a
# single option and records every class; ci/coverage-report.sh only
# reports our own. The data file is written when the JVM exits.
VMOPTIONS: -javaagent:/jacoco/jacocoagent.jar=destfile=/coverage/${OIE_CONFIGURATION}.exec
volumes:
- ${JACOCO_AGENT}:/jacoco/jacocoagent.jar:ro
- ${WORKSPACE}/ci/coverage:/coverage
16 changes: 15 additions & 1 deletion ci/run-configuration.sh
Original file line number Diff line number Diff line change
Expand Up @@ -23,9 +23,23 @@ export HOST_UID="$(id -u)"
export HOST_GID="$(id -g)"
mkdir -p "$WORKSPACE/ci/test-results"

compose=(docker compose -f "$1" -f ci/harness.compose.yml -p "oie-ci-${OIE_CONFIGURATION//[^a-z0-9-]/-}-$$")
compose=(docker compose -f "$1" -f ci/harness.compose.yml)
if [[ -n "${JACOCO_AGENT:-}" ]]; then
export JACOCO_AGENT="$(realpath "$JACOCO_AGENT")"
mkdir -p "$WORKSPACE/ci/coverage"
# The server runs as its own user, whose UID differs between the images.
chmod 777 "$WORKSPACE/ci/coverage"
# The agent appends, so start from a clean file.
rm -f "$WORKSPACE/ci/coverage/$OIE_CONFIGURATION.exec"
compose+=(-f ci/coverage.compose.yml)
fi
compose+=(-p "oie-ci-${OIE_CONFIGURATION//[^a-z0-9-]/-}-$$")

cleanup() {
if [[ -n "${JACOCO_AGENT:-}" ]]; then
# Give the server time to shut down cleanly so JaCoCo writes its data file.
"${compose[@]}" stop -t 120 oie || true
fi
"${compose[@]}" down -v --remove-orphans || true
}
trap cleanup EXIT
Expand Down
4 changes: 4 additions & 0 deletions gradle/libs.versions.toml
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ httpclient = "4.5.13"
itext = "2.1.7"
# do not bump past 2.15.x before reading the note on jackson-core
jackson = "2.14.3"
jacoco = "0.8.14"
jasper = "8.5.70"
jaxb-ri = "2.4.0-b180725.0644"
jaxws-ri = "2.3.0.2"
Expand Down Expand Up @@ -142,6 +143,8 @@ jackson-databind = { module = "com.fasterxml.jackson.core:jackson-databind", ver
jackson-dataformat-cbor = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-cbor", version.ref = "jackson" }
jackson-dataformat-yaml = { module = "com.fasterxml.jackson.dataformat:jackson-dataformat-yaml", version.ref = "jackson" }
jackson-datatype-jsr310 = { module = "com.fasterxml.jackson.datatype:jackson-datatype-jsr310", version.ref = "jackson" }
jacoco-agent = { module = "org.jacoco:org.jacoco.agent", version.ref = "jacoco" }
jacoco-cli = { module = "org.jacoco:org.jacoco.cli", version.ref = "jacoco" }
jakarta-activation = { module = "com.sun.activation:jakarta.activation", version = "1.2.1" }
jakarta-mail = { module = "com.sun.mail:jakarta.mail", version = "1.6.8" }
jasper-apache-el = { module = "org.mortbay.jasper:apache-el", version.ref = "jasper" }
Expand Down Expand Up @@ -653,3 +656,4 @@ server-test = [

[plugins]
owasp-dependencycheck = { id = "org.owasp.dependencycheck", version = "12.2.2" }
sonarqube = { id = "org.sonarqube", version = "7.5.0.8588" }
Loading
Loading