From fcea3e0584a4bdfe62989841530438e71b231ee9 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 2 Aug 2026 18:20:48 +0100 Subject: [PATCH 1/5] feat(examples): hold the committed previews to the code that renders them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit README and the showcase site read files under assets/readme rather than rendering anything, and nothing held those files to the catalogue. A change to an example, a theme or the engine moved the render while the committed file stayed where it was, and the first anybody knew was a release publishing it — which is how a deck went two releases without the bold weights its styles asked for. Twenty-three of the sixty-seven previews were behind the code at the point this was written; they are re-rendered here. All twenty-two of the PDFs among them rasterise to the same pixels as before, so nothing visible was carrying the drift, and nothing visible is buried by refreshing them. Every preview is now compared against a fresh render on each build. The comparison is exact and drops only what a machine writes rather than an author: a PDF's clock-seeded identifier, a package's zip and creation stamps, the platform's line separator after an XML declaration, the box a freeform's path is normalised against, and one named watermark whose antialiasing differs between machines. Each was measured by rendering the catalogue on both platforms at one version rather than argued from — 99 of 104 documents came out byte-identical under it — so the gate needs no tolerance, and every exemption is a line somebody had to write. A committed preview is rendered at the released version while a working tree is already on the next one, so the version the previews carry is recorded next to them and the module's tests render at it. Without that the coordinate pill alone would differ and the comparison would fail on every preview that has one. The README hero is checked too. It is not part of the catalogue — the release script renders it alone — which is exactly why it drifts unnoticed between releases. Editing a preview by hand now runs the job that checks it. --- .github/workflows/ci.yml | 5 + CHANGELOG.md | 13 + assets/readme/examples/block-align.pdf | Bin 1641 -> 1682 bytes assets/readme/examples/book-template.pdf | Bin 35426 -> 35430 bytes assets/readme/examples/container-bookmark.pdf | Bin 1382 -> 1386 bytes assets/readme/examples/content-bleed.pdf | Bin 1271 -> 1276 bytes assets/readme/examples/debug-overlay.pdf | Bin 1787 -> 1791 bytes assets/readme/examples/in-pdf-navigation.pdf | Bin 2482 -> 2485 bytes assets/readme/examples/line-cap.pdf | Bin 1148 -> 1153 bytes assets/readme/examples/line-fill.pdf | Bin 1161 -> 1166 bytes .../readme/examples/module-first-profile.pdf | Bin 1409 -> 1408 bytes .../examples/multi-section-document.pdf | Bin 2832 -> 2836 bytes assets/readme/examples/page-numbering.pdf | Bin 2549 -> 2553 bytes assets/readme/examples/page-reference.pdf | Bin 1291 -> 1295 bytes assets/readme/examples/per-page-margin.pdf | Bin 1466 -> 1470 bytes assets/readme/examples/row-columns.pdf | Bin 1152 -> 1157 bytes assets/readme/examples/row-flex.pdf | Bin 1382 -> 1387 bytes assets/readme/examples/row-vertical-align.pdf | Bin 1331 -> 1336 bytes assets/readme/examples/svg-icon-gallery.pdf | Bin 71631 -> 71879 bytes assets/readme/examples/table-advanced.pdf | Bin 5831 -> 5809 bytes assets/readme/examples/table-of-contents.pdf | Bin 2507 -> 2509 bytes assets/readme/examples/transforms.pdf | Bin 2656 -> 2642 bytes assets/readme/examples/vector-path.pdf | Bin 2848 -> 2869 bytes assets/readme/examples/viewer-preferences.pdf | Bin 1377 -> 1380 bytes .../examples/word-export-companion.docx | Bin 7991 -> 7952 bytes examples/pom.xml | 25 + .../examples/CommittedAssetDriftTest.java | 184 +++++++ .../examples/PptxCanonicalContentTest.java | 513 ------------------ .../demcha/examples/support/AssetContent.java | 267 +++++++++ .../examples/support/AssetContentTest.java | 292 ++++++++++ 30 files changed, 786 insertions(+), 513 deletions(-) create mode 100644 examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java delete mode 100644 examples/src/test/java/com/demcha/examples/PptxCanonicalContentTest.java create mode 100644 examples/src/test/java/com/demcha/examples/support/AssetContent.java create mode 100644 examples/src/test/java/com/demcha/examples/support/AssetContentTest.java diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 4b8b48004..3459774c0 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -91,6 +91,11 @@ jobs: # The examples module renders figures from the committed perf # baseline, so a baseline-only refresh changes generated output. - 'baselines/**' + # The committed previews are compared against a fresh render + # (CommittedAssetDriftTest), so editing one is a change the + # examples job has to check — otherwise a hand-edited or + # hand-reverted preview lands with nothing looking at it. + - 'assets/readme/**' - '.mvn/**' - 'mvnw' - 'mvnw.cmd' diff --git a/CHANGELOG.md b/CHANGELOG.md index 347361daa..8a9ff7c3d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -7,6 +7,19 @@ follow semantic versioning; release dates are ISO 8601. ### Build +- **A committed preview cannot fall behind the code that renders it.** README and + the showcase site read files under `assets/readme/**` rather than rendering + anything, and nothing held those files to the catalogue: a change to an example, + a theme or the engine moved the render while the committed file stayed put, and + the first anybody knew was a release publishing it. Twenty-three of the + sixty-seven were behind — invisibly, as it turned out, since all of them + rasterise to the same pixels — and they are re-rendered here. Every one is now + compared against a fresh render on each build, exactly: the comparison drops + only what a machine writes rather than an author (a PDF's clock-seeded `/ID`, an + OOXML package's zip and creation stamps, the platform's line separator, and one + named watermark whose antialiasing differs between machines), all of it measured + by rendering the catalogue on both platforms rather than assumed. Editing a + preview by hand now runs the job that checks it. - **The CI guard job runs every guard it names.** It selected eight test classes while scoping the reactor to `graph-compose-core`: two had been deleted months earlier and two live in `graph-compose-qa` and `graph-compose-render-pdf`, so diff --git a/assets/readme/examples/block-align.pdf b/assets/readme/examples/block-align.pdf index 5a3ec41811b9bd9047732ed19530a7f4af08914d..266956224bc4932b88f0a6c1fe5e9fb572709c1b 100644 GIT binary patch delta 1130 zcmaFKGl_RXBD1A|<>WuC%Jo~ryz`~a1@?Uo|B%7V=gZfwzkowv^=cK@zy;eH;%WlJ zS4X_`a#B*w`~2EiMRn4|tKAJh7GC9PzrSan;&q$YpKp|3r@it2_OhcrtUlto zv&AFU#B&T>xe5x)mv4UYqSMQAcFdlKr?2im{OD@l*W3rQ-zGhHyy4D%FT3)Ztq*@M zZ`42j*7*N#`+XAAKI=b@`|vaAGlZAG1pKmA6X_we4Y^O1~ z=-*MTZn40doxIm>o|Ec+kUKGvY0{S9O^aqYTGjiComzN#2|wSQ3b%#y#vID9|Xe*L~&^WQ&z zsEI}Wx#yc+-+z5=^(S66j`y;n(uV(PTGjOjA)z0yZ(hX{u+>`5@)wlVu+UN=uo>(S!aZ9~K6_bU@Mz52lQF6HgLZ<5phcHG6eX_QN@hO)DEX z4xM(H@M4R{6i41Q8M9fq>aWf8(N75c=2z--^5P7~hZhA)&$tP-PMhhjC889%I9N{e zJ^ORa@}pPF2n(e0hO?5C&4>#$4*2fJpUcVaICmM30`vA*f? zG2}$-_O-qz-fYkfyDB^P(z8`18azj?nK%5ixO*~lVn#ggnp;;`w62}@(O(c)|83s+ z8y?DUjeDi87)md+l<85K?W25KYVQKU1r4YB7ysY+CUEh=a|%+-FE53aaI)v<{jFdP zZhdLAz5L~)?LU*wPxItEr@rOaCV`1OCD$CqbvYf6iL<|sH~*89G~XuR&80w#k4Ik| zaI6Y9XtVc`_>{ivNB)=p6Z;HS$@0!Vr5+_T=S0DT47-hyGB=rb{^Z-S+tg&TBb)GM zMivvsdJ_XK{h<8(5(PsA1BD5G8V@nrjQ%e&^ zM*|}xLqkIg3pZC+BWGh*Q)e?rV>bgg3u6}xLvu%C6K6*^3v)XKVvUf*X~yKUtV&FV k29rOqdh&E@F)Oc@W@a$@!XyZEx;&?;0hg+(tG^o;0DrpwT>t<8 delta 1102 zcmbQl`;uouBD00L@nl6d<@&8*-ucp&Jo{dUznHV>Jl?tXvtDDB4A|0jNK__yP(&h~b1%ik)yX8usHlm9qDr2ePC zV?lkba;AVtc0pBp+l9~7r5{OVHaYp{l`a>08rhUDrT<;g*{6{x(*>>6etlRuP zU16Tz*dw}WtK#WTS8lgFn8*~ptoU-tx;bfkp9{~p*VMPq<-@1rItp5g4WAZloSDD> z4ExTR@^ar^xdd+szF4`&*Kql)H9b$|-?biGp=hi8azgc|;!?r&PiEiU|N8yzw~2Op zyjsKROE2I2Tc7&+-_CZ4g96I#hfjVsR&jsR5X^bgl4Y^ehdZ-;7k`N={BVX}Iz)kC zl7m}eD3j-M>4oBdrf#$~^RmnB_-Y}sbDe*xJ}QK%cOS7?c!zBs$8mp#e!Wo5-F`)t^qw%xi&($+_ue}*O5Ug6eOvj)VBs`=hmT(@&g~17T5Pav+pLJ62k$zpcdlA3 z-MNx&s^a3_wQJW$^uBvBg}>h`d+K*Si8L0M0LOGLmF1z2c#a)dwas%u(%chYc;<<` zk2yX^hF>UTnM`1N(vrZ)*cnOnNlQ;o!B%>%}XgRDN0Su1+nszvNo%*m@?KIn{nv}<>!|u7%CVj1aawmx+p~3 zm>OFen;W~DS{NC)x;i=U_J zx;eTzIXRj-yI8n5+bIxhgd|QgCVymAVzitr%I3+zt<21z^^3)vj&@ z3zP7`d+|0W&wN)^&dgcbRU#@Om?RQ#ghegTNiukb8!Knp1f|1kO7*if+z)REFEyF= zIC$tvDblbAQBkB09w;reblUH|E)?wFmNdta(dn=EYkmvisa=r@k%?4I1Je(^bD zakC6}#1Wxs8=FGcB+ABK_kViKBQNg6+WQ+eHu~lyN?YG3{jK-s+lAua`cGh($%o}MO`adVpS(-=Oc#={Of7@Af&D^5`1VA<+n!?}-hmC#XF z0hw8=HQwvbOL_Kt(nOPuMXtLzdpHceJPcYhr%X&@+q^>IuuM^iVZQANsgoBq-dvUO zeexqjueL4Do!M8N*|Ku#>Bj}}1y#Ds^^2aGeVMj7;@r6>f7HKhTiqXC^woLY{`P$x zOtK{#TT<5~h-OC|XEVE4{5U4B@9DLvcmDB8N8dQcSGKXeR{Zx> s;?Um0<{9 diff --git a/assets/readme/examples/container-bookmark.pdf b/assets/readme/examples/container-bookmark.pdf index 293e7d3456bf9b28efae00afe9a2561a3f09aa90..599536992492f6429061e8faefd04f9bb1812efb 100644 GIT binary patch delta 223 zcmaFH^@?kQ8cV&YA(wtoetwC9p@M-z5SPBEi$b)Gp^=-3rGb%~rJ19drGcw~v!Sc8 zg{7mLv7w2pfs3h=nT45!v4Mf5rKzK{iJ^hBk%5V$sjH!r1yGN%vyro%0dwE&d|U9JZ9krA76j;=B-{8ujBb(T5M LhpBb46RQRQp^-a; diff --git a/assets/readme/examples/content-bleed.pdf b/assets/readme/examples/content-bleed.pdf index c13769579afca554cd4083ab39d4df9dd0e92249..02cc6281e1f91503ae48506bce1efd84a9f21c81 100644 GIT binary patch delta 185 zcmey)`G<4E5$1Xm11|lb{QMFHLj?ncATE7R7lmjW3ll>Fb5kcvOA9v>GiMhQ7iU*9 zV^?Q$14DC5XG2prGZSMMOJfsfH)A6s7iVWj7Xv2~Gh+)=7biaP~oLtZ5$7Qf-T{;7Utu51|$(PwQ02ezhTL1t6 delta 182 zcmey*`GfPK11!9em#A(Ll ZXf|a=gUR)5erya3#u7}rldrRB003i~E06#H diff --git a/assets/readme/examples/in-pdf-navigation.pdf b/assets/readme/examples/in-pdf-navigation.pdf index 5a9363dd228085bf45207354a39e22927dc7aff1..07ad3872030d579cc9e594bfd5742cd853ba2392 100644 GIT binary patch delta 268 zcmdlayj6IE2S>f75tn{YetwC9p@M-z5SPBEi$b)Gxw(m%qmiYfv8925ftiVkv5B#( zg`=^#i<_mHxv7zvv!l6zvyrirrIWFxnWdSFtE+*FfvKUHiIb(dp}B>todU5&Na8eO z@&gWKMw7`boQ4tZLQMf83>&^PifVW5O^)61O=jNB*_jb3YJwlV16{?|hbeh~%$y&5 zVWD^bBs=D|9tq=xNvC>dS#nQX6R|n*be}DI-yDm}1rJa4TJz6a`NK`?{zvv3-Q2$> MD{=@=p35l;0FA3s9{>OV delta 265 zcmdlgyh(V22S>eyC6|6setwC9p@M-z5SPBEi$b)Gg^P=eqqB>ltGR)hnW3wtk+F%f ztBIwftFeo-lc}kTqmiYnle3{KP#CDn$l2W7#L~^kz}(2y$j#i<*xXKmSR*8HnlbqS zhcct_WEM`t(Bpzl0U`_sz6-htJAK6z5bMKw3$RRv= H0jDSc-`7)X diff --git a/assets/readme/examples/line-cap.pdf b/assets/readme/examples/line-cap.pdf index 1c0a99dfc07bddb63b0697be9faa271f503d6f3f..c6f375030a4d4a00b7e02fb820b70885d55b9674 100644 GIT binary patch delta 190 zcmeyv(a5>Mleyl+fJ;9pKfgr5P{BYUh)dtoMIqY8)yUGw(!|8f*~rnz&Dh1rz{1ti z#m(8w!oa}A%+<}sz}VEu#MIT)$->Rd)!f3-+|AU@#L3d!*wNI|#MIKjPJvh>BypNC i`606slcB+678V~)<;{}J3`S9mZzeM`2~W;o5di=X7%a&E delta 185 zcmZqV{KK)qleym5j7vW#Kfgr5P{BYUh)dtoMIqY8$Z z#lqFn)X>Pp(b3Y()y&z_(9qJr)!5n1$;HUc+`z!l(aglj(b>Sv)z#G8PJvh>BypNC d`606sqvd2)79UmyZAr#WlNp(WC+D(=002H~D_;Nr diff --git a/assets/readme/examples/line-fill.pdf b/assets/readme/examples/line-fill.pdf index a5274664425eada7cdf2e2adae8a702b51aba945..7b4e7a8845d9e3a32f060a6806a7fc6f5437a81f 100644 GIT binary patch delta 186 zcmeC=?Bm=J#$0b=z@;CQpI@S2s9>NF#HH`)q7ZH4V&URs>geQXWNvO~YGC5*Y+!8Q zVrF1zVPNWFXkhMQVc_CyXliO|>1b?WWN74UZfs`kY--`;YT)MRXl7z!r$DR`k~qzn d{Ek_P$NF#HH`)q7ZFk?C9ofWNK(&WNGed= zW@heeW@%w&YT#sPZ0uz2>TF;HlyEd~GBP%FaddUHbagQ{c66~ccX72-Al3*;oMudZ X$E?I?Ia!Fshm}Fwm~sE)bQX00Wtl4S diff --git a/assets/readme/examples/module-first-profile.pdf b/assets/readme/examples/module-first-profile.pdf index 4cc7888ca44f14d7a33bfca754c388ca04c4028c..6a0d9f05e24b3982e2716ca318b996bdfac2256a 100644 GIT binary patch delta 903 zcmZqVZs4BaCuXW(ppc)G#bsl|rSFrPmtK;gU}j`CF-5(8j)6ei`^v7Jcf2~8?UX~O zZh9lM^_A{Y*FNz0xVi<6RlcrZnBP z+jUad!{QC!#k0ruO>eE;5$*hNvBaUL-5I6tZ#942mm?i~@NvNPV}Jj!?4F*nySvUM z|M>!~r!Lt`pKMx?9lWRhRvP~{%W1t^`S@}TD|WYUYh1KxS!4dQy9eI>4X9wj?eB)T*o;RD@9GOCyi-WZ|_>|lYel>qmXagUa+U1>|s;% z?2@{aDpj`4!ZcMdrc!tHsq-vP<{ar*D9-w44wuyD=$RUt!cxYkKIjSWoLWEUSkuag zz?qLc9%^Y7&(!|+^#2qc#f@>hb|!lX7N1jH_DEvNLQZMTCt-;}D-X>UnPCwqGx>Ru zck-PlBJNU#S1g3R&q|)l{h%A}(!gz9oi$S#YLcsMweeK#CJUKcZi_R}kSA4_9T>M4P;WB657iH>eLxCrj(Ggi!OOBQLoJ@2vbU+MiZPgppA-n{9O_IST!-xZl%R}2^Cd=xx( ztL|XY49i_XT25atE#AGWc$sx-?%l2T4w;H9%UphG2A9mMPcMA>ZJ%(xDzgvy_A9{Z z{^4i!AAg4&Ki_=3^xom65&!ivG%XUR&)eFx(w*n7kD&KFfo*#PQ~mkB9Zn1`=(wA> zT6^Nz8i{np>ZW^BLSLVH#?kol!Y(PhQ~y}*iQRB|uu=N?wcOi>Kkr<;a7O)wm1=*= z^_h0ue^RieckZ7b&Ik2*Cl*}e!|u7y{F85SPBEi$b)Gv!%14v8$V#rJ0+fsj;E4lcSlN zlas53v!S7Zp`n?JrLl#Rv7?2lk-4k6siUj8tAV+brID$*iJOt7iLsG|odU5&Na8eO z@)H&%CPRbC?5y@;M@~FoPcmjUl2xA5+>A?A)z#mP3jiR) BfrJ16 delta 908 zcmZqRZseZeCuXi-ppc)G#bsl|rSFrPmtK;gU}kJMnV(g;zIV#)yu}6rZSO0)cE0gi z%WbE;HZ03yMcGzyTbI4%os%yVdE{nn&EKEwapkR8C6riUXiJ$ynl?=v#rak`&Im5 zZsi)CoC43>*?hP5S%0}}DZ%p~=l$U~oi98V3r||)@^9iZ+xqm^&Z$=1$3%Z`+U$DW z+FLD!(=%ww)N_-!Zacb9vpeBRab#$nlEQNpHzQl^2j4?3%!ohf;yLrejQYT~@|h>5 zJ~0vVIofqf#GNZVaOUZlkMi?_H?3YU;`i!pRfHpOqr4zJUJ(qIj2PHgm zDfF3lTC}(Dey~jDk$}TKI;QCs+mCFIG1OGsAml4#_WekYUy6iOu$Ewe!5gh5hnDl5 z`pE5WV3xo;Z_XU2B@D7@FAGHs4oB6OO;T_cY5d-rB)nmxLg&Ad1^ZfJlM?#wv1V8u z;;l<*E>0>4=V8cVn4rOQy--c%Y(bAw!R_LjMfHcLPoAToXm2(9M9vTM!xLvYO}A2; za@Oup#5?~8K4JH8FV_|Gs!PtVn``-I^ESIJ_YSd&EXzE8Ng`YBRnc>w_S~m3ugWfm z*MCcjwz_}t)4%T#xBB-UFTHnoWrY2;9U^l!oX>qD%W%x^lDyihs81~$e!kVJ-my8K z?{|x{Dtk_c>Z4iak9AV`FU;JO6?^lILPB12_~GcAZx7yP+Rb%GzHO)L?XnLQZ>m?k zw>J$D{ug?jX-DL<4V!yqV}H9}iNDEHQ_RbinwL^MS)GM9a68#o!8I+<9QIU75g znOGPao12)qnY*|+J6kxJm^iu^m>8LwS-6>+8yZ`hI=MNy8JoG7o0+(}y1BU8DG+Of zBu+CX-)B)`w4BV$YA?)k8w+m0sX650uFyvBIb@g}S F0svl4cnkml diff --git a/assets/readme/examples/multi-section-document.pdf b/assets/readme/examples/multi-section-document.pdf index 14503b3e564e3ba62ef7ccb6275d01e4a0dc7f53..819e4af278591be56e20659be816ff4066a9e283 100644 GIT binary patch delta 265 zcmbOrHbrd1GR}GvF8!eV{1OF21p|d3E`3iIg=iZS17l-LM^|SjWDXG>QTBUd96Q!_&YS3@I5V^>!fLkj~7GdBZAQ$tHbb5}DHHv?xo1)`0Rz-7i{ zTP|frlgWu(hEcb99UMej9?pz?U3a~qtHjww{FlH*!HXQla{JorjTr0}H6G5;@Y~}e z)N%4#gpjA|v diff --git a/assets/readme/examples/page-numbering.pdf b/assets/readme/examples/page-numbering.pdf index 3eeac42f751bae65bf60d106c8c1a6d76107908a..0dbf3dfd47e1234609b22efdded4810d203a2991 100644 GIT binary patch delta 271 zcmew={8M;C2S>d*mwr%weu;vif`LL1m%gWqLbQ#mxr>vLn~AfDnX`q7g^`i7sjH)< zxrvL3i?OS-g^`)7k(slTg`ts~n}wO1xuv6_rK6ddi>sNlqpOL5tBIqX0?|fD;4)*f zJf||F@nmOC!`N8f1_KG_i)G2le;zaNs=PSB#=}1O*PE7vBry@}d~zFCI)Z z5K)ga;uKPyuEJ{Ae8XYhH~o|BC5!i56aCBgi~9`U>sgM`5eFdzmwr%weu;vif`LL1m%gWqLbQ#Gk-3|ji@BMxrKOpxvzwcRk%6nZ zsiTFlv$2tdtDB>Xv9Xh@rLmEjp{uEZvyr2tsj;Pnv9XJjiHm`op@pfP0?|fD;4)*f zJf||F(PU>%!$?owh5!-gi{Jdu)W|GoWRbmbfJ2Id&%n7!!DB&wp_sXDL;r~a3Ercf z(-i_Ii3EAM>L{2^SpGNr6XRW#^`*}HoBuH#PP}WB)vY}v?X_RkNh5>1KNu%Qa~n;* H$f*GU1>a8+ diff --git a/assets/readme/examples/page-reference.pdf b/assets/readme/examples/page-reference.pdf index ed37ecd0452289e589fea280cebf6cda3deab80e..f1d61eb9fb98e2687b989498544834331e477709 100644 GIT binary patch delta 186 zcmeC?>gU>UhPmF$iUIj(bUn=PJvh>BypNC eIfX@;(PVN5OBmOI6MLE&8IshQ4@`c-q7DG5UM`sc delta 182 zcmeC@>gL*ThPmFvoJ&6_Kfgr5P{BYUh)dtoMIqY8%*DXS(b>t_(8STe$im#f)xyZx z(8c diff --git a/assets/readme/examples/per-page-margin.pdf b/assets/readme/examples/per-page-margin.pdf index f0cef8f46b80215bbd204f249dc59f9d8635a317..53af616397fef289026fbdfc56cf8ccd087f16db 100644 GIT binary patch delta 186 zcmdnRy^niC6-<A(wtoetwC9p@M-z5SPBEi$b)Gi=&~1g|U&5iL;Y|tD~ENsil#D zrKP2XiJOtLv$?Ucfw_y3sgs+bnW>3^xuv;_o1vSfp|J@NIk~z52|ER1jgZ7?#$+K@ bWk!?9W~^ac2`Qe*3=AQhOtU6WV$}cuI>Re9 delta 182 zcmdnTy^DK86-&K|IhTG=etwC9p@M-z5SPBEi$b)GlasNNiGi_^fti7Ysi~oXk)@-v ztDC8bp_7G^xvPbNp^2f9v!$W2p@FNZi?O8>P{`TI$<)!s)!fk$sMAh?SR*8HnlV|3 XRhiLvvKea_8v}z^A(Q^(X{;Imim)l* diff --git a/assets/readme/examples/row-columns.pdf b/assets/readme/examples/row-columns.pdf index cdbbb871e093b2d559ed0421de22465cff7d3894..ec510c03dce27c36473c1d0027779b23c4beaac9 100644 GIT binary patch delta 186 zcmZqRY~|eG%Uo|_z@;CQpI@S2s9>NF#HH`)q7ZFkNF#HH`)q7ZFkZeVF_YG7t!YGLMRZf;?2Y;NRc zU|?Wj>gH-};%MP&;$~`WWa(_`WNdEi=Im_X=;-7M6m>MQaJ6)EF|e>xAl3*;oMudZ X%B;j_Ihm8ihm}EFo^k8sI2LsP{&*?Y diff --git a/assets/readme/examples/row-flex.pdf b/assets/readme/examples/row-flex.pdf index 7728bb760a2c04a6e82fdba4e8a8f0417dd304ca..e55b0ce8629a01651935ea5740ab917b11f670b0 100644 GIT binary patch delta 186 zcmaFH^_pvg5lg*^0hfMIetwC9p@M-z5SPBEi$b)GtDB*-v89E%g{hmHv#YD2p{t>> zg}Jerk&&Z`k)eg7v5AGVqlt@~k(q&usgtFfiJPITk)xxlxv9B}xq-QfodU5&Na8eO e@+lT2CPRbCZ&`de-NY){7|fS4?VoJNssR9(!z~^F delta 181 zcmaFO^^9wS5lg+X8JB)getwC9p@M-z5SPBEi$b)Gv89EJfvdBnqlK%HfrYWFp`)Rp zo3V+7qq(!IlcS-NrK6jLnWK?`siTvNql=@dvyr8Nk%ft+xtXE4o0El;odU5&Na8eO Z@+lT2M$5_XSbSI+bY3&fp6tx30RYn7EOr0@ diff --git a/assets/readme/examples/row-vertical-align.pdf b/assets/readme/examples/row-vertical-align.pdf index ff57810afa6dcb9f4265ad5dec70c5204ddd3df2..048315f19426423081277cfc1528b06e19d7e0e4 100644 GIT binary patch delta 172 zcmdnYwS#NJe`Y2VgUO66!u1A*mX6Mb#wI3?PDYMS7EUJ4j*b>at|pdF<|YP)W`=HN zZsx{rZf?#-=5EeTCg#Rwrmlug2Igkw&Zg$B7Up&eHk25_0y1OrbQUEhLxah?SbR9$ Ro|*G7SO_pjPUdFS001G~D7^px delta 167 zcmdnNwV7+fe`Y3Qv&oDs!u95kjuz&I21dp%=EkOO#?Ed=rUsU-rcP#-&W5gLZWfLv zu1>}-Zbl|%1{RjiMo#8Vu9j{_=EfGr=B`dao}GdXB}TA-%$Pi#MTyaJ@@^I%RtBAR Krp(C#tQr7a=qJhm diff --git a/assets/readme/examples/svg-icon-gallery.pdf b/assets/readme/examples/svg-icon-gallery.pdf index 13671a81c42198be73d7cea798fafa894b7bd291..2d038bc3ed64e7391ff83a0a381838f31d3a25a5 100644 GIT binary patch delta 71622 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 delta 71420 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 diff --git a/assets/readme/examples/table-of-contents.pdf b/assets/readme/examples/table-of-contents.pdf index 6b8570fce37212b90500c5e3bfdf89634c3216b3..a721966e1cdd33e6c0213b696e2cb6abf56512d5 100644 GIT binary patch delta 302 zcmX>td{%fvAV<9gmwr%weu;vif`LL1m%gWqLbQ#cvzdW~iKDZdiHU`)tFft>nVE%! zvw^FLi?N%bxudIzo1>YFld+MDp__@Jxv9CEv4yLdv9YO%vy-{2k(q&=0?|fD;4)+K z3l3#Q)5$!XhV?r|4><_1Tzq$x>xkyNV1)xm4~O!lI7_v-YPB$$9;;8CvSfa!_Di{* z7A8s6su$7ce@qP)mbIu|wi8$eF%n s$Ahlfr?rd|G%zAV<9!mwr%weu;vif`LL1m%gWqLbQ#EiGc_`Cb~QD2 za&vNYadNb@G&FTJFmQHsa&mSuGH`J*b2K+MFmp0+GIn-0G;?)vb+%I=+6W0;W=wv; zq0DG9nTOM`{vN1Fq;=GZ{)>HqZ%H1NXDYxmgJI{YuZ1|} zq+wP@z8hm?wCKk42RSu-g{Pjr_NJ#%x~V9b97L7AHccqQ#hW{fFm1u9k$pkOS5Amw z1>S?6WC*ngMC!xAxVf|$wOw1v2#+OwjZaAQo2e0hC(o|G2c%z?jS5e2LT=aGf+Tz$ z*@}P?&$X?8t9XgJles2#lF!5K+Az(z>BnL$3n5c66oFZa)Zm816L2pIHOD6g861?o z)b+*nQEWIP&P)>c+qoJTrwkMmRGN%d*2-amvIaV3_BeRRlwK#Wn0HiRoc``-Dd4II zi_K-m8I**uJ?~x*Ri_jA3lCTbGAe?>B}#g1iPM)kOgvL)8<;(m!ayg8CbUYY7A>EA z1gYit#2jeqL=#Ng535$c&ENyHHW4iv5h?n#@uI0kCN9}9yNwPdDVziK^syB+@ zelsN9?_#f#P`}PWubQyAx(uACg;y=lKEjRVwJmZ`z5nSbJ`bv`_gfUW7xOBxm#%UT z>RNjg3(^c;4Nq0ZZ5Q$GG|YPto#Z=Ys6N0Xn|PatVhaebkJ z%-avr3V+~kDMR}DFhb8Pq#vXcL>j)*PqkSCDbqnTJ4AvKd`Et0DBj-XVV;X+YTLL& zVtc@{TudU(UnBYZ#opBylOfsd`GC^!hS?X?k5V6G~)5Sve@AUn{A8 zHt|e<#L}7vLQg`u^){QGp>oa+QL99CU$4T=k$2K@O?PwTCk)VEYuXAzn# zD}`>Sr@Z)aJ9hyOMZ2@eNopQ)?9ezqhw!k�`anLc}Wzg4yKOC-W%1u5AS za&NGN1Czz9oH%~Aijtv2Q)8rW*m)VVu~mLFb)uG0B3mgS%W8HC{mYAZ)yy2WcU#yo zeMO1s)ppf)eZ>u7!5r>QBJ!7+=W_Ql2(tYYqp;yUdZR-LL`vRgpl^FAqEvo=Ou^I( z$Cun-GqH+~`MKcneL+5%joo&f9Wv843!%r&1=*;V3$qH@I;Dg}vkaUc*W@$1Bw zQf2-{a-(nTT1;lQODjy&Nmb<4?vgF;))<>DUmueiLeGevUA?{_uGMOI*b*{s`F`l1 zP-b_S#t;Fs*|wb-X)*d_Y8YB|f20f7p8Pu+7$T|3ZE9~M8-R^sWaA^og6nM>lm>aF zPmumIDot4ti_Ro-WAA95zw<27FNwI7v%al++OALaIFj7^T>8S!!9}tqJI6m|YI@({ z*PkEEm#p-cU&FVDo&Rilv-nNN?Kyf62190xR`W}i!)zub0&#^bf}g!3H$NBSpq)@@ z-6kIrq=wZm5k%4+oEGH44}?*i32XD8YDSj49Ur2KY-xQFU3*LXm6wk~h#!@|M&v0M zPw9m%&-wIJ>I)MBztG%0_XWoK@|$xw6K2!C38S)i;mQb+)Q{fw=#K6Jq_-_P&nU~P zN-YJHMR0UPH{Fw{`I&FKY!s9worPq>rCOWww*nn36^xbJ>c^K;G|isvBpF;}a#*6% zLGz^@&pHnaI&Xy&9az>N<@$Q)dfhVm;g8;=CN@^(Gb2j8 zKdFFaxY?V#VxSN&=T6v82R@d!D zs8e<$pd-M%!alo0!2B=%=}?0|OQ(r?5P*(TSXiVY=ugR=06KPX#TzEp)`nL4db)-% zurAoZ5)L!eg&V?*^z=b`R=OY<#KO=*-vVN31&4ycV5pHU)CdlNSi@mZs1X=!Xas|r zDgM6*ng7Yi?h;ht0cEoU&1I+q3;aG1UeMoSWbn&V{$4D!&ygdhZ;z(@*A)UFB&cfu LP*bzIWDWQitivA2 delta 2205 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 Q4obt!W%Tv!iFlcR0VU delta 2033 zcma*f`9Bkk1Hf_QoNFE`^N6`ZO*Wey8O_)*S94#_NFgc$U&4 zNzNsnl%zvK2vI8aeg1*(>-GHb`SJ7aJitAWA1nriT?@6M7fWzVx&r0jZ&7{9Ss4NC zk21=aJQ{6MPHYaIqtEbpO+y}w`Oh{TyOTnSfoeQRO4Fe z7xm{trBzqnyh99YB&pD_otjYwQ6_|$qye~j6FjmEqeIh>@YzW!pH0uM=;Tglek;%O~*8t z^$G#}MhjhIw+;J-st|pRmS6oxlAqaF{|x^;!ldsz9emyP_$Q}%%~*p}Cl#sIok2&Q z{{92TZy1p#f83g4rx+l~)iyw^3r zkygO0!c*NSPiNHLy)kXABwf*K=eFs^u62WPPW6! zG|w4Wq_5qS1}aD?xXC}}vRT2?4Qh7dysZF!&llXMzBC9wJCk)#{qlXhE5y=Lk+Vjb*jP8N(zy95$u;M&dm(ux;iDyClU^T1 zyEXHGQdp^QH7g96q=^_BAE6lM?jxv0voz%Y-L{6d4kit<%7p9|zVHXDgr(msG(DTe z*a=+m-*4`tSV9I{A*!oP4m}CLmB@7V^#yvfg&bH8+571wlBU@oRZW;1)=R-%Yr!BF zn*V7PqjK77ahg-_-6UEfwT|BEd?ISPTfB8dJj4IQRR!_bQW+IDhiW;$rBkrr=M45l zUIZ2|z=6|lY2)m6xsQFMhi(GL?BV^glr(m14P#2->W;USa-HYzJIg`OJ|&ViMGQZ} zM!hW@YecJKH`o$n}R)!1#{vOg=OA(=H?==r34th#Y`=A(2{F5R*a>d6}DP5yg( zP=fOOX(?^Ut;rqA1Yx=tM2~XyCjNZ&=%mVRnSC3)L>x$N@=} z?TNvH6k#EKGLc7JH;U~%L`8w3S|4Y`R%M_-blWv0w`_*{Z8A>MZznK_K~xGsi~B(XgAw!!7A>`3OnRTI5M^P3|N)oV&1 z6SyNAR1HGl!Ydt(V?!P*Cq;m=1@B47^bZ`|y+E%Ho>c^GtkAw_Pk4&f!+|#0s!R5C<@6bbM$A1QNrCvzr=)Jo;UbV8#0j+E~7L?L|iKuBdPK5fqqjeqe?h;81AP zy*MDYXiL>nT7M2o5tN|%9z#&Ucd5&nfmv#Vk1_JWWq$n}IpfN3us1_W8$1z8PtVSt z?Y``?R1@d-=&*9eJ&Xg%2%xIkRo@)t?9%WihuL7$ZQ?J*D)1|PI9)p2a7H~=xR@Pc zkbH&ZyL)`-zVnJ!?rc8MRoh#De&XvC6wo@d=h&`-Y{5;+X`DlZb}8g??|m5_@qaw! z&+FaY5ZJ;ukp)@x=|4Ew zVVW1@busbvL9^ujr}`f>8x9>)pV)zj9FEdT``#kA&U<8^Bb@s=A@SZBs*^$vpA|^H z82YuTFL4$>KCs~D${+K&o;hyUeWmur_YwwO)Jp{@E)NJj7eb9Y9~v0daYM>ljE=wn zj9sFmpwzCHNViDE|D*_&Bg2AJZ7!(v~iog@BP^MN$H~~S#piPmcc(f_Z ziU5P7hzK~*(u81QVr2@2nn2-50>;wP0{mYI(El~qOn{CkihTnBGa8f^0RzR`%+D8a zO|p4pRgGfEQh8m)bXZK?-ntU-T0Z%tZ~_L=G^c&rXUUeA)s%rE0eX4_XCmN#lT@y` diff --git a/assets/readme/examples/viewer-preferences.pdf b/assets/readme/examples/viewer-preferences.pdf index 105e573e996e5ba7fb8ab7851f2d8f2cc3f88aec..a1f3705460b224d1b94e6af6359aa2662f885b7a 100644 GIT binary patch delta 208 zcmaFJ^@MAKC<~LR!DMk3`Fe9VV`o<<7XuR~S7#F!6EjB(M;A92GjlUbS4S5ES0i(0 z3pXbx3u7lYV>d@vM+*Z(Gc#8s3r9C+7ehBE7gsw48%m5|0huv*CyO$p+2p${hI*$N z83YAf4qk8mYAMj3VxD?yVIHf@MviC7+}_jH-blYX>&N|%u%q delta 205 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 h-rr*Yb0`S`1.6.0 17 + + 2.1.0 + 6.1.2 3.27.7 @@ -175,6 +186,20 @@ org.apache.maven.plugins maven-surefire-plugin 3.5.6 + + + + + ${graphcompose.examples.assetVersion} + + + diff --git a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java new file mode 100644 index 000000000..6550a24f4 --- /dev/null +++ b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java @@ -0,0 +1,184 @@ +package com.demcha.examples; + +import com.demcha.examples.support.AssetContent; +import com.demcha.examples.support.ReadmeBannerV2Renderer; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.io.TempDir; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.TreeSet; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Holds the committed previews to the code that produces them. + * + *

The repository commits a preview of part of the example catalogue, and README and the + * showcase site read those files rather than rendering anything. Nothing regenerated them, so + * they drifted quietly: a change to an example, a theme or the engine moved the render while the + * committed file stayed where it was, and the first time anybody noticed was when a release + * published it. At the point this guard was written, 23 of the 67 committed previews were behind + * the code — and the way that surfaced was a deck losing its bold weights for two releases + * without a single test going red.

+ * + *

The comparison is {@link AssetContent}: exact, with the handful of differences a machine + * rather than an author writes named and dropped. So a failure here means the render moved, and + * the fix is to re-render the named files or to revert what moved them — never to widen the + * comparison.

+ * + *

A committed preview is rendered at the released version while a working tree is + * already on the next one, so this compares like with like only because the examples module runs + * its tests with {@code graphcompose.examples.displayVersion} pinned to + * {@code graphcompose.examples.assetVersion} — the version the committed files were rendered at, + * recorded in {@code examples/pom.xml} and moved by the release script. Without that the pill in + * every hero would differ and this would fail on every asset that carries one.

+ */ +class CommittedAssetDriftTest { + + private static final Path ASSETS = Path.of("..", "assets", "readme"); + private static final Path PREVIEWS = ASSETS.resolve("examples"); + private static final String HERO = "repository_showcase_render.png"; + private static final int HERO_DPI = 200; + + /** The decks whose preview the repository commits — see {@link #UNPUBLISHED_DECKS}. */ + private static final Set CURATED_DECKS = Set.of( + "business-report.pptx", + "financial-report.pptx", + "master-showcase.pptx", + "maven-banner.pptx", + "social-card.pptx", + "twin-output.pptx"); + + /** + * Decks the catalogue renders but the repository deliberately does not commit. + * + *

Which decks are published is a decision, and both lists exist so that no deck can arrive + * without somebody making it. A subset alone is not enough — asserting only that the curated + * decks are among the generated ones lets a new example ship a deck nobody decided about.

+ */ + private static final Set UNPUBLISHED_DECKS = Set.of( + "engine-deck.pptx", + "linkedin-carousel.pptx"); + + @BeforeAll + static void generateEveryExample() throws Exception { + GeneratedCatalogue.generateOnce(); + } + + @Test + void everyCommittedPreviewMatchesWhatTheCatalogueRenders() throws Exception { + Map generated = generatedByName(); + List missing = new ArrayList<>(); + List drifted = new ArrayList<>(); + + for (String name : committedPreviews()) { + Path fresh = generated.get(name); + if (fresh == null) { + missing.add(name); + } else if (!AssetContent.digestOf(PREVIEWS.resolve(name)) + .equals(AssetContent.digestOf(fresh))) { + drifted.add(name); + } + } + + assertThat(missing) + .describedAs("a committed preview that no example renders is a file nothing can " + + "refresh: delete it, or restore the example that produced it") + .isEmpty(); + assertThat(drifted) + .describedAs("a committed preview no longer matches what its example renders. The " + + "comparison drops what the machine writes, so this is a change to the " + + "document: re-render these files, or revert what moved them") + .isEmpty(); + } + + /** + * The README hero is committed too, and nothing else regenerates it. + * + *

It is not part of the catalogue — the release script renders it on its own — which is + * exactly why it drifts unnoticed between releases. Rendering it here costs one page and + * closes the one committed asset the catalogue comparison cannot see.

+ */ + @Test + void theCommittedHeroMatchesWhatTheBannerRendererRenders(@TempDir Path directory) + throws Exception { + Path fresh = ReadmeBannerV2Renderer.render(directory.resolve(HERO), HERO_DPI); + + assertThat(AssetContent.digestOf(fresh)) + .describedAs("the committed README hero no longer matches what the banner " + + "renderer produces at %d DPI: re-render it, or revert what moved it", + HERO_DPI) + .isEqualTo(AssetContent.digestOf(ASSETS.resolve(HERO))); + } + + @Test + void theTwoDeckListsDoNotOverlap() { + assertThat(CURATED_DECKS) + .describedAs("a deck cannot be both published and deliberately unpublished") + .doesNotContainAnyElementsOf(UNPUBLISHED_DECKS); + } + + @Test + void everyRenderedDeckIsEitherPublishedOrDeliberatelyNot() throws Exception { + Set committedDecks = new TreeSet<>(committedPreviews()); + committedDecks.removeIf(name -> !name.endsWith(".pptx")); + Set renderedDecks = new TreeSet<>(generatedByName().keySet()); + renderedDecks.removeIf(name -> !name.endsWith(".pptx")); + + assertThat(committedDecks) + .describedAs("the committed decks and the curated list have to agree: a deck added " + + "to the folder without a decision, or removed from it without one, is " + + "exactly what this list exists to surface") + .isEqualTo(new TreeSet<>(CURATED_DECKS)); + + Set accountedFor = new TreeSet<>(CURATED_DECKS); + accountedFor.addAll(UNPUBLISHED_DECKS); + assertThat(renderedDecks) + .describedAs("the catalogue and the two lists have to cover each other exactly: a " + + "deck listed but not rendered is a committed file nothing can refresh, " + + "and a deck rendered but on neither list is one nobody has decided to " + + "publish or to leave out") + .isEqualTo(accountedFor); + } + + private static Set committedPreviews() throws IOException { + Set names = new TreeSet<>(); + try (var files = Files.list(PREVIEWS)) { + files.filter(Files::isRegularFile) + .map(path -> path.getFileName().toString()) + .forEach(names::add); + } + return names; + } + + /** + * The catalogue keyed by file name, which is how the flat preview folder addresses it. + * + *

Two rendered documents sharing a name would leave one standing in for the other, and + * whichever the walk reached first would decide what a committed file is compared against.

+ */ + private static Map generatedByName() throws IOException { + Map byName = new TreeMap<>(); + try (var files = Files.walk(GeneratedCatalogue.ROOT)) { + files.filter(Files::isRegularFile).forEach(path -> { + Path clash = byName.put(path.getFileName().toString(), path); + if (clash != null) { + throw new IllegalStateException( + "two rendered documents share the name " + path.getFileName() + + " (" + clash + " and " + path + "); the committed folder is flat, so " + + "one would silently stand in for the other"); + } + }); + } + return byName; + } +} diff --git a/examples/src/test/java/com/demcha/examples/PptxCanonicalContentTest.java b/examples/src/test/java/com/demcha/examples/PptxCanonicalContentTest.java deleted file mode 100644 index 15defbe55..000000000 --- a/examples/src/test/java/com/demcha/examples/PptxCanonicalContentTest.java +++ /dev/null @@ -1,513 +0,0 @@ -package com.demcha.examples; - -import com.demcha.examples.flagships.MavenBannerPptxExample; - -import org.junit.jupiter.api.BeforeAll; -import org.junit.jupiter.api.Test; - -import java.awt.image.BufferedImage; -import java.io.ByteArrayInputStream; -import java.io.IOException; -import java.nio.ByteBuffer; -import java.nio.charset.StandardCharsets; -import java.nio.file.Files; -import java.nio.file.Path; -import java.security.MessageDigest; -import java.util.HexFormat; -import java.util.List; -import java.util.Map; -import java.util.Set; -import java.util.TreeMap; -import java.util.TreeSet; -import java.util.regex.Matcher; -import java.util.regex.Pattern; -import java.util.zip.ZipEntry; -import java.util.zip.ZipInputStream; - -import javax.imageio.ImageIO; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Pins the canonical PPTX comparison the asset gate will be built on. - * - *

