From d69f5daa7e7e5b6ec21b59f75227d8599ee1a482 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 13 Jul 2026 11:36:32 +0100 Subject: [PATCH 1/2] test(release-smoke): add external consumer smoke projects for the 2.0 modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Prove the modular 2.0 release works for a real consumer resolving from Maven Central, outside the reactor and with no local install behind it. Six standalone Maven projects (no , so the reactor never builds them), each verifying one published-artifact scenario: - graph-compose renders a PDF out of the box (the drop-in wrapper); - graph-compose-core alone throws MissingBackendException naming graph-compose-render-pdf, and its dependency tree pulls no PDFBox / POI / ZXing / templates / fonts / emoji (maven-enforcer bannedDependencies); - graph-compose-core + graph-compose-render-pdf render via the ServiceLoader SPI; - graph-compose-templates composes a built-in template through the PDF stack; - graph-compose-testing round-trips a layout snapshot (LayoutSnapshotAssertions); - graph-compose-bundle renders a template, exposes the bundled fonts, and makes the colour-emoji set resolvable. A dual-shell harness (run.sh / run.ps1) runs each project against a dedicated local repository that never receives an install of GraphCompose, evicting the graph-compose-* artifacts before each scenario so they must re-resolve from Central; it prints per-scenario RESULT lines and a machine-readable SUMMARY. Maven's own plugins stay cached (re-downloading them tests Maven, not the release). Not wired into CI — run on demand around a release. Tests: all six scenarios pass against the published 2.0.0 on Maven Central, both warm and with GraphCompose evicted per scenario (graph-compose-core-2.0.0.jar fetched from repo.maven.apache.org confirms Central resolution). --- scripts/release-smoke/README.md | 58 ++++++++++++ scripts/release-smoke/run.ps1 | 56 +++++++++++ scripts/release-smoke/run.sh | 61 ++++++++++++ .../release-smoke/s1-graph-compose/pom.xml | 52 +++++++++++ .../demcha/smoke/WrapperRendersPdfTest.java | 39 ++++++++ scripts/release-smoke/s2-core-only/pom.xml | 92 +++++++++++++++++++ .../smoke/CoreOnlyMissingBackendTest.java | 41 +++++++++ .../release-smoke/s3-core-render-pdf/pom.xml | 58 ++++++++++++ .../com/demcha/smoke/CoreRenderPdfTest.java | 40 ++++++++ scripts/release-smoke/s4-templates/pom.xml | 58 ++++++++++++ .../com/demcha/smoke/TemplatesRenderTest.java | 53 +++++++++++ scripts/release-smoke/s5-testing/pom.xml | 59 ++++++++++++ .../com/demcha/smoke/TestingHelperTest.java | 50 ++++++++++ scripts/release-smoke/s6-bundle/pom.xml | 53 +++++++++++ .../com/demcha/smoke/BundleRendersTest.java | 63 +++++++++++++ 15 files changed, 833 insertions(+) create mode 100644 scripts/release-smoke/README.md create mode 100644 scripts/release-smoke/run.ps1 create mode 100755 scripts/release-smoke/run.sh create mode 100644 scripts/release-smoke/s1-graph-compose/pom.xml create mode 100644 scripts/release-smoke/s1-graph-compose/src/test/java/com/demcha/smoke/WrapperRendersPdfTest.java create mode 100644 scripts/release-smoke/s2-core-only/pom.xml create mode 100644 scripts/release-smoke/s2-core-only/src/test/java/com/demcha/smoke/CoreOnlyMissingBackendTest.java create mode 100644 scripts/release-smoke/s3-core-render-pdf/pom.xml create mode 100644 scripts/release-smoke/s3-core-render-pdf/src/test/java/com/demcha/smoke/CoreRenderPdfTest.java create mode 100644 scripts/release-smoke/s4-templates/pom.xml create mode 100644 scripts/release-smoke/s4-templates/src/test/java/com/demcha/smoke/TemplatesRenderTest.java create mode 100644 scripts/release-smoke/s5-testing/pom.xml create mode 100644 scripts/release-smoke/s5-testing/src/test/java/com/demcha/smoke/TestingHelperTest.java create mode 100644 scripts/release-smoke/s6-bundle/pom.xml create mode 100644 scripts/release-smoke/s6-bundle/src/test/java/com/demcha/smoke/BundleRendersTest.java diff --git a/scripts/release-smoke/README.md b/scripts/release-smoke/README.md new file mode 100644 index 00000000..25047b38 --- /dev/null +++ b/scripts/release-smoke/README.md @@ -0,0 +1,58 @@ +# Release smoke — external consumer projects + +Standalone Maven projects that consume the **published** GraphCompose +coordinates from Maven Central, proving the modular 2.0 release resolves and +behaves as documented *outside* this repository — no reactor, no local +`mvn install` of GraphCompose beforehand. + +These projects are **not** part of the root reactor (the root `pom.xml` +`` does not list them and each pom has no ``), so a normal +build never touches them. Run them explicitly with the harness below. + +## Scenarios + +| Dir | Coordinate(s) under test | Proves | +|---|---|---| +| `s1-graph-compose` | `graph-compose` | the drop-in wrapper renders a PDF out of the box | +| `s2-core-only` | `graph-compose-core` | a lean core throws `MissingBackendException` naming `graph-compose-render-pdf`, and its dependency tree pulls no PDFBox / POI / ZXing / templates / fonts / emoji (enforced by `maven-enforcer` bannedDependencies) | +| `s3-core-render-pdf` | `graph-compose-core` + `graph-compose-render-pdf` | the explicit lean + backend combination renders a PDF | +| `s4-templates` | `graph-compose-templates` | a built-in template composes and renders through the PDF stack | +| `s5-testing` | `graph-compose` + `graph-compose-testing` | the consumer testing helper (`LayoutSnapshotAssertions`) resolves and round-trips a layout snapshot | +| `s6-bundle` | `graph-compose-bundle` | the batteries-included aggregate renders a templated document, exposes the bundled fonts (`DefaultFonts.bundledFontNames()`), and makes the colour-emoji set resolvable (`GraphComposeEmoji.isAvailable()`) | + +The "must pull core + render-pdf" (wrapper) and "must pull the documented +aggregate" (bundle) assertions are proven positively: `s1` / `s6` can only +render because the backend and companions were pulled transitively. + +## Running + +```bash +# Evict the GraphCompose artifacts before each scenario (Central-only, isolated): +./scripts/release-smoke/run.sh + +# Fast dev iteration — keep everything cached: +./scripts/release-smoke/run.sh --warm +``` + +```powershell +pwsh ./scripts/release-smoke/run.ps1 # isolated +pwsh ./scripts/release-smoke/run.ps1 -Warm # warm +``` + +The harness uses one dedicated local repository under `target/` that never +receives an `mvn install` of GraphCompose. In the default (isolated) mode it +**evicts the GraphCompose artifacts** (`io/github/demchaav/**`) before each +scenario, so every scenario must re-resolve `graph-compose-*` from Maven Central +— proving the release resolves with no local reactor build behind it. Maven's +own plugins and third-party libraries (PDFBox, JUnit, …) stay cached, because +re-downloading Maven's core plugins onto an empty repository is heavy, flaky, +and tests Maven rather than this release. Classpath isolation between scenarios +(e.g. the lean core never seeing the PDF backend) comes from each project's +declared dependencies and the `s2` enforcer rule, not from the repository state. + +The harness prints one `RESULT PASS|FAIL` line per project and ends +with a machine-readable `SUMMARY {"passed":N,"failed":M,"total":T}` line. Exit +code is non-zero if any scenario fails. + +The version under test is the `gc.version` property in each pom (currently +`2.0.0`). Bump it to smoke-test a later release. diff --git a/scripts/release-smoke/run.ps1 b/scripts/release-smoke/run.ps1 new file mode 100644 index 00000000..6e3adb31 --- /dev/null +++ b/scripts/release-smoke/run.ps1 @@ -0,0 +1,56 @@ +<# + Release smoke harness (PowerShell) — runs every external consumer project + against the published GraphCompose coordinates on Maven Central. See README.md. + + Isolation model: a dedicated local repository under target/ that never receives + an `mvn install` of GraphCompose. Before each scenario the GraphCompose + artifacts (io\github\demchaav\**) are EVICTED, so every scenario must re-resolve + graph-compose-* from Maven Central — proving the release resolves cleanly with + no local reactor build behind it. Maven's own plugins and third-party libraries + (PDFBox, JUnit, ...) are kept cached: re-downloading Maven's core plugins on an + empty repo is heavy and flaky and tests Maven, not this release. + + Usage: + pwsh ./scripts/release-smoke/run.ps1 # evict GraphCompose per scenario (Central-only, isolated) + pwsh ./scripts/release-smoke/run.ps1 -Warm # keep everything cached (fast dev iteration) +#> +param( + [switch]$Warm +) + +$ErrorActionPreference = 'Continue' +$here = Split-Path -Parent $MyInvocation.MyCommand.Path +$repoRoot = (Resolve-Path (Join-Path $here '..\..')).Path +$mvnw = Join-Path $repoRoot 'mvnw.cmd' + +$scenarios = @('s1-graph-compose', 's2-core-only', 's3-core-render-pdf', 's4-templates', 's5-testing', 's6-bundle') +$repo = Join-Path $repoRoot 'target\release-smoke-m2\repo' +New-Item -ItemType Directory -Force -Path $repo | Out-Null + +$pass = 0 +$fail = 0 +$results = @() + +foreach ($s in $scenarios) { + if (-not $Warm) { + $gc = Join-Path $repo 'io\github\demchaav' + if (Test-Path $gc) { Remove-Item -Recurse -Force $gc } + } + Write-Host "" + Write-Host "==================================================================" + Write-Host "=== SMOKE $s (maven.repo.local=$repo, GraphCompose evicted=$(if ($Warm) { 'no' } else { 'yes' }))" + Write-Host "==================================================================" + & $mvnw -B -ntp -f (Join-Path $here "$s\pom.xml") "-Dmaven.repo.local=$repo" clean verify + if ($LASTEXITCODE -eq 0) { + $results += "$s PASS"; $pass++ + } else { + $results += "$s FAIL"; $fail++ + } +} + +Write-Host "" +Write-Host "===================== RELEASE SMOKE SUMMARY =====================" +foreach ($r in $results) { Write-Host "RESULT $r" } +Write-Host ("SUMMARY {""passed"":$pass,""failed"":$fail,""total"":$($pass + $fail)}") + +if ($fail -ne 0) { exit 1 } else { exit 0 } diff --git a/scripts/release-smoke/run.sh b/scripts/release-smoke/run.sh new file mode 100755 index 00000000..5c44f021 --- /dev/null +++ b/scripts/release-smoke/run.sh @@ -0,0 +1,61 @@ +#!/usr/bin/env bash +# +# Release smoke harness — runs every external consumer project against the +# published GraphCompose coordinates on Maven Central. See README.md. +# +# Isolation model: a dedicated local repository under target/ that never receives +# an `mvn install` of GraphCompose. Before each scenario the GraphCompose +# artifacts (io/github/demchaav/**) are EVICTED, so every scenario must re-resolve +# graph-compose-* from Maven Central — proving the release resolves cleanly with +# no local reactor build behind it. Maven's own plugins and third-party libraries +# (PDFBox, JUnit, ...) are kept cached: re-downloading Maven's core plugins on an +# empty repo is heavy and flaky and tests Maven, not this release. +# +# Usage: +# ./scripts/release-smoke/run.sh # evict GraphCompose per scenario (Central-only, isolated) +# ./scripts/release-smoke/run.sh --warm # keep everything cached (fast dev iteration) +# +set -u + +HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +REPO_ROOT="$(cd "$HERE/../.." && pwd)" +MVNW="$REPO_ROOT/mvnw" + +SCENARIOS=(s1-graph-compose s2-core-only s3-core-render-pdf s4-templates s5-testing s6-bundle) +REPO="$REPO_ROOT/target/release-smoke-m2/repo" + +WARM=0 +[ "${1:-}" = "--warm" ] && WARM=1 +mkdir -p "$REPO" + +pass=0 +fail=0 +declare -a results + +for s in "${SCENARIOS[@]}"; do + if [ "$WARM" = "0" ]; then + # Evict only the GraphCompose coordinates so they must come from Central. + rm -rf "$REPO/io/github/demchaav" + fi + echo "" + echo "==================================================================" + echo "=== SMOKE $s (maven.repo.local=$REPO, GraphCompose evicted=$([ "$WARM" = "0" ] && echo yes || echo no))" + echo "==================================================================" + "$MVNW" -B -ntp -f "$HERE/$s/pom.xml" -Dmaven.repo.local="$REPO" clean verify + if [ $? -eq 0 ]; then + results+=("$s PASS") + pass=$((pass + 1)) + else + results+=("$s FAIL") + fail=$((fail + 1)) + fi +done + +echo "" +echo "===================== RELEASE SMOKE SUMMARY =====================" +for r in "${results[@]}"; do + echo "RESULT $r" +done +echo "SUMMARY {\"passed\":$pass,\"failed\":$fail,\"total\":$((pass + fail))}" + +[ "$fail" = "0" ] diff --git a/scripts/release-smoke/s1-graph-compose/pom.xml b/scripts/release-smoke/s1-graph-compose/pom.xml new file mode 100644 index 00000000..ce855022 --- /dev/null +++ b/scripts/release-smoke/s1-graph-compose/pom.xml @@ -0,0 +1,52 @@ + + + 4.0.0 + + + com.demcha.smoke + graph-compose-smoke-graph-compose + 1.0.0 + + + UTF-8 + 17 + 2.0.0 + 5.11.4 + 3.27.3 + 3.5.2 + + + + + io.github.demchaav + graph-compose + ${gc.version} + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + + + diff --git a/scripts/release-smoke/s1-graph-compose/src/test/java/com/demcha/smoke/WrapperRendersPdfTest.java b/scripts/release-smoke/s1-graph-compose/src/test/java/com/demcha/smoke/WrapperRendersPdfTest.java new file mode 100644 index 00000000..22dc01b3 --- /dev/null +++ b/scripts/release-smoke/s1-graph-compose/src/test/java/com/demcha/smoke/WrapperRendersPdfTest.java @@ -0,0 +1,39 @@ +package com.demcha.smoke; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Scenario 1 — the drop-in {@code graph-compose} wrapper renders a PDF out of + * the box, exactly as a 1.x caller expects after upgrading to 2.0. The wrapper + * pulls {@code graph-compose-core} + {@code graph-compose-render-pdf} + * transitively, so a successful render is positive proof both were resolved. + */ +class WrapperRendersPdfTest { + + @Test + void graphComposeWrapperRendersPdfOutOfTheBox() throws Exception { + Path out = Files.createTempFile("gc-smoke-wrapper", ".pdf"); + try (DocumentSession document = GraphCompose.document(out) + .pageSize(DocumentPageSize.A4) + .margin(36f, 36f, 36f, 36f) + .create()) { + document.add(document.dsl().paragraph() + .text("graph-compose 2.0.0 renders PDF out of the box.") + .build()); + document.buildPdf(); + } + + assertThat(Files.size(out)).isGreaterThan(0L); + byte[] head = Arrays.copyOf(Files.readAllBytes(out), 5); + assertThat(new String(head)).isEqualTo("%PDF-"); + } +} diff --git a/scripts/release-smoke/s2-core-only/pom.xml b/scripts/release-smoke/s2-core-only/pom.xml new file mode 100644 index 00000000..d1d90e2e --- /dev/null +++ b/scripts/release-smoke/s2-core-only/pom.xml @@ -0,0 +1,92 @@ + + + 4.0.0 + + + com.demcha.smoke + graph-compose-smoke-core-only + 1.0.0 + + + UTF-8 + 17 + 2.0.0 + 5.11.4 + 3.27.3 + 3.5.2 + 3.5.0 + + + + + io.github.demchaav + graph-compose-core + ${gc.version} + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + + + + org.apache.maven.plugins + maven-enforcer-plugin + ${enforcer.version} + + + core-stays-lean + + enforce + + + + + true + + org.apache.pdfbox + org.apache.poi + com.google.zxing + io.github.demchaav:graph-compose-render-pdf + io.github.demchaav:graph-compose-render-docx + io.github.demchaav:graph-compose-render-pptx + io.github.demchaav:graph-compose-templates + io.github.demchaav:graph-compose-fonts + io.github.demchaav:graph-compose-emoji + + + + true + + + + + + + diff --git a/scripts/release-smoke/s2-core-only/src/test/java/com/demcha/smoke/CoreOnlyMissingBackendTest.java b/scripts/release-smoke/s2-core-only/src/test/java/com/demcha/smoke/CoreOnlyMissingBackendTest.java new file mode 100644 index 00000000..2753c706 --- /dev/null +++ b/scripts/release-smoke/s2-core-only/src/test/java/com/demcha/smoke/CoreOnlyMissingBackendTest.java @@ -0,0 +1,41 @@ +package com.demcha.smoke; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.exceptions.MissingBackendException; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Scenario 2 — a lean {@code graph-compose-core} consumer with no render + * backend on the classpath. Asking it to build a PDF must fail with a + * {@link MissingBackendException} whose message names the artifact to add + * ({@code graph-compose-render-pdf}), rather than an opaque NPE or ISE. The + * dependency-tree leanness is asserted separately by the enforcer rule in the + * pom. + */ +class CoreOnlyMissingBackendTest { + + @Test + void coreOnlyRenderThrowsMissingBackendNamingRenderPdf() throws Exception { + Path out = Files.createTempFile("gc-smoke-core-only", ".pdf"); + assertThatThrownBy(() -> { + try (DocumentSession document = GraphCompose.document(out) + .pageSize(DocumentPageSize.A4) + .margin(36f, 36f, 36f, 36f) + .create()) { + document.add(document.dsl().paragraph() + .text("no backend on the classpath") + .build()); + document.buildPdf(); + } + }) + .isInstanceOf(MissingBackendException.class) + .hasMessageContaining("graph-compose-render-pdf"); + } +} diff --git a/scripts/release-smoke/s3-core-render-pdf/pom.xml b/scripts/release-smoke/s3-core-render-pdf/pom.xml new file mode 100644 index 00000000..8e104cce --- /dev/null +++ b/scripts/release-smoke/s3-core-render-pdf/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + com.demcha.smoke + graph-compose-smoke-core-render-pdf + 1.0.0 + + + UTF-8 + 17 + 2.0.0 + 5.11.4 + 3.27.3 + 3.5.2 + + + + + io.github.demchaav + graph-compose-core + ${gc.version} + + + io.github.demchaav + graph-compose-render-pdf + ${gc.version} + runtime + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + + + diff --git a/scripts/release-smoke/s3-core-render-pdf/src/test/java/com/demcha/smoke/CoreRenderPdfTest.java b/scripts/release-smoke/s3-core-render-pdf/src/test/java/com/demcha/smoke/CoreRenderPdfTest.java new file mode 100644 index 00000000..930fa2da --- /dev/null +++ b/scripts/release-smoke/s3-core-render-pdf/src/test/java/com/demcha/smoke/CoreRenderPdfTest.java @@ -0,0 +1,40 @@ +package com.demcha.smoke; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Scenario 3 — the explicit bring-your-own-backend combination + * ({@code graph-compose-core} + {@code graph-compose-render-pdf}) renders a PDF. + * The PDF backend registers its {@code FixedLayoutBackendProvider} / + * {@code FontMetricsProvider} via {@code META-INF/services}, so the core + * discovers it at runtime through the ServiceLoader SPI. + */ +class CoreRenderPdfTest { + + @Test + void coreWithRenderPdfRendersPdf() throws Exception { + Path out = Files.createTempFile("gc-smoke-core-render-pdf", ".pdf"); + try (DocumentSession document = GraphCompose.document(out) + .pageSize(DocumentPageSize.A4) + .margin(36f, 36f, 36f, 36f) + .create()) { + document.add(document.dsl().paragraph() + .text("graph-compose-core + graph-compose-render-pdf renders via the SPI.") + .build()); + document.buildPdf(); + } + + assertThat(Files.size(out)).isGreaterThan(0L); + byte[] head = Arrays.copyOf(Files.readAllBytes(out), 5); + assertThat(new String(head)).isEqualTo("%PDF-"); + } +} diff --git a/scripts/release-smoke/s4-templates/pom.xml b/scripts/release-smoke/s4-templates/pom.xml new file mode 100644 index 00000000..259b95b5 --- /dev/null +++ b/scripts/release-smoke/s4-templates/pom.xml @@ -0,0 +1,58 @@ + + + 4.0.0 + + + com.demcha.smoke + graph-compose-smoke-templates + 1.0.0 + + + UTF-8 + 17 + 2.0.0 + 5.11.4 + 3.27.3 + 3.5.2 + + + + + io.github.demchaav + graph-compose + ${gc.version} + + + io.github.demchaav + graph-compose-templates + ${gc.version} + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + + + diff --git a/scripts/release-smoke/s4-templates/src/test/java/com/demcha/smoke/TemplatesRenderTest.java b/scripts/release-smoke/s4-templates/src/test/java/com/demcha/smoke/TemplatesRenderTest.java new file mode 100644 index 00000000..9a6708cf --- /dev/null +++ b/scripts/release-smoke/s4-templates/src/test/java/com/demcha/smoke/TemplatesRenderTest.java @@ -0,0 +1,53 @@ +package com.demcha.smoke; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec; +import com.demcha.compose.document.templates.invoice.presets.ModernInvoice; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Scenario 4 — a built-in template ({@code ModernInvoice}) from the opt-in + * {@code graph-compose-templates} module composes over the canonical DSL and + * renders through the PDF stack. + */ +class TemplatesRenderTest { + + @Test + void modernInvoiceTemplateComposesAndRenders() throws Exception { + InvoiceDocumentSpec spec = InvoiceDocumentSpec.builder() + .title("Smoke Invoice") + .invoiceNumber("SMOKE-001") + .issueDate("2026-07-13") + .dueDate("2026-08-13") + .status("DUE") + .fromParty(p -> p.name("Acme Studio")) + .billToParty(p -> p.name("Client Ltd")) + .lineItem("Consulting", "Release smoke test", "1", "$100.00", "$100.00") + .totalRow("Total", "$100.00") + .build(); + + DocumentTemplate template = ModernInvoice.create(); + + Path out = Files.createTempFile("gc-smoke-templates", ".pdf"); + try (DocumentSession document = GraphCompose.document(out) + .pageSize(DocumentPageSize.A4) + .margin(36f, 36f, 36f, 36f) + .create()) { + template.compose(document, spec); + document.buildPdf(); + } + + assertThat(Files.size(out)).isGreaterThan(0L); + byte[] head = Arrays.copyOf(Files.readAllBytes(out), 5); + assertThat(new String(head)).isEqualTo("%PDF-"); + } +} diff --git a/scripts/release-smoke/s5-testing/pom.xml b/scripts/release-smoke/s5-testing/pom.xml new file mode 100644 index 00000000..2ae6e839 --- /dev/null +++ b/scripts/release-smoke/s5-testing/pom.xml @@ -0,0 +1,59 @@ + + + 4.0.0 + + + com.demcha.smoke + graph-compose-smoke-testing + 1.0.0 + + + UTF-8 + 17 + 2.0.0 + 5.11.4 + 3.27.3 + 3.5.2 + + + + + io.github.demchaav + graph-compose + ${gc.version} + + + io.github.demchaav + graph-compose-testing + ${gc.version} + test + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + + + diff --git a/scripts/release-smoke/s5-testing/src/test/java/com/demcha/smoke/TestingHelperTest.java b/scripts/release-smoke/s5-testing/src/test/java/com/demcha/smoke/TestingHelperTest.java new file mode 100644 index 00000000..0e4c2bd0 --- /dev/null +++ b/scripts/release-smoke/s5-testing/src/test/java/com/demcha/smoke/TestingHelperTest.java @@ -0,0 +1,50 @@ +package com.demcha.smoke; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.testing.layout.LayoutSnapshotAssertions; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Scenario 5 — the consumer testing support {@code graph-compose-testing} + * resolves and its documented helper ({@code LayoutSnapshotAssertions}) works + * end to end: extract a layout snapshot, write a baseline in update mode, then + * assert the freshly rendered layout matches it. Baselines are written under + * {@code target/} so nothing is left in the source tree. + */ +class TestingHelperTest { + + @Test + void layoutSnapshotAssertionsRoundTrips() throws Exception { + Path expected = Path.of("target", "smoke-snapshots", "expected"); + Path actual = Path.of("target", "smoke-snapshots", "actual"); + Path out = Files.createTempFile("gc-smoke-testing", ".pdf"); + + try (DocumentSession document = GraphCompose.document(out) + .pageSize(DocumentPageSize.A4) + .margin(36f, 36f, 36f, 36f) + .create()) { + document.add(document.dsl().paragraph() + .text("graph-compose-testing layout snapshot smoke.") + .build()); + + // 1) update mode: extract + serialize the snapshot to the baseline root. + System.setProperty(LayoutSnapshotAssertions.UPDATE_PROPERTY, "true"); + LayoutSnapshotAssertions.assertMatches(document, expected, actual, "release-smoke"); + + // 2) compare mode: the freshly rendered layout must match the baseline. + System.setProperty(LayoutSnapshotAssertions.UPDATE_PROPERTY, "false"); + LayoutSnapshotAssertions.assertMatches(document, expected, actual, "release-smoke"); + } finally { + System.clearProperty(LayoutSnapshotAssertions.UPDATE_PROPERTY); + } + + assertThat(Files.exists(expected.resolve("release-smoke.json"))).isTrue(); + } +} diff --git a/scripts/release-smoke/s6-bundle/pom.xml b/scripts/release-smoke/s6-bundle/pom.xml new file mode 100644 index 00000000..6ca5a9f9 --- /dev/null +++ b/scripts/release-smoke/s6-bundle/pom.xml @@ -0,0 +1,53 @@ + + + 4.0.0 + + + com.demcha.smoke + graph-compose-smoke-bundle + 1.0.0 + + + UTF-8 + 17 + 2.0.0 + 5.11.4 + 3.27.3 + 3.5.2 + + + + + io.github.demchaav + graph-compose-bundle + ${gc.version} + + + + org.junit.jupiter + junit-jupiter + ${junit.version} + test + + + org.assertj + assertj-core + ${assertj.version} + test + + + + + + + org.apache.maven.plugins + maven-surefire-plugin + ${surefire.version} + + + + diff --git a/scripts/release-smoke/s6-bundle/src/test/java/com/demcha/smoke/BundleRendersTest.java b/scripts/release-smoke/s6-bundle/src/test/java/com/demcha/smoke/BundleRendersTest.java new file mode 100644 index 00000000..05e4cc01 --- /dev/null +++ b/scripts/release-smoke/s6-bundle/src/test/java/com/demcha/smoke/BundleRendersTest.java @@ -0,0 +1,63 @@ +package com.demcha.smoke; + +import com.demcha.compose.GraphCompose; +import com.demcha.compose.document.api.DocumentPageSize; +import com.demcha.compose.document.api.DocumentSession; +import com.demcha.compose.document.templates.api.DocumentTemplate; +import com.demcha.compose.document.templates.data.invoice.InvoiceDocumentSpec; +import com.demcha.compose.document.templates.invoice.presets.ModernInvoice; +import com.demcha.compose.emoji.GraphComposeEmoji; +import com.demcha.compose.font.DefaultFonts; +import org.junit.jupiter.api.Test; + +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Arrays; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Scenario 6 — the batteries-included {@code graph-compose-bundle} aggregate. + * A single dependency renders a templated document through the PDF stack, + * exposes the bundled font catalogue, and makes the colour-emoji set + * resolvable — proving the bundle pulled the documented aggregate set + * (wrapper + templates + fonts + emoji) transitively. + */ +class BundleRendersTest { + + @Test + void bundleRendersTemplateAndExposesFontsAndEmoji() throws Exception { + InvoiceDocumentSpec spec = InvoiceDocumentSpec.builder() + .title("Bundle Smoke Invoice") + .invoiceNumber("SMOKE-BUNDLE-001") + .issueDate("2026-07-13") + .dueDate("2026-08-13") + .status("DUE") + .fromParty(p -> p.name("Acme Studio")) + .billToParty(p -> p.name("Client Ltd")) + .lineItem("Consulting", "Bundle smoke test", "1", "$100.00", "$100.00") + .totalRow("Total", "$100.00") + .build(); + + DocumentTemplate template = ModernInvoice.create(); + + Path out = Files.createTempFile("gc-smoke-bundle", ".pdf"); + try (DocumentSession document = GraphCompose.document(out) + .pageSize(DocumentPageSize.A4) + .margin(36f, 36f, 36f, 36f) + .create()) { + template.compose(document, spec); + document.buildPdf(); + } + + assertThat(Files.size(out)).isGreaterThan(0L); + byte[] head = Arrays.copyOf(Files.readAllBytes(out), 5); + assertThat(new String(head)).isEqualTo("%PDF-"); + + // Bundled font catalogue is on the classpath (graph-compose-fonts). + assertThat(DefaultFonts.bundledFontNames()).isNotEmpty(); + + // Colour-emoji set is resolvable (graph-compose-emoji). + assertThat(GraphComposeEmoji.isAvailable()).isTrue(); + } +} From fd57474eecafb621000ebe2d54303212487872e6 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Mon, 13 Jul 2026 12:22:32 +0100 Subject: [PATCH 2/2] test(release-smoke): pin Central-only resolution, add a dispatch workflow - Isolated settings.xml (mirrorOf=*) forces every artifact and plugin request through Maven Central, passed to each scenario with -s, so a scenario can only pass if the coordinate is genuinely resolvable on Central. - Hard-fail if the GraphCompose cache directory cannot be evicted, and confirm io/github/demchaav is gone before each scenario, so a stale cache can never mask a broken publish. - Accept a version override (--version / -Version, default 2.0.0) passed as -Dgc.version; release smoke tests published artifacts only, never a SNAPSHOT. - Add a workflow_dispatch workflow (.github/workflows/release-smoke.yml) that runs the harness manually with a version input. Tests: all six scenarios pass against 2.0.0 on Maven Central through the Central-only mirror with GraphCompose evicted per scenario (SUMMARY {"version":"2.0.0","passed":6,"failed":0,"total":6}). --- .github/workflows/release-smoke.yml | 41 +++++++++++++++++++++++ scripts/release-smoke/README.md | 45 +++++++++++++++++--------- scripts/release-smoke/run.ps1 | 36 ++++++++++++++------- scripts/release-smoke/run.sh | 50 +++++++++++++++++++++-------- scripts/release-smoke/settings.xml | 21 ++++++++++++ 5 files changed, 151 insertions(+), 42 deletions(-) create mode 100644 .github/workflows/release-smoke.yml create mode 100644 scripts/release-smoke/settings.xml diff --git a/.github/workflows/release-smoke.yml b/.github/workflows/release-smoke.yml new file mode 100644 index 00000000..9f18aa62 --- /dev/null +++ b/.github/workflows/release-smoke.yml @@ -0,0 +1,41 @@ +name: Release Smoke (consumer verification) + +# Manually verifies that a PUBLISHED GraphCompose release resolves and works for +# real external consumers straight from Maven Central (scripts/release-smoke/). +# Not tied to a tag or push — dispatch it after a publish (allowing for Central +# indexing delay), or any time, to re-check a shipped version. It never touches a +# -SNAPSHOT: release smoke tests published artifacts only. + +on: + workflow_dispatch: + inputs: + version: + description: 'Published GraphCompose version to smoke-test from Maven Central' + required: true + type: string + default: '2.0.0' + +permissions: + contents: read + +jobs: + smoke: + name: Smoke ${{ inputs.version }} from Maven Central + runs-on: ubuntu-latest + env: + JAVA_TOOL_OPTIONS: -Djava.awt.headless=true + steps: + - name: Check out repository + uses: actions/checkout@v7 + + - name: Set up Temurin JDK 17 + uses: actions/setup-java@v5 + with: + distribution: temurin + java-version: '17' + + - name: Run release smoke against Maven Central + # The harness forces Central-only resolution (its settings.xml) and evicts + # the GraphCompose artifacts before each scenario, so this genuinely tests + # the published ${{ inputs.version }} coordinates end to end. + run: bash scripts/release-smoke/run.sh --version "${{ inputs.version }}" diff --git a/scripts/release-smoke/README.md b/scripts/release-smoke/README.md index 25047b38..5d052a05 100644 --- a/scripts/release-smoke/README.md +++ b/scripts/release-smoke/README.md @@ -30,29 +30,42 @@ render because the backend and companions were pulled transitively. # Evict the GraphCompose artifacts before each scenario (Central-only, isolated): ./scripts/release-smoke/run.sh +# Smoke-test a different published version: +./scripts/release-smoke/run.sh --version 2.0.1 + # Fast dev iteration — keep everything cached: ./scripts/release-smoke/run.sh --warm ``` ```powershell -pwsh ./scripts/release-smoke/run.ps1 # isolated -pwsh ./scripts/release-smoke/run.ps1 -Warm # warm +pwsh ./scripts/release-smoke/run.ps1 # isolated +pwsh ./scripts/release-smoke/run.ps1 -Version 2.0.1 # a different published version +pwsh ./scripts/release-smoke/run.ps1 -Warm # warm ``` -The harness uses one dedicated local repository under `target/` that never -receives an `mvn install` of GraphCompose. In the default (isolated) mode it -**evicts the GraphCompose artifacts** (`io/github/demchaav/**`) before each -scenario, so every scenario must re-resolve `graph-compose-*` from Maven Central -— proving the release resolves with no local reactor build behind it. Maven's -own plugins and third-party libraries (PDFBox, JUnit, …) stay cached, because -re-downloading Maven's core plugins onto an empty repository is heavy, flaky, -and tests Maven rather than this release. Classpath isolation between scenarios -(e.g. the lean core never seeing the PDF backend) comes from each project's -declared dependencies and the `s2` enforcer rule, not from the repository state. +Or dispatch the **Release Smoke (consumer verification)** GitHub Actions workflow +(`.github/workflows/release-smoke.yml`) with a `version` input — handy after a +publish, once Central has indexed the release. + +The harness passes an isolated `settings.xml` (`-s`) whose `mirrorOf=*` mirror +forces **every** artifact and plugin request through Maven Central, and uses one +dedicated local repository under `target/` that never receives an `mvn install` +of GraphCompose. In the default (isolated) mode it **evicts the GraphCompose +artifacts** (`io/github/demchaav/**`) before each scenario — hard-failing if the +eviction does not take — so every scenario must re-resolve `graph-compose-*` from +Central, proving the release resolves with no local reactor build behind it. +Maven's own plugins and third-party libraries (PDFBox, JUnit, …) stay cached, +because re-downloading Maven's core plugins onto an empty repository is heavy, +flaky, and tests Maven rather than this release. Classpath isolation between +scenarios (e.g. the lean core never seeing the PDF backend) comes from each +project's declared dependencies and the `s2` enforcer rule, not from the +repository state. The harness prints one `RESULT PASS|FAIL` line per project and ends -with a machine-readable `SUMMARY {"passed":N,"failed":M,"total":T}` line. Exit -code is non-zero if any scenario fails. +with a machine-readable `SUMMARY {"version":"…","passed":N,"failed":M,"total":T}` +line. Exit code is non-zero if any scenario fails. -The version under test is the `gc.version` property in each pom (currently -`2.0.0`). Bump it to smoke-test a later release. +The version under test defaults to the current published release (`2.0.0`) and is +overridable with `--version` / `-Version` (or the workflow's `version` input); it +is passed to Maven as `-Dgc.version`, overriding the `gc.version` property in each +pom. Release smoke always tests **published** artifacts — never a `-SNAPSHOT`. diff --git a/scripts/release-smoke/run.ps1 b/scripts/release-smoke/run.ps1 index 6e3adb31..07cb51fc 100644 --- a/scripts/release-smoke/run.ps1 +++ b/scripts/release-smoke/run.ps1 @@ -3,25 +3,31 @@ against the published GraphCompose coordinates on Maven Central. See README.md. Isolation model: a dedicated local repository under target/ that never receives - an `mvn install` of GraphCompose. Before each scenario the GraphCompose - artifacts (io\github\demchaav\**) are EVICTED, so every scenario must re-resolve - graph-compose-* from Maven Central — proving the release resolves cleanly with - no local reactor build behind it. Maven's own plugins and third-party libraries - (PDFBox, JUnit, ...) are kept cached: re-downloading Maven's core plugins on an - empty repo is heavy and flaky and tests Maven, not this release. + an `mvn install` of GraphCompose, plus an isolated settings.xml whose mirror + forces ALL resolution through Maven Central. Before each scenario the GraphCompose + artifacts (io\github\demchaav\**) are EVICTED — and the harness hard-fails if the + eviction does not take — so every scenario must re-resolve graph-compose-* from + Central. Maven's own plugins and third-party libraries stay cached: re-downloading + Maven's core plugins on an empty repo is heavy, flaky, and tests Maven, not this + release. Usage: - pwsh ./scripts/release-smoke/run.ps1 # evict GraphCompose per scenario (Central-only, isolated) - pwsh ./scripts/release-smoke/run.ps1 -Warm # keep everything cached (fast dev iteration) + pwsh ./scripts/release-smoke/run.ps1 # isolated, tests 2.0.0 + pwsh ./scripts/release-smoke/run.ps1 -Version 2.0.1 # test a different published version + pwsh ./scripts/release-smoke/run.ps1 -Warm # keep everything cached (fast dev iteration) #> param( - [switch]$Warm + [switch]$Warm, + # Default version under test: the currently published release. Release smoke + # must test PUBLISHED artifacts — never a -SNAPSHOT. + [string]$Version = '2.0.0' ) $ErrorActionPreference = 'Continue' $here = Split-Path -Parent $MyInvocation.MyCommand.Path $repoRoot = (Resolve-Path (Join-Path $here '..\..')).Path $mvnw = Join-Path $repoRoot 'mvnw.cmd' +$settings = Join-Path $here 'settings.xml' $scenarios = @('s1-graph-compose', 's2-core-only', 's3-core-render-pdf', 's4-templates', 's5-testing', 's6-bundle') $repo = Join-Path $repoRoot 'target\release-smoke-m2\repo' @@ -33,14 +39,19 @@ $results = @() foreach ($s in $scenarios) { if (-not $Warm) { + # Evict only the GraphCompose coordinates, then hard-fail if it did not take. $gc = Join-Path $repo 'io\github\demchaav' if (Test-Path $gc) { Remove-Item -Recurse -Force $gc } + if (Test-Path $gc) { + Write-Error "FATAL: could not remove the GraphCompose cache directory: $gc" + exit 3 + } } Write-Host "" Write-Host "==================================================================" - Write-Host "=== SMOKE $s (maven.repo.local=$repo, GraphCompose evicted=$(if ($Warm) { 'no' } else { 'yes' }))" + Write-Host "=== SMOKE $s (version=$Version, repo=$repo, evicted=$(if ($Warm) { 'no' } else { 'yes' }))" Write-Host "==================================================================" - & $mvnw -B -ntp -f (Join-Path $here "$s\pom.xml") "-Dmaven.repo.local=$repo" clean verify + & $mvnw -B -ntp -s $settings "-Dgc.version=$Version" -f (Join-Path $here "$s\pom.xml") "-Dmaven.repo.local=$repo" clean verify if ($LASTEXITCODE -eq 0) { $results += "$s PASS"; $pass++ } else { @@ -50,7 +61,8 @@ foreach ($s in $scenarios) { Write-Host "" Write-Host "===================== RELEASE SMOKE SUMMARY =====================" +Write-Host "version-under-test: $Version" foreach ($r in $results) { Write-Host "RESULT $r" } -Write-Host ("SUMMARY {""passed"":$pass,""failed"":$fail,""total"":$($pass + $fail)}") +Write-Host ("SUMMARY {""version"":""$Version"",""passed"":$pass,""failed"":$fail,""total"":$($pass + $fail)}") if ($fail -ne 0) { exit 1 } else { exit 0 } diff --git a/scripts/release-smoke/run.sh b/scripts/release-smoke/run.sh index 5c44f021..96f472a8 100755 --- a/scripts/release-smoke/run.sh +++ b/scripts/release-smoke/run.sh @@ -4,28 +4,41 @@ # published GraphCompose coordinates on Maven Central. See README.md. # # Isolation model: a dedicated local repository under target/ that never receives -# an `mvn install` of GraphCompose. Before each scenario the GraphCompose -# artifacts (io/github/demchaav/**) are EVICTED, so every scenario must re-resolve -# graph-compose-* from Maven Central — proving the release resolves cleanly with -# no local reactor build behind it. Maven's own plugins and third-party libraries -# (PDFBox, JUnit, ...) are kept cached: re-downloading Maven's core plugins on an -# empty repo is heavy and flaky and tests Maven, not this release. +# an `mvn install` of GraphCompose, plus an isolated settings.xml whose mirror +# forces ALL resolution through Maven Central. Before each scenario the GraphCompose +# artifacts (io/github/demchaav/**) are EVICTED — and the harness hard-fails if the +# eviction does not take — so every scenario must re-resolve graph-compose-* from +# Central. Maven's own plugins and third-party libraries stay cached: re-downloading +# Maven's core plugins on an empty repo is heavy, flaky, and tests Maven, not this +# release. # # Usage: -# ./scripts/release-smoke/run.sh # evict GraphCompose per scenario (Central-only, isolated) -# ./scripts/release-smoke/run.sh --warm # keep everything cached (fast dev iteration) +# ./scripts/release-smoke/run.sh # isolated, tests gc.version=2.0.0 +# ./scripts/release-smoke/run.sh --version 2.0.1 # test a different published version +# ./scripts/release-smoke/run.sh --warm # keep everything cached (fast dev iteration) # set -u HERE="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd "$HERE/../.." && pwd)" MVNW="$REPO_ROOT/mvnw" +SETTINGS="$HERE/settings.xml" SCENARIOS=(s1-graph-compose s2-core-only s3-core-render-pdf s4-templates s5-testing s6-bundle) REPO="$REPO_ROOT/target/release-smoke-m2/repo" +# Default version under test: the currently published release. Release smoke must +# test PUBLISHED artifacts — never a -SNAPSHOT. +GC_VERSION="2.0.0" WARM=0 -[ "${1:-}" = "--warm" ] && WARM=1 +while [ $# -gt 0 ]; do + case "$1" in + --warm) WARM=1; shift ;; + --version) GC_VERSION="${2:?--version needs a value}"; shift 2 ;; + --version=*) GC_VERSION="${1#*=}"; shift ;; + *) echo "unknown argument: $1" >&2; exit 2 ;; + esac +done mkdir -p "$REPO" pass=0 @@ -34,14 +47,22 @@ declare -a results for s in "${SCENARIOS[@]}"; do if [ "$WARM" = "0" ]; then - # Evict only the GraphCompose coordinates so they must come from Central. - rm -rf "$REPO/io/github/demchaav" + # Evict only the GraphCompose coordinates so they must come from Central, + # then hard-fail if the eviction did not take (a stale cache would mask a + # broken publish). + gc_dir="$REPO/io/github/demchaav" + rm -rf "$gc_dir" + if [ -d "$gc_dir" ]; then + echo "FATAL: could not remove the GraphCompose cache directory: $gc_dir" >&2 + exit 3 + fi fi echo "" echo "==================================================================" - echo "=== SMOKE $s (maven.repo.local=$REPO, GraphCompose evicted=$([ "$WARM" = "0" ] && echo yes || echo no))" + echo "=== SMOKE $s (version=$GC_VERSION, repo=$REPO, evicted=$([ "$WARM" = "0" ] && echo yes || echo no))" echo "==================================================================" - "$MVNW" -B -ntp -f "$HERE/$s/pom.xml" -Dmaven.repo.local="$REPO" clean verify + "$MVNW" -B -ntp -s "$SETTINGS" -Dgc.version="$GC_VERSION" \ + -f "$HERE/$s/pom.xml" -Dmaven.repo.local="$REPO" clean verify if [ $? -eq 0 ]; then results+=("$s PASS") pass=$((pass + 1)) @@ -53,9 +74,10 @@ done echo "" echo "===================== RELEASE SMOKE SUMMARY =====================" +echo "version-under-test: $GC_VERSION" for r in "${results[@]}"; do echo "RESULT $r" done -echo "SUMMARY {\"passed\":$pass,\"failed\":$fail,\"total\":$((pass + fail))}" +echo "SUMMARY {\"version\":\"$GC_VERSION\",\"passed\":$pass,\"failed\":$fail,\"total\":$((pass + fail))}" [ "$fail" = "0" ] diff --git a/scripts/release-smoke/settings.xml b/scripts/release-smoke/settings.xml new file mode 100644 index 00000000..834bf4a3 --- /dev/null +++ b/scripts/release-smoke/settings.xml @@ -0,0 +1,21 @@ + + + + + + central-only + Maven Central only + https://repo.maven.apache.org/maven2 + * + + +