View Javadoc
1   /*-
2    * #%L
3    * io.earcam.instrumental.compile
4    * %%
5    * Copyright (C) 2018 earcam
6    * %%
7    * SPDX-License-Identifier: (BSD-3-Clause OR EPL-1.0 OR Apache-2.0 OR MIT)
8    * 
9    * You <b>must</b> choose to accept, in full - any individual or combination of 
10   * the following licenses:
11   * <ul>
12   * 	<li><a href="https://opensource.org/licenses/BSD-3-Clause">BSD-3-Clause</a></li>
13   * 	<li><a href="https://www.eclipse.org/legal/epl-v10.html">EPL-1.0</a></li>
14   * 	<li><a href="https://www.apache.org/licenses/LICENSE-2.0">Apache-2.0</a></li>
15   * 	<li><a href="https://opensource.org/licenses/MIT">MIT</a></li>
16   * </ul>
17   * #L%
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   * @see HotspotCompilerTest
92   * @see EclipseCompilerTest
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 				// EARCAM_SNIPPET_BEGIN: filesystem
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 				// EARCAM_SNIPPET_END: filesystem
137 			}
138 		}
139 
140 		@Nested
141 		class FromString {
142 
143 			// EARCAM_SNIPPET_BEGIN: string
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 			// EARCAM_SNIPPET_END: string
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 	// TODO the encoding is not passed on to file sources and is irrelevant to String sources
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")));  // should be compiler error/warning
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 }