diff --git a/src/test/java/com/demcha/documentation/EnginePoiIsolationGuardTest.java b/src/test/java/com/demcha/documentation/EnginePoiIsolationGuardTest.java new file mode 100644 index 00000000..86d00eea --- /dev/null +++ b/src/test/java/com/demcha/documentation/EnginePoiIsolationGuardTest.java @@ -0,0 +1,92 @@ +package com.demcha.documentation; + +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.ArrayList; +import java.util.LinkedHashMap; +import java.util.List; +import java.util.Map; +import java.util.Set; +import java.util.TreeSet; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Architectural guard that keeps Apache POI out of the lean engine module. + * + *

The semantic DOCX/PPTX backend — the only code that ever touched POI — + * lives in the separate {@code graph-compose-render-docx} module. The engine + * (root module) must stay free of every {@code org.apache.poi.*} import or + * fully-qualified reference so it never drags {@code poi-ooxml} back onto its + * classpath.

+ * + *

This freezes that invariant at the source level: the retired {@code no-poi} + * CI job only ran the suite without POI on the classpath, it never + * asserted the sources were POI-free. Mirrors + * {@link PdfBackendIsolationGuardTest}'s grep-style walk/assert.

+ */ +class EnginePoiIsolationGuardTest { + + private static final Path PROJECT_ROOT = Path.of("").toAbsolutePath().normalize(); + private static final String POI_IMPORT_PREFIX = "import org.apache.poi."; + private static final String POI_REFERENCE = "org.apache.poi."; + + private static final List ENGINE_ROOTS = List.of( + PROJECT_ROOT.resolve("src/main/java/com/demcha/compose"), + PROJECT_ROOT.resolve("src/test/java/com/demcha/compose")); + + @Test + void poiShouldStayOutOfTheEngineModule() throws IOException { + Map> violations = new LinkedHashMap<>(); + for (Path file : engineJavaFiles()) { + Set references = poiReferencesIn(file); + if (!references.isEmpty()) { + violations.put(relative(file), references); + } + } + + assertThat(violations) + .describedAs("Apache POI must stay out of the lean engine module. The semantic " + + "DOCX/PPTX backend lives in graph-compose-render-docx — keep every " + + "org.apache.poi.* import and reference there, not under com.demcha.compose.") + .isEmpty(); + } + + private List engineJavaFiles() throws IOException { + List files = new ArrayList<>(); + for (Path root : ENGINE_ROOTS) { + if (Files.notExists(root)) { + continue; + } + try (var stream = Files.walk(root)) { + stream.filter(Files::isRegularFile) + .filter(p -> p.toString().endsWith(".java")) + .forEach(files::add); + } + } + return files; + } + + private Set poiReferencesIn(Path file) throws IOException { + Set references = new TreeSet<>(); + for (String line : Files.readAllLines(file)) { + String trimmed = line.trim(); + if (trimmed.startsWith(POI_IMPORT_PREFIX)) { + int semicolon = trimmed.indexOf(';'); + if (semicolon >= 0) { + references.add(trimmed.substring("import ".length(), semicolon).trim()); + } + } else if (trimmed.contains(POI_REFERENCE)) { + references.add(trimmed); + } + } + return references; + } + + private String relative(Path path) { + return PROJECT_ROOT.relativize(path).toString().replace('\\', '/'); + } +}