A deck cannot be compared byte for byte. Every entry in the package carries the zip - * timestamp of the run that wrote it, so two renders of an unchanged document differ. The - * engine can pin those — both fixed backends take a {@code deterministic(...)} instant — - * but {@code buildPptx(Path)} routes through the convenience path, which resolves the - * backend from the provider and gives a caller no way to configure it. Until that seam - * exists, a gate comparing bytes would either fail forever or drop the decks.

- * - *

Nor can it be compared part for part: three things about a package are decided by the - * machine rather than by the document, and all three were measured on these decks rather - * than assumed. See {@link #canonicalise(String, String, byte[])}. So the comparison is - * defined here as the sorted package parts reduced to what the document decides — every - * shape, relationship and run of text, with each freeform's path read where it lands on the - * slide and every image read pixel for pixel bar the one part {@link #UNSTABLE_RASTERS} - * names.

- * - *

What this class does not do is hold a committed asset against a fresh render. A - * committed asset is rendered at the released version while a working tree renders the - * next one, so the two are different documents by construction; and a committed asset is - * not otherwise guaranteed to be level with the code that produces it. Comparing them - * needs the display version as an input and a way to refresh whatever has drifted — one - * job, belonging to the asset gate rather than to a unit test. The comparator is proven - * here so that the gate arrives with it already working.

- */ -class PptxCanonicalContentTest { - - private static final Path COMMITTED = Path.of("..", "assets", "readme", "examples"); - - @BeforeAll - static void generateEveryExample() throws Exception { - GeneratedCatalogue.generateOnce(); - } - - /** - * The decks whose preview the repository commits. - * - *

Which decks those are is a decision, and this list plus {@link #UNPUBLISHED_DECKS} - * exist so that no deck can arrive without somebody making it. A subset alone is not - * enough: asserting that the curated decks are among the generated ones lets a new - * example ship a deck nobody has decided about, which is the same silence the pairing - * this replaced used to keep. Together the two lists cover the catalogue exactly, so a - * deck added, removed or renamed lands here first.

- */ - private static final Set CURATED_DECKS = Set.of( - "business-report.pptx", - "financial-report.pptx", - "master-showcase.pptx", - "maven-banner.pptx", - "social-card.pptx", - "twin-output.pptx"); - - /** Decks the catalogue renders but the repository deliberately does not commit. */ - private static final Set UNPUBLISHED_DECKS = Set.of( - "engine-deck.pptx", - "linkedin-carousel.pptx"); - - @Test - void theTwoDeckListsDoNotOverlap() { - assertThat(CURATED_DECKS) - .describedAs("a deck cannot be both published and deliberately unpublished") - .doesNotContainAnyElementsOf(UNPUBLISHED_DECKS); - } - - @Test - void theCommittedDecksAreExactlyTheCuratedSubset() throws Exception { - Set committed = new TreeSet<>(); - try (var files = Files.list(COMMITTED)) { - files.map(path -> path.getFileName().toString()) - .filter(name -> name.endsWith(".pptx")) - .forEach(committed::add); - } - Map generated = new TreeMap<>(); - try (var decks = Files.walk(GeneratedCatalogue.ROOT)) { - decks.filter(path -> path.toString().endsWith(".pptx")) - .forEach(path -> { - Path clash = generated.put(path.getFileName().toString(), path); - if (clash != null) { - throw new IllegalStateException( - "two generated decks share the name " + path.getFileName() - + " (" + clash + " and " + path + "); the committed gallery is " - + "flat, so one would silently stand in for the other"); - } - }); - } - - assertThat(committed) - .describedAs("the committed decks and the curated list have to agree: a deck " - + "added to the folder without a decision, or removed from it without " - + "one, is exactly what this list exists to surface") - .isEqualTo(new TreeSet<>(CURATED_DECKS)); - Set accountedFor = new TreeSet<>(CURATED_DECKS); - accountedFor.addAll(UNPUBLISHED_DECKS); - assertThat(generated.keySet()) - .describedAs("the catalogue and the two lists have to cover each other exactly: a " - + "deck listed but not generated is a committed file nothing can refresh, " - + "and a deck generated but on neither list is one nobody has decided to " - + "publish or to leave out") - .isEqualTo(accountedFor); - } - - /** - * Rendering the same deck twice produces one document under this digest. - * - *

Only the equality is asserted. Whether the two files also differ in bytes depends - * on the clock — zip entry timestamps have two-second granularity, so two renders in - * quick succession can land on the same stamp and produce identical files, while two - * a moment apart do not. That is the reason the gate cannot hash the file, and it is - * also the reason it cannot be asserted: the property is real but intermittent, and a - * test that pins it fails on whichever machine happens to be fast.

- */ - @Test - void theSameDeckRenderedTwiceIsOneDocument() throws Exception { - Path deck = GeneratedCatalogue.ROOT.resolve("flagships").resolve("maven-banner.pptx"); - assertThat(deck).exists(); - - String firstDigest = canonicalDigest(deck); - MavenBannerPptxExample.generate(); - - assertThat(canonicalDigest(deck)) - .describedAs("the same document rendered twice must be one document") - .isEqualTo(firstDigest); - } - - /** - * The same path drawn in a different box is one shape; a path that moves is not. - * - *

Dropping the box is the load-bearing half of the comparison, so it is checked in - * both directions: the three shapes below are the same freeform normalised the two ways - * two machines normalised it, and then genuinely moved. Absorbing the third as well - * would leave the guard reporting success on a deck whose artwork had shifted.

- */ - @Test - void theBoxIsAbsorbedButAMoveIsNot() { - String box = freeform("", - "", ""); - String sameLineOtherBox = freeform( - "", - "", ""); - String movedLine = freeform("", - "", ""); - - assertThat(freeformsInSlideSpace(sameLineOtherBox)) - .describedAs("the same line from (1000,2000) to (1600,2400), normalised against " - + "an origin 50 EMU away — the difference two machines produce") - .isEqualTo(freeformsInSlideSpace(box)); - assertThat(freeformsInSlideSpace(movedLine)) - .describedAs("a line that actually moved by 50 EMU must not be absorbed") - .isNotEqualTo(freeformsInSlideSpace(box)); - } - - /** - * The rewrite reaches the markup POI actually writes, not just the shape of it. - * - *

{@link #theBoxIsAbsorbedButAMoveIsNot} works on markup written here, so it would - * still pass if POI reordered an attribute and the rewrite quietly stopped matching — - * leaving a comparison that absorbs nothing and a red gate nobody can read. A deck the - * repository commits has to come out changed.

- */ - @Test - void theRewriteReachesRealMarkup() throws Exception { - String slide = part(COMMITTED.resolve("twin-output.pptx"), "ppt/slides/slide1.xml"); - - assertThat(freeformsInSlideSpace(slide)) - .describedAs("no freeform in a deck full of them was rewritten — the markup no " - + "longer looks the way the patterns expect") - .isNotEqualTo(slide); - } - - /** - * An image of the same size is not the same image. - * - *

Reading a raster part by its dimensions would let a logo be swapped, a screenshot - * be replaced or a watermark be retyped without the comparison noticing, which is a - * quiet way for a gate to report that nothing changed. Only the one part measured as - * machine-dependent is read that way; the fixtures below are the same watermark from - * two machines, so they are the closest two images this repository has, and even they - * must come out different when compared as images.

- */ - @Test - void sameDimensionsButDifferentPixelsAreNotEqual() throws Exception { - BufferedImage windows = decode(WATERMARK_WINDOWS); - BufferedImage linux = decode(WATERMARK_LINUX); - assertThat(windows.getWidth()).isEqualTo(linux.getWidth()); - assertThat(windows.getHeight()).isEqualTo(linux.getHeight()); - - assertThat(canonicalise("business-report.pptx", WATERMARK_PART, fixture(WATERMARK_WINDOWS))) - .describedAs("two images of one size are not one image — and the deck this part " - + "sits in is not the one the allowlist names") - .isNotEqualTo(canonicalise("business-report.pptx", WATERMARK_PART, - fixture(WATERMARK_LINUX))); - } - - /** - * The named part absorbs the difference that was measured, and the fixtures carry it. - * - *

The two files are {@code ppt/media/image1.png} of the showcase deck rendered on - * Windows and on the runner: same glyphs in the same places, different coverage along - * their edges. Pinning them here keeps the allowlist honest — if the difference ever - * stops being antialiasing the entry stops being justified, and this is where that - * shows.

- */ - @Test - void theNamedUnstableRasterAbsorbsTheMeasuredDifference() throws Exception { - byte[] windows = fixture(WATERMARK_WINDOWS); - byte[] linux = fixture(WATERMARK_LINUX); - assertThat(windows) - .describedAs("the fixtures have to be two renders, not one file twice") - .isNotEqualTo(linux); - - assertThat(canonicalise(WATERMARK_DECK, WATERMARK_PART, windows)) - .describedAs("the watermark named in the allowlist is the one difference the " - + "comparison is allowed to pass over") - .isEqualTo(canonicalise(WATERMARK_DECK, WATERMARK_PART, linux)); - } - - /** - * A part written with either line ending is one part. - * - *

The normalisation this pins is the difference that failed six decks on the runner, - * and it is the one canonicalisation whose absence shows up nowhere else: every test - * here runs on one machine, where both sides carry the same line ending and the bug - * hides.

- */ - @Test - void theSamePartWithEitherLineEndingIsOnePart() throws Exception { - String declaration = ""; - byte[] unix = (declaration + "\n").getBytes(StandardCharsets.UTF_8); - byte[] windows = (declaration + "\r\n").getBytes(StandardCharsets.UTF_8); - - assertThat(canonicalise(WATERMARK_DECK, "ppt/slides/slide1.xml", windows)) - .describedAs("POI ends the XML declaration with the platform's line separator; " - + "that is the machine writing, not the document") - .isEqualTo(canonicalise(WATERMARK_DECK, "ppt/slides/slide1.xml", unix)); - } - - /** - * Every allowlisted part names a part that is really there. - * - *

A misspelled deck or part would match nothing, and an entry that matches nothing - * exempts nothing — the comparison would quietly go back to reading the watermark by - * its pixels and fail on a runner for a reason the entry was added to explain.

- */ - @Test - void everyAllowlistedRasterExists() throws Exception { - for (String entry : new TreeSet<>(UNSTABLE_RASTERS)) { - String[] split = entry.split("!", 2); - assertThat(split).describedAs("%s is not deck!part", entry).hasSize(2); - for (Path deck : List.of(COMMITTED.resolve(split[0]), generatedDeck(split[0]))) { - assertThat(deck).describedAs("allowlisted deck %s is missing", split[0]).exists(); - assertThat(partNames(deck)) - .describedAs("allowlisted part %s is not in %s", split[1], deck) - .contains(split[1]); - } - } - } - - private static Path generatedDeck(String name) throws IOException { - try (var decks = Files.walk(GeneratedCatalogue.ROOT)) { - return decks.filter(path -> path.getFileName().toString().equals(name)) - .findFirst() - .orElseThrow(() -> new IllegalStateException( - "the catalogue no longer renders " + name)); - } - } - - private static Set partNames(Path pptx) throws IOException { - Set names = new TreeSet<>(); - try (ZipInputStream zip = - new ZipInputStream(new ByteArrayInputStream(Files.readAllBytes(pptx)))) { - for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { - names.add(entry.getName()); - } - } - return names; - } - - private static final String WATERMARK_WINDOWS = "watermark-windows.png"; - private static final String WATERMARK_LINUX = "watermark-linux.png"; - private static final String WATERMARK_DECK = "master-showcase.pptx"; - private static final String WATERMARK_PART = "ppt/media/image1.png"; - - private static byte[] fixture(String name) throws IOException { - try (var in = PptxCanonicalContentTest.class.getResourceAsStream("/pptx-media/" + name)) { - assertThat(in).describedAs("missing test fixture %s", name).isNotNull(); - return in.readAllBytes(); - } - } - - private static BufferedImage decode(String name) throws IOException { - BufferedImage image = ImageIO.read(new ByteArrayInputStream(fixture(name))); - assertThat(image).describedAs("fixture %s does not decode as an image", name).isNotNull(); - return image; - } - - private static String part(Path pptx, String name) throws Exception { - try (ZipInputStream zip = - new ZipInputStream(new ByteArrayInputStream(Files.readAllBytes(pptx)))) { - for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { - if (entry.getName().equals(name)) { - return new String(zip.readAllBytes(), StandardCharsets.UTF_8) - .replace("\r\n", "\n"); - } - } - } - throw new IllegalStateException(pptx + " has no part " + name); - } - - private static String freeform(String xfrm, String from, String to) { - return "" - + "" + xfrm + "" - + "" - + "" + from + "" + to + "" - + ""; - } - - /** The package's parts and their contents, with everything the machine adds left out. */ - private static String canonicalDigest(Path pptx) throws Exception { - Map parts = new TreeMap<>(); - try (ZipInputStream zip = - new ZipInputStream(new ByteArrayInputStream(Files.readAllBytes(pptx)))) { - String deck = pptx.getFileName().toString(); - for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { - if (!entry.isDirectory()) { - parts.put(entry.getName(), - canonicalise(deck, entry.getName(), zip.readAllBytes())); - } - } - } - - MessageDigest digest = MessageDigest.getInstance("SHA-256"); - parts.forEach((name, content) -> { - digest.update(name.getBytes(StandardCharsets.UTF_8)); - digest.update((byte) 0); - digest.update(content); - digest.update((byte) 0); - }); - return HexFormat.of().formatHex(digest.digest()); - } - - /** - * Reduces a part to what the document decides, dropping what the machine decides. - * - *

Three differences were measured between decks written on Windows and the same - * decks written on the Linux runner, none of them a change to the document:

- * - *
    - *
  • Line endings. POI ends the XML declaration with the platform's line - * separator, so every XML part differs by one byte — sixteen parts of an - * otherwise untouched deck.
  • - *
  • Freeform boxes. The box a freeform declares, and the origin its path - * coordinates are measured from, are not stable: one icon came out with every - * point shifted by a constant 272 EMU across and 489 down, and its declared - * extent smaller by the same amount, so that the path landed on exactly the same - * place on the slide. The points are therefore compared where they land, and the - * box that only says how they were normalised is dropped. A freeform that - * actually moves still moves its points.
  • - *
  • Rasterised text. One embedded image — a text watermark — differed along - * the glyph edges alone: same glyphs, same positions, different antialiasing - * coverage. Nothing canonicalises that, so {@link #UNSTABLE_RASTERS} names the - * part and it alone is read by its dimensions. Every other image is compared - * pixel for pixel.
  • - *
- */ - private static byte[] canonicalise(String deck, String name, byte[] content) - throws IOException { - if (name.startsWith("ppt/media/")) { - return image(content, UNSTABLE_RASTERS.contains(deck + "!" + name)); - } - if (!name.endsWith(".xml") && !name.endsWith(".rels")) { - return content; - } - String text = new String(content, StandardCharsets.UTF_8).replace("\r\n", "\n"); - return freeformsInSlideSpace(text).getBytes(StandardCharsets.UTF_8); - } - - /** - * The one raster part a machine is allowed to disagree about, as {@code deck!part}. - * - *

Naming it costs a line and buys the difference between a comparison that tolerates - * one measured artefact and one that stops reading images altogether. Anything added - * here stops being compared by content, so it wants the same measurement behind it that - * put this entry here.

- */ - private static final Set UNSTABLE_RASTERS = - Set.of(WATERMARK_DECK + "!" + WATERMARK_PART); - - /** - * An image's pixels in one colour model, or — for a named unstable part — its size. - * - *

{@code getRGB} converts whatever the decoder produced into sRGB, so a part is - * compared by what it looks like rather than by how it was stored. A part that decodes - * to nothing is vector, and its bytes are its content.

- */ - private static byte[] image(byte[] content, boolean sizeOnly) throws IOException { - BufferedImage image = ImageIO.read(new ByteArrayInputStream(content)); - if (image == null) { - return content; - } - int width = image.getWidth(); - int height = image.getHeight(); - if (sizeOnly) { - return "%dx%d antialiasing not compared".formatted(width, height) - .getBytes(StandardCharsets.UTF_8); - } - ByteBuffer pixels = ByteBuffer.allocate(8 + 4 * width * height); - pixels.putInt(width).putInt(height); - for (int argb : image.getRGB(0, 0, width, height, null, 0, width)) { - pixels.putInt(argb); - } - return pixels.array(); - } - - private static final Pattern SHAPE = Pattern.compile(".*?", Pattern.DOTALL); - private static final Pattern OFFSET = Pattern.compile(""); - private static final Pattern EXTENT = Pattern.compile(""); - private static final Pattern PATH_TAG = Pattern.compile("]*)>"); - private static final Pattern POINT = Pattern.compile(""); - private static final Pattern BOX_ATTRIBUTE = Pattern.compile(" [wh]=\"\\d+\""); - private static final Pattern PATH_WIDTH = Pattern.compile("\\bw=\"(\\d+)\""); - private static final Pattern PATH_HEIGHT = Pattern.compile("\\bh=\"(\\d+)\""); - - /** Reads each freeform's path where it lands on the slide rather than inside its box. */ - private static String freeformsInSlideSpace(String xml) { - Matcher shapes = SHAPE.matcher(xml); - StringBuilder out = new StringBuilder(); - while (shapes.find()) { - shapes.appendReplacement(out, Matcher.quoteReplacement(inSlideSpace(shapes.group()))); - } - shapes.appendTail(out); - return out.toString(); - } - - private static String inSlideSpace(String shape) { - Matcher offset = OFFSET.matcher(shape); - Matcher extent = EXTENT.matcher(shape); - Matcher pathTag = PATH_TAG.matcher(shape); - if (!offset.find() || !extent.find() || !pathTag.find()) { - return shape; - } - long pathWidth = attribute(PATH_WIDTH, pathTag.group(1)); - long pathHeight = attribute(PATH_HEIGHT, pathTag.group(1)); - if (pathWidth <= 0 || pathHeight <= 0) { - return shape; - } - long offsetX = Long.parseLong(offset.group(1)); - long offsetY = Long.parseLong(offset.group(2)); - double scaleX = Long.parseLong(extent.group(1)) / (double) pathWidth; - double scaleY = Long.parseLong(extent.group(2)) / (double) pathHeight; - - Matcher points = POINT.matcher(shape); - StringBuilder out = new StringBuilder(); - while (points.find()) { - long x = offsetX + Math.round(Long.parseLong(points.group(1)) * scaleX); - long y = offsetY + Math.round(Long.parseLong(points.group(2)) * scaleY); - points.appendReplacement(out, - Matcher.quoteReplacement("".formatted(x, y))); - } - points.appendTail(out); - - String slideSpace = OFFSET.matcher(out.toString()).replaceFirst(""); - slideSpace = EXTENT.matcher(slideSpace).replaceFirst(""); - return withoutPathBox(slideSpace); - } - - private static String withoutPathBox(String shape) { - Matcher tags = PATH_TAG.matcher(shape); - StringBuilder out = new StringBuilder(); - while (tags.find()) { - String rest = BOX_ATTRIBUTE.matcher(" " + tags.group(1)).replaceAll("").trim(); - tags.appendReplacement(out, Matcher.quoteReplacement( - rest.isEmpty() ? "" : "")); - } - tags.appendTail(out); - return out.toString(); - } - - private static long attribute(Pattern attribute, String attributes) { - Matcher value = attribute.matcher(attributes); - return value.find() ? Long.parseLong(value.group(1)) : -1; - } -} diff --git a/examples/src/test/java/com/demcha/examples/support/AssetContent.java b/examples/src/test/java/com/demcha/examples/support/AssetContent.java new file mode 100644 index 000000000..0fb75a98c --- /dev/null +++ b/examples/src/test/java/com/demcha/examples/support/AssetContent.java @@ -0,0 +1,267 @@ +package com.demcha.examples.support; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.ByteBuffer; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.security.MessageDigest; +import java.security.NoSuchAlgorithmException; +import java.util.HexFormat; +import java.util.Map; +import java.util.Set; +import java.util.TreeMap; +import java.util.regex.Matcher; +import java.util.regex.Pattern; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.imageio.ImageIO; + +/** + * What a rendered document can be compared by, once the machine is left out of it. + * + *

Two renders of an unchanged document are not the same file. A PDF carries a clock-seeded + * {@code /ID}; an OOXML package carries a zip timestamp on every entry and a creation stamp in + * its properties; POI ends each XML declaration with the platform's line separator. None of + * that is the document, and a comparison that reads it reports a change on every run.

+ * + *

Everything this drops was measured on this repository's own catalogue rather than assumed, + * by rendering it on Windows and on the Linux runner at the same version: of 104 documents, 99 + * came out byte-identical under the reduction below. The five that did not are named in + * {@link #UNSTABLE_PARTS} or are decks the repository does not commit — see there for what each + * one was. So the comparison stays exact: no tolerance, no sampling, and every exemption is a + * line somebody had to write.

+ */ +public final class AssetContent { + + private AssetContent() { + } + + /** + * The parts a machine is allowed to disagree about, as {@code document!part}. + * + *

One entry, and it earns itself: the showcase deck embeds an image of a text watermark, + * and the same glyphs in the same places came out with different antialiasing coverage along + * their edges on the two machines. Averaging it away is not available — 4-pixel blocks still + * differ by 138 of 255 — so the part is read by its size and the rest of the deck is read by + * its content. Anything added here stops being compared, so it wants the same measurement + * behind it that put this entry here.

+ */ + public static final Set UNSTABLE_PARTS = + Set.of("master-showcase.pptx!ppt/media/image1.png"); + + /** Extensions this can reduce; anything else is compared as the bytes it is. */ + private static final Set PACKAGES = Set.of(".pptx", ".docx"); + + private static final Pattern PDF_ID = + Pattern.compile("/ID \\[<[0-9A-Fa-f]+> <[0-9A-Fa-f]+>\\]"); + private static final Pattern XML_TIMESTAMP = + Pattern.compile(">\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z<"); + + /** + * A stable identity for a rendered document. + * + *

Two documents with the same digest are the same document; two with different digests + * differ in something an author wrote. The name matters — it selects the reduction, and for + * a package it also selects any {@link #UNSTABLE_PARTS} entry — so pass the name the file is + * committed under, not a temporary one.

+ * + * @param document the file to read + * @return a hex SHA-256 over the document's content + * @throws IOException if the file cannot be read + */ + public static String digestOf(Path document) throws IOException { + String name = document.getFileName().toString(); + byte[] bytes = Files.readAllBytes(document); + if (PACKAGES.stream().anyMatch(name::endsWith)) { + return digest(packageParts(name, bytes)); + } + if (name.endsWith(".pdf")) { + return digest(Map.of(name, withoutPdfId(bytes))); + } + if (name.endsWith(".png")) { + return digest(Map.of(name, pixels(bytes, false))); + } + return digest(Map.of(name, bytes)); + } + + /** The zip's parts, each reduced, with the entry order and timestamps dropped. */ + private static Map packageParts(String document, byte[] bytes) + throws IOException { + Map parts = new TreeMap<>(); + try (ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(bytes))) { + for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { + if (!entry.isDirectory()) { + parts.put(entry.getName(), + part(document, entry.getName(), zip.readAllBytes())); + } + } + } + return parts; + } + + /** + * Reduces one part of a package to what the document decides. + * + *

Three differences were measured between a package written on Windows and the same + * package written on the runner, none of them a change to the document: the platform's line + * separator after each XML declaration, the creation stamp in {@code docProps}, and the box a + * freeform's path is normalised against. The first two are dropped; the third is why the + * points are read where they land — see {@link #freeformsInSlideSpace}.

+ */ + static byte[] part(String document, String name, byte[] content) throws IOException { + if (name.startsWith("ppt/media/") || name.startsWith("word/media/")) { + return pixels(content, UNSTABLE_PARTS.contains(document + "!" + name)); + } + if (!name.endsWith(".xml") && !name.endsWith(".rels")) { + return content; + } + String text = new String(content, StandardCharsets.UTF_8).replace("\r\n", "\n"); + if (name.startsWith("docProps/")) { + text = XML_TIMESTAMP.matcher(text).replaceAll("><"); + } + return freeformsInSlideSpace(text).getBytes(StandardCharsets.UTF_8); + } + + /** A PDF with the one thing in it that the clock writes taken out. */ + static byte[] withoutPdfId(byte[] content) { + String text = new String(content, StandardCharsets.ISO_8859_1); + return PDF_ID.matcher(text).replaceAll("/ID []").getBytes(StandardCharsets.ISO_8859_1); + } + + /** + * A digest of an image's pixels in one colour model — or, for a named unstable part, of its + * size alone. + * + *

{@code getRGB} converts whatever the decoder produced into sRGB, so an image is compared + * by what it looks like rather than by how it was stored. The pixels are read a row at a time + * and folded into the digest rather than buffered: the README hero is four megapixels, and + * holding two of those as byte arrays to compare them is a waste of a test's heap. A part that + * decodes to nothing is vector, and its bytes are its content.

+ */ + static byte[] pixels(byte[] content, boolean sizeOnly) throws IOException { + BufferedImage image = ImageIO.read(new ByteArrayInputStream(content)); + if (image == null) { + return content; + } + int width = image.getWidth(); + int height = image.getHeight(); + if (sizeOnly) { + return "%dx%d antialiasing not compared".formatted(width, height) + .getBytes(StandardCharsets.UTF_8); + } + ByteBuffer row = ByteBuffer.allocate(4 * width); + MessageDigest digest = sha256(); + digest.update("%dx%d".formatted(width, height).getBytes(StandardCharsets.UTF_8)); + int[] pixels = new int[width]; + for (int y = 0; y < height; y++) { + image.getRGB(0, y, width, 1, pixels, 0, width); + row.clear(); + for (int argb : pixels) { + row.putInt(argb); + } + digest.update(row.array()); + } + return digest.digest(); + } + + private static final Pattern SHAPE = Pattern.compile(".*?", Pattern.DOTALL); + private static final Pattern OFFSET = + Pattern.compile(""); + private static final Pattern EXTENT = Pattern.compile(""); + private static final Pattern PATH_TAG = Pattern.compile("]*)>"); + private static final Pattern POINT = Pattern.compile(""); + private static final Pattern BOX_ATTRIBUTE = Pattern.compile(" [wh]=\"\\d+\""); + private static final Pattern PATH_WIDTH = Pattern.compile("\\bw=\"(\\d+)\""); + private static final Pattern PATH_HEIGHT = Pattern.compile("\\bh=\"(\\d+)\""); + + /** + * Reads each freeform's path where it lands on the slide rather than inside its box. + * + *

The box a freeform declares, and the origin its coordinates are measured from, are not + * stable: one icon came out with every point shifted by a constant 272 EMU across and 489 + * down and its declared extent smaller by exactly as much, so that the path landed on the + * same place on the slide to the unit. Only the normalisation moved. A freeform that + * actually moves still moves its points.

+ */ + static String freeformsInSlideSpace(String xml) { + Matcher shapes = SHAPE.matcher(xml); + StringBuilder out = new StringBuilder(); + while (shapes.find()) { + shapes.appendReplacement(out, Matcher.quoteReplacement(inSlideSpace(shapes.group()))); + } + shapes.appendTail(out); + return out.toString(); + } + + private static String inSlideSpace(String shape) { + Matcher offset = OFFSET.matcher(shape); + Matcher extent = EXTENT.matcher(shape); + Matcher pathTag = PATH_TAG.matcher(shape); + if (!offset.find() || !extent.find() || !pathTag.find()) { + return shape; + } + long pathWidth = attribute(PATH_WIDTH, pathTag.group(1)); + long pathHeight = attribute(PATH_HEIGHT, pathTag.group(1)); + if (pathWidth <= 0 || pathHeight <= 0) { + return shape; + } + long offsetX = Long.parseLong(offset.group(1)); + long offsetY = Long.parseLong(offset.group(2)); + double scaleX = Long.parseLong(extent.group(1)) / (double) pathWidth; + double scaleY = Long.parseLong(extent.group(2)) / (double) pathHeight; + + Matcher points = POINT.matcher(shape); + StringBuilder out = new StringBuilder(); + while (points.find()) { + long x = offsetX + Math.round(Long.parseLong(points.group(1)) * scaleX); + long y = offsetY + Math.round(Long.parseLong(points.group(2)) * scaleY); + points.appendReplacement(out, + Matcher.quoteReplacement("".formatted(x, y))); + } + points.appendTail(out); + + String slideSpace = OFFSET.matcher(out.toString()).replaceFirst(""); + slideSpace = EXTENT.matcher(slideSpace).replaceFirst(""); + return withoutPathBox(slideSpace); + } + + private static String withoutPathBox(String shape) { + Matcher tags = PATH_TAG.matcher(shape); + StringBuilder out = new StringBuilder(); + while (tags.find()) { + String rest = BOX_ATTRIBUTE.matcher(" " + tags.group(1)).replaceAll("").trim(); + tags.appendReplacement(out, Matcher.quoteReplacement( + rest.isEmpty() ? "" : "")); + } + tags.appendTail(out); + return out.toString(); + } + + private static long attribute(Pattern attribute, String attributes) { + Matcher value = attribute.matcher(attributes); + return value.find() ? Long.parseLong(value.group(1)) : -1; + } + + private static MessageDigest sha256() { + try { + return MessageDigest.getInstance("SHA-256"); + } catch (NoSuchAlgorithmException impossible) { + throw new IllegalStateException("SHA-256 is required of every JVM", impossible); + } + } + + private static String digest(Map parts) { + MessageDigest digest = sha256(); + new TreeMap<>(parts).forEach((name, content) -> { + digest.update(name.getBytes(StandardCharsets.UTF_8)); + digest.update((byte) 0); + digest.update(content); + digest.update((byte) 0); + }); + return HexFormat.of().formatHex(digest.digest()); + } +} diff --git a/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java b/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java new file mode 100644 index 000000000..25da8d953 --- /dev/null +++ b/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java @@ -0,0 +1,292 @@ +package com.demcha.examples.support; + +import com.demcha.examples.GeneratedCatalogue; +import com.demcha.examples.flagships.MavenBannerPptxExample; + +import org.junit.jupiter.api.BeforeAll; +import org.junit.jupiter.api.Test; + +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.List; +import java.util.Set; +import java.util.TreeSet; +import java.util.zip.ZipEntry; +import java.util.zip.ZipInputStream; + +import javax.imageio.ImageIO; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Proves the comparison drops what the machine writes and nothing else. + * + *

