1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19 package io.earcam.instrumental.compile;
20
21 import static io.earcam.instrumental.compile.ClassAssertions.assertValidClass;
22 import static io.earcam.instrumental.compile.CompilationTarget.toBlackhole;
23 import static io.earcam.instrumental.compile.CompilationTarget.toByteArrays;
24 import static io.earcam.instrumental.compile.CompilationTarget.toClassLoader;
25 import static io.earcam.instrumental.compile.CompilationTarget.toFileSystem;
26 import static io.earcam.instrumental.compile.SourceSource.foundFor;
27 import static io.earcam.instrumental.compile.SourceSource.from;
28 import static java.nio.charset.StandardCharsets.ISO_8859_1;
29 import static java.nio.charset.StandardCharsets.UTF_8;
30 import static java.util.Collections.singletonList;
31 import static java.util.Locale.CHINESE;
32 import static javax.lang.model.SourceVersion.RELEASE_8;
33 import static javax.lang.model.SourceVersion.latestSupported;
34 import static javax.tools.StandardLocation.SOURCE_OUTPUT;
35 import static org.hamcrest.MatcherAssert.assertThat;
36 import static org.hamcrest.Matchers.allOf;
37 import static org.hamcrest.Matchers.containsString;
38 import static org.hamcrest.Matchers.containsStringIgnoringCase;
39 import static org.hamcrest.Matchers.equalTo;
40 import static org.hamcrest.Matchers.hasKey;
41 import static org.hamcrest.Matchers.hasToString;
42 import static org.hamcrest.Matchers.is;
43 import static org.hamcrest.Matchers.not;
44 import static org.hamcrest.Matchers.sameInstance;
45 import static org.hamcrest.io.FileMatchers.anExistingFile;
46 import static org.junit.jupiter.api.Assertions.*;
47 import static org.junit.jupiter.api.Assumptions.assumeFalse;
48
49 import java.io.BufferedWriter;
50 import java.io.File;
51 import java.io.IOException;
52 import java.io.StringWriter;
53 import java.io.UncheckedIOException;
54 import java.nio.file.Files;
55 import java.nio.file.Path;
56 import java.nio.file.Paths;
57 import java.util.Collections;
58 import java.util.Locale;
59 import java.util.Map;
60 import java.util.Set;
61 import java.util.UUID;
62 import java.util.concurrent.atomic.AtomicBoolean;
63 import java.util.concurrent.atomic.AtomicReference;
64
65 import javax.annotation.concurrent.NotThreadSafe;
66 import javax.annotation.processing.AbstractProcessor;
67 import javax.annotation.processing.ProcessingEnvironment;
68 import javax.annotation.processing.Processor;
69 import javax.annotation.processing.RoundEnvironment;
70 import javax.annotation.processing.SupportedAnnotationTypes;
71 import javax.annotation.processing.SupportedOptions;
72 import javax.annotation.processing.SupportedSourceVersion;
73 import javax.lang.model.SourceVersion;
74 import javax.lang.model.element.TypeElement;
75 import javax.tools.Diagnostic.Kind;
76 import javax.tools.FileObject;
77 import javax.tools.JavaFileObject;
78
79 import org.junit.jupiter.api.Nested;
80 import org.junit.jupiter.api.Test;
81
82 import io.earcam.acme.AnnotatedPojo;
83 import io.earcam.acme.HasNativeHeaders;
84 import io.earcam.acme.Pojo;
85 import io.earcam.instrumental.lade.ClassLoaders;
86 import io.earcam.instrumental.reflect.Methods;
87 import io.earcam.instrumental.reflect.Names;
88
89
90
91
92
93
94
95 public abstract class DefaultCompilerTest {
96
97 protected abstract Compiler compiling();
98
99 @Nested
100 class Simple {
101
102 @Nested
103 class FromFileSystem {
104
105 @Test
106 void map()
107 {
108 Map<String, byte[]> map;
109
110 map = compiling()
111 .versionAt(SourceVersion.latestSupported())
112 .source(foundFor(Pojo.class))
113 .compile(toByteArrays());
114
115 byte[] bytes = map.get(Names.typeToResourceName(Pojo.class));
116
117 assertValidClass(bytes, Pojo.class.getCanonicalName());
118 }
119
120
121 @Test
122 void filesystem() throws IOException
123 {
124 Path path = Paths.get(".", "target", FromFileSystem.class.getCanonicalName(), "filesystem", UUID.randomUUID().toString());
125
126
127 compiling()
128 .versionAt(SourceVersion.latestSupported())
129 .source(foundFor(Pojo.class))
130 .compile(toFileSystem(path));
131
132 Path compiled = path.resolve(Names.typeToResourceName(Pojo.class));
133 byte[] bytes = Files.readAllBytes(compiled);
134
135 assertValidClass(bytes, Pojo.class.getCanonicalName());
136
137 }
138 }
139
140 @Nested
141 class FromString {
142
143
144 final String fqn = "com.acme.Thing";
145
146 final String text = "" +
147 "package com.acme; \n" +
148 " \n" +
149 "class Thing { \n" +
150
151 " int fortyTwo() \n" +
152 " { \n" +
153 " return 42; \n" +
154 " } \n" +
155 "} \n";
156
157
158 @Test
159 void map()
160 {
161 Map<String, byte[]> map;
162
163 map = compiling()
164 .versionAt(latestSupported())
165 .source(from(text))
166 .compile(toByteArrays());
167
168 byte[] bytes = map.get(Names.typeToResourceName(fqn));
169
170 assertValidClass(bytes, fqn);
171 }
172
173
174
175 @Test
176 void filesystem() throws IOException
177 {
178 Path path = Paths.get(".", "target", FromString.class.getCanonicalName(), "filesystem", UUID.randomUUID().toString());
179
180 compiling()
181 .versionAt(SourceVersion.latestSupported())
182 .source(from(text))
183 .compile(toFileSystem(path));
184
185 Path compiled = path.resolve(Names.typeToResourceName(fqn));
186 byte[] bytes = Files.readAllBytes(compiled);
187
188 assertValidClass(bytes, fqn);
189 }
190 }
191 }
192
193
194
195 @Test
196 void encodingCurrentlyAppearsToDoAbsolutelyNothing() throws ReflectiveOperationException
197 {
198 final String fqn = "com.acme.Smile";
199
200 final String text = "" +
201 "package com.acme; \n" +
202 " \n" +
203 "public class Smile { \n" +
204
205 " public String smile() \n" +
206 " { \n" +
207 " return \"\u263B\"; \n" +
208 " } \n" +
209 "} \n";
210
211 Map<String, byte[]> map;
212
213 map = compiling()
214 .versionAt(latestSupported())
215 .encodedAs(ISO_8859_1)
216 .source(from(text))
217 .compile(toByteArrays());
218
219 byte[] bytes = map.get(Names.typeToResourceName(fqn));
220
221 assertValidClass(bytes, fqn);
222
223 Object instance = ClassLoaders.load(bytes).newInstance();
224
225 String returned = (String) Methods.getMethod(instance, "smile")
226 .orElseThrow(NullPointerException::new)
227 .invoke(instance);
228
229 assertThat(returned, is(equalTo("\u263B")));
230 }
231
232 @Nested
233 class SupportedVersions {
234
235 }
236
237 @Nested
238 class Native {
239 @Test
240 void filesystem() throws IOException
241 {
242
243 Path path = Paths.get(".", "target", Native.class.getCanonicalName(), "filesystem", UUID.randomUUID().toString());
244 Path bin = path.resolve("bin");
245 Path src = path.resolve("src");
246 Path cpp = path.resolve("cpp");
247
248 compiling()
249 .versionAt(latestSupported())
250 .source(foundFor(HasNativeHeaders.class))
251 .compile(toFileSystem()
252 .generatingBinariesIn(bin)
253 .generatingSourcesIn(src)
254 .generatingNativeHeadersIn(cpp));
255
256 byte[] bytes = bytesForClass(bin, HasNativeHeaders.class);
257 assertValidClass(bytes, HasNativeHeaders.class.getCanonicalName());
258
259 Path header = cpp.resolve(HasNativeHeaders.class.getCanonicalName().replace('.', '_') + ".h");
260 assertThat(header.toFile(), is(anExistingFile()));
261 }
262
263
264 private byte[] bytesForClass(Path basePath, Class<HasNativeHeaders> type) throws IOException
265 {
266 Path classFile = basePath.resolve(Names.typeToResourceName(type));
267 assertThat(classFile.toFile(), is(anExistingFile()));
268 byte[] bytes = Files.readAllBytes(classFile);
269 return bytes;
270 }
271
272
273 @Test
274 void filesystemAllInSameDirectory() throws IOException
275 {
276 Path path = Paths.get(".", "target", Native.class.getCanonicalName(), "filesystemAllInSameDirectory", UUID.randomUUID().toString());
277
278 compiling()
279 .versionAt(latestSupported())
280 .source(foundFor(HasNativeHeaders.class))
281 .compile(toFileSystem(path));
282
283 byte[] bytes = bytesForClass(path, HasNativeHeaders.class);
284 assertValidClass(bytes, HasNativeHeaders.class.getCanonicalName());
285
286 Path header = path.resolve(HasNativeHeaders.class.getCanonicalName().replace('.', '_') + ".h");
287 assertThat(header.toFile(), is(anExistingFile()));
288 }
289 }
290
291 @Nested
292 class Classpath {
293
294 @Test
295 public void implicitlyUsesCurrentClasspath() throws Exception
296 {
297 Class<?> loaded = compiling()
298 .versionAt(RELEASE_8)
299 .source(foundFor(DefaultCompilerTest.class))
300 .compile(toClassLoader())
301 .loadClass(DefaultCompilerTest.class.getCanonicalName());
302
303 assertThat(loaded, is(not(sameInstance(DefaultCompilerTest.class))));
304 assertThat(loaded.getCanonicalName(), is(equalTo(DefaultCompilerTest.class.getCanonicalName())));
305 }
306
307
308 @Test
309 public void explicitClasspathReplacesImplicitInheritedClasspath() throws Exception
310 {
311 try {
312 compiling()
313 .versionAt(RELEASE_8)
314 .source(foundFor(DefaultCompilerTest.class))
315 .withClasspath(Paths.get("/", "tmp", UUID.randomUUID().toString()))
316 .compile(toClassLoader());
317 fail();
318 } catch(Exception e) {}
319 }
320
321 }
322
323 @Nested
324 class CustomFileObjectProvider {
325
326 @Test
327 void testName()
328 {
329 Path fakeSourceRoot = Paths.get("src", "test", "resources", "source");
330
331 Path notOnClasspathHasDependencies = Paths.get("io", "earcam", "acme", "resource", "NotOnClasspathHasDependencies.java");
332 Path notOnClasspathPojo = Paths.get("io", "earcam", "acme", "resource", "NotOnClasspathPojo.java");
333
334 Path sourceFile = fakeSourceRoot.resolve(notOnClasspathHasDependencies);
335 Path dependencyFile = fakeSourceRoot.resolve(notOnClasspathPojo);
336
337 FileObjectProvider customProvider = (l, p, k, r) -> {
338 if(notOnClasspathHasDependencies.getParent().toString().replace('/', '.').equals(p)) {
339 String name = notOnClasspathPojo.toString().replace('/', '.');
340 return singletonList(
341 new FileObjectProvider.CustomJavaFileObject(name.substring(0, name.length() - 5), JavaFileObject.Kind.SOURCE, dependencyFile));
342 }
343 return Collections.emptyList();
344 };
345 compiling()
346 .versionAt(RELEASE_8)
347 .source(from(sourceFile))
348 .withDependencies(customProvider)
349 .compile(toClassLoader());
350 }
351 }
352
353 private static final String OPTION = "key";
354 private static final Kind MESSAGE_KIND = Kind.NOTE;
355 private static final String DIAGNOSTIC_MESSAGE = "Hello Writer";
356
357 @Nested
358 class AnnotationProcessing {
359
360 private final class MessageLoggingProcessor extends AbstractProcessor {
361
362 private final Kind kind;
363 private final String message;
364
365
366 public MessageLoggingProcessor()
367 {
368 this(MESSAGE_KIND, DIAGNOSTIC_MESSAGE);
369 }
370
371
372 public MessageLoggingProcessor(Kind kind, String message)
373 {
374 this.kind = kind;
375 this.message = message;
376 }
377
378
379 @Override
380 public Set<String> getSupportedAnnotationTypes()
381 {
382 return Collections.singleton("*");
383 }
384
385
386 @Override
387 public SourceVersion getSupportedSourceVersion()
388 {
389 return SourceVersion.latestSupported();
390 }
391
392
393 @Override
394 public synchronized void init(ProcessingEnvironment processingEnv)
395 {
396 processingEnv.getMessager().printMessage(kind, message);
397 super.init(processingEnv);
398 }
399
400
401 @Override
402 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
403 {
404 return false;
405 }
406 };
407
408 @SupportedAnnotationTypes("javax.annotation.concurrent.NotThreadSafe")
409 @SupportedSourceVersion(RELEASE_8)
410 @SupportedOptions(OPTION)
411 private final class DummyProcessor extends AbstractProcessor {
412
413 String annotation;
414 String rootElement;
415 String option;
416
417
418 @Override
419 public synchronized void init(ProcessingEnvironment processingEnv)
420 {
421 super.init(processingEnv);
422 this.option = processingEnv.getOptions().get(OPTION);
423 }
424
425
426 @Override
427 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
428 {
429 if(!annotations.isEmpty()) {
430 this.annotation = annotations.iterator().next().getQualifiedName().toString();
431 }
432 if(!roundEnv.getRootElements().isEmpty()) {
433 this.rootElement = roundEnv.getRootElements().iterator().next().toString();
434 }
435 return true;
436 }
437 }
438
439 private final class SourceGeneratingProcessor extends AbstractProcessor {
440
441 String paquet = "com.acme.doo.dah.day";
442 String name = "Yippee";
443 String fqn = paquet + '.' + name;
444
445 String resourcePath = "resources";
446 String resourceName = "config.properties";
447 String resourceFqn = resourcePath + "/" + resourceName;
448 String property1 = "some=property";
449 String property2 = "another=prop";
450
451
452 @Override
453 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
454 {
455 if(roundEnv.processingOver()) {
456 try {
457 JavaFileObject jfo = processingEnv.getFiler().createSourceFile(fqn);
458 try(BufferedWriter bw = new BufferedWriter(jfo.openWriter())) {
459 bw.append("package ").append(paquet).append(";\n\n");
460 bw.append("public class ").append(name).append(" {}");
461 }
462
463 FileObject resource = processingEnv.getFiler().createResource(SOURCE_OUTPUT, resourcePath, resourceName);
464 try(BufferedWriter bw = new BufferedWriter(resource.openWriter())) {
465 bw.append(property1).append("\n").append(property2);
466 }
467
468 } catch(IOException e) {
469 throw new UncheckedIOException(e);
470 }
471 }
472 return true;
473 }
474
475
476 @Override
477 public SourceVersion getSupportedSourceVersion()
478 {
479 return SourceVersion.RELEASE_8;
480 }
481
482
483 @Override
484 public Set<String> getSupportedAnnotationTypes()
485 {
486 return Collections.singleton(NotThreadSafe.class.getCanonicalName());
487 }
488 }
489
490
491 @Test
492 public void annotationProcessorIsInvokedOnCompiledClass() throws IOException
493 {
494 DummyProcessor processor = new DummyProcessor();
495
496 compiling()
497 .source(foundFor(AnnotatedPojo.class))
498 .versionAt(RELEASE_8)
499 .processedBy(processor)
500 .compile(toBlackhole());
501
502 assertThat(processor.rootElement, is(equalTo(AnnotatedPojo.class.getCanonicalName())));
503 assertThat(processor.annotation, is(equalTo(NotThreadSafe.class.getCanonicalName())));
504 }
505
506
507 @Test
508 public void annotationProcessorRecievesOptions() throws IOException
509 {
510 DummyProcessor processor = new DummyProcessor();
511 String value = "value";
512
513 compiling()
514 .source(foundFor(AnnotatedPojo.class))
515 .versionAt(RELEASE_8)
516 .processedBy(processor)
517 .withProcessorOption(OPTION, value)
518 .compile(toBlackhole());
519
520 assertThat(processor.option, is(equalTo(value)));
521 }
522
523
524 @Test
525 public void failsOnDiagnosticError() throws Exception
526 {
527 String message = "Oi Goose, Boo!";
528 MessageLoggingProcessor messageLoggingProcessor = new MessageLoggingProcessor(Kind.ERROR, message);
529 StringWriter diagnostics = new StringWriter();
530
531 try {
532 compiling()
533 .source(foundFor(AnnotatedPojo.class))
534 .versionAt(RELEASE_8)
535 .processedBy(messageLoggingProcessor)
536 .consumingDiagnositicMessages(diagnostics::write)
537 .compile(toBlackhole());
538 fail();
539 } catch(Exception e) {}
540
541 assertThat(diagnostics, hasToString(equalTo(message)));
542 }
543
544
545 @Test
546 public void doesNotFailOnDiagnosticErrorWhenIgnoreErrorsSet() throws Exception
547 {
548 String message = "Oi Goose, Boo!";
549 MessageLoggingProcessor messageLoggingProcessor = new MessageLoggingProcessor(Kind.ERROR, message);
550 StringWriter diagnostics = new StringWriter();
551
552 compiling()
553 .source(foundFor(AnnotatedPojo.class))
554 .versionAt(RELEASE_8)
555 .processedBy(messageLoggingProcessor)
556 .consumingDiagnositicMessages(diagnostics::write)
557 .ignoreCompilationErrors()
558 .compile(toBlackhole());
559
560 assertThat(diagnostics, hasToString(equalTo(message)));
561 }
562
563
564 @Test
565 public void diagnosticsWriter() throws Exception
566 {
567 MessageLoggingProcessor messageLoggingProcessor = new MessageLoggingProcessor();
568 StringWriter diagnostics = new StringWriter();
569
570 compiling()
571 .source(foundFor(AnnotatedPojo.class))
572 .versionAt(RELEASE_8)
573 .processedBy(messageLoggingProcessor)
574 .consumingDiagnositicMessages(diagnostics::write)
575 .compile(toBlackhole());
576
577 assertThat(diagnostics, hasToString(equalTo(DIAGNOSTIC_MESSAGE)));
578 }
579
580
581 @Test
582 public void loggingOutput() throws Exception
583 {
584 MessageLoggingProcessor messageLoggingProcessor = new MessageLoggingProcessor();
585 StringWriter diagnostics = new StringWriter();
586
587 compiling()
588 .source(foundFor(AnnotatedPojo.class))
589 .versionAt(RELEASE_8)
590 .processedBy(messageLoggingProcessor)
591 .loggingTo(diagnostics)
592 .compile(toBlackhole());
593
594 assertThat(diagnostics, hasToString(allOf(
595 containsString(DIAGNOSTIC_MESSAGE),
596 containsStringIgnoringCase(MESSAGE_KIND.name()))));
597 }
598
599
600 @Test
601 public void generatedSourceOutputToMap() throws Exception
602 {
603 SourceGeneratingProcessor processor = new SourceGeneratingProcessor();
604
605 Map<String, byte[]> map;
606 map = compiling()
607 .source(foundFor(AnnotatedPojo.class))
608 .versionAt(RELEASE_8)
609 .processedBy(processor)
610 .compile(toByteArrays());
611
612 assertThat(map, hasKey(processor.fqn.replace('.', '/') + ".java"));
613
614 assertThat(map, hasKey(processor.resourceFqn));
615 }
616
617
618 @Test
619 public void generatedSourceOutputToFilesystem() throws Exception
620 {
621 Path path = Paths.get(".", "target", AnnotationProcessing.class.getCanonicalName(), "generatedSourceOutputToFilesystem",
622 UUID.randomUUID().toString());
623
624 SourceGeneratingProcessor processor = new SourceGeneratingProcessor();
625
626 compiling()
627 .source(foundFor(AnnotatedPojo.class))
628 .versionAt(RELEASE_8)
629 .processedBy(processor)
630 .compile(toFileSystem(path));
631
632 File sourceFile = path.resolve(processor.fqn.replace('.', File.separatorChar) + ".java").toFile();
633 assertThat(sourceFile, is(anExistingFile()));
634
635 Path resource = path.resolve(processor.resourceFqn);
636
637 assertThat(resource.toFile(), is(anExistingFile()));
638 String contents = new String(Files.readAllBytes(resource), UTF_8);
639 assertThat(contents, allOf(
640 containsString(processor.property1),
641 containsString(processor.property2)));
642 }
643
644
645 @Test
646 void localeIsAvailableForAnnotationProcessor()
647 {
648 AtomicBoolean processed = new AtomicBoolean(false);
649 AtomicReference<Locale> locale = new AtomicReference<Locale>();
650 Processor processor = new AbstractProcessor() {
651
652 @Override
653 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv)
654 {
655 processed.set(true);
656 locale.set(processingEnv.getLocale());
657 return false;
658 }
659
660
661 @Override
662 public Set<String> getSupportedAnnotationTypes()
663 {
664 return Collections.singleton(NotThreadSafe.class.getCanonicalName());
665 }
666 };
667 compiling()
668 .source(foundFor(AnnotatedPojo.class))
669 .versionAt(RELEASE_8)
670 .localisedTo(CHINESE)
671 .processedBy(processor)
672 .compile(toBlackhole());
673
674 assertThat(processed.get(), is(true));
675
676 assumeFalse(System.getProperty("java.home").contains("8"), "Not working in default jdk.compile for v8");
677
678 assertThat(locale.get(), is(CHINESE));
679 }
680 }
681 }