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.api.plugin.testing;
20  
21  import java.io.BufferedReader;
22  import java.io.File;
23  import java.io.InputStream;
24  import java.io.Reader;
25  import java.io.StringReader;
26  import java.lang.reflect.AccessibleObject;
27  import java.lang.reflect.Field;
28  import java.lang.reflect.InvocationTargetException;
29  import java.lang.reflect.Method;
30  import java.net.URL;
31  import java.nio.file.Files;
32  import java.nio.file.Path;
33  import java.nio.file.Paths;
34  import java.util.ArrayList;
35  import java.util.Arrays;
36  import java.util.Collection;
37  import java.util.Collections;
38  import java.util.Date;
39  import java.util.HashMap;
40  import java.util.LinkedHashSet;
41  import java.util.List;
42  import java.util.Map;
43  import java.util.Objects;
44  import java.util.Optional;
45  import java.util.Properties;
46  import java.util.Set;
47  import java.util.function.Supplier;
48  import java.util.stream.Collectors;
49  import java.util.stream.Stream;
50  
51  import com.google.inject.Binder;
52  import com.google.inject.Module;
53  import com.google.inject.internal.ProviderMethodsModule;
54  import org.apache.maven.RepositoryUtils;
55  import org.apache.maven.api.di.Provides;
56  import org.apache.maven.execution.DefaultMavenExecutionRequest;
57  import org.apache.maven.execution.MavenExecutionRequest;
58  import org.apache.maven.execution.MavenExecutionRequestPopulator;
59  import org.apache.maven.execution.MavenSession;
60  import org.apache.maven.execution.scope.internal.MojoExecutionScope;
61  import org.apache.maven.internal.aether.DefaultRepositorySystemSessionFactory;
62  import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
63  import org.apache.maven.model.Build;
64  import org.apache.maven.model.Plugin;
65  import org.apache.maven.model.Resource;
66  import org.apache.maven.plugin.Mojo;
67  import org.apache.maven.plugin.MojoExecution;
68  import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
69  import org.apache.maven.plugin.descriptor.MojoDescriptor;
70  import org.apache.maven.plugin.descriptor.Parameter;
71  import org.apache.maven.plugin.descriptor.PluginDescriptor;
72  import org.apache.maven.plugin.descriptor.PluginDescriptorBuilder;
73  import org.apache.maven.plugin.logging.Log;
74  import org.apache.maven.plugin.testing.MojoLogWrapper;
75  import org.apache.maven.project.MavenProject;
76  import org.apache.maven.session.scope.internal.SessionScope;
77  import org.codehaus.plexus.DefaultPlexusContainer;
78  import org.codehaus.plexus.PlexusContainer;
79  import org.codehaus.plexus.component.configurator.BasicComponentConfigurator;
80  import org.codehaus.plexus.component.configurator.ComponentConfigurator;
81  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
82  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
83  import org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
84  import org.codehaus.plexus.component.repository.ComponentDescriptor;
85  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
86  import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
87  import org.codehaus.plexus.testing.PlexusExtension;
88  import org.codehaus.plexus.util.InterpolationFilterReader;
89  import org.codehaus.plexus.util.ReflectionUtils;
90  import org.codehaus.plexus.util.xml.XmlStreamReader;
91  import org.codehaus.plexus.util.xml.Xpp3Dom;
92  import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
93  import org.eclipse.aether.RepositorySystemSession;
94  import org.junit.jupiter.api.extension.ExtensionContext;
95  import org.junit.jupiter.api.extension.ParameterContext;
96  import org.junit.jupiter.api.extension.ParameterResolutionException;
97  import org.junit.jupiter.api.extension.ParameterResolver;
98  import org.junit.platform.commons.support.AnnotationSupport;
99  import org.junit.platform.commons.support.HierarchyTraversalMode;
100 import org.mockito.Mockito;
101 import org.slf4j.LoggerFactory;
102 
103 import static org.mockito.Mockito.clearInvocations;
104 import static org.mockito.Mockito.lenient;
105 import static org.mockito.Mockito.mockingDetails;
106 import static org.mockito.Mockito.spy;
107 
108 /**
109  * JUnit Jupiter extension that provides support for testing Maven plugins (Mojos).
110  * This extension handles the lifecycle of Mojo instances in tests, including instantiation,
111  * configuration, and dependency injection.
112  *
113  * <p>The extension is automatically registered when using the {@link MojoTest} annotation
114  * on a test class. It provides the following features:</p>
115  * <ul>
116  *   <li>Automatic Mojo instantiation based on {@link InjectMojo} annotations</li>
117  *   <li>Parameter injection using {@link MojoParameter} annotations</li>
118  *   <li>POM configuration handling</li>
119  *   <li>Project stub creation and configuration</li>
120  *   <li>Maven session and build context setup</li>
121  *   <li>Component dependency injection</li>
122  * </ul>
123  *
124  * <p>Example usage in a test class:</p>
125  * <pre>
126  * {@code
127  * @MojoTest
128  * class MyMojoTest {
129  *     @Test
130  *     @InjectMojo(goal = "my-goal")
131  *     @MojoParameter(name = "outputDirectory", value = "${project.build.directory}/generated")
132  *     void testMojoExecution(MyMojo mojo) throws Exception {
133  *         mojo.execute();
134  *         // verify execution results
135  *     }
136  * }
137  * }
138  * </pre>
139  **
140  * <p>For custom POM configurations, you can specify a POM file using the {@link InjectMojo#pom()}
141  * attribute. The extension will merge this configuration with default test project settings.</p>
142  *
143  * <p><b>NOTE:</b> only plugin configuration is taken from provided POM, all other tags are ignored.</p>
144  *
145  *
146  * @see MojoTest
147  * @see InjectMojo
148  * @see MojoParameter
149  * @see Basedir
150  * @since 3.4.0
151  */
152 public class MojoExtension extends PlexusExtension implements ParameterResolver {
153 
154     // Namespace for storing/retrieving data related to MojoExtension
155     private static final ExtensionContext.Namespace MOJO_EXTENSION = ExtensionContext.Namespace.create("MojoExtension");
156 
157     public static final String BASEDIR_IS_SET_KEY = "basedirIsSet";
158 
159     @Override
160     public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
161             throws ParameterResolutionException {
162         return Mojo.class.isAssignableFrom(parameterContext.getParameter().getType())
163                 && (parameterContext.isAnnotated(InjectMojo.class)
164                         || parameterContext.getDeclaringExecutable().isAnnotationPresent(InjectMojo.class));
165     }
166 
167     @Override
168     public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
169             throws ParameterResolutionException {
170         try {
171             InjectMojo injectMojo = parameterContext
172                     .findAnnotation(InjectMojo.class)
173                     .orElseGet(() -> parameterContext.getDeclaringExecutable().getAnnotation(InjectMojo.class));
174 
175             Set<MojoParameter> mojoParameters = new LinkedHashSet<>();
176 
177             extensionContext.getEnclosingTestClasses().forEach(testClass -> {
178                 mojoParameters.addAll(Arrays.asList(testClass.getAnnotationsByType(MojoParameter.class)));
179             });
180 
181             extensionContext
182                     .getTestClass()
183                     .map(c -> c.getAnnotationsByType(MojoParameter.class))
184                     .map(Arrays::asList)
185                     .ifPresent(mojoParameters::addAll);
186 
187             Optional.ofNullable(parameterContext.getDeclaringExecutable().getAnnotation(MojoParameter.class))
188                     .ifPresent(mojoParameters::add);
189 
190             Optional.ofNullable(parameterContext.getDeclaringExecutable().getAnnotation(MojoParameters.class))
191                     .map(MojoParameters::value)
192                     .map(Arrays::asList)
193                     .ifPresent(mojoParameters::addAll);
194 
195             mojoParameters.addAll(parameterContext.findRepeatableAnnotations(MojoParameter.class));
196 
197             Class<?> holder = parameterContext.getTarget().get().getClass();
198             PluginDescriptor descriptor =
199                     extensionContext.getStore(MOJO_EXTENSION).get(PluginDescriptor.class, PluginDescriptor.class);
200             return lookupMojo(extensionContext, holder, injectMojo, mojoParameters, descriptor);
201         } catch (Exception e) {
202             throw new ParameterResolutionException("Unable to resolve parameter", e);
203         }
204     }
205 
206     @Override
207     public void beforeEach(ExtensionContext context) throws Exception {
208         String basedir = AnnotationSupport.findAnnotation(context.getElement().get(), Basedir.class)
209                 .map(Basedir::value)
210                 .orElseGet(() -> {
211                     return AnnotationSupport.findAnnotation(
212                                     context.getRequiredTestClass(), Basedir.class, context.getEnclosingTestClasses())
213                             .map(Basedir::value)
214                             .orElse(null);
215                 });
216 
217         if (basedir == null) {
218             basedir = getBasedir();
219         } else {
220             context.getStore(MOJO_EXTENSION).put(BASEDIR_IS_SET_KEY, Boolean.TRUE);
221         }
222 
223         URL resource = context.getRequiredTestClass().getResource(basedir);
224         if (resource != null) {
225             basedir = Paths.get(resource.toURI()).toString();
226         }
227 
228         // as PluginParameterExpressionEvaluator changes the basedir to absolute path, we need to normalize it here too
229         basedir = new File(basedir).getAbsolutePath();
230 
231         setTestBasedir(basedir, context);
232 
233         PlexusContainer plexusContainer = getContainer(context);
234 
235         context.getRequiredTestInstances().getAllInstances().forEach(testInstance -> ((DefaultPlexusContainer)
236                         plexusContainer)
237                 .addPlexusInjector(Collections.emptyList(), binder -> {
238                     binder.install(ProviderMethodsModule.forObject(testInstance));
239                     binder.install(new MavenProvidesModule(testInstance));
240                 }));
241 
242         addMock(plexusContainer, Log.class, () -> spy(new MojoLogWrapper(LoggerFactory.getLogger("anonymous"))));
243         MavenProject mavenProject = addMock(plexusContainer, MavenProject.class, this::mockMavenProject);
244         MojoExecution mojoExecution = addMock(plexusContainer, MojoExecution.class, this::mockMojoExecution);
245         MavenSession mavenSession = addMock(plexusContainer, MavenSession.class, this::mockMavenSession);
246 
247         // prepare MavenExecutionRequest to be available in BeforeEach methods in test classes
248         createMavenExecutionRequest(context);
249 
250         SessionScope sessionScope = plexusContainer.lookup(SessionScope.class);
251         sessionScope.enter();
252         sessionScope.seed(MavenSession.class, mavenSession);
253 
254         MojoExecutionScope executionScope = plexusContainer.lookup(MojoExecutionScope.class);
255         executionScope.enter();
256         executionScope.seed(MavenProject.class, mavenProject);
257         executionScope.seed(MojoExecution.class, mojoExecution);
258 
259         context.getRequiredTestInstances().getAllInstances().forEach(testInstance -> ((DefaultPlexusContainer)
260                         plexusContainer)
261                 .addPlexusInjector(Collections.emptyList(), binder -> {
262                     binder.requestInjection(testInstance);
263                 }));
264 
265         Map<Object, Object> map = plexusContainer.getContext().getContextData();
266 
267         ClassLoader classLoader = context.getRequiredTestClass().getClassLoader();
268         try (InputStream is = Objects.requireNonNull(
269                         classLoader.getResourceAsStream(getPluginDescriptorLocation()),
270                         "Unable to find plugin descriptor: " + getPluginDescriptorLocation());
271                 Reader reader = new BufferedReader(new XmlStreamReader(is));
272                 InterpolationFilterReader interpolationReader = new InterpolationFilterReader(reader, map, "${", "}")) {
273 
274             PluginDescriptor pluginDescriptor = new PluginDescriptorBuilder().build(interpolationReader);
275             Plugin plugin = new Plugin();
276             plugin.setGroupId(pluginDescriptor.getGroupId());
277             plugin.setArtifactId(pluginDescriptor.getArtifactId());
278             plugin.setVersion(pluginDescriptor.getVersion());
279             pluginDescriptor.setPlugin(plugin);
280             context.getStore(MOJO_EXTENSION).put(PluginDescriptor.class, pluginDescriptor);
281 
282             for (ComponentDescriptor<?> desc : pluginDescriptor.getComponents()) {
283                 plexusContainer.addComponentDescriptor(desc);
284             }
285         }
286     }
287 
288     private <T> T addMock(PlexusContainer container, Class<T> role, Supplier<T> supplier)
289             throws ComponentLookupException {
290         if (!container.hasComponent(role)) {
291             T mock = supplier.get();
292             container.addComponent(mock, role, "default");
293             return mock;
294         } else {
295             return container.lookup(role);
296         }
297     }
298 
299     @Override
300     public void afterEach(ExtensionContext context) throws Exception {
301         SessionScope sessionScope = getContainer(context).lookup(SessionScope.class);
302         sessionScope.exit();
303 
304         MojoExecutionScope executionScope = getContainer(context).lookup(MojoExecutionScope.class);
305         executionScope.exit();
306 
307         super.afterEach(context);
308     }
309 
310     /**
311      * Default MojoExecution mock
312      *
313      * @return a MojoExecution mock
314      */
315     private MojoExecution mockMojoExecution() {
316         return spy(new MojoExecution(null));
317     }
318 
319     /**
320      * Default MavenSession mock
321      *
322      * @return a MavenSession mock
323      */
324     private MavenSession mockMavenSession() {
325         MavenSession session = Mockito.mock(MavenSession.class);
326         lenient().when(session.getUserProperties()).thenReturn(new Properties());
327         lenient().when(session.getSystemProperties()).thenReturn(new Properties());
328         return session;
329     }
330 
331     /**
332      * Default MavenProject mock
333      *
334      * @return a MavenProject mock
335      */
336     private MavenProject mockMavenProject() {
337         MavenProject mavenProject = spy(new MavenProject());
338         Build build = spy(new Build());
339 
340         build.setDirectory(Paths.get(getBasedir(), "target").toString());
341         build.setOutputDirectory(Paths.get(getBasedir(), "target", "classes").toString());
342         build.setTestOutputDirectory(
343                 Paths.get(getBasedir(), "target", "test-classes").toString());
344         build.setSourceDirectory(Paths.get(getBasedir(), "src", "main", "java").toString());
345         build.setTestSourceDirectory(
346                 Paths.get(getBasedir(), "src", "test", "java").toString());
347 
348         Resource resource = spy(new Resource());
349         resource.setDirectory(Paths.get(getBasedir(), "src", "main", "resource").toString());
350         build.setResources(Arrays.asList(resource));
351 
352         Resource testResource = spy(new Resource());
353         testResource.setDirectory(
354                 Paths.get(getBasedir(), "src", "test", "resource").toString());
355         build.setTestResources(Arrays.asList(resource));
356 
357         mavenProject.setBuild(build);
358         mavenProject.addCompileSourceRoot(build.getSourceDirectory());
359         mavenProject.addTestCompileSourceRoot(build.getTestSourceDirectory());
360 
361         try {
362             // there is no setter for basedir, so set it via reflection
363             setVariableValueToObject(
364                     mavenProject, "basedir", Paths.get(getBasedir()).toFile());
365         } catch (IllegalAccessException e) {
366             // should not happen
367             throw new RuntimeException(e);
368         }
369 
370         return mavenProject;
371     }
372 
373     protected String getPluginDescriptorLocation() {
374         return "META-INF/maven/plugin.xml";
375     }
376 
377     private Mojo lookupMojo(
378             ExtensionContext extensionContext,
379             Class<?> holder,
380             InjectMojo injectMojo,
381             Collection<MojoParameter> mojoParameters,
382             PluginDescriptor descriptor)
383             throws Exception {
384         String goal = injectMojo.goal();
385         String pom = injectMojo.pom();
386         Path basedir = Paths.get(getTestBasedir(extensionContext));
387         String[] coord = mojoCoordinates(goal, descriptor);
388         Xpp3Dom pomDom = null;
389         Path pomPath = null;
390         if (pom.startsWith("file:")) {
391             pomPath = basedir.resolve(pom.substring("file:".length()));
392         } else if (pom.startsWith("classpath:")) {
393             URL url = holder.getResource(pom.substring("classpath:".length()));
394             if (url == null) {
395                 throw new IllegalStateException("Unable to find pom on classpath: " + pom);
396             }
397             pomPath = Paths.get(url.toURI());
398         } else if (pom.contains("<project>")) {
399             pomDom = Xpp3DomBuilder.build(new StringReader(pom));
400         } else if (!pom.isEmpty()) {
401             pomPath = basedir.resolve(pom);
402         } else if (isBasedirSet(extensionContext)) {
403             // only look for a pom.xml if basedir is explicitly set
404             pomPath = basedir.resolve("pom.xml");
405         }
406 
407         if (pomDom == null) {
408             if (pomPath != null && Files.exists(pomPath)) {
409                 pomDom = Xpp3DomBuilder.build(new XmlStreamReader(pomPath.toFile()));
410             } else {
411                 pomDom = new Xpp3Dom("");
412             }
413         }
414 
415         Xpp3Dom pluginConfiguration = extractPluginConfiguration(coord[1], pomDom);
416         if (!mojoParameters.isEmpty()) {
417             List<Xpp3Dom> children = mojoParameters.stream()
418                     .map(mp -> {
419                         Xpp3Dom c = new Xpp3Dom(mp.name());
420                         c.setValue(mp.value());
421                         return c;
422                     })
423                     .collect(Collectors.toList());
424             Xpp3Dom config = new Xpp3Dom("configuration");
425             children.forEach(config::addChild);
426             pluginConfiguration = Xpp3Dom.mergeXpp3Dom(config, pluginConfiguration);
427         }
428         return lookupMojo(extensionContext, coord, pluginConfiguration, descriptor, pomPath);
429     }
430 
431     private boolean isBasedirSet(ExtensionContext extensionContext) {
432         return extensionContext.getStore(MOJO_EXTENSION).getOrDefault(BASEDIR_IS_SET_KEY, Boolean.class, Boolean.FALSE);
433     }
434 
435     protected String[] mojoCoordinates(String goal, PluginDescriptor pluginDescriptor) throws Exception {
436         if (goal.matches(".*:.*:.*:.*")) {
437             return goal.split(":");
438         } else {
439             String artifactId = pluginDescriptor.getArtifactId();
440             String groupId = pluginDescriptor.getGroupId();
441             String version = pluginDescriptor.getVersion();
442             return new String[] {groupId, artifactId, version, goal};
443         }
444     }
445 
446     /**
447      * lookup the mojo while we have all the relevent information
448      */
449     protected Mojo lookupMojo(
450             ExtensionContext extensionContext,
451             String[] coord,
452             Xpp3Dom pluginConfiguration,
453             PluginDescriptor descriptor,
454             Path pomPath)
455             throws Exception {
456         PlexusContainer plexusContainer = getContainer(extensionContext);
457 
458         MavenExecutionRequest request = setupMavenExecutionRequest(extensionContext);
459         plexusContainer.lookup(MavenExecutionRequestPopulator.class).populateDefaults(request);
460         setupRepositorySession(extensionContext, request);
461 
462         // pluginkey = groupId : artifactId : version : goal
463         Mojo mojo = plexusContainer.lookup(Mojo.class, coord[0] + ":" + coord[1] + ":" + coord[2] + ":" + coord[3]);
464 
465         Optional<MojoDescriptor> mojoDescriptor = descriptor.getMojos().stream()
466                 .filter(md ->
467                         Objects.equals(md.getImplementation(), mojo.getClass().getName()))
468                 .findFirst();
469 
470         if (mojoDescriptor.isPresent()) {
471             pluginConfiguration = finalizeConfig(pluginConfiguration, mojoDescriptor.get());
472         }
473 
474         MavenSession session = plexusContainer.lookup(MavenSession.class);
475         MavenProject mavenProject = plexusContainer.lookup(MavenProject.class);
476         MojoExecution mojoExecution = plexusContainer.lookup(MojoExecution.class);
477 
478         if (mockingDetails(session).isMock()) {
479             lenient().doReturn(mavenProject).when(session).getCurrentProject();
480             lenient().doReturn(request.getLocalRepository()).when(session).getLocalRepository();
481         }
482 
483         if (mockingDetails(mavenProject).isMock()) {
484             File pomFile = Optional.ofNullable(pomPath).map(Path::toFile).orElse(null);
485             if (mockingDetails(mavenProject).isSpy()) {
486                 // we only set the pom file
487                 // setFile also change a basedir, so should not be used here
488                 mavenProject.setPomFile(pomFile);
489             } else {
490                 lenient().doReturn(pomFile).when(mavenProject).getFile();
491             }
492         }
493 
494         if (mojoDescriptor.isPresent() && mockingDetails(mojoExecution).isMock()) {
495             if (mockingDetails(mojoExecution).isSpy()) {
496                 mojoExecution.setMojoDescriptor(mojoDescriptor.get());
497             } else {
498                 lenient().doReturn(mojoDescriptor.get()).when(mojoExecution).getMojoDescriptor();
499             }
500         }
501 
502         if (pluginConfiguration != null) {
503             ExpressionEvaluator evaluator =
504                     new WrapEvaluator(plexusContainer, new PluginParameterExpressionEvaluator(session, mojoExecution));
505             ComponentConfigurator configurator = new BasicComponentConfigurator();
506             configurator.configureComponent(
507                     mojo,
508                     new XmlPlexusConfiguration(pluginConfiguration),
509                     evaluator,
510                     plexusContainer.getContainerRealm());
511         }
512 
513         mojo.setLog(plexusContainer.lookup(Log.class));
514 
515         // clear invocations on mocks to avoid test interference
516         if (mockingDetails(session).isMock()) {
517             clearInvocations(session);
518         }
519 
520         if (mockingDetails(mavenProject).isMock()) {
521             clearInvocations(mavenProject);
522         }
523 
524         if (mockingDetails(mojoExecution).isMock()) {
525             clearInvocations(mojoExecution);
526         }
527 
528         return mojo;
529     }
530 
531     private boolean isRealRepositorySessionNotRequired(ExtensionContext context) {
532         return !AnnotationSupport.findAnnotation(
533                         context.getRequiredTestClass(), MojoTest.class, context.getEnclosingTestClasses())
534                 .map(MojoTest::realRepositorySession)
535                 .orElse(false);
536     }
537 
538     /**
539      * Create a MavenExecutionRequest if not already present in the MavenSession
540      */
541     private void createMavenExecutionRequest(ExtensionContext context) throws ComponentLookupException {
542         PlexusContainer container = getContainer(context);
543         MavenSession session = container.lookup(MavenSession.class);
544         MavenExecutionRequest request = session.getRequest();
545 
546         if (request == null && mockingDetails(session).isMock()) {
547             lenient()
548                     .doReturn(spy(new DefaultMavenExecutionRequest()))
549                     .when(session)
550                     .getRequest();
551         }
552     }
553 
554     private MavenExecutionRequest setupMavenExecutionRequest(ExtensionContext context) throws ComponentLookupException {
555         PlexusContainer container = getContainer(context);
556         MavenSession session = container.lookup(MavenSession.class);
557         MavenExecutionRequest request = session.getRequest();
558 
559         if (request == null) {
560             // user can provide own MavenSession instance without a request
561             request = new DefaultMavenExecutionRequest();
562         }
563 
564         if (request.getStartTime() == null) {
565             request.setStartTime(new Date());
566         }
567 
568         if (request.getUserProperties().isEmpty()) {
569             request.setUserProperties(session.getUserProperties());
570         }
571 
572         if (request.getSystemProperties().isEmpty()) {
573             request.setSystemProperties(session.getSystemProperties());
574         }
575 
576         // set a default local repository path if none is set
577         if (request.getLocalRepositoryPath() == null && request.getLocalRepository() == null) {
578             request.setLocalRepositoryPath(getTestBasedir(context) + "/target/local-repo");
579         }
580 
581         if (request.getBaseDirectory() == null) {
582             request.setBaseDirectory(new File(getTestBasedir(context)));
583         }
584 
585         return request;
586     }
587 
588     private void setupRepositorySession(ExtensionContext context, MavenExecutionRequest request)
589             throws ComponentLookupException {
590 
591         if (isRealRepositorySessionNotRequired(context)) {
592             return;
593         }
594 
595         PlexusContainer container = getContainer(context);
596 
597         MavenProject mavenProject = container.lookup(MavenProject.class);
598         if (mockingDetails(mavenProject).isMock()) {
599             lenient()
600                     .doReturn(request.getRemoteRepositories())
601                     .when(mavenProject)
602                     .getRemoteArtifactRepositories();
603             lenient()
604                     .doReturn(request.getPluginArtifactRepositories())
605                     .when(mavenProject)
606                     .getPluginArtifactRepositories();
607             lenient()
608                     .doReturn(RepositoryUtils.toRepos(request.getRemoteRepositories()))
609                     .when(mavenProject)
610                     .getRemoteProjectRepositories();
611             lenient()
612                     .doReturn(RepositoryUtils.toRepos(request.getPluginArtifactRepositories()))
613                     .when(mavenProject)
614                     .getRemotePluginRepositories();
615         }
616 
617         RepositorySystemSession repositorySystemSession =
618                 container.lookup(DefaultRepositorySystemSessionFactory.class).newRepositorySession(request);
619 
620         MavenSession session = container.lookup(MavenSession.class);
621         if (mockingDetails(session).isMock()) {
622             lenient().doReturn(repositorySystemSession).when(session).getRepositorySession();
623         }
624     }
625 
626     private Xpp3Dom finalizeConfig(Xpp3Dom config, MojoDescriptor mojoDescriptor) {
627         List<Xpp3Dom> children = new ArrayList<>();
628         if (mojoDescriptor != null && mojoDescriptor.getParameters() != null) {
629             Xpp3Dom defaultConfiguration = MojoDescriptorCreator.convert(mojoDescriptor);
630             for (Parameter parameter : mojoDescriptor.getParameters()) {
631                 Xpp3Dom parameterConfiguration = config.getChild(parameter.getName());
632                 if (parameterConfiguration == null) {
633                     parameterConfiguration = config.getChild(parameter.getAlias());
634                 }
635                 Xpp3Dom parameterDefaults = defaultConfiguration.getChild(parameter.getName());
636                 parameterConfiguration = Xpp3Dom.mergeXpp3Dom(parameterConfiguration, parameterDefaults, Boolean.TRUE);
637                 if (parameterConfiguration != null) {
638                     if (isEmpty(parameterConfiguration.getAttribute("implementation"))
639                             && !isEmpty(parameter.getImplementation())) {
640                         parameterConfiguration.setAttribute("implementation", parameter.getImplementation());
641                     }
642                     children.add(parameterConfiguration);
643                 }
644             }
645         }
646         Xpp3Dom c = new Xpp3Dom("configuration");
647         children.forEach(c::addChild);
648         return c;
649     }
650 
651     private boolean isEmpty(String str) {
652         return str == null || str.isEmpty();
653     }
654 
655     private static Optional<Xpp3Dom> child(Xpp3Dom element, String name) {
656         return Optional.ofNullable(element.getChild(name));
657     }
658 
659     private static Stream<Xpp3Dom> children(Xpp3Dom element) {
660         return Stream.of(element.getChildren());
661     }
662 
663     public static Xpp3Dom extractPluginConfiguration(String artifactId, Xpp3Dom pomDom) throws Exception {
664         Xpp3Dom pluginConfigurationElement = child(pomDom, "build")
665                 .flatMap(buildElement -> child(buildElement, "plugins"))
666                 .map(MojoExtension::children)
667                 .orElseGet(Stream::empty)
668                 .filter(e -> e.getChild("artifactId").getValue().equals(artifactId))
669                 .findFirst()
670                 .flatMap(buildElement -> child(buildElement, "configuration"))
671                 .orElse(Xpp3DomBuilder.build(new StringReader("<configuration/>")));
672         return pluginConfigurationElement;
673     }
674 
675     /**
676      * Convenience method to obtain the value of a variable on a mojo that might not have a getter.
677      * <br>
678      * Note: the caller is responsible for casting to what the desired type is.
679      */
680     @SuppressWarnings("unchecked")
681     public static <T> T getVariableValueFromObject(Object object, String variable) throws IllegalAccessException {
682         Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
683         field.setAccessible(true);
684         return (T) field.get(object);
685     }
686 
687     /**
688      * Convenience method to obtain all variables and values from the mojo (including its superclasses)
689      * <br>
690      * Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
691      */
692     public static Map<String, Object> getVariablesAndValuesFromObject(Object object) throws IllegalAccessException {
693         return getVariablesAndValuesFromObject(object.getClass(), object);
694     }
695 
696     /**
697      * Convenience method to obtain all variables and values from the mojo (including its superclasses)
698      * <br>
699      * Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
700      *
701      * @return map of variable names and values
702      */
703     public static Map<String, Object> getVariablesAndValuesFromObject(Class<?> clazz, Object object)
704             throws IllegalAccessException {
705         Map<String, Object> map = new HashMap<>();
706         Field[] fields = clazz.getDeclaredFields();
707         AccessibleObject.setAccessible(fields, true);
708         for (Field field : fields) {
709             map.put(field.getName(), field.get(object));
710         }
711         Class<?> superclass = clazz.getSuperclass();
712         if (!Object.class.equals(superclass)) {
713             map.putAll(getVariablesAndValuesFromObject(superclass, object));
714         }
715         return map;
716     }
717 
718     /**
719      * Gets the base directory for test resources.
720      * If not explicitly set via {@link Basedir}, returns the plugin base directory.
721      */
722     public static String getBasedir() {
723         return PlexusExtension.getBasedir();
724     }
725 
726     /**
727      * Gets the file according to base directory for test resources.
728      */
729     public static File getTestFile(String path) {
730         return PlexusExtension.getTestFile(path);
731     }
732 
733     /**
734      * Gets the path according to base directory for test resources.
735      */
736     public static String getTestPath(String path) {
737         return PlexusExtension.getTestPath(path);
738     }
739 
740     /**
741      * Convenience method to set values to variables in objects that don't have setters
742      */
743     public static void setVariableValueToObject(Object object, String variable, Object value)
744             throws IllegalAccessException {
745         Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
746         Objects.requireNonNull(field, "Field " + variable + " not found");
747         field.setAccessible(true);
748         field.set(object, value);
749     }
750 
751     private static class WrapEvaluator implements TypeAwareExpressionEvaluator {
752 
753         private final PlexusContainer container;
754 
755         private final TypeAwareExpressionEvaluator evaluator;
756 
757         WrapEvaluator(PlexusContainer container, TypeAwareExpressionEvaluator evaluator) {
758             this.container = container;
759             this.evaluator = evaluator;
760         }
761 
762         @Override
763         public Object evaluate(String expression) throws ExpressionEvaluationException {
764             return evaluate(expression, null);
765         }
766 
767         @Override
768         public Object evaluate(String expression, Class<?> type) throws ExpressionEvaluationException {
769             Object value = evaluator.evaluate(expression, type);
770             if (value == null) {
771                 String expr = stripTokens(expression);
772                 if (expr != null) {
773                     try {
774                         value = container.lookup(type, expr);
775                     } catch (ComponentLookupException e) {
776                         // nothing
777                     }
778                 }
779             }
780             return value;
781         }
782 
783         private String stripTokens(String expr) {
784             if (expr.startsWith("${") && expr.endsWith("}")) {
785                 return expr.substring(2, expr.length() - 1);
786             }
787             return null;
788         }
789 
790         @Override
791         public File alignToBaseDirectory(File path) {
792             return evaluator.alignToBaseDirectory(path);
793         }
794     }
795 
796     private static class MavenProvidesModule implements Module {
797         private final Object testInstance;
798 
799         MavenProvidesModule(Object testInstance) {
800             this.testInstance = testInstance;
801         }
802 
803         @Override
804         @SuppressWarnings("unchecked")
805         public void configure(Binder binder) {
806             List<Method> providesMethods = AnnotationSupport.findAnnotatedMethods(
807                     testInstance.getClass(), Provides.class, HierarchyTraversalMode.BOTTOM_UP);
808 
809             for (Method method : providesMethods) {
810                 if (method.getParameterCount() > 0) {
811                     throw new IllegalArgumentException("Parameterized method are not supported " + method);
812                 }
813                 try {
814                     method.setAccessible(true);
815                     Object value = method.invoke(testInstance);
816                     if (value == null) {
817                         throw new IllegalArgumentException("Provides method returned null: " + method);
818                     }
819                     binder.bind((Class<Object>) method.getReturnType()).toInstance(value);
820                 } catch (IllegalAccessException | InvocationTargetException e) {
821                     throw new IllegalArgumentException(e);
822                 }
823             }
824         }
825     }
826 }