16
16
*/
17
17
package org .apache .logging .log4j .core .config .plugins .processor ;
18
18
19
+ import static java .nio .charset .StandardCharsets .UTF_8 ;
19
20
import static java .util .Arrays .asList ;
20
21
import static java .util .Collections .emptyList ;
21
22
import static java .util .Collections .singletonList ;
23
+ import static java .util .Objects .requireNonNull ;
22
24
import static net .javacrumbs .jsonunit .assertj .JsonAssertions .assertThatJson ;
23
25
import static net .javacrumbs .jsonunit .assertj .JsonAssertions .json ;
24
26
import static org .assertj .core .api .Assertions .assertThat ;
27
+ import static org .mockito .Mockito .when ;
25
28
29
+ import java .io .File ;
26
30
import java .io .IOException ;
27
31
import java .net .URL ;
28
- import java .nio .charset .StandardCharsets ;
32
+ import java .nio .file .Files ;
33
+ import java .nio .file .Path ;
34
+ import java .nio .file .Paths ;
35
+ import java .util .ArrayList ;
29
36
import java .util .Collections ;
30
37
import java .util .LinkedHashMap ;
38
+ import java .util .List ;
39
+ import java .util .Locale ;
31
40
import java .util .Map ;
41
+ import java .util .stream .Collectors ;
32
42
import java .util .stream .Stream ;
33
- import org .apache .commons .io .IOUtils ;
43
+ import javax .annotation .processing .Messager ;
44
+ import javax .annotation .processing .ProcessingEnvironment ;
45
+ import javax .lang .model .util .Elements ;
46
+ import javax .tools .Diagnostic ;
47
+ import javax .tools .DiagnosticCollector ;
48
+ import javax .tools .JavaCompiler ;
49
+ import javax .tools .JavaFileObject ;
50
+ import javax .tools .StandardJavaFileManager ;
51
+ import javax .tools .StandardLocation ;
52
+ import javax .tools .ToolProvider ;
53
+ import org .jspecify .annotations .Nullable ;
34
54
import org .junit .jupiter .api .BeforeAll ;
55
+ import org .junit .jupiter .api .Test ;
56
+ import org .junit .jupiter .api .io .CleanupMode ;
57
+ import org .junit .jupiter .api .io .TempDir ;
35
58
import org .junit .jupiter .params .ParameterizedTest ;
36
59
import org .junit .jupiter .params .provider .Arguments ;
37
60
import org .junit .jupiter .params .provider .MethodSource ;
61
+ import org .mockito .Mockito ;
38
62
39
63
class GraalVmProcessorTest {
40
64
65
+ private static final String FAKE_PLUGIN_NAME = "example.FakePlugin" ;
41
66
private static final Object FAKE_PLUGIN = asMap (
42
67
"name" ,
43
- FakePlugin . class . getName () ,
68
+ FAKE_PLUGIN_NAME ,
44
69
"methods" ,
45
70
asList (
46
71
asMap ("name" , "<init>" , "parameterTypes" , emptyList ()),
@@ -57,9 +82,10 @@ class GraalVmProcessorTest {
57
82
"java.lang.String" ))),
58
83
"fields" ,
59
84
emptyList ());
85
+ private static final String FAKE_PLUGIN_BUILDER_NAME = FAKE_PLUGIN_NAME + "$Builder" ;
60
86
private static final Object FAKE_PLUGIN_BUILDER = asMap (
61
87
"name" ,
62
- FakePlugin . Builder . class . getName () ,
88
+ FAKE_PLUGIN_BUILDER_NAME ,
63
89
"methods" ,
64
90
emptyList (),
65
91
"fields" ,
@@ -71,21 +97,27 @@ class GraalVmProcessorTest {
71
97
asMap ("name" , "loggerContext" ),
72
98
asMap ("name" , "node" ),
73
99
asMap ("name" , "value" )));
74
- private static final Object FAKE_PLUGIN_NESTED = onlyNoArgsConstructor (FakePlugin .Nested .class );
75
- private static final Object FAKE_CONSTRAINT_VALIDATOR =
76
- onlyNoArgsConstructor (FakeAnnotations .FakeConstraintValidator .class );
77
- private static final Object FAKE_PLUGIN_VISITOR = onlyNoArgsConstructor (FakeAnnotations .FakePluginVisitor .class );
100
+ private static final String FAKE_PLUGIN_NESTED_NAME = FAKE_PLUGIN_NAME + "$Nested" ;
101
+ private static final Object FAKE_PLUGIN_NESTED = onlyNoArgsConstructor (FAKE_PLUGIN_NESTED_NAME );
102
+ private static final String FAKE_CONSTRAINT_VALIDATOR_NAME = "example.FakeAnnotations$FakeConstraintValidator" ;
103
+ private static final Object FAKE_CONSTRAINT_VALIDATOR = onlyNoArgsConstructor (FAKE_CONSTRAINT_VALIDATOR_NAME );
104
+ private static final String FAKE_PLUGIN_VISITOR_NAME = "example.FakeAnnotations$FakePluginVisitor" ;
105
+ private static final Object FAKE_PLUGIN_VISITOR = onlyNoArgsConstructor (FAKE_PLUGIN_VISITOR_NAME );
106
+
107
+ private static final String GROUP_ID = "groupId" ;
108
+ private static final String ARTIFACT_ID = "artifactId" ;
109
+ private static final String FALLBACK_METADATA_FOLDER = "fooBar" ;
78
110
79
111
/**
80
112
* Generates a metadata element with just a single no-arg constructor.
81
113
*
82
- * @param clazz The name of the metadata element.
114
+ * @param className The name of the metadata element.
83
115
* @return A GraalVM metadata element.
84
116
*/
85
- private static Object onlyNoArgsConstructor (Class <?> clazz ) {
117
+ private static Object onlyNoArgsConstructor (String className ) {
86
118
return asMap (
87
119
"name" ,
88
- clazz . getName () ,
120
+ className ,
89
121
"methods" ,
90
122
singletonList (asMap ("name" , "<init>" , "parameterTypes" , emptyList ())),
91
123
"fields" ,
@@ -103,43 +135,131 @@ private static Object onlyNoArgsConstructor(Class<?> clazz) {
103
135
return map ;
104
136
}
105
137
106
- private static String reachabilityMetadata ;
138
+ private static Path sourceDir ;
139
+
140
+ @ TempDir
141
+ private static Path outputDir ;
107
142
108
143
@ BeforeAll
109
- static void setup () throws IOException {
110
- // There are two descriptors, choose the one in `test-classes`
111
- URL reachabilityMetadataUrl = null ;
112
- for (URL url : Collections .list (GraalVmProcessor .class
113
- .getClassLoader ()
114
- .getResources ("META-INF/native-image/org.apache.logging.log4j/log4j-core-test/reflect-config.json" ))) {
115
- if (url .getPath ().contains ("test-classes" )) {
116
- reachabilityMetadataUrl = url ;
117
- break ;
118
- }
119
- }
120
- assertThat (reachabilityMetadataUrl ).isNotNull ();
121
- reachabilityMetadata = IOUtils .toString (reachabilityMetadataUrl , StandardCharsets .UTF_8 );
144
+ static void setup () throws Exception {
145
+ URL sourceUrl = requireNonNull (GraalVmProcessorTest .class .getResource ("/GraalVmProcessorTest/java" ));
146
+ sourceDir = Paths .get (sourceUrl .toURI ());
147
+ // Generate metadata
148
+ List <String > diagnostics = generateDescriptor (sourceDir , GROUP_ID , ARTIFACT_ID , outputDir );
149
+ assertThat (diagnostics ).isEmpty ();
122
150
}
123
151
124
152
static Stream <Arguments > containsSpecificEntries () {
125
153
return Stream .of (
126
- Arguments .of (FakePlugin . class , FAKE_PLUGIN ),
127
- Arguments .of (FakePlugin . Builder . class , FAKE_PLUGIN_BUILDER ),
128
- Arguments .of (FakePlugin . Nested . class , FAKE_PLUGIN_NESTED ),
129
- Arguments .of (FakeAnnotations . FakeConstraintValidator . class , FAKE_CONSTRAINT_VALIDATOR ),
130
- Arguments .of (FakeAnnotations . FakePluginVisitor . class , FAKE_PLUGIN_VISITOR ));
154
+ Arguments .of (FAKE_PLUGIN_NAME , FAKE_PLUGIN ),
155
+ Arguments .of (FAKE_PLUGIN_BUILDER_NAME , FAKE_PLUGIN_BUILDER ),
156
+ Arguments .of (FAKE_PLUGIN_NESTED_NAME , FAKE_PLUGIN_NESTED ),
157
+ Arguments .of (FAKE_CONSTRAINT_VALIDATOR_NAME , FAKE_CONSTRAINT_VALIDATOR ),
158
+ Arguments .of (FAKE_PLUGIN_VISITOR_NAME , FAKE_PLUGIN_VISITOR ));
131
159
}
132
160
133
161
@ ParameterizedTest
134
162
@ MethodSource
135
- void containsSpecificEntries (Class <?> clazz , Object expectedJson ) {
163
+ void containsSpecificEntries (String className , Object expectedJson ) throws IOException {
164
+ // Read metadata
165
+ Path reachabilityMetadataPath =
166
+ outputDir .resolve ("META-INF/native-image/log4j-generated/groupId/artifactId/reflect-config.json" );
167
+ String reachabilityMetadata = new String (Files .readAllBytes (reachabilityMetadataPath ), UTF_8 );
136
168
assertThatJson (reachabilityMetadata )
137
- .inPath (filterByName ( clazz ))
169
+ .inPath (String . format ( "$[?(@.name == '%s')]" , className ))
138
170
.isArray ()
139
171
.contains (json (expectedJson ));
140
172
}
141
173
142
- private String filterByName (Class <?> clazz ) {
143
- return String .format ("$[?(@.name == '%s')]" , clazz .getName ());
174
+ static Stream <Arguments > reachabilityMetadataPath () {
175
+ return Stream .of (
176
+ Arguments .of (
177
+ "groupId" ,
178
+ "artifactId" ,
179
+ "META-INF/native-image/log4j-generated/groupId/artifactId/reflect-config.json" ),
180
+ Arguments .of (null , "artifactId" , "META-INF/native-image/log4j-generated/fooBar/reflect-config.json" ),
181
+ Arguments .of ("groupId" , null , "META-INF/native-image/log4j-generated/fooBar/reflect-config.json" ),
182
+ Arguments .of (null , null , "META-INF/native-image/log4j-generated/fooBar/reflect-config.json" ));
183
+ }
184
+
185
+ @ ParameterizedTest
186
+ @ MethodSource
187
+ void reachabilityMetadataPath (@ Nullable String groupId , @ Nullable String artifactId , String expected ) {
188
+ Messager messager = Mockito .mock (Messager .class );
189
+ Elements elements = Mockito .mock (Elements .class );
190
+ ProcessingEnvironment processingEnv = Mockito .mock (ProcessingEnvironment .class );
191
+ when (processingEnv .getMessager ()).thenReturn (messager );
192
+ when (processingEnv .getElementUtils ()).thenReturn (elements );
193
+ GraalVmProcessor processor = new GraalVmProcessor ();
194
+ processor .init (processingEnv );
195
+ assertThat (processor .getReachabilityMetadataPath (groupId , artifactId , FALLBACK_METADATA_FOLDER ))
196
+ .isEqualTo (expected );
197
+ }
198
+
199
+ @ Test
200
+ void whenNoGroupIdAndArtifactId_thenWarningIsPrinted (@ TempDir (cleanup = CleanupMode .NEVER ) Path outputDir )
201
+ throws Exception {
202
+ List <String > diagnostics = generateDescriptor (sourceDir , null , null , outputDir );
203
+ assertThat (diagnostics ).hasSize (1 );
204
+ // The warning message should contain the information about the missing groupId and artifactId arguments
205
+ assertThat (diagnostics .get (0 ))
206
+ .contains (
207
+ "recommended" ,
208
+ "-A" + GraalVmProcessor .GROUP_ID + "=<groupId>" ,
209
+ "-A" + GraalVmProcessor .ARTIFACT_ID + "=<artifactId>" );
210
+ Path path = outputDir .resolve ("META-INF/native-image/log4j-generated" );
211
+ List <Path > reachabilityMetadataFolders ;
212
+ try (Stream <Path > files = Files .list (path )) {
213
+ reachabilityMetadataFolders = files .filter (Files ::isDirectory ).collect (Collectors .toList ());
214
+ }
215
+ // The generated folder name should be deterministic and based solely on the descriptor content.
216
+ // If the descriptor changes, this test and the expected folder name must be updated accordingly.
217
+ assertThat (reachabilityMetadataFolders ).hasSize (1 ).containsExactly (path .resolve ("62162090" ));
218
+ assertThat (reachabilityMetadataFolders .get (0 ).resolve ("reflect-config.json" ))
219
+ .as ("Reachability metadata file" )
220
+ .exists ();
221
+ }
222
+
223
+ private static List <String > generateDescriptor (
224
+ Path sourceDir , @ Nullable String groupId , @ Nullable String artifactId , Path outputDir ) throws Exception {
225
+ // Instantiate the tooling
226
+ final JavaCompiler compiler = ToolProvider .getSystemJavaCompiler ();
227
+ final StandardJavaFileManager fileManager = compiler .getStandardFileManager (null , Locale .ROOT , UTF_8 );
228
+
229
+ // Populate sources
230
+ final Iterable <? extends JavaFileObject > sources ;
231
+ try (final Stream <Path > files = Files .walk (sourceDir )) {
232
+ File [] sourceFiles =
233
+ files .filter (Files ::isRegularFile ).map (Path ::toFile ).toArray (File []::new );
234
+ sources = fileManager .getJavaFileObjects (sourceFiles );
235
+ }
236
+
237
+ // Set the target path used by `DescriptorGenerator` to dump the generated files
238
+ fileManager .setLocation (StandardLocation .CLASS_OUTPUT , Collections .singleton (outputDir .toFile ()));
239
+
240
+ // Prepare the compiler options
241
+ List <String > options = new ArrayList <>();
242
+ options .add ("-proc:only" );
243
+ options .add ("-processor" );
244
+ options .add (GraalVmProcessor .class .getName ());
245
+ if (groupId != null ) {
246
+ options .add ("-A" + GraalVmProcessor .GROUP_ID + "=" + groupId );
247
+ }
248
+ if (artifactId != null ) {
249
+ options .add ("-A" + GraalVmProcessor .ARTIFACT_ID + "=" + artifactId );
250
+ }
251
+
252
+ // Compile the sources
253
+ final Path descriptorFilePath = outputDir .resolve ("plugins.xml" );
254
+ final DiagnosticCollector <JavaFileObject > diagnosticCollector = new DiagnosticCollector <>();
255
+ final JavaCompiler .CompilationTask task =
256
+ compiler .getTask (null , fileManager , diagnosticCollector , options , null , sources );
257
+ task .call ();
258
+
259
+ // Verify successful compilation
260
+ return diagnosticCollector .getDiagnostics ().stream ()
261
+ .filter (d -> d .getKind () != Diagnostic .Kind .NOTE )
262
+ .map (d -> d .getMessage (Locale .ROOT ))
263
+ .collect (Collectors .toList ());
144
264
}
145
265
}
0 commit comments