View Javadoc
1   /*
2    * Licensed to the Apache Software Foundation (ASF) under one
3    * or more contributor license agreements.  See the NOTICE file
4    * distributed with this work for additional information
5    * regarding copyright ownership.  The ASF licenses this file
6    * to you under the Apache License, Version 2.0 (the
7    * "License"); you may not use this file except in compliance
8    * with the License.  You may obtain a copy of the License at
9    *
10   *   http://www.apache.org/licenses/LICENSE-2.0
11   *
12   * Unless required by applicable law or agreed to in writing,
13   * software distributed under the License is distributed on an
14   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15   * KIND, either express or implied.  See the License for the
16   * specific language governing permissions and limitations
17   * under the License.
18   */
19  package org.apache.maven.plugin.compiler;
20  
21  import java.io.File;
22  import java.io.IOException;
23  import java.io.UncheckedIOException;
24  import java.net.URI;
25  import java.nio.file.Files;
26  import java.nio.file.Path;
27  import java.time.Instant;
28  import java.time.temporal.ChronoUnit;
29  import java.util.ArrayList;
30  import java.util.HashMap;
31  import java.util.List;
32  import java.util.Map;
33  
34  import org.apache.maven.api.PathScope;
35  import org.apache.maven.api.Project;
36  import org.apache.maven.api.Session;
37  import org.apache.maven.api.di.Inject;
38  import org.apache.maven.api.di.Provides;
39  import org.apache.maven.api.di.Singleton;
40  import org.apache.maven.api.model.Build;
41  import org.apache.maven.api.model.Model;
42  import org.apache.maven.api.plugin.Log;
43  import org.apache.maven.api.plugin.testing.Basedir;
44  import org.apache.maven.api.plugin.testing.InjectMojo;
45  import org.apache.maven.api.plugin.testing.MojoExtension;
46  import org.apache.maven.api.plugin.testing.MojoParameter;
47  import org.apache.maven.api.plugin.testing.MojoTest;
48  import org.apache.maven.api.plugin.testing.stubs.ProducedArtifactStub;
49  import org.apache.maven.api.plugin.testing.stubs.ProjectStub;
50  import org.apache.maven.api.plugin.testing.stubs.SessionMock;
51  import org.apache.maven.api.services.ArtifactManager;
52  import org.apache.maven.api.services.MessageBuilderFactory;
53  import org.apache.maven.api.services.ToolchainManager;
54  import org.apache.maven.impl.DefaultMessageBuilderFactory;
55  import org.apache.maven.impl.InternalSession;
56  import org.apache.maven.plugin.compiler.stubs.CompilerStub;
57  import org.junit.jupiter.api.Test;
58  
59  import static org.junit.jupiter.api.Assertions.assertArrayEquals;
60  import static org.junit.jupiter.api.Assertions.assertEquals;
61  import static org.junit.jupiter.api.Assertions.assertFalse;
62  import static org.junit.jupiter.api.Assertions.assertNull;
63  import static org.junit.jupiter.api.Assertions.assertThrows;
64  import static org.junit.jupiter.api.Assertions.assertTrue;
65  import static org.junit.jupiter.api.Assertions.fail;
66  import static org.mockito.ArgumentMatchers.any;
67  import static org.mockito.ArgumentMatchers.eq;
68  import static org.mockito.ArgumentMatchers.startsWith;
69  import static org.mockito.Mockito.doAnswer;
70  import static org.mockito.Mockito.doReturn;
71  import static org.mockito.Mockito.mock;
72  import static org.mockito.Mockito.never;
73  import static org.mockito.Mockito.verify;
74  
75  @MojoTest
76  public class CompilerMojoTestCase {
77  
78      private static final String LOCAL_REPO = "/target/local-repo";
79  
80      @Inject
81      private Session session;
82  
83      /**
84       * Verifies that the {@value CompilerStub#OUTPUT_FILE} file exists, then deletes it.
85       * The deletion is necessary for preventing an {@link IndexOutOfBoundsException} in
86       * {@code maven-dependency-plugin} version 3.8.1, because the output file is empty.
87       *
88       * @param mojo the tested mojo
89       */
90      private static void assertCompilerStubOutputFileExists(AbstractCompilerMojo mojo) {
91          try {
92              Files.delete(assertOutputFileExists(mojo, CompilerStub.OUTPUT_FILE));
93          } catch (IOException e) {
94              throw new UncheckedIOException(e);
95          }
96      }
97  
98      /**
99       * Verifies that the given output file exists.
100      *
101      * @param mojo the tested mojo
102      * @param first the first path element
103      * @param more the other path elements, if any
104      * @return the file
105      */
106     private static Path assertOutputFileExists(AbstractCompilerMojo mojo, String first, String... more) {
107         Path file = mojo.getOutputDirectory().resolve(Path.of(first, more));
108         assertTrue(Files.isRegularFile(file), () -> "File not found: " + file);
109         return file;
110     }
111 
112     /**
113      * Verifies that the given output file does not exist.
114      *
115      * @param mojo the tested mojo
116      * @param first the first path element
117      * @param more the other path elements, if any
118      */
119     private static void assertOutputFileDoesNotExist(AbstractCompilerMojo mojo, String first, String... more) {
120         Path file = mojo.getOutputDirectory().resolve(Path.of(first, more));
121         assertFalse(Files.exists(file), () -> "File should not exist: " + file);
122     }
123 
124     /**
125      * Tests the ability of the plugin to compile a basic file.
126      * This test does not declare a Java release version. Therefore, a warning should be emitted.
127      */
128     @Test
129     @Basedir("${basedir}/target/test-classes/unit/compiler-basic-test")
130     public void testCompilerBasic(
131             @InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
132             @InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
133                     @MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
134                     TestCompilerMojo testCompileMojo) {
135 
136         Log log = mock(Log.class);
137         compileMojo.logger = log;
138         compileMojo.execute();
139         verify(log).warn(startsWith("No explicit value set for --release or --target."));
140         assertOutputFileExists(compileMojo, "foo", "TestCompile0.class");
141         assertTrue(
142                 session.getArtifactPath(compileMojo.projectArtifact).isPresent(),
143                 "MCOMPILER-94: artifact file should only be null if there is nothing to compile");
144 
145         testCompileMojo.execute();
146         assertOutputFileExists(testCompileMojo, "foo", "TestCompile0Test.class");
147         assertOutputFileDoesNotExist(compileMojo, "foo", "TestCompile0Test.class");
148     }
149 
150     /**
151      * A project with a source and target version specified.
152      * No warning should be logged.
153      */
154     @Test
155     @Basedir("${basedir}/target/test-classes/unit/compiler-basic-sourcetarget")
156     public void testCompilerBasicSourceTarget(
157             @InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo) {
158 
159         Log log = mock(Log.class);
160         compileMojo.logger = log;
161         compileMojo.execute();
162         verify(log, never()).warn(startsWith("No explicit value set for --release or --target."));
163     }
164 
165     /**
166      * Tests the ability of the plugin to respond to empty source.
167      */
168     @Test
169     @Basedir("${basedir}/target/test-classes/unit/compiler-empty-source-test")
170     public void testCompilerEmptySource(
171             @InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
172             @InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
173                     @MojoParameter(name = "compileSourceRoots", value = "${basedir}/src/test/java")
174                     TestCompilerMojo testCompileMojo) {
175 
176         compileMojo.execute();
177         assertFalse(Files.exists(compileMojo.getOutputDirectory()));
178         assertNull(
179                 session.getArtifactPath(compileMojo.projectArtifact).orElse(null),
180                 "MCOMPILER-94: artifact file should be null if there is nothing to compile");
181 
182         testCompileMojo.execute();
183         assertFalse(Files.exists(testCompileMojo.getOutputDirectory()));
184     }
185 
186     /**
187      * Tests the ability of the plugin to respond to includes and excludes correctly.
188      */
189     @Test
190     @Basedir("${basedir}/target/test-classes/unit/compiler-includes-excludes-test")
191     public void testCompilerIncludesExcludes(
192             @InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
193             @InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
194                     @MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
195                     TestCompilerMojo testCompileMojo) {
196 
197         compileMojo.execute();
198         assertOutputFileDoesNotExist(compileMojo, "foo", "TestCompile2.class");
199         assertOutputFileDoesNotExist(compileMojo, "foo", "TestCompile3.class");
200         assertOutputFileExists(compileMojo, "foo", "TestCompile4.class");
201 
202         testCompileMojo.execute();
203         assertOutputFileDoesNotExist(testCompileMojo, "foo", "TestCompile2TestCase.class");
204         assertOutputFileDoesNotExist(testCompileMojo, "foo", "TestCompile3TestCase.class");
205         assertOutputFileExists(testCompileMojo, "foo", "TestCompile4TestCase.class");
206         assertOutputFileDoesNotExist(compileMojo, "foo", "TestCompile4TestCase.class");
207     }
208 
209     /**
210      * Tests the ability of the plugin to fork and successfully compile.
211      */
212     @Test
213     @Basedir("${basedir}/target/test-classes/unit/compiler-fork-test")
214     public void testCompilerFork(
215             @InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
216             @InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
217                     @MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
218                     TestCompilerMojo testCompileMojo) {
219 
220         // JAVA_HOME doesn't have to be on the PATH.
221         String javaHome = System.getenv("JAVA_HOME");
222         if (javaHome != null) {
223             String command = new File(javaHome, "bin/javac").getPath();
224             compileMojo.executable = command;
225             testCompileMojo.executable = command;
226         }
227         compileMojo.execute();
228         assertOutputFileExists(compileMojo, "foo", "TestCompile1.class");
229 
230         testCompileMojo.execute();
231         assertOutputFileExists(testCompileMojo, "foo", "TestCompile1TestCase.class");
232         assertOutputFileDoesNotExist(compileMojo, "foo", "TestCompile1TestCase.class");
233     }
234 
235     /**
236      * Tests the use of a custom compiler.
237      * The dummy compiler used in this test generates only one file, despite having more than one source.
238      */
239     @Test
240     @Basedir("${basedir}/target/test-classes/unit/compiler-one-output-file-test")
241     public void testOneOutputFileForAllInput(
242             @InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
243             @InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
244                     @MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
245                     TestCompilerMojo testCompileMojo) {
246 
247         assertEquals(CompilerStub.COMPILER_ID, compileMojo.compilerId);
248         compileMojo.execute();
249         assertCompilerStubOutputFileExists(compileMojo);
250 
251         assertEquals(CompilerStub.COMPILER_ID, testCompileMojo.compilerId);
252         testCompileMojo.execute();
253         assertCompilerStubOutputFileExists(testCompileMojo);
254     }
255 
256     /**
257      * Verifies that the options in the {@code <compilerArgs>} elements are given to the compiler.
258      */
259     @Test
260     @Basedir("${basedir}/target/test-classes/unit/compiler-args-test")
261     public void testCompilerArgs(@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo) {
262 
263         assertEquals(CompilerStub.COMPILER_ID, compileMojo.compilerId);
264         compileMojo.execute();
265 
266         assertCompilerStubOutputFileExists(compileMojo);
267         assertArrayEquals(
268                 new String[] {"key1=value1", "-Xlint", "-my&special:param-with+chars/not>allowed_in_XML_element_names"},
269                 compileMojo.compilerArgs.toArray(String[]::new));
270 
271         List<String> options = CompilerStub.getOptions();
272         assertArrayEquals(
273                 new String[] {
274                     "--module-version", // Added by the plugin
275                     "1.0-SNAPSHOT",
276                     "key1=value1", // Specified in <compilerArgs>
277                     "-Xlint",
278                     "-my&special:param-with+chars/not>allowed_in_XML_element_names",
279                     "param", // Specified in <compilerArgument>
280                     "value"
281                 },
282                 options.toArray(String[]::new));
283     }
284 
285     /**
286      * Tests the {@code <implicit>} option when set to "none".
287      */
288     @Test
289     @Basedir("${basedir}/target/test-classes/unit/compiler-implicit-test")
290     public void testImplicitFlagNone(
291             @InjectMojo(goal = "compile", pom = "plugin-config-none.xml") CompilerMojo compileMojo) {
292 
293         assertEquals("none", compileMojo.implicit);
294         compileMojo.execute();
295     }
296 
297     /**
298      * Tests the {@code <implicit>} option when not set.
299      */
300     @Test
301     @Basedir("${basedir}/target/test-classes/unit/compiler-implicit-test")
302     public void testImplicitFlagNotSet(
303             @InjectMojo(goal = "compile", pom = "plugin-config-not-set.xml") CompilerMojo compileMojo) {
304 
305         assertNull(compileMojo.implicit);
306         compileMojo.execute();
307     }
308 
309     /**
310      * Tests the compilation of a project having a {@code module-info.java} file, together with its tests.
311      * The compilation of tests requires a {@code --patch-module} option, otherwise compilation will fail.
312      *
313      * <h4>Requirements on Windows</h4>
314      * Executing the tests on Windows requires the developer mode.
315      * This is enabled with {@literal Settings > Update & Security > For Developers}.
316      */
317     @Test
318     @Basedir("${basedir}/target/test-classes/unit/compiler-modular-project")
319     public void testModularProject(
320             @InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
321             @InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
322                     @MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
323                     TestCompilerMojo testCompileMojo) {
324 
325         compileMojo.execute();
326         assertOutputFileExists(compileMojo, SourceDirectory.MODULE_INFO + SourceDirectory.CLASS_FILE_SUFFIX);
327         assertOutputFileExists(compileMojo, "foo", "TestModular.class");
328 
329         testCompileMojo.execute();
330         assertOutputFileExists(testCompileMojo, "foo", "TestModularTestCase.class");
331         assertOutputFileDoesNotExist(compileMojo, "foo", "TestModularTestCase.class");
332     }
333 
334     /**
335      * Tests a compilation task which is expected to fail.
336      */
337     @Test
338     @Basedir("${basedir}/target/test-classes/unit/compiler-fail-test")
339     public void testCompileFailure(@InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo) {
340         assertThrows(CompilationFailureException.class, compileMojo::execute, "Should throw an exception");
341         assertOutputFileExists(compileMojo, "..", "javac.args"); // Command-line that user can execute.
342     }
343 
344     /**
345      * Tests a compilation task which is expected to fail, but where test failure are ignored.
346      */
347     @Test
348     @Basedir("${basedir}/target/test-classes/unit/compiler-failonerror-test")
349     public void testCompileFailOnError(
350             @InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo) {
351 
352         try {
353             compileMojo.execute();
354         } catch (CompilationFailureException e) {
355             fail("The compilation error should have been consumed because failOnError = false");
356         }
357         assertOutputFileExists(compileMojo, "..", "javac.args"); // Command-line that user can execute.
358     }
359 
360     /**
361      * Tests that setting {@code skipMain} to true skips compilation of the main Java source files,
362      * but that test Java source files are still compiled.
363      */
364     @Test
365     @Basedir("${basedir}/target/test-classes/unit/compiler-skip-main")
366     public void testCompileSkipMain(
367             @InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
368             @InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
369                     @MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
370                     TestCompilerMojo testCompileMojo) {
371 
372         compileMojo.skipMain = true;
373         compileMojo.execute();
374         assertOutputFileDoesNotExist(compileMojo, "foo", "TestSkipMainCompile0.class");
375 
376         testCompileMojo.execute();
377         assertOutputFileExists(testCompileMojo, "foo", "TestSkipMainCompile0Test.class");
378         assertOutputFileDoesNotExist(compileMojo, "foo", "TestSkipMainCompile0Test.class");
379     }
380 
381     /**
382      * Tests that setting {@code skip} to true skips compilation of the test Java source files,
383      * but that main Java source files are still compiled.
384      */
385     @Test
386     @Basedir("${basedir}/target/test-classes/unit/compiler-skip-test")
387     public void testCompileSkipTest(
388             @InjectMojo(goal = "compile", pom = "plugin-config.xml") CompilerMojo compileMojo,
389             @InjectMojo(goal = "testCompile", pom = "plugin-config.xml")
390                     @MojoParameter(name = "compileSourceRoots", value = "${project.basedir}/src/test/java")
391                     TestCompilerMojo testCompileMojo) {
392 
393         compileMojo.execute();
394         assertOutputFileExists(compileMojo, "foo/TestSkipTestCompile0.class");
395 
396         testCompileMojo.skip = true;
397         testCompileMojo.execute();
398         assertOutputFileDoesNotExist(testCompileMojo, "foo", "TestSkipTestCompile0Test.class");
399         assertOutputFileDoesNotExist(compileMojo, "foo", "TestSkipTestCompile0Test.class");
400     }
401 
402     @Provides
403     @Singleton
404     @SuppressWarnings("unused")
405     private static InternalSession createSession() {
406         InternalSession session = SessionMock.getMockSession(MojoExtension.getBasedir() + LOCAL_REPO);
407 
408         ToolchainManager toolchainManager = mock(ToolchainManager.class);
409         doReturn(toolchainManager).when(session).getService(ToolchainManager.class);
410 
411         doAnswer(iom -> Instant.now().minus(200, ChronoUnit.MILLIS))
412                 .when(session)
413                 .getStartTime();
414 
415         var junit = new ProducedArtifactStub("junit", "junit", null, "3.8.1", "jar");
416 
417         MessageBuilderFactory messageBuilderFactory = new DefaultMessageBuilderFactory();
418         doReturn(messageBuilderFactory).when(session).getService(MessageBuilderFactory.class);
419 
420         Map<String, String> props = new HashMap<>();
421         props.put("basedir", MojoExtension.getBasedir());
422         doReturn(props).when(session).getUserProperties();
423 
424         List<Path> artifacts = new ArrayList<>();
425         try {
426             Path artifactFile;
427             String localRepository = System.getProperty("localRepository");
428             if (localRepository != null) {
429                 artifactFile = Path.of(
430                         localRepository,
431                         "org",
432                         "junit",
433                         "jupiter",
434                         "junit-jupiter-api",
435                         "5.10.2",
436                         "junit-jupiter-api-5.10.2.jar");
437             } else {
438                 // for IDE
439                 String junitURI = Test.class.getResource("Test.class").toURI().toString();
440                 junitURI = junitURI.substring("jar:".length(), junitURI.indexOf('!'));
441                 artifactFile = new File(URI.create(junitURI)).toPath();
442             }
443             ArtifactManager artifactManager = session.getService(ArtifactManager.class);
444             artifactManager.setPath(junit, artifactFile);
445             artifacts.add(artifactFile);
446         } catch (Exception e) {
447             throw new RuntimeException("Unable to setup junit jar path", e);
448         }
449 
450         doAnswer(iom -> List.of()).when(session).resolveDependencies(any(), eq(PathScope.MAIN_COMPILE));
451         doAnswer(iom -> artifacts).when(session).resolveDependencies(any(), eq(PathScope.TEST_COMPILE));
452 
453         return session;
454     }
455 
456     @Provides
457     @Singleton
458     @SuppressWarnings("unused")
459     private static Project createProject() {
460         ProjectStub stub = new ProjectStub();
461         var artifact = new ProducedArtifactStub("myGroupId", "myArtifactId", null, "1.0-SNAPSHOT", "jar");
462         stub.setMainArtifact(artifact);
463         stub.setModel(Model.newBuilder()
464                 .groupId(artifact.getGroupId())
465                 .artifactId(artifact.getArtifactId())
466                 .version(artifact.getVersion().toString())
467                 .build(Build.newBuilder()
468                         .directory(MojoExtension.getBasedir() + "/target")
469                         .outputDirectory(MojoExtension.getBasedir() + "/target/classes")
470                         .sourceDirectory(MojoExtension.getBasedir() + "/src/main/java")
471                         .testOutputDirectory(MojoExtension.getBasedir() + "/target/test-classes")
472                         .build())
473                 .build());
474         stub.setBasedir(Path.of(MojoExtension.getBasedir()));
475         return stub;
476     }
477 }