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 javax.tools.DiagnosticListener; 22 import javax.tools.JavaFileObject; 23 import javax.tools.OptionChecker; 24 25 import java.io.IOException; 26 import java.nio.file.Files; 27 import java.nio.file.Path; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Set; 31 32 import org.apache.maven.api.PathScope; 33 import org.apache.maven.api.annotations.Nonnull; 34 import org.apache.maven.api.annotations.Nullable; 35 import org.apache.maven.api.plugin.MojoException; 36 import org.apache.maven.api.plugin.annotations.Mojo; 37 import org.apache.maven.api.plugin.annotations.Parameter; 38 import org.apache.maven.api.services.MessageBuilder; 39 40 import static org.apache.maven.plugin.compiler.SourceDirectory.CLASS_FILE_SUFFIX; 41 import static org.apache.maven.plugin.compiler.SourceDirectory.JAVA_FILE_SUFFIX; 42 import static org.apache.maven.plugin.compiler.SourceDirectory.MODULE_INFO; 43 44 /** 45 * Compiles application test sources. 46 * Each instance shall be used only once, then discarded. 47 * 48 * @author <a href="mailto:jason@maven.org">Jason van Zyl</a> 49 * @author Martin Desruisseaux 50 * @see <a href="https://docs.oracle.com/en/java/javase/17/docs/specs/man/javac.html">javac Command</a> 51 * @since 2.0 52 */ 53 @Mojo(name = "testCompile", defaultPhase = "test-compile") 54 public class TestCompilerMojo extends AbstractCompilerMojo { 55 /** 56 * Whether to bypass compilation of test sources. 57 * Its use is not recommended, but quite convenient on occasion. 58 * 59 * @see CompilerMojo#skipMain 60 */ 61 @Parameter(property = "maven.test.skip") 62 protected boolean skip; 63 64 /** 65 * Specify where to place generated source files created by annotation processing. 66 * 67 * @see CompilerMojo#generatedSourcesDirectory 68 * @since 2.2 69 */ 70 @Parameter(defaultValue = "${project.build.directory}/generated-test-sources/test-annotations") 71 protected Path generatedTestSourcesDirectory; 72 73 /** 74 * A set of inclusion filters for the compiler. 75 * 76 * @see CompilerMojo#includes 77 */ 78 @Parameter 79 protected Set<String> testIncludes; 80 81 /** 82 * A set of exclusion filters for the compiler. 83 * 84 * @see CompilerMojo#excludes 85 */ 86 @Parameter 87 protected Set<String> testExcludes; 88 89 /** 90 * A set of exclusion filters for the incremental calculation. 91 * Updated files, if excluded by this filter, will not cause the project to be rebuilt. 92 * 93 * @see CompilerMojo#incrementalExcludes 94 * @since 3.11 95 */ 96 @Parameter 97 protected Set<String> testIncrementalExcludes; 98 99 /** 100 * The {@code --source} argument for the test Java compiler. 101 * 102 * @see CompilerMojo#source 103 * @since 2.1 104 */ 105 @Parameter(property = "maven.compiler.testSource") 106 protected String testSource; 107 108 /** 109 * The {@code --target} argument for the test Java compiler. 110 * 111 * @see CompilerMojo#target 112 * @since 2.1 113 */ 114 @Parameter(property = "maven.compiler.testTarget") 115 protected String testTarget; 116 117 /** 118 * the {@code --release} argument for the test Java compiler 119 * 120 * @see CompilerMojo#release 121 * @since 3.6 122 */ 123 @Parameter(property = "maven.compiler.testRelease") 124 protected String testRelease; 125 126 /** 127 * The arguments to be passed to the test compiler. 128 * If this parameter is specified, it replaces {@link #compilerArgs}. 129 * Otherwise, the {@code compilerArgs} parameter is used. 130 * 131 * @see CompilerMojo#compilerArgs 132 * @since 4.0.0 133 */ 134 @Parameter 135 protected List<String> testCompilerArgs; 136 137 /** 138 * The arguments to be passed to test compiler. 139 * 140 * @deprecated Replaced by {@link #testCompilerArgs} for consistency with the main phase. 141 * 142 * @since 2.1 143 */ 144 @Parameter 145 @Deprecated(since = "4.0.0") 146 protected Map<String, String> testCompilerArguments; 147 148 /** 149 * The single argument string to be passed to the test compiler. 150 * If this parameter is specified, it replaces {@link #compilerArgument}. 151 * Otherwise, the {@code compilerArgument} parameter is used. 152 * 153 * @deprecated Use {@link #testCompilerArgs} instead. 154 * 155 * @see CompilerMojo#compilerArgument 156 * @since 2.1 157 */ 158 @Parameter 159 @Deprecated(since = "4.0.0") 160 protected String testCompilerArgument; 161 162 /** 163 * The directory where compiled test classes go. 164 * This parameter should only be modified in special cases. 165 * See the {@link CompilerMojo#outputDirectory} for more information. 166 * 167 * @see CompilerMojo#outputDirectory 168 * @see #getOutputDirectory() 169 */ 170 @Parameter(defaultValue = "${project.build.testOutputDirectory}", required = true) 171 protected Path outputDirectory; 172 173 /** 174 * The output directory of the main classes. 175 * This directory will be added to the class-path or module-path. 176 * Its value should be the same as {@link CompilerMojo#outputDirectory}. 177 * 178 * @see CompilerMojo#outputDirectory 179 */ 180 @Parameter(defaultValue = "${project.build.outputDirectory}", required = true, readonly = true) 181 protected Path mainOutputDirectory; 182 183 /** 184 * Whether to place the main classes on the module path when {@code module-info} is present. 185 * When {@code false}, always places the main classes on the class path. 186 * Dependencies are also placed on the class-path, unless their type is {@code module-jar}. 187 * 188 * @since 3.11 189 * 190 * @deprecated Use {@code "claspath-jar"} dependency type instead, and avoid {@code module-info.java} in tests. 191 */ 192 @Deprecated(since = "4.0.0") 193 @Parameter(defaultValue = "true") 194 protected boolean useModulePath = true; 195 196 /** 197 * Whether a {@code module-info.java} file is defined in the test sources. 198 * In such case, it has precedence over the {@code module-info.java} in main sources. 199 * This is defined for compatibility with Maven 3, but not recommended. 200 * 201 * <p>This field exists in this class only for transferring this information 202 * to {@link ToolExecutorForTest#hasTestModuleInfo}, which is the class that 203 * needs this information.</p> 204 * 205 * @deprecated Avoid {@code module-info.java} in tests. 206 */ 207 @Deprecated(since = "4.0.0") 208 transient boolean hasTestModuleInfo; 209 210 /** 211 * Whether a {@code module-info.java} file is defined in the main sources. 212 */ 213 private transient boolean hasMainModuleInfo; 214 215 /** 216 * Path to the {@code module-info.class} file of the main code, or {@code null} if that file does not exist. 217 * This field exists only for transferring this information to {@link ToolExecutorForTest#mainModulePath}, 218 * and should be {@code null} the rest of the time. 219 */ 220 transient Path mainModulePath; 221 222 /** 223 * The file where to dump the command-line when debug is activated or when the compilation failed. 224 * For example, if the value is {@code "javac-test"}, then the Java compiler can be launched 225 * from the command-line by typing {@code javac @target/javac-test.args}. 226 * The debug file will contain the compiler options together with the list of source files to compile. 227 * 228 * <p>By default, this debug file is written only if the compilation of test code failed. 229 * The writing of the debug files can be forced by setting the {@link #verbose} flag to {@code true} 230 * or by specifying the {@code --verbose} option to Maven on the command-line.</p> 231 * 232 * @see CompilerMojo#debugFileName 233 * @since 3.10.0 234 */ 235 @Parameter(defaultValue = "javac-test.args") 236 protected String debugFileName; 237 238 /** 239 * Creates a new compiler <abbr>MOJO</abbr> for the tests. 240 */ 241 public TestCompilerMojo() { 242 super(PathScope.TEST_COMPILE); 243 } 244 245 /** 246 * Runs the Java compiler on the test source code. 247 * If {@link #skip} is {@code true}, then this method logs a message and does nothing else. 248 * Otherwise, this method executes the steps described in the method of the parent class. 249 * 250 * @throws MojoException if the compiler cannot be run. 251 */ 252 @Override 253 public void execute() throws MojoException { 254 if (skip) { 255 logger.info("Not compiling test sources"); 256 return; 257 } 258 super.execute(); 259 } 260 261 /** 262 * Parses the parameters declared in the <abbr>MOJO</abbr>. 263 * 264 * @param compiler the tools to use for verifying the validity of options 265 * @return the options after validation 266 */ 267 @Override 268 @SuppressWarnings("deprecation") 269 public Options parseParameters(final OptionChecker compiler) { 270 Options configuration = super.parseParameters(compiler); 271 configuration.addUnchecked( 272 testCompilerArgs == null || testCompilerArgs.isEmpty() ? compilerArgs : testCompilerArgs); 273 if (testCompilerArguments != null) { 274 for (Map.Entry<String, String> entry : testCompilerArguments.entrySet()) { 275 configuration.addUnchecked(List.of(entry.getKey(), entry.getValue())); 276 } 277 } 278 configuration.addUnchecked(testCompilerArgument == null ? compilerArgument : testCompilerArgument); 279 return configuration; 280 } 281 282 /** 283 * {@return the path where to place generated source files created by annotation processing on the test classes} 284 */ 285 @Nullable 286 @Override 287 protected Path getGeneratedSourcesDirectory() { 288 return generatedTestSourcesDirectory; 289 } 290 291 /** 292 * {@return the inclusion filters for the compiler, or an empty set for all Java source files} 293 */ 294 @Override 295 protected Set<String> getIncludes() { 296 return (testIncludes != null) ? testIncludes : Set.of(); 297 } 298 299 /** 300 * {@return the exclusion filters for the compiler, or an empty set if none} 301 */ 302 @Override 303 protected Set<String> getExcludes() { 304 return (testExcludes != null) ? testExcludes : Set.of(); 305 } 306 307 /** 308 * {@return the exclusion filters for the incremental calculation, or an empty set if none} 309 */ 310 @Override 311 protected Set<String> getIncrementalExcludes() { 312 return (testIncrementalExcludes != null) ? testIncrementalExcludes : Set.of(); 313 } 314 315 /** 316 * If a different source version has been specified for the tests, returns that version. 317 * Otherwise returns the same source version as the main code. 318 * 319 * @return the {@code --source} argument for the Java compiler 320 */ 321 @Nullable 322 @Override 323 protected String getSource() { 324 return testSource == null ? source : testSource; 325 } 326 327 /** 328 * If a different target version has been specified for the tests, returns that version. 329 * Otherwise returns the same target version as the main code. 330 * 331 * @return the {@code --target} argument for the Java compiler 332 */ 333 @Nullable 334 @Override 335 protected String getTarget() { 336 return testTarget == null ? target : testTarget; 337 } 338 339 /** 340 * If a different release version has been specified for the tests, returns that version. 341 * Otherwise returns the same release version as the main code. 342 * 343 * @return the {@code --release} argument for the Java compiler 344 */ 345 @Nullable 346 @Override 347 protected String getRelease() { 348 return testRelease == null ? release : testRelease; 349 } 350 351 /** 352 * {@return the destination directory for test class files} 353 */ 354 @Nonnull 355 @Override 356 protected Path getOutputDirectory() { 357 return outputDirectory; 358 } 359 360 /** 361 * {@return the file where to dump the command-line when debug is activated or when the compilation failed} 362 * 363 * @see #debugFileName 364 */ 365 @Nullable 366 @Override 367 protected String getDebugFileName() { 368 return debugFileName; 369 } 370 371 /** 372 * {@return the module name declared in the test sources} 373 * We have to parse the source instead of the {@code module-info.class} file 374 * because the classes may not have been compiled yet. 375 * This is not very reliable, but putting a {@code module-info.java} file in the tests is deprecated anyway. 376 */ 377 final String getTestModuleName(List<SourceDirectory> compileSourceRoots) throws IOException { 378 for (SourceDirectory directory : compileSourceRoots) { 379 if (directory.moduleName != null) { 380 return directory.moduleName; 381 } 382 String name = parseModuleInfoName(directory.getModuleInfo().orElse(null)); 383 if (name != null) { 384 return name; 385 } 386 } 387 return null; 388 } 389 390 /** 391 * {@return whether the project has at least one {@code module-info.class} file} 392 * 393 * @param roots root directories of the sources to compile 394 * @throws IOException if this method needed to read a module descriptor and failed 395 */ 396 @Override 397 final boolean hasModuleDeclaration(final List<SourceDirectory> roots) throws IOException { 398 for (SourceDirectory root : roots) { 399 hasMainModuleInfo |= root.moduleName != null; 400 hasTestModuleInfo |= root.getModuleInfo().isPresent(); 401 if (hasMainModuleInfo & hasTestModuleInfo) { 402 break; 403 } 404 } 405 if (hasTestModuleInfo) { 406 MessageBuilder message = messageBuilderFactory.builder(); 407 message.a("Overwriting the ") 408 .warning(MODULE_INFO + JAVA_FILE_SUFFIX) 409 .a(" file in the test directory is deprecated. Use ") 410 .info(ModuleInfoPatch.FILENAME) 411 .a(" instead."); 412 logger.warn(message.toString()); 413 if (SUPPORT_LEGACY) { 414 return useModulePath; 415 } 416 } 417 return useModulePath && hasMainModuleInfo; 418 } 419 420 /** 421 * Creates a new task for compiling the test classes. 422 * 423 * @param listener where to send compilation warnings, or {@code null} for the Maven logger 424 * @throws MojoException if this method identifies an invalid parameter in this <abbr>MOJO</abbr> 425 * @return the task to execute for compiling the tests using the configuration in this <abbr>MOJO</abbr> 426 * @throws IOException if an error occurred while creating the output directory or scanning the source directories 427 */ 428 @Override 429 public ToolExecutor createExecutor(DiagnosticListener<? super JavaFileObject> listener) throws IOException { 430 try { 431 Path file = mainOutputDirectory.resolve(MODULE_INFO + CLASS_FILE_SUFFIX); 432 if (Files.isRegularFile(file)) { 433 mainModulePath = file; 434 hasMainModuleInfo = true; 435 } 436 return new ToolExecutorForTest(this, listener); 437 } finally { 438 // Reset the fields that were used only for transfering information to `ToolExecutorForTest`. 439 hasTestModuleInfo = false; 440 hasMainModuleInfo = false; 441 mainModulePath = null; 442 } 443 } 444 }