-
-
Notifications
You must be signed in to change notification settings - Fork 8
Expand file tree
/
Copy pathInternalEnginePackageMarkerTest.java
More file actions
173 lines (152 loc) · 7.78 KB
/
Copy pathInternalEnginePackageMarkerTest.java
File metadata and controls
173 lines (152 loc) · 7.78 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
package com.demcha.documentation;
import static org.assertj.core.api.Assertions.assertThat;
import java.io.IOException;
import java.lang.reflect.Modifier;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.ArrayList;
import java.util.List;
import java.util.stream.Stream;
import org.junit.jupiter.api.Test;
import com.demcha.compose.document.api.Internal;
/**
* Module-local twin of the engine's {@code InternalAnnotationCoverageTest}, and of the
* copy in {@code graph-compose-render-pdf}.
*
* <p>This module ships no {@code com.demcha.compose.engine.*} package today — its whole
* public surface is {@code document.backend.fixed.pptx.*}, which is Experimental and
* carries {@code @Beta}. The guard is here as a tripwire rather than a description of
* today: the PPTX backend is the fixed-layout twin of the PDF one, and the PDF backend
* does keep part of itself under {@code engine.render.pdf}, so the day a PPTX sibling
* lands the marker has to land with it. The scan it runs is proven live by the
* self-check below, so an armed-but-idle guard is not a guard that silently stopped
* looking.</p>
*
* <p>Two assertions, because the marker has two jobs. The package-level one is what
* policy and the japicmp excludes hang off. The type-level one is what a reader sees:
* Javadoc renders a package annotation on the package-summary page and nowhere else, so
* a class in a marked package still publishes an unqualified public class page unless it
* declares {@code @Internal} itself.</p>
*
* <p>Both lists are derived from the source tree rather than hard-coded, so a
* <em>new</em> {@code com.demcha.compose.engine.*} package or public type fails the
* build until it carries the marker.</p>
*/
class InternalEnginePackageMarkerTest {
/** The package prefix {@code docs/api-stability.md} maps to the Internal tier. */
private static final String ENGINE_ROOT = "com.demcha.compose.engine";
/** Surefire runs with the module directory as the working directory. */
private static final Path SOURCE_ROOT = Path.of("src/main/java");
@Test
void everyEnginePackageInThisModuleCarriesTheInternalMarker() throws IOException {
List<String> mainPackages = packagesUnderMainSources();
// Without this, a scan that silently found nothing would report zero
// unmarked packages below and pass while inspecting nothing at all.
assertThat(mainPackages)
.describedAs("Found no packages under %s — the engine assertion below would"
+ " then pass without inspecting anything", SOURCE_ROOT)
.isNotEmpty();
List<String> unmarked = mainPackages.stream()
.filter(InternalEnginePackageMarkerTest::isEnginePackage)
.filter(pkg -> !isMarkedInternal(pkg))
.toList();
assertThat(unmarked)
.describedAs("docs/api-stability.md puts %s.* in the Internal tier — removable"
+ " in any release, with no deprecation window. These packages ship"
+ " from this module without the package-level @Internal marker, so"
+ " their published Javadoc reads as a supported surface", ENGINE_ROOT)
.isEmpty();
}
/** Every package under {@code src/main/java} that holds at least one source file. */
private static List<String> packagesUnderMainSources() throws IOException {
try (Stream<Path> tree = Files.walk(SOURCE_ROOT)) {
return tree.filter(Files::isRegularFile)
.filter(file -> file.getFileName().toString().endsWith(".java"))
.map(InternalEnginePackageMarkerTest::packageOf)
.distinct()
.sorted()
.toList();
}
}
@Test
void everyPublicTypeInThoseEnginePackagesCarriesItToo() throws IOException {
List<String> enginePackages = packagesUnderMainSources().stream()
.filter(InternalEnginePackageMarkerTest::isEnginePackage)
.toList();
List<String> publicTypes = publicTopLevelTypesInEnginePackages();
// A silently-empty type scan would satisfy the assertion below without reading
// a single class; an engine package with no public type in it cannot happen.
assertThat(enginePackages.isEmpty() || !publicTypes.isEmpty())
.describedAs("Found engine packages %s but no public type in any of them —"
+ " the type scan is not reading this module", enginePackages)
.isTrue();
List<String> unmarked = new ArrayList<>();
for (String type : publicTypes) {
if (loadClass(type).getAnnotation(Internal.class) == null) {
unmarked.add(type);
}
}
assertThat(unmarked)
.describedAs("These public types sit in an Internal-tier package but declare"
+ " no @Internal of their own, so their published Javadoc page carries"
+ " no marker at all")
.isEmpty();
}
/** Top-level public types declared in this module's engine packages. */
private static List<String> publicTopLevelTypesInEnginePackages() throws IOException {
List<String> types = new ArrayList<>();
try (Stream<Path> tree = Files.walk(SOURCE_ROOT)) {
for (Path file : tree.filter(Files::isRegularFile).toList()) {
String fileName = file.getFileName().toString();
if (!fileName.endsWith(".java") || fileName.equals("package-info.java")) {
continue;
}
String pkg = packageOf(file);
if (!isEnginePackage(pkg)) {
continue;
}
String name = pkg + "." + fileName.substring(0, fileName.length() - ".java".length());
if (Modifier.isPublic(loadClass(name).getModifiers())) {
types.add(name);
}
}
}
return types;
}
private static Class<?> loadClass(String binaryName) {
try {
return Class.forName(binaryName, false,
InternalEnginePackageMarkerTest.class.getClassLoader());
} catch (ClassNotFoundException e) {
throw new AssertionError(
"Source file for " + binaryName + " exists but nothing compiled to it", e);
}
}
/** The package a source file under {@link #SOURCE_ROOT} declares, by its location. */
private static String packageOf(Path sourceFile) {
Path directory = SOURCE_ROOT.relativize(sourceFile.getParent());
return directory.toString().replace(directory.getFileSystem().getSeparator(), ".");
}
private static boolean isEnginePackage(String packageName) {
return packageName.equals(ENGINE_ROOT) || packageName.startsWith(ENGINE_ROOT + ".");
}
/**
* Reads the package-level marker off the compiled {@code package-info} class, which
* is where javac stores package annotations. Anything the reflection cannot confirm
* counts as unmarked, so the guard fails closed.
*/
private static boolean isMarkedInternal(String packageName) {
try {
Class<?> packageInfo = Class.forName(
packageName + ".package-info",
false,
InternalEnginePackageMarkerTest.class.getClassLoader());
return packageInfo.getAnnotation(Internal.class) != null;
} catch (ClassNotFoundException noCompiledPackageInfo) {
// javac emits package-info.class only for an annotated package
// declaration, so a package carrying prose alone lands here — exactly
// the gap this guard exists to catch.
return false;
}
}
}