{@link AssetContent} is what the asset gate reads a document through, so each reduction it + * makes is checked in both directions: the difference it is meant to absorb comes out equal, and + * the change it must never absorb comes out different. A reduction that quietly stops matching — + * a reordered attribute, a mistyped part name — would leave a gate that compares nothing and + * passes everything, so the ones driven by patterns are also run against markup POI really + * wrote.

+ */ +class AssetContentTest { + + private static final Path COMMITTED = Path.of("..", "assets", "readme", "examples"); + private static final String WATERMARK_WINDOWS = "watermark-windows.png"; + private static final String WATERMARK_LINUX = "watermark-linux.png"; + private static final String WATERMARK_DECK = "master-showcase.pptx"; + private static final String WATERMARK_PART = "ppt/media/image1.png"; + + @BeforeAll + static void generateEveryExample() throws Exception { + GeneratedCatalogue.generateOnce(); + } + + /** + * Rendering the same deck twice produces one document under this comparison. + * + *

Only the equality is asserted. Whether the two files also differ in bytes depends on the + * clock — zip entry timestamps have two-second granularity, so two renders in quick + * succession can land on the same stamp and produce identical files, while two a moment apart + * do not. That is the reason the gate cannot hash the file, and it is also the reason it + * cannot be asserted: the property is real but intermittent, and a test that pins it fails on + * whichever machine happens to be fast.

+ */ + @Test + void theSameDeckRenderedTwiceIsOneDocument() throws Exception { + Path deck = GeneratedCatalogue.ROOT.resolve("flagships").resolve("maven-banner.pptx"); + assertThat(deck).exists(); + + String first = AssetContent.digestOf(deck); + MavenBannerPptxExample.generate(); + + assertThat(AssetContent.digestOf(deck)) + .describedAs("the same document rendered twice must be one document") + .isEqualTo(first); + } + + /** A PDF differs between two renders only in the identifier the clock seeds. */ + @Test + void theSamePdfRenderedTwiceIsOneDocument() throws Exception { + Path pdf = GeneratedCatalogue.ROOT.resolve("flagships").resolve("maven-banner.pdf"); + assertThat(pdf).exists(); + byte[] rendered = Files.readAllBytes(pdf); + byte[] reseeded = new String(rendered, StandardCharsets.ISO_8859_1) + .replaceFirst("/ID \\[<[0-9A-Fa-f]+> <[0-9A-Fa-f]+>\\]", + "/ID [<" + "A".repeat(64) + "> <" + "A".repeat(64) + ">]") + .getBytes(StandardCharsets.ISO_8859_1); + + assertThat(reseeded) + .describedAs("the fixture must actually carry an /ID for this to prove anything") + .isNotEqualTo(rendered); + assertThat(AssetContent.withoutPdfId(reseeded)) + .describedAs("a fresh /ID is the clock writing, not the document") + .isEqualTo(AssetContent.withoutPdfId(rendered)); + } + + /** + * The same path drawn in a different box is one shape; a path that moves is not. + * + *

Dropping the box is the load-bearing half of the comparison, so it is checked in both + * directions: the shapes below are the same freeform normalised the two ways two machines + * normalised it, and then genuinely moved. Absorbing the third as well would leave the gate + * reporting success on a deck whose artwork had shifted.

+ */ + @Test + void theBoxIsAbsorbedButAMoveIsNot() { + String box = freeform("", + "", ""); + String sameLineOtherBox = freeform( + "", + "", ""); + String movedLine = freeform("", + "", ""); + + assertThat(AssetContent.freeformsInSlideSpace(sameLineOtherBox)) + .describedAs("the same line from (1000,2000) to (1600,2400), normalised against " + + "an origin 50 EMU away — the difference two machines produce") + .isEqualTo(AssetContent.freeformsInSlideSpace(box)); + assertThat(AssetContent.freeformsInSlideSpace(movedLine)) + .describedAs("a line that actually moved by 50 EMU must not be absorbed") + .isNotEqualTo(AssetContent.freeformsInSlideSpace(box)); + } + + /** + * The rewrite reaches the markup POI actually writes, not just the shape of it. + * + *

{@link #theBoxIsAbsorbedButAMoveIsNot} works on markup written here, so it would still + * pass if POI reordered an attribute and the rewrite quietly stopped matching — leaving a + * comparison that absorbs nothing and a red gate nobody can read. A deck the repository + * commits has to come out changed.

+ */ + @Test + void theRewriteReachesRealMarkup() throws Exception { + String slide = part(COMMITTED.resolve("twin-output.pptx"), "ppt/slides/slide1.xml"); + + assertThat(AssetContent.freeformsInSlideSpace(slide)) + .describedAs("no freeform in a deck full of them was rewritten — the markup no " + + "longer looks the way the patterns expect") + .isNotEqualTo(slide); + } + + /** + * An image of the same size is not the same image. + * + *

Reading a raster part by its dimensions would let a logo be swapped, a screenshot be + * replaced or a watermark be retyped without the comparison noticing, which is a quiet way + * for a gate to report that nothing changed. Only the parts named in + * {@link AssetContent#UNSTABLE_PARTS} are read that way; the fixtures are the same watermark + * from two machines, so they are the closest two images this repository has, and even they + * must come out different when compared as images.

+ */ + @Test + void sameDimensionsButDifferentPixelsAreNotEqual() throws Exception { + BufferedImage windows = decode(WATERMARK_WINDOWS); + BufferedImage linux = decode(WATERMARK_LINUX); + assertThat(windows.getWidth()).isEqualTo(linux.getWidth()); + assertThat(windows.getHeight()).isEqualTo(linux.getHeight()); + + assertThat(AssetContent.part("business-report.pptx", WATERMARK_PART, + fixture(WATERMARK_WINDOWS))) + .describedAs("two images of one size are not one image — and the deck this part " + + "sits in is not one the allowlist names") + .isNotEqualTo(AssetContent.part("business-report.pptx", WATERMARK_PART, + fixture(WATERMARK_LINUX))); + } + + /** + * The named part absorbs the difference that was measured, and the fixtures carry it. + * + *

The two files are {@code ppt/media/image1.png} of the showcase deck rendered on Windows + * and on the runner: same glyphs in the same places, different coverage along their edges. + * Pinning them here keeps the allowlist honest — if the difference ever stops being + * antialiasing the entry stops being justified, and this is where that shows.

+ */ + @Test + void theNamedUnstablePartAbsorbsTheMeasuredDifference() throws Exception { + byte[] windows = fixture(WATERMARK_WINDOWS); + byte[] linux = fixture(WATERMARK_LINUX); + assertThat(windows) + .describedAs("the fixtures have to be two renders, not one file twice") + .isNotEqualTo(linux); + + assertThat(AssetContent.part(WATERMARK_DECK, WATERMARK_PART, windows)) + .describedAs("the watermark named in the allowlist is the one difference the " + + "comparison is allowed to pass over") + .isEqualTo(AssetContent.part(WATERMARK_DECK, WATERMARK_PART, linux)); + } + + /** + * A part written with either line ending is one part. + * + *

This is the reduction that failed six decks on the runner, and the one whose absence + * shows up nowhere else: every test here runs on one machine, where both sides carry the same + * line ending and the difference hides.

+ */ + @Test + void theSamePartWithEitherLineEndingIsOnePart() throws Exception { + String declaration = ""; + byte[] unix = (declaration + "\n").getBytes(StandardCharsets.UTF_8); + byte[] windows = (declaration + "\r\n").getBytes(StandardCharsets.UTF_8); + + assertThat(AssetContent.part(WATERMARK_DECK, "ppt/slides/slide1.xml", windows)) + .describedAs("POI ends the XML declaration with the platform's line separator; " + + "that is the machine writing, not the document") + .isEqualTo(AssetContent.part(WATERMARK_DECK, "ppt/slides/slide1.xml", unix)); + } + + /** The stamp a package records for when it was written is not part of the document. */ + @Test + void theCreationStampIsNotPartOfTheDocument() throws Exception { + String properties = "%s" + + ""; + byte[] earlier = properties.formatted("2026-08-02T16:15:46Z") + .getBytes(StandardCharsets.UTF_8); + byte[] later = properties.formatted("2026-08-02T16:46:46Z") + .getBytes(StandardCharsets.UTF_8); + + assertThat(AssetContent.part("word-export-companion.docx", "docProps/core.xml", earlier)) + .describedAs("two renders of one document minutes apart are one document") + .isEqualTo(AssetContent.part("word-export-companion.docx", "docProps/core.xml", + later)); + } + + /** + * Every allowlisted part names a part that is really there. + * + *

A misspelled document or part would match nothing, and an entry that matches nothing + * exempts nothing — the comparison would quietly go back to reading the watermark by its + * pixels and fail on a runner for the reason the entry was added to explain.

+ */ + @Test + void everyAllowlistedPartExists() throws Exception { + for (String entry : new TreeSet<>(AssetContent.UNSTABLE_PARTS)) { + String[] split = entry.split("!", 2); + assertThat(split).describedAs("%s is not document!part", entry).hasSize(2); + for (Path document : List.of(COMMITTED.resolve(split[0]), generated(split[0]))) { + assertThat(document) + .describedAs("allowlisted document %s is missing", split[0]).exists(); + assertThat(partNames(document)) + .describedAs("allowlisted part %s is not in %s", split[1], document) + .contains(split[1]); + } + } + } + + private static Path generated(String name) throws IOException { + try (var files = Files.walk(GeneratedCatalogue.ROOT)) { + return files.filter(path -> path.getFileName().toString().equals(name)) + .findFirst() + .orElseThrow(() -> new IllegalStateException( + "the catalogue no longer renders " + name)); + } + } + + private static Set partNames(Path archive) throws IOException { + Set names = new TreeSet<>(); + try (ZipInputStream zip = + new ZipInputStream(new ByteArrayInputStream(Files.readAllBytes(archive)))) { + for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { + names.add(entry.getName()); + } + } + return names; + } + + private static String part(Path archive, String name) throws IOException { + try (ZipInputStream zip = + new ZipInputStream(new ByteArrayInputStream(Files.readAllBytes(archive)))) { + for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { + if (entry.getName().equals(name)) { + return new String(zip.readAllBytes(), StandardCharsets.UTF_8) + .replace("\r\n", "\n"); + } + } + } + throw new IllegalStateException(archive + " has no part " + name); + } + + private static byte[] fixture(String name) throws IOException { + try (var in = AssetContentTest.class.getResourceAsStream("/pptx-media/" + name)) { + assertThat(in).describedAs("missing test fixture %s", name).isNotNull(); + return in.readAllBytes(); + } + } + + private static BufferedImage decode(String name) throws IOException { + BufferedImage image = ImageIO.read(new ByteArrayInputStream(fixture(name))); + assertThat(image).describedAs("fixture %s does not decode as an image", name).isNotNull(); + return image; + } + + private static String freeform(String xfrm, String from, String to) { + return "" + + "" + xfrm + "" + + "" + + "" + from + "" + to + "" + + ""; + } +} From 325e7219382445720a6182babf9e80e122b34a0b Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 2 Aug 2026 18:32:16 +0100 Subject: [PATCH 2/5] fix(examples): say which committed assets cannot be compared, and why MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The runner rejected the README hero and nothing else: all sixty-seven previews matched across the two platforms, and the hero did not, because it is not a document — it is a page rasterised to a PNG, and rasterising text is exactly where two machines disagree. Comparing it would take a pixel budget, which is a number nobody can defend a year later. It is also not the only one: eight rasterised figures sit beside the preview folder, all of them the same kind of file for the same reason. They are written down and asserted as a set, so a ninth arrives as a decision rather than as a commit nobody read. The figures of the 1.5 line are left out of that entirely — re-rendering those would be the bug. --- .../examples/CommittedAssetDriftTest.java | 55 ++++++++++++------- 1 file changed, 36 insertions(+), 19 deletions(-) diff --git a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java index 6550a24f4..84fb7dd1d 100644 --- a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java +++ b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java @@ -1,11 +1,9 @@ package com.demcha.examples; import com.demcha.examples.support.AssetContent; -import com.demcha.examples.support.ReadmeBannerV2Renderer; import org.junit.jupiter.api.BeforeAll; import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.io.TempDir; import java.io.IOException; import java.nio.file.Files; @@ -39,15 +37,13 @@ * already on the next one, so this compares like with like only because the examples module runs * its tests with {@code graphcompose.examples.displayVersion} pinned to * {@code graphcompose.examples.assetVersion} — the version the committed files were rendered at, - * recorded in {@code examples/pom.xml} and moved by the release script. Without that the pill in - * every hero would differ and this would fail on every asset that carries one.

