Skip to content

Commit 677d095

Browse files
committed
build(release): give the Maven banner a PDF, and make the smoke test assert
The banner deck never reached the site. GenerateAllExamples called only MavenBannerPptxExample.generate(), which writes flagships/maven-banner.pptx, while the class also exposes generatePdf(). ShowcaseSync walks .pdf files, keys one card per PDF, and copies a .pptx only as that PDF's same-named companion — so a deck with no PDF twin lands in target and stops there, and the metadata entry written for it goes unused. The banner appeared in web/examples.json zero times. Generating the PDF first puts it on the site: the sync now reports 91 PDFs and 8 PPTX twins where it reported 90 and 7. everyGeneratedDeckHasAPdfTwin states that invariant so the next deck cannot repeat it. The smoke test around it asserted nothing — its own comment said "at least one PDF should have been written" above assertThat(true).isTrue(). It now reads the output tree: PDFs exist, every deck has its twin, and the catalogue spans templates, features and flagships rather than one corner of it. ShowcaseSync collected the PDF list after clearing the published trees, so a generation run that succeeded while writing nothing would have deleted the whole site and written an empty manifest, with the failure visible only once the site was live. The list is now collected first and an empty one aborts before anything is removed.
1 parent c05f8fd commit 677d095

21 files changed

Lines changed: 160 additions & 75 deletions

File tree

‎examples/src/main/java/com/demcha/examples/GenerateAllExamples.java‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -225,6 +225,10 @@ public static void main(String[] args) throws Exception {
225225
System.out.println("Generated: " + EngineDeckExample.generate());
226226
System.out.println("Generated: " + EngineDeckV2Example.generate());
227227
System.out.println("Generated: " + EngineDeckPptxExample.generate());
228+
// The PDF first: ShowcaseSync builds a card per PDF and publishes a PPTX only
229+
// as the same-named companion beside one, so a deck without its PDF twin never
230+
// reaches the site at all.
231+
System.out.println("Generated: " + MavenBannerPptxExample.generatePdf());
228232
System.out.println("Generated: " + MavenBannerPptxExample.generate());
229233
System.out.println("Generated: " + SocialCardExample.generate());
230234
System.out.println("Generated: " + SocialCardExample.generatePptx());

‎examples/src/main/java/com/demcha/examples/support/ShowcaseSync.java‎

Lines changed: 72 additions & 61 deletions
Original file line numberDiff line numberDiff line change
@@ -73,6 +73,23 @@ public static void main(String[] args) throws Exception {
7373
+ ". Run GenerateAllExamples first.");
7474
}
7575

76+
// Collect BEFORE clearing. The directory existing is not evidence that anything
77+
// was produced: a generation run that succeeds while writing nothing would
78+
// otherwise delete the whole published site and replace the manifest with an
79+
// empty one, and the failure would only be visible once the site was live.
80+
List<Path> pdfs;
81+
try (Stream<Path> walk = Files.walk(generatedPdfs)) {
82+
pdfs = walk
83+
.filter(p -> p.toString().endsWith(".pdf"))
84+
.sorted()
85+
.toList();
86+
}
87+
if (pdfs.isEmpty()) {
88+
throw new IllegalStateException("No PDFs under " + generatedPdfs
89+
+ ". Run GenerateAllExamples first — refusing to clear the published"
90+
+ " showcase and write an empty manifest.");
91+
}
92+
7693
// The copy below replaces files but never removes them, so an example that was
7794
// renamed or deleted kept its artifact published on GitHub Pages indefinitely —
7895
// reachable by URL, absent from the manifest, and rendered from source that no
@@ -88,71 +105,65 @@ public static void main(String[] args) throws Exception {
88105
int rendered = 0;
89106
int decks = 0;
90107

91-
try (Stream<Path> walk = Files.walk(generatedPdfs)) {
92-
List<Path> pdfs = walk
93-
.filter(p -> p.toString().endsWith(".pdf"))
94-
.sorted()
95-
.toList();
96-
for (Path pdf : pdfs) {
97-
Path rel = generatedPdfs.relativize(pdf);
98-
String[] parts = rel.toString().replace('\\', '/').split("/");
99-
if (parts.length < 2) {
100-
continue;
101-
}
102-
String category;
103-
String group;
104-
String fileName = parts[parts.length - 1];
105-
String basename = fileName.substring(0, fileName.length() - 4);
106-
if (parts.length == 2) {
107-
category = parts[0];
108-
group = "default";
109-
} else {
110-
category = parts[0];
111-
group = parts[1];
112-
}
113-
114-
Path pdfTarget = showcaseRoot.resolve("pdf").resolve(category).resolve(group).resolve(fileName);
115-
Path pngTarget = showcaseRoot.resolve("screenshots").resolve(category).resolve(group)
116-
.resolve(basename + ".png");
117-
Files.createDirectories(pdfTarget.getParent());
118-
Files.createDirectories(pngTarget.getParent());
108+
for (Path pdf : pdfs) {
109+
Path rel = generatedPdfs.relativize(pdf);
110+
String[] parts = rel.toString().replace('\\', '/').split("/");
111+
if (parts.length < 2) {
112+
continue;
113+
}
114+
String category;
115+
String group;
116+
String fileName = parts[parts.length - 1];
117+
String basename = fileName.substring(0, fileName.length() - 4);
118+
if (parts.length == 2) {
119+
category = parts[0];
120+
group = "default";
121+
} else {
122+
category = parts[0];
123+
group = parts[1];
124+
}
119125

120-
Files.copy(pdf, pdfTarget, StandardCopyOption.REPLACE_EXISTING);
121-
copied++;
122-
renderPreview(pdf, pngTarget);
123-
rendered++;
126+
Path pdfTarget = showcaseRoot.resolve("pdf").resolve(category).resolve(group).resolve(fileName);
127+
Path pngTarget = showcaseRoot.resolve("screenshots").resolve(category).resolve(group)
128+
.resolve(basename + ".png");
129+
Files.createDirectories(pdfTarget.getParent());
130+
Files.createDirectories(pngTarget.getParent());
124131

125-
// A twin flagship renders the same composition to a deck beside its
126-
// PDF. The deck is published as a second download on the same card:
127-
// it shares the PDF's preview by construction — both backends draw
128-
// the same resolved layout — and POI has no page rasterizer to make
129-
// a separate one with.
130-
String pptxUrl = null;
131-
Path pptxSource = pdf.resolveSibling(basename + ".pptx");
132-
if (Files.isRegularFile(pptxSource)) {
133-
Path pptxTarget = showcaseRoot.resolve("pptx").resolve(category).resolve(group)
134-
.resolve(basename + ".pptx");
135-
Files.createDirectories(pptxTarget.getParent());
136-
Files.copy(pptxSource, pptxTarget, StandardCopyOption.REPLACE_EXISTING);
137-
copied++;
138-
decks++;
139-
pptxUrl = relativeUrl(showcaseRoot, pptxTarget, siteRoot);
140-
}
132+
Files.copy(pdf, pdfTarget, StandardCopyOption.REPLACE_EXISTING);
133+
copied++;
134+
renderPreview(pdf, pngTarget);
135+
rendered++;
141136

142-
ShowcaseMetadata.Entry meta = ShowcaseMetadata.lookup(basename, category, group);
143-
ManifestEntry entry = new ManifestEntry(
144-
basename,
145-
meta.title(),
146-
meta.description(),
147-
meta.tags(),
148-
relativeUrl(showcaseRoot, pdfTarget, siteRoot),
149-
pptxUrl,
150-
relativeUrl(showcaseRoot, pngTarget, siteRoot),
151-
meta.codeUrl());
152-
tree.computeIfAbsent(category, c -> new TreeMap<>())
153-
.computeIfAbsent(group, g -> new ArrayList<>())
154-
.add(entry);
137+
// A twin flagship renders the same composition to a deck beside its
138+
// PDF. The deck is published as a second download on the same card:
139+
// it shares the PDF's preview by construction — both backends draw
140+
// the same resolved layout — and POI has no page rasterizer to make
141+
// a separate one with.
142+
String pptxUrl = null;
143+
Path pptxSource = pdf.resolveSibling(basename + ".pptx");
144+
if (Files.isRegularFile(pptxSource)) {
145+
Path pptxTarget = showcaseRoot.resolve("pptx").resolve(category).resolve(group)
146+
.resolve(basename + ".pptx");
147+
Files.createDirectories(pptxTarget.getParent());
148+
Files.copy(pptxSource, pptxTarget, StandardCopyOption.REPLACE_EXISTING);
149+
copied++;
150+
decks++;
151+
pptxUrl = relativeUrl(showcaseRoot, pptxTarget, siteRoot);
155152
}
153+
154+
ShowcaseMetadata.Entry meta = ShowcaseMetadata.lookup(basename, category, group);
155+
ManifestEntry entry = new ManifestEntry(
156+
basename,
157+
meta.title(),
158+
meta.description(),
159+
meta.tags(),
160+
relativeUrl(showcaseRoot, pdfTarget, siteRoot),
161+
pptxUrl,
162+
relativeUrl(showcaseRoot, pngTarget, siteRoot),
163+
meta.codeUrl());
164+
tree.computeIfAbsent(category, c -> new TreeMap<>())
165+
.computeIfAbsent(group, g -> new ArrayList<>())
166+
.add(entry);
156167
}
157168

158169
for (Map<String, List<ManifestEntry>> groups : tree.values()) {

‎examples/src/test/java/com/demcha/examples/GenerateAllExamplesSmokeTest.java‎

Lines changed: 74 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -2,38 +2,98 @@
22

33
import static org.assertj.core.api.Assertions.assertThat;
44

5+
import org.junit.jupiter.api.BeforeAll;
56
import org.junit.jupiter.api.Test;
67

8+
import java.io.IOException;
9+
import java.nio.file.Files;
10+
import java.nio.file.Path;
11+
import java.util.List;
12+
import java.util.Set;
13+
import java.util.TreeSet;
14+
import java.util.stream.Collectors;
15+
import java.util.stream.Stream;
16+
717
/**
818
* CI-coverage smoke test: runs {@link GenerateAllExamples#main(String[])}
9-
* end-to-end and verifies it completes without exceptions.
19+
* end-to-end and then asserts against what it actually wrote.
1020
*
1121
* <p>Each individual example invokes the full canonical pipeline
1222
* ({@code DocumentSession} -> {@code DocumentDsl} -> layout compile ->
13-
* PDF render) and writes a PDF to {@code examples/target/generated-pdfs}.
23+
* render) and writes its output to {@code examples/target/generated-pdfs}.
1424
* If any example breaks at runtime, this test fails — closing the
1525
* audit's L4 gap that flagged {@code GenerateAllExamples} as never
1626
* exercised by CI.</p>
1727
*
28+
* <p>Completing without throwing is necessary but not sufficient, so the
29+
* assertions below read the output tree rather than the exit path.</p>
30+
*
1831
* <p>Lives under the sibling {@code examples/} Maven module so the
1932
* smoke runner's JUnit dependency does not pollute the main project's
2033
* test classpath.</p>
2134
*/
2235
class GenerateAllExamplesSmokeTest {
2336

24-
@Test
25-
void generateAllExamplesCompletesWithoutErrors() throws Exception {
26-
// The main method prints "Generated: <path>" lines for each
27-
// example. We assert that it returns normally — any
28-
// RuntimeException, IOException, or template wiring break
29-
// surfaces here as a CI failure.
37+
private static final Path GENERATED_ROOT = Path.of("target", "generated-pdfs");
38+
39+
@BeforeAll
40+
static void generateEveryExample() throws Exception {
3041
GenerateAllExamples.main(new String[0]);
42+
}
43+
44+
@Test
45+
void generateAllExamplesWritesPdfs() {
46+
assertThat(GENERATED_ROOT)
47+
.describedAs("GenerateAllExamples must write its output under %s", GENERATED_ROOT)
48+
.isDirectoryRecursivelyContaining("glob:**/*.pdf");
49+
}
50+
51+
/**
52+
* Every generated deck needs a PDF of the same name beside it.
53+
*
54+
* <p>{@code ShowcaseSync} builds one card per PDF and copies a {@code .pptx} only
55+
* as the same-named companion of one. A deck generated without its PDF twin is
56+
* therefore invisible to the published site: it lands in {@code target}, never
57+
* reaches {@code web/showcase}, and its hand-written {@code ShowcaseMetadata} entry
58+
* goes silently unused. That is exactly what happened to the Maven banner, whose
59+
* example exposes both {@code generate()} and {@code generatePdf()} while only the
60+
* first was wired into the runner.</p>
61+
*/
62+
@Test
63+
void everyGeneratedDeckHasAPdfTwin() throws IOException {
64+
Set<String> orphanDecks = new TreeSet<>();
65+
try (Stream<Path> walk = Files.walk(GENERATED_ROOT)) {
66+
List<Path> decks = walk
67+
.filter(path -> path.toString().endsWith(".pptx"))
68+
.toList();
69+
for (Path deck : decks) {
70+
String fileName = deck.getFileName().toString();
71+
String stem = fileName.substring(0, fileName.length() - ".pptx".length());
72+
if (!Files.isRegularFile(deck.resolveSibling(stem + ".pdf"))) {
73+
orphanDecks.add(GENERATED_ROOT.relativize(deck).toString().replace('\\', '/'));
74+
}
75+
}
76+
}
77+
78+
assertThat(orphanDecks)
79+
.describedAs("a .pptx with no same-named .pdf beside it never reaches the "
80+
+ "published showcase — ShowcaseSync keys every card on a PDF and "
81+
+ "copies a deck only as that PDF's companion")
82+
.isEmpty();
83+
}
84+
85+
/** Guards against a runner that silently narrows to one corner of the catalogue. */
86+
@Test
87+
void generatedCatalogueCoversEveryCategory() throws IOException {
88+
try (Stream<Path> walk = Files.walk(GENERATED_ROOT)) {
89+
Set<String> categories = walk
90+
.filter(path -> path.toString().endsWith(".pdf"))
91+
.map(path -> GENERATED_ROOT.relativize(path).getName(0).toString())
92+
.collect(Collectors.toCollection(TreeSet::new));
3193

32-
// Sanity: at least one PDF should have been written. Each
33-
// example resolves its own output path inside generate(), so we
34-
// do not assert a fixed file list — that would couple this
35-
// smoke test to the example list. Reaching this assertion at
36-
// all means main() completed.
37-
assertThat(true).isTrue();
94+
assertThat(categories)
95+
.describedAs("the runner should cover the whole catalogue, not one corner")
96+
.contains("templates", "features", "flagships");
97+
}
3898
}
3999
}

‎web/examples.json‎

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -946,6 +946,16 @@
946946
"screenshot": "showcase/screenshots/flagships/default/master-showcase.png",
947947
"code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/flagships/MasterShowcaseExample.java"
948948
},
949+
{
950+
"id": "maven-banner",
951+
"title": "Maven Central Banner",
952+
"description": "A single-slide brand banner emitted through the PPTX backend — gradient, rounded panels, native paths and text frames arriving in PowerPoint as an editable copy of the rendered page.",
953+
"tags": ["flagship", "showcase", "pptx"],
954+
"pdf": "showcase/pdf/flagships/default/maven-banner.pdf",
955+
"pptx": "showcase/pptx/flagships/default/maven-banner.pptx",
956+
"screenshot": "showcase/screenshots/flagships/default/maven-banner.png",
957+
"code": "https://github.com/DemchaAV/GraphCompose/blob/develop/examples/src/main/java/com/demcha/examples/flagships/MavenBannerPptxExample.java"
958+
},
949959
{
950960
"id": "module-first-profile",
951961
"title": "Module-First Authoring",
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.
0 Bytes
Binary file not shown.

0 commit comments

Comments
 (0)