+ * recorded in {@code examples/pom.xml} and moved by the release script. Without that the + * coordinate pill alone would differ and this would fail on every preview that carries one.

*/ class CommittedAssetDriftTest { private static final Path ASSETS = Path.of("..", "assets", "readme"); private static final Path PREVIEWS = ASSETS.resolve("examples"); - private static final String HERO = "repository_showcase_render.png"; - private static final int HERO_DPI = 200; /** The decks whose preview the repository commits — see {@link #UNPUBLISHED_DECKS}. */ private static final Set CURATED_DECKS = Set.of( @@ -102,22 +98,43 @@ void everyCommittedPreviewMatchesWhatTheCatalogueRenders() throws Exception { } /** - * The README hero is committed too, and nothing else regenerates it. + * The figures beside the preview folder are rasters, and this cannot compare a raster. * - *

It is not part of the catalogue — the release script renders it on its own — which is - * exactly why it drifts unnoticed between releases. Rendering it here costs one page and - * closes the one committed asset the catalogue comparison cannot see.

+ *

Everything under {@link #PREVIEWS} is a document, and two machines write one document the + * same way once the reduction is applied. These are not: each is a page rasterised to a PNG for + * README, and rasterising text is where two machines disagree — the same measurement that put + * the showcase watermark in {@link AssetContent#UNSTABLE_PARTS}, over a whole page rather than + * one band of it. Comparing them would mean a pixel budget, and a budget is a number nobody can + * defend a year later.

+ * + *

So they are written down instead. The list is what this guard can say about them: a + * ninth figure appearing here is a file nothing checks, and it should arrive as a decision + * rather than as a commit nobody read. {@code assets/readme/v1.5} is left out entirely — those + * are the figures of a released line, and re-rendering them would be the bug.

*/ + private static final Set RASTER_FIGURES = Set.of( + "barcode-showcase.png", + "chart-showcase.png", + "feature-catalog.png", + "repository_showcase_render.png", + "social-card.png", + "twin-output-editing.png", + "twin-output-pdf.png", + "twin-output-pptx.png"); + @Test - void theCommittedHeroMatchesWhatTheBannerRendererRenders(@TempDir Path directory) - throws Exception { - Path fresh = ReadmeBannerV2Renderer.render(directory.resolve(HERO), HERO_DPI); - - assertThat(AssetContent.digestOf(fresh)) - .describedAs("the committed README hero no longer matches what the banner " - + "renderer produces at %d DPI: re-render it, or revert what moved it", - HERO_DPI) - .isEqualTo(AssetContent.digestOf(ASSETS.resolve(HERO))); + void theOnlyAssetsThisCannotCompareAreTheOnesWrittenDown() throws Exception { + Set beside = new TreeSet<>(); + try (var files = Files.list(ASSETS)) { + files.filter(Files::isRegularFile) + .map(path -> path.getFileName().toString()) + .forEach(beside::add); + } + + assertThat(beside) + .describedAs("an asset beside %s is compared by nothing: either move it in with the " + + "previews so it is, or add it here with the reason it cannot be", PREVIEWS) + .isEqualTo(new TreeSet<>(RASTER_FIGURES)); } @Test From 5c56963d1565f0c1081339d17fa7f0f5ea17e87d Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 2 Aug 2026 18:44:51 +0100 Subject: [PATCH 3/5] fix(examples): drop what the hero left behind, and name the DOCX for what it did MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit The hero test took a raster comparison down with it when the runner showed rasters cannot be compared, and two pieces of it stayed: a branch reading a PNG by its pixels, which no caller reaches and which says the opposite of the list beside it, and a comment justifying itself by a caller that is gone. The guard over the assets it cannot compare listed files and passed over folders, which are easier to add without noticing, not harder. Both are asserted now. The CHANGELOG said all twenty-three refreshed previews rasterise to the same pixels. Twenty-two of them do; the twenty-third is a DOCX, which nothing rasterised, and whose change was real — it asked for a font named Helvetica-Bold, which Word honours only if that font is installed, and now marks bold. --- CHANGELOG.md | 6 ++++-- .../examples/CommittedAssetDriftTest.java | 20 +++++++++++++------ .../demcha/examples/support/AssetContent.java | 9 +++------ .../examples/support/AssetContentTest.java | 4 ++-- 4 files changed, 23 insertions(+), 16 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8a9ff7c3d..715c40269 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -12,8 +12,10 @@ follow semantic versioning; release dates are ISO 8601. anything, and nothing held those files to the catalogue: a change to an example, a theme or the engine moved the render while the committed file stayed put, and the first anybody knew was a release publishing it. Twenty-three of the - sixty-seven were behind — invisibly, as it turned out, since all of them - rasterise to the same pixels — and they are re-rendered here. Every one is now + sixty-seven were behind and are re-rendered here; the twenty-two PDFs among + them rasterise to the same pixels as before, so nothing visible had been + carrying the drift, and the one DOCX now marks bold as `` rather than by + asking for a font named `Helvetica-Bold`. Every preview is now compared against a fresh render on each build, exactly: the comparison drops only what a machine writes rather than an author (a PDF's clock-seeded `/ID`, an OOXML package's zip and creation stamps, the platform's line separator, and one diff --git a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java index 84fb7dd1d..0a5f4f3a5 100644 --- a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java +++ b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java @@ -108,9 +108,10 @@ void everyCommittedPreviewMatchesWhatTheCatalogueRenders() throws Exception { * defend a year later.

* *

So they are written down instead. The list is what this guard can say about them: a - * ninth figure appearing here is a file nothing checks, and it should arrive as a decision - * rather than as a commit nobody read. {@code assets/readme/v1.5} is left out entirely — those - * are the figures of a released line, and re-rendering them would be the bug.

+ * figure arriving here that is not on it is a file nothing checks, and it should arrive as a + * decision rather than as a commit nobody read. The same holds for a folder — {@code v1.5} is + * the figures of a released line, where re-rendering would itself be the bug, and a second + * folder wants that said out loud rather than assumed.

*/ private static final Set RASTER_FIGURES = Set.of( "barcode-showcase.png", @@ -122,19 +123,26 @@ void everyCommittedPreviewMatchesWhatTheCatalogueRenders() throws Exception { "twin-output-pdf.png", "twin-output-pptx.png"); + /** The folders under {@code assets/readme}: the previews this compares, and 1.5's figures. */ + private static final Set ASSET_FOLDERS = Set.of("examples", "v1.5"); + @Test void theOnlyAssetsThisCannotCompareAreTheOnesWrittenDown() throws Exception { Set beside = new TreeSet<>(); + Set folders = new TreeSet<>(); try (var files = Files.list(ASSETS)) { - files.filter(Files::isRegularFile) - .map(path -> path.getFileName().toString()) - .forEach(beside::add); + files.forEach(path -> (Files.isDirectory(path) ? folders : beside) + .add(path.getFileName().toString())); } assertThat(beside) .describedAs("an asset beside %s is compared by nothing: either move it in with the " + "previews so it is, or add it here with the reason it cannot be", PREVIEWS) .isEqualTo(new TreeSet<>(RASTER_FIGURES)); + assertThat(folders) + .describedAs("a folder of assets beside %s is compared by nothing either, and a " + + "whole folder is easier to add without noticing than a file", PREVIEWS) + .isEqualTo(new TreeSet<>(ASSET_FOLDERS)); } @Test diff --git a/examples/src/test/java/com/demcha/examples/support/AssetContent.java b/examples/src/test/java/com/demcha/examples/support/AssetContent.java index 0fb75a98c..272f6701d 100644 --- a/examples/src/test/java/com/demcha/examples/support/AssetContent.java +++ b/examples/src/test/java/com/demcha/examples/support/AssetContent.java @@ -82,9 +82,6 @@ public static String digestOf(Path document) throws IOException { if (name.endsWith(".pdf")) { return digest(Map.of(name, withoutPdfId(bytes))); } - if (name.endsWith(".png")) { - return digest(Map.of(name, pixels(bytes, false))); - } return digest(Map.of(name, bytes)); } @@ -138,9 +135,9 @@ static byte[] withoutPdfId(byte[] content) { * *

{@code getRGB} converts whatever the decoder produced into sRGB, so an image is compared * by what it looks like rather than by how it was stored. The pixels are read a row at a time - * and folded into the digest rather than buffered: the README hero is four megapixels, and - * holding two of those as byte arrays to compare them is a waste of a test's heap. A part that - * decodes to nothing is vector, and its bytes are its content.

+ * and folded into the digest rather than buffered: an embedded screenshot runs to megapixels, + * and holding two of those as byte arrays to compare them is a waste of a test's heap. A part + * that decodes to nothing is vector, and its bytes are its content.

*/ static byte[] pixels(byte[] content, boolean sizeOnly) throws IOException { BufferedImage image = ImageIO.read(new ByteArrayInputStream(content)); diff --git a/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java b/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java index 25da8d953..e13ebf268 100644 --- a/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java +++ b/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java @@ -68,9 +68,9 @@ void theSameDeckRenderedTwiceIsOneDocument() throws Exception { .isEqualTo(first); } - /** A PDF differs between two renders only in the identifier the clock seeds. */ + /** A PDF that differs only in the identifier the clock seeds is one document. */ @Test - void theSamePdfRenderedTwiceIsOneDocument() throws Exception { + void aPdfCarryingADifferentIdentifierIsTheSameDocument() throws Exception { Path pdf = GeneratedCatalogue.ROOT.resolve("flagships").resolve("maven-banner.pdf"); assertThat(pdf).exists(); byte[] rendered = Files.readAllBytes(pdf); From 902bcce2919444f0f426b0cefa8fe95ebe655e37 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 2 Aug 2026 19:27:36 +0100 Subject: [PATCH 4/5] =?UTF-8?q?fix(examples):=20close=20what=20the=20gate?= =?UTF-8?q?=20could=20not=20see=20=E2=80=94=20a=20deletion=20and=20a=20swa?= =?UTF-8?q?pped=20image?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Comparing the files that are there means a deleted preview leaves its own guard: README loses a figure, the folder shrinks, and every test stays green. The catalogue is now pinned against the folder plus a list of the documents deliberately left unpublished, so the three have to account for each other exactly — a deletion, an example nobody decided to publish, and a rename that lands as one of each all fail. The list is the shorter half of the decision, and it replaces the deck-only lists, which were the same idea applied to six files out of sixty-seven. The exemption for the showcase watermark exempted the path rather than the renders. Any image of the same size passed: a different word, a swapped logo, a blank. It now names the pixel digest of each machine's version, and only those two collapse onto one token; anything else keeps its own digest. Two tests hold that shut — an unknown image of exactly the right size is not absorbed, and the part actually inside both decks has to be one of the two renders the fixtures account for, since naming a path protects nothing once the deck stops carrying what was measured. The creation stamp was dropped by matching any ISO instant anywhere under docProps, which would have taken a date somebody meant along with it. It names dcterms:created in docProps/core.xml, which is the one element that was measured varying, and a custom date property now proves it survives. The property recording the previews' version said the release script moves it. No release step does yet, and until one exists a release re-renders the showcase at the new version while the previews stay behind — with this gate comparing both sides at the old one and staying green. Both notes say so. --- examples/pom.xml | 10 +- .../examples/CommittedAssetDriftTest.java | 124 ++++++++++++------ .../demcha/examples/support/AssetContent.java | 74 +++++++---- .../examples/support/AssetContentTest.java | 77 ++++++++++- 4 files changed, 216 insertions(+), 69 deletions(-) diff --git a/examples/pom.xml b/examples/pom.xml index f000016f6..468f043d2 100644 --- a/examples/pom.xml +++ b/examples/pom.xml @@ -27,8 +27,14 @@ its coordinate pill, so a working tree already on the next version renders a different document by definition; CommittedAssetDriftTest can only compare like with like because the surefire configuration below - pins the display version to this value. cut-release.ps1 moves it with - the tag, in the same step that re-renders the previews. + pins the display version to this value. + + Nothing moves it yet. Until cut-release.ps1 re-renders the previews and + bumps this in the same commit as the tag, a release leaves the previews + at the version below while the showcase site goes out at the new one — + and the drift gate, comparing both sides at this value, stays green + through it. That step is the reason this property exists; it is not + written yet. --> 2.1.0 diff --git a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java index 0a5f4f3a5..1e4b9a36e 100644 --- a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java +++ b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java @@ -37,33 +37,71 @@ * already on the next one, so this compares like with like only because the examples module runs * its tests with {@code graphcompose.examples.displayVersion} pinned to * {@code graphcompose.examples.assetVersion} — the version the committed files were rendered at, - * recorded in {@code examples/pom.xml} and moved by the release script. Without that the - * coordinate pill alone would differ and this would fail on every preview that carries one.

+ * recorded in {@code examples/pom.xml}. Without that the coordinate pill alone would differ and + * this would fail on every preview that carries one.

+ * + *

Nothing moves that property yet, and this guard cannot notice: it compares both sides at + * whatever the property says, so a release that re-renders the showcase site at a new version + * while leaving the previews behind passes here. Closing that is the release step's job, and it + * is the reason to land it alongside rather than long after.

*/ class CommittedAssetDriftTest { private static final Path ASSETS = Path.of("..", "assets", "readme"); private static final Path PREVIEWS = ASSETS.resolve("examples"); - /** The decks whose preview the repository commits — see {@link #UNPUBLISHED_DECKS}. */ - private static final Set CURATED_DECKS = Set.of( - "business-report.pptx", - "financial-report.pptx", - "master-showcase.pptx", - "maven-banner.pptx", - "social-card.pptx", - "twin-output.pptx"); + /** What counts as a document: the catalogue also writes a preview image and a data file. */ + private static final Set DOCUMENTS = Set.of(".pdf", ".pptx", ".docx"); /** - * Decks the catalogue renders but the repository deliberately does not commit. + * The documents the catalogue renders and the repository deliberately does not commit. + * + *

Publishing a preview is a decision, and this is the half of it worth writing down: the + * other half is the folder itself, and the two together have to account for the catalogue + * exactly. Without that, comparing only the files that happen to be there means deleting one + * removes it from its own guard — README would lose a figure and every test would stay + * green.

* - *

Which decks are published is a decision, and both lists exist so that no deck can arrive - * without somebody making it. A subset alone is not enough — asserting only that the curated - * decks are among the generated ones lets a new example ship a deck nobody decided about.

+ *

Listing what is not published rather than what is keeps the shorter list, and + * puts the decision where it is actually made: a new example ships unpublished unless somebody + * says otherwise, and saying so is adding a file to the folder and a name off this list.

*/ - private static final Set UNPUBLISHED_DECKS = Set.of( + private static final Set UNPUBLISHED_PREVIEWS = Set.of( + "cover-letter-blue-banner-v2.pdf", + "cover-letter-boxed-sections-v2.pdf", + "cover-letter-centered-headline-v2.pdf", + "cover-letter-classic-serif-v2.pdf", + "cover-letter-compact-mono-v2.pdf", + "cover-letter-editorial-blue-v2.pdf", + "cover-letter-engineering-resume-v2.pdf", + "cover-letter-executive-v2.pdf", + "cover-letter-mint-editorial-v2.pdf", + "cover-letter-modern-professional-v2.pdf", + "cover-letter-monogram-sidebar-v2.pdf", + "cover-letter-nordic-clean-v2.pdf", + "cover-letter-panel-v2.pdf", + "cover-letter-sidebar-portrait-v2.pdf", + "cover-letter-timeline-minimal-v2.pdf", + "cv-blue-banner-v2.pdf", + "cv-boxed-sections-v2.pdf", + "cv-centered-headline-v2.pdf", + "cv-editorial-blue-v2.pdf", + "cv-executive-v2.pdf", + "cv-minimal-underlined-v2.pdf", + "cv-mint-editorial-v2-custom.pdf", + "cv-mint-editorial-v2.pdf", + "cv-monogram-sidebar-v2.pdf", + "cv-sidebar-portrait-v2.pdf", + "emoji-clip-path.pdf", + "emoji-gallery.pdf", + "emoji-svg-vs-png.pdf", "engine-deck.pptx", - "linkedin-carousel.pptx"); + "invoice-modern-v2.pdf", + "linkedin-carousel.pdf", + "linkedin-carousel.pptx", + "photo-clip.pdf", + "poetry-title.pdf", + "proposal-modern-v2.pdf"); @BeforeAll static void generateEveryExample() throws Exception { @@ -145,33 +183,35 @@ void theOnlyAssetsThisCannotCompareAreTheOnesWrittenDown() throws Exception { .isEqualTo(new TreeSet<>(ASSET_FOLDERS)); } + /** + * The catalogue is exactly the published previews plus the deliberately unpublished ones. + * + *

This is what makes a deletion visible. The comparison above only reads the files that + * are there, so removing one takes it out of its own guard: README loses a figure and nothing + * goes red. Pinning the whole catalogue against the folder plus the list catches that, and + * catches its opposites too — a new example nobody decided to publish, a preview added + * without a source, a rename that lands as one of each.

+ */ @Test - void theTwoDeckListsDoNotOverlap() { - assertThat(CURATED_DECKS) - .describedAs("a deck cannot be both published and deliberately unpublished") - .doesNotContainAnyElementsOf(UNPUBLISHED_DECKS); - } - - @Test - void everyRenderedDeckIsEitherPublishedOrDeliberatelyNot() throws Exception { - Set committedDecks = new TreeSet<>(committedPreviews()); - committedDecks.removeIf(name -> !name.endsWith(".pptx")); - Set renderedDecks = new TreeSet<>(generatedByName().keySet()); - renderedDecks.removeIf(name -> !name.endsWith(".pptx")); - - assertThat(committedDecks) - .describedAs("the committed decks and the curated list have to agree: a deck added " - + "to the folder without a decision, or removed from it without one, is " - + "exactly what this list exists to surface") - .isEqualTo(new TreeSet<>(CURATED_DECKS)); - - Set accountedFor = new TreeSet<>(CURATED_DECKS); - accountedFor.addAll(UNPUBLISHED_DECKS); - assertThat(renderedDecks) - .describedAs("the catalogue and the two lists have to cover each other exactly: a " - + "deck listed but not rendered is a committed file nothing can refresh, " - + "and a deck rendered but on neither list is one nobody has decided to " - + "publish or to leave out") + void theCatalogueIsExactlyThePublishedPreviewsPlusTheUnpublishedOnes() throws Exception { + Set committed = new TreeSet<>(committedPreviews()); + assertThat(committed) + .describedAs("a preview cannot be published and deliberately unpublished at once") + .doesNotContainAnyElementsOf(UNPUBLISHED_PREVIEWS); + + Set accountedFor = new TreeSet<>(committed); + accountedFor.addAll(UNPUBLISHED_PREVIEWS); + + Set rendered = new TreeSet<>(generatedByName().keySet()); + rendered.removeIf(name -> DOCUMENTS.stream().noneMatch(name::endsWith)); + + assertThat(rendered) + .describedAs("the catalogue, the committed folder and the unpublished list have to " + + "cover each other exactly. A document rendered but neither committed nor " + + "listed is one nobody decided about; a preview committed but no longer " + + "rendered is a file nothing can refresh; and a published preview deleted " + + "is a figure README loses — the one this comparison cannot see on its " + + "own, since a file that is gone is a file it never looks at") .isEqualTo(accountedFor); } diff --git a/examples/src/test/java/com/demcha/examples/support/AssetContent.java b/examples/src/test/java/com/demcha/examples/support/AssetContent.java index 272f6701d..5a816f659 100644 --- a/examples/src/test/java/com/demcha/examples/support/AssetContent.java +++ b/examples/src/test/java/com/demcha/examples/support/AssetContent.java @@ -41,25 +41,38 @@ private AssetContent() { } /** - * The parts a machine is allowed to disagree about, as {@code document!part}. + * The parts a machine is allowed to disagree about, and the renders it may disagree between. * *

One entry, and it earns itself: the showcase deck embeds an image of a text watermark, * and the same glyphs in the same places came out with different antialiasing coverage along * their edges on the two machines. Averaging it away is not available — 4-pixel blocks still - * differ by 138 of 255 — so the part is read by its size and the rest of the deck is read by - * its content. Anything added here stops being compared, so it wants the same measurement - * behind it that put this entry here.

+ * differ by 138 of 255.

+ * + *

What is exempted is not the part but the pair of renders. Each value is the pixel digest + * of one machine's version of that part; those two collapse to one token, and anything else — + * a different watermark, a swapped logo, an empty image of the same size — keeps its own + * digest and fails the comparison. Adding a key here without its digests would exempt the + * part itself, which is the hole this shape exists to close.

*/ - public static final Set UNSTABLE_PARTS = - Set.of("master-showcase.pptx!ppt/media/image1.png"); + public static final Map> UNSTABLE_PARTS = Map.of( + "master-showcase.pptx!ppt/media/image1.png", + Set.of("1f629c6a16dd2d5c18ead1594788ce04e0341360f57af68421b428d56cfb03a8", + "9b4a3b3d0dcae564393372bccb71430b0367b6c189e36bfff664a4a67f515225")); /** Extensions this can reduce; anything else is compared as the bytes it is. */ private static final Set PACKAGES = Set.of(".pptx", ".docx"); private static final Pattern PDF_ID = Pattern.compile("/ID \\[<[0-9A-Fa-f]+> <[0-9A-Fa-f]+>\\]"); - private static final Pattern XML_TIMESTAMP = - Pattern.compile(">\\d{4}-\\d{2}-\\d{2}T\\d{2}:\\d{2}:\\d{2}Z<"); + /** + * The one element in a package's properties that records when it was written. + * + *

Named rather than matched by shape: a pattern for "any ISO instant under + * {@code docProps}" would also drop a date somebody meant, and a document whose custom + * property is a date has every right to be compared by it.

+ */ + private static final Pattern CREATION_STAMP = + Pattern.compile("(]*>)[^<]*()"); /** * A stable identity for a rendered document. @@ -111,18 +124,37 @@ private static Map packageParts(String document, byte[] bytes) */ static byte[] part(String document, String name, byte[] content) throws IOException { if (name.startsWith("ppt/media/") || name.startsWith("word/media/")) { - return pixels(content, UNSTABLE_PARTS.contains(document + "!" + name)); + return raster(document + "!" + name, content); } if (!name.endsWith(".xml") && !name.endsWith(".rels")) { return content; } String text = new String(content, StandardCharsets.UTF_8).replace("\r\n", "\n"); - if (name.startsWith("docProps/")) { - text = XML_TIMESTAMP.matcher(text).replaceAll("><"); + if (name.equals("docProps/core.xml")) { + text = CREATION_STAMP.matcher(text).replaceAll("$1$2"); } return freeformsInSlideSpace(text).getBytes(StandardCharsets.UTF_8); } + /** + * A raster part read by its pixels, unless it is one of a pair a machine writes differently. + * + *

An exempted part is not waved through: its pixels still decide, and only the two renders + * written down in {@link #UNSTABLE_PARTS} collapse onto one token. Anything else keeps the + * digest of what it actually is, so an image swapped for another of the same size differs + * from both the token and the other machine's render.

+ */ + private static byte[] raster(String key, byte[] content) throws IOException { + String pixels = pixelDigest(content); + if (pixels == null) { + return content; + } + if (UNSTABLE_PARTS.getOrDefault(key, Set.of()).contains(pixels)) { + return ("a known render of " + key).getBytes(StandardCharsets.UTF_8); + } + return pixels.getBytes(StandardCharsets.UTF_8); + } + /** A PDF with the one thing in it that the clock writes taken out. */ static byte[] withoutPdfId(byte[] content) { String text = new String(content, StandardCharsets.ISO_8859_1); @@ -130,26 +162,24 @@ static byte[] withoutPdfId(byte[] content) { } /** - * A digest of an image's pixels in one colour model — or, for a named unstable part, of its - * size alone. + * A digest of an image's size and pixels in one colour model, or {@code null} if it is vector. * *

{@code getRGB} converts whatever the decoder produced into sRGB, so an image is compared * by what it looks like rather than by how it was stored. The pixels are read a row at a time * and folded into the digest rather than buffered: an embedded screenshot runs to megapixels, - * and holding two of those as byte arrays to compare them is a waste of a test's heap. A part - * that decodes to nothing is vector, and its bytes are its content.

+ * and holding two of those as byte arrays to compare them is a waste of a test's heap.

+ * + * @param content the bytes of the part + * @return a hex SHA-256 over the image, or {@code null} when nothing decodes it + * @throws IOException if the bytes cannot be read */ - static byte[] pixels(byte[] content, boolean sizeOnly) throws IOException { + public static String pixelDigest(byte[] content) throws IOException { BufferedImage image = ImageIO.read(new ByteArrayInputStream(content)); if (image == null) { - return content; + return null; } int width = image.getWidth(); int height = image.getHeight(); - if (sizeOnly) { - return "%dx%d antialiasing not compared".formatted(width, height) - .getBytes(StandardCharsets.UTF_8); - } ByteBuffer row = ByteBuffer.allocate(4 * width); MessageDigest digest = sha256(); digest.update("%dx%d".formatted(width, height).getBytes(StandardCharsets.UTF_8)); @@ -162,7 +192,7 @@ static byte[] pixels(byte[] content, boolean sizeOnly) throws IOException { } digest.update(row.array()); } - return digest.digest(); + return HexFormat.of().formatHex(digest.digest()); } private static final Pattern SHAPE = Pattern.compile(".*?", Pattern.DOTALL); diff --git a/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java b/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java index e13ebf268..74ccf6346 100644 --- a/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java +++ b/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java @@ -179,6 +179,66 @@ void theNamedUnstablePartAbsorbsTheMeasuredDifference() throws Exception { .isEqualTo(AssetContent.part(WATERMARK_DECK, WATERMARK_PART, linux)); } + /** + * The exemption is for two known renders, not for whatever occupies that path. + * + *

Exempting the part itself would let the watermark be swapped for another image of the + * same size — a different word, a logo, a blank — and the comparison would report a deck + * unchanged. Only the two digests written down collapse; a third image keeps its own.

+ */ + @Test + void anUnknownImageOfTheSameSizeIsNotAbsorbed() throws Exception { + BufferedImage watermark = decode(WATERMARK_WINDOWS); + byte[] blank = blankPng(watermark.getWidth(), watermark.getHeight()); + assertThat(AssetContent.pixelDigest(blank)) + .describedAs("the substitute must decode, or this proves nothing") + .isNotNull(); + + assertThat(AssetContent.part(WATERMARK_DECK, WATERMARK_PART, blank)) + .describedAs("an image the allowlist has never seen is compared by its pixels, " + + "however well it matches the size of the one that is exempt") + .isNotEqualTo(AssetContent.part(WATERMARK_DECK, WATERMARK_PART, + fixture(WATERMARK_WINDOWS))); + } + + /** + * The part the allowlist exempts is still the watermark that was measured. + * + *

Naming a path and pinning two digests protects nothing if the deck stopped carrying + * either of them: the entry would silently become an exemption for an image nobody has + * looked at. Both the committed deck and the one the catalogue renders have to hold a + * version the fixtures account for.

+ */ + @Test + void theAllowlistedPartInBothDecksIsAKnownRender() throws Exception { + Set known = AssetContent.UNSTABLE_PARTS.get(WATERMARK_DECK + "!" + WATERMARK_PART); + assertThat(known).describedAs("the allowlist entry has no digests").isNotEmpty(); + + for (Path deck : List.of(COMMITTED.resolve(WATERMARK_DECK), generated(WATERMARK_DECK))) { + assertThat(AssetContent.pixelDigest(bytesOfPart(deck, WATERMARK_PART))) + .describedAs("%s carries a watermark neither fixture accounts for — the " + + "exemption no longer describes what it exempts", deck) + .isIn(known); + } + } + + /** A date somebody wrote is not a date the machine wrote. */ + @Test + void aCustomDatePropertyIsNotAbsorbed() throws Exception { + String properties = "%s" + + ""; + byte[] earlier = properties.formatted("2026-01-01T00:00:00Z") + .getBytes(StandardCharsets.UTF_8); + byte[] later = properties.formatted("2026-08-02T00:00:00Z") + .getBytes(StandardCharsets.UTF_8); + + assertThat(AssetContent.part("word-export-companion.docx", "docProps/custom.xml", earlier)) + .describedAs("only the element that records when the package was written is " + + "dropped; a date the document states is the document's") + .isNotEqualTo(AssetContent.part("word-export-companion.docx", + "docProps/custom.xml", later)); + } + /** * A part written with either line ending is one part. * @@ -223,7 +283,7 @@ void theCreationStampIsNotPartOfTheDocument() throws Exception { */ @Test void everyAllowlistedPartExists() throws Exception { - for (String entry : new TreeSet<>(AssetContent.UNSTABLE_PARTS)) { + for (String entry : new TreeSet<>(AssetContent.UNSTABLE_PARTS.keySet())) { String[] split = entry.split("!", 2); assertThat(split).describedAs("%s is not document!part", entry).hasSize(2); for (Path document : List.of(COMMITTED.resolve(split[0]), generated(split[0]))) { @@ -257,18 +317,29 @@ private static Set partNames(Path archive) throws IOException { } private static String part(Path archive, String name) throws IOException { + return new String(bytesOfPart(archive, name), StandardCharsets.UTF_8).replace("\r\n", "\n"); + } + + private static byte[] bytesOfPart(Path archive, String name) throws IOException { try (ZipInputStream zip = new ZipInputStream(new ByteArrayInputStream(Files.readAllBytes(archive)))) { for (ZipEntry entry = zip.getNextEntry(); entry != null; entry = zip.getNextEntry()) { if (entry.getName().equals(name)) { - return new String(zip.readAllBytes(), StandardCharsets.UTF_8) - .replace("\r\n", "\n"); + return zip.readAllBytes(); } } } throw new IllegalStateException(archive + " has no part " + name); } + /** A blank image of a given size — a stand-in for whatever else could occupy that path. */ + private static byte[] blankPng(int width, int height) throws IOException { + BufferedImage blank = new BufferedImage(width, height, BufferedImage.TYPE_INT_ARGB); + var bytes = new java.io.ByteArrayOutputStream(); + ImageIO.write(blank, "png", bytes); + return bytes.toByteArray(); + } + private static byte[] fixture(String name) throws IOException { try (var in = AssetContentTest.class.getResourceAsStream("/pptx-media/" + name)) { assertThat(in).describedAs("missing test fixture %s", name).isNotNull(); From 888fa900b732ab3d4ea6a832be31f2ee684c3619 Mon Sep 17 00:00:00 2001 From: DemchaAV Date: Sun, 2 Aug 2026 20:34:27 +0100 Subject: [PATCH 5/5] fix(examples): stop rewriting every freeform to absorb two of them MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Reading each freeform's path in slide coordinates dropped the box those coordinates were measured against — and a box is not bookkeeping. It is the centre a rot turns a shape around and the axis a flipH mirrors it in, and five of the six committed decks carry one or the other. Two shapes with the same path and the same angle in differently sized boxes turn about different points and draw differently; the comparison called them one shape. The scale it applied came from the first path in a shape and was used for every point in it, which is wrong the moment a shape has two paths with different extents. Comparing all six decks shape by shape against a render from the runner found the difference in exactly two shapes, both in one deck. Those two are named now, with the digest of each machine's version, the way the unstable watermark already was — so the exemption covers a pair of renders rather than a kind of shape, and every other freeform is compared as it was written. The preview folder was listed for its files, with folders stepped over in silence: a preview one level down was in none of the sets these tests compare. The folder is asserted flat, which is how the site reads it. --- .../examples/CommittedAssetDriftTest.java | 19 ++- .../demcha/examples/support/AssetContent.java | 128 ++++++++---------- .../examples/support/AssetContentTest.java | 92 ++++++++----- 3 files changed, 132 insertions(+), 107 deletions(-) diff --git a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java index 1e4b9a36e..e47ce9524 100644 --- a/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java +++ b/examples/src/test/java/com/demcha/examples/CommittedAssetDriftTest.java @@ -215,13 +215,26 @@ void theCatalogueIsExactlyThePublishedPreviewsPlusTheUnpublishedOnes() throws Ex .isEqualTo(accountedFor); } + /** + * The preview folder's files, and it has to be flat to have only files. + * + *

Listing it and keeping the regular files would step over a folder in silence, and a + * preview one level down would be in none of the sets these tests compare: not held to the + * catalogue, not published, not unpublished, not named as something that cannot be compared. + * The site reads this folder flat, so a folder inside it is a mistake worth naming rather + * than passing over.

+ */ private static Set committedPreviews() throws IOException { Set names = new TreeSet<>(); + Set folders = new TreeSet<>(); try (var files = Files.list(PREVIEWS)) { - files.filter(Files::isRegularFile) - .map(path -> path.getFileName().toString()) - .forEach(names::add); + files.forEach(path -> (Files.isDirectory(path) ? folders : names) + .add(path.getFileName().toString())); } + assertThat(folders) + .describedAs("%s is read flat, so a folder inside it holds previews nothing " + + "compares: move them up beside the others", PREVIEWS) + .isEmpty(); return names; } diff --git a/examples/src/test/java/com/demcha/examples/support/AssetContent.java b/examples/src/test/java/com/demcha/examples/support/AssetContent.java index 5a816f659..8811163db 100644 --- a/examples/src/test/java/com/demcha/examples/support/AssetContent.java +++ b/examples/src/test/java/com/demcha/examples/support/AssetContent.java @@ -30,10 +30,11 @@ * *

Everything this drops was measured on this repository's own catalogue rather than assumed, * by rendering it on Windows and on the Linux runner at the same version: of 104 documents, 99 - * came out byte-identical under the reduction below. The five that did not are named in - * {@link #UNSTABLE_PARTS} or are decks the repository does not commit — see there for what each - * one was. So the comparison stays exact: no tolerance, no sampling, and every exemption is a - * line somebody had to write.

+ * came out byte-identical under the reduction below. The five that did not are accounted for by + * {@link #UNSTABLE_PARTS} and {@link #UNSTABLE_SHAPES} or are decks the repository does not + * commit. So the comparison stays exact: no tolerance, no sampling, and every exemption names the + * one thing it covers and the two renders it covers it between — never a rule applied to + * everything of that kind.

*/ public final class AssetContent { @@ -59,6 +60,29 @@ private AssetContent() { Set.of("1f629c6a16dd2d5c18ead1594788ce04e0341360f57af68421b428d56cfb03a8", "9b4a3b3d0dcae564393372bccb71430b0367b6c189e36bfff664a4a67f515225")); + /** + * The shapes a machine is allowed to disagree about, as {@code document!part!shape}. + * + *

Two, and both in one deck: the box a freeform declares, and the origin its points are + * measured from, came out differently on the two machines — one icon with every point shifted + * by a constant 272 EMU across and 489 down and its extent smaller by exactly as much, so the + * path landed on the same place on the slide to the unit. Comparing all six committed decks + * shape by shape found these two and nothing else.

+ * + *

Like {@link #UNSTABLE_PARTS}, the value is the digest of each machine's version, so the + * exemption is for the pair of renders rather than for the shape: change either one and it + * stops being absorbed. Every other shape in every deck is compared as it was written, + * including the box — which is not decoration, but the centre a {@code rot} turns a shape + * around and the axis a {@code flipH} mirrors it in.

+ */ + public static final Map> UNSTABLE_SHAPES = Map.of( + "twin-output.pptx!ppt/slides/slide1.xml!Freeform 41", + Set.of("18dacede01e07a4408f19b8f2a6fc072887e6d3f5796064db4caf1b933a0f741", + "4d126750bda49ababc68b4020984bc1295d7e94b02dd2629bc3d971d05346656"), + "twin-output.pptx!ppt/slides/slide1.xml!Freeform 51", + Set.of("c59aeb5cc0676a9dc35e01aff745d0b2d66500ab7dfca4afb24d4f4a60c15262", + "33904bc4a82c84c86384832ca4b6c9cad738bcca558d756a43d25b9676e1b65e")); + /** Extensions this can reduce; anything else is compared as the bytes it is. */ private static final Set PACKAGES = Set.of(".pptx", ".docx"); @@ -118,9 +142,9 @@ private static Map packageParts(String document, byte[] bytes) * *

Three differences were measured between a package written on Windows and the same * package written on the runner, none of them a change to the document: the platform's line - * separator after each XML declaration, the creation stamp in {@code docProps}, and the box a - * freeform's path is normalised against. The first two are dropped; the third is why the - * points are read where they land — see {@link #freeformsInSlideSpace}.

+ * separator after each XML declaration, the creation stamp in {@code docProps}, and two + * freeform shapes in one deck. The first two are dropped wherever they appear; the third is + * dropped only for those two shapes — see {@link #UNSTABLE_SHAPES}.

*/ static byte[] part(String document, String name, byte[] content) throws IOException { if (name.startsWith("ppt/media/") || name.startsWith("word/media/")) { @@ -133,7 +157,8 @@ static byte[] part(String document, String name, byte[] content) throws IOExcept if (name.equals("docProps/core.xml")) { text = CREATION_STAMP.matcher(text).replaceAll("$1$2"); } - return freeformsInSlideSpace(text).getBytes(StandardCharsets.UTF_8); + return knownShapesCollapsed(document + "!" + name, text) + .getBytes(StandardCharsets.UTF_8); } /** @@ -196,81 +221,46 @@ public static String pixelDigest(byte[] content) throws IOException { } private static final Pattern SHAPE = Pattern.compile(".*?", Pattern.DOTALL); - private static final Pattern OFFSET = - Pattern.compile(""); - private static final Pattern EXTENT = Pattern.compile(""); - private static final Pattern PATH_TAG = Pattern.compile("]*)>"); - private static final Pattern POINT = Pattern.compile(""); - private static final Pattern BOX_ATTRIBUTE = Pattern.compile(" [wh]=\"\\d+\""); - private static final Pattern PATH_WIDTH = Pattern.compile("\\bw=\"(\\d+)\""); - private static final Pattern PATH_HEIGHT = Pattern.compile("\\bh=\"(\\d+)\""); + private static final Pattern SHAPE_NAME = Pattern.compile("The box a freeform declares, and the origin its coordinates are measured from, are not - * stable: one icon came out with every point shifted by a constant 272 EMU across and 489 - * down and its declared extent smaller by exactly as much, so that the path landed on the - * same place on the slide to the unit. Only the normalisation moved. A freeform that - * actually moves still moves its points.

+ *

An earlier version of this rewrote every freeform: it read the path in slide + * coordinates and dropped the box those coordinates were measured against. That absorbed the + * measured difference and a good deal more — the box a shape declares is also the centre a + * {@code rot} turns it around and the axis a {@code flipH} mirrors it in, and five of the six + * committed decks carry one or the other. Two shapes drawn around different centres would + * have compared equal while PowerPoint drew them differently.

+ * + *

What was actually measured is two shapes in one deck, so two shapes in one deck are what + * is exempted — by the digest of each machine's version of them, the way an unstable image + * is. Every other shape is compared as it was written.

*/ - static String freeformsInSlideSpace(String xml) { + static String knownShapesCollapsed(String part, String xml) { + if (UNSTABLE_SHAPES.keySet().stream().noneMatch(key -> key.startsWith(part + "!"))) { + return xml; + } Matcher shapes = SHAPE.matcher(xml); StringBuilder out = new StringBuilder(); while (shapes.find()) { - shapes.appendReplacement(out, Matcher.quoteReplacement(inSlideSpace(shapes.group()))); + shapes.appendReplacement(out, Matcher.quoteReplacement(collapse(part, shapes.group()))); } shapes.appendTail(out); return out.toString(); } - private static String inSlideSpace(String shape) { - Matcher offset = OFFSET.matcher(shape); - Matcher extent = EXTENT.matcher(shape); - Matcher pathTag = PATH_TAG.matcher(shape); - if (!offset.find() || !extent.find() || !pathTag.find()) { + private static String collapse(String part, String shape) { + Matcher name = SHAPE_NAME.matcher(shape); + if (!name.find()) { return shape; } - long pathWidth = attribute(PATH_WIDTH, pathTag.group(1)); - long pathHeight = attribute(PATH_HEIGHT, pathTag.group(1)); - if (pathWidth <= 0 || pathHeight <= 0) { - return shape; - } - long offsetX = Long.parseLong(offset.group(1)); - long offsetY = Long.parseLong(offset.group(2)); - double scaleX = Long.parseLong(extent.group(1)) / (double) pathWidth; - double scaleY = Long.parseLong(extent.group(2)) / (double) pathHeight; - - Matcher points = POINT.matcher(shape); - StringBuilder out = new StringBuilder(); - while (points.find()) { - long x = offsetX + Math.round(Long.parseLong(points.group(1)) * scaleX); - long y = offsetY + Math.round(Long.parseLong(points.group(2)) * scaleY); - points.appendReplacement(out, - Matcher.quoteReplacement("".formatted(x, y))); - } - points.appendTail(out); - - String slideSpace = OFFSET.matcher(out.toString()).replaceFirst(""); - slideSpace = EXTENT.matcher(slideSpace).replaceFirst(""); - return withoutPathBox(slideSpace); - } - - private static String withoutPathBox(String shape) { - Matcher tags = PATH_TAG.matcher(shape); - StringBuilder out = new StringBuilder(); - while (tags.find()) { - String rest = BOX_ATTRIBUTE.matcher(" " + tags.group(1)).replaceAll("").trim(); - tags.appendReplacement(out, Matcher.quoteReplacement( - rest.isEmpty() ? "" : "")); - } - tags.appendTail(out); - return out.toString(); - } - - private static long attribute(Pattern attribute, String attributes) { - Matcher value = attribute.matcher(attributes); - return value.find() ? Long.parseLong(value.group(1)) : -1; + String key = part + "!" + name.group(1); + String digest = HexFormat.of().formatHex( + sha256().digest(shape.getBytes(StandardCharsets.UTF_8))); + return UNSTABLE_SHAPES.getOrDefault(key, Set.of()).contains(digest) + ? "
" + : shape; } private static MessageDigest sha256() { diff --git a/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java b/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java index 74ccf6346..362a356cb 100644 --- a/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java +++ b/examples/src/test/java/com/demcha/examples/support/AssetContentTest.java @@ -39,6 +39,7 @@ class AssetContentTest { private static final String WATERMARK_LINUX = "watermark-linux.png"; private static final String WATERMARK_DECK = "master-showcase.pptx"; private static final String WATERMARK_PART = "ppt/media/image1.png"; + private static final String TWIN_SLIDE = "twin-output.pptx!ppt/slides/slide1.xml"; @BeforeAll static void generateEveryExample() throws Exception { @@ -88,48 +89,67 @@ void aPdfCarryingADifferentIdentifierIsTheSameDocument() throws Exception { } /** - * The same path drawn in a different box is one shape; a path that moves is not. + * Two shapes drawn around different centres are two shapes. * - *

Dropping the box is the load-bearing half of the comparison, so it is checked in both - * directions: the shapes below are the same freeform normalised the two ways two machines - * normalised it, and then genuinely moved. Absorbing the third as well would leave the gate - * reporting success on a deck whose artwork had shifted.

+ *

An earlier comparison read every freeform's path in slide coordinates and dropped the box + * those coordinates were measured against. The paths below land in the same place and turn by + * the same angle, and they are still different pictures: {@code rot} turns a shape around the + * centre of its box, so a 100-wide box and a 200-wide one spin the same line about different + * points. Nothing may collapse them.

*/ @Test - void theBoxIsAbsorbedButAMoveIsNot() { - String box = freeform("", - "", ""); - String sameLineOtherBox = freeform( - "", - "", ""); - String movedLine = freeform("", - "", ""); - - assertThat(AssetContent.freeformsInSlideSpace(sameLineOtherBox)) - .describedAs("the same line from (1000,2000) to (1600,2400), normalised against " - + "an origin 50 EMU away — the difference two machines produce") - .isEqualTo(AssetContent.freeformsInSlideSpace(box)); - assertThat(AssetContent.freeformsInSlideSpace(movedLine)) - .describedAs("a line that actually moved by 50 EMU must not be absorbed") - .isNotEqualTo(AssetContent.freeformsInSlideSpace(box)); + void rotatedFreeformsWithDifferentPivotsAreNotEqual() { + String narrow = rotatedFreeform(""); + String wide = rotatedFreeform(""); + + assertThat(AssetContent.knownShapesCollapsed(TWIN_SLIDE, wide)) + .describedAs("the same line turned 90 degrees about a different centre is not the " + + "same picture, however the points are written") + .isNotEqualTo(AssetContent.knownShapesCollapsed(TWIN_SLIDE, narrow)); } /** - * The rewrite reaches the markup POI actually writes, not just the shape of it. + * The two shapes the allowlist names collapse; a shape it does not name is untouched. * - *

{@link #theBoxIsAbsorbedButAMoveIsNot} works on markup written here, so it would still - * pass if POI reordered an attribute and the rewrite quietly stopped matching — leaving a - * comparison that absorbs nothing and a red gate nobody can read. A deck the repository - * commits has to come out changed.

+ *

What was measured is two freeforms in one deck, so two freeforms in one deck are what is + * exempted. A shape reaching this with any other name — or either of these two after somebody + * changed it — comes out of the comparison exactly as it went in.

*/ @Test - void theRewriteReachesRealMarkup() throws Exception { + void onlyTheNamedShapesCollapse() throws Exception { String slide = part(COMMITTED.resolve("twin-output.pptx"), "ppt/slides/slide1.xml"); + String collapsed = AssetContent.knownShapesCollapsed(TWIN_SLIDE, slide); + + assertThat(collapsed) + .describedAs("the two measured shapes have to be recognised in the deck they were " + + "measured in, or the exemption describes nothing") + .contains("Freeform 41/>", "Freeform 51/>"); + assertThat(collapsed) + .describedAs("a shape the allowlist does not name is compared as it was written") + .contains("The digests are of what two machines wrote at the time they were measured. If the example + * moves either shape, both sides move together and the pair silently stops being exempt — the + * comparison would go back to reading them and fail on a runner for a reason the entry was + * added to explain.

+ */ + @Test + void theNamedShapesAreStillWhatBothDecksCarry() throws Exception { + for (Path deck : List.of(COMMITTED.resolve("twin-output.pptx"), + generated("twin-output.pptx"))) { + String slide = part(deck, "ppt/slides/slide1.xml"); + assertThat(AssetContent.knownShapesCollapsed(TWIN_SLIDE, slide)) + .describedAs("%s no longer carries the shapes the allowlist was measured on", + deck) + .contains("Freeform 41/>", "Freeform 51/>"); + } } /** @@ -353,11 +373,13 @@ private static BufferedImage decode(String name) throws IOException { return image; } - private static String freeform(String xfrm, String from, String to) { + /** One line, turned a quarter turn, inside whatever box the caller gives it. */ + private static String rotatedFreeform(String xfrm) { return "" - + "" + xfrm + "" - + "" - + "" + from + "" + to + "" + + "" + xfrm + "" + + "" + + "" + + "" + ""; } }