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(
350                 Paths.get(getBasedir(), "src", "main", "resources").toString());
351         build.setResources(Arrays.asList(resource));
352 
353         Resource testResource = spy(new Resource());
354         testResource.setDirectory(
355                 Paths.get(getBasedir(), "src", "test", "resources").toString());
356         build.setTestResources(Arrays.asList(resource));
357 
358         mavenProject.setBuild(build);
359         mavenProject.addCompileSourceRoot(build.getSourceDirectory());
360         mavenProject.addTestCompileSourceRoot(build.getTestSourceDirectory());
361 
362         try {
363             // there is no setter for basedir, so set it via reflection
364             setVariableValueToObject(
365                     mavenProject, "basedir", Paths.get(getBasedir()).toFile());
366         } catch (IllegalAccessException e) {
367             // should not happen
368             throw new RuntimeException(e);
369         }
370 
371         return mavenProject;
372     }
373 
374     protected String getPluginDescriptorLocation() {
375         return "META-INF/maven/plugin.xml";
376     }
377 
378     private Mojo lookupMojo(
379             ExtensionContext extensionContext,
380             Class<?> holder,
381             InjectMojo injectMojo,
382             Collection<MojoParameter> mojoParameters,
383             PluginDescriptor descriptor)
384             throws Exception {
385         String goal = injectMojo.goal();
386         String pom = injectMojo.pom();
387         Path basedir = Paths.get(getTestBasedir(extensionContext));
388         String[] coord = mojoCoordinates(goal, descriptor);
389         Xpp3Dom pomDom = null;
390         Path pomPath = null;
391         if (pom.startsWith("file:")) {
392             pomPath = basedir.resolve(pom.substring("file:".length()));
393         } else if (pom.startsWith("classpath:")) {
394             URL url = holder.getResource(pom.substring("classpath:".length()));
395             if (url == null) {
396                 throw new IllegalStateException("Unable to find pom on classpath: " + pom);
397             }
398             pomPath = Paths.get(url.toURI());
399         } else if (pom.contains("<project>")) {
400             pomDom = Xpp3DomBuilder.build(new StringReader(pom));
401         } else if (!pom.isEmpty()) {
402             pomPath = basedir.resolve(pom);
403         } else if (isBasedirSet(extensionContext)) {
404             // only look for a pom.xml if basedir is explicitly set
405             pomPath = basedir.resolve("pom.xml");
406         }
407 
408         if (pomDom == null) {
409             if (pomPath != null && Files.exists(pomPath)) {
410                 pomDom = Xpp3DomBuilder.build(new XmlStreamReader(pomPath.toFile()));
411             } else {
412                 pomDom = new Xpp3Dom("");
413             }
414         }
415 
416         Xpp3Dom pluginConfiguration = extractPluginConfiguration(coord[1], pomDom);
417         if (!mojoParameters.isEmpty()) {
418             List<Xpp3Dom> children = mojoParameters.stream()
419                     .map(mp -> {
420                         Xpp3Dom c = new Xpp3Dom(mp.name());
421                         c.setValue(mp.value());
422                         return c;
423                     })
424                     .collect(Collectors.toList());
425             Xpp3Dom config = new Xpp3Dom("configuration");
426             children.forEach(config::addChild);
427             pluginConfiguration = Xpp3Dom.mergeXpp3Dom(config, pluginConfiguration);
428         }
429         return lookupMojo(extensionContext, coord, pluginConfiguration, descriptor, pomPath);
430     }
431 
432     private boolean isBasedirSet(ExtensionContext extensionContext) {
433         return extensionContext.getStore(MOJO_EXTENSION).getOrDefault(BASEDIR_IS_SET_KEY, Boolean.class, Boolean.FALSE);
434     }
435 
436     protected String[] mojoCoordinates(String goal, PluginDescriptor pluginDescriptor) throws Exception {
437         if (goal.matches(".*:.*:.*:.*")) {
438             return goal.split(":");
439         } else {
440             String artifactId = pluginDescriptor.getArtifactId();
441             String groupId = pluginDescriptor.getGroupId();
442             String version = pluginDescriptor.getVersion();
443             return new String[] {groupId, artifactId, version, goal};
444         }
445     }
446 
447     /**
448      * lookup the mojo while we have all the relevent information
449      */
450     protected Mojo lookupMojo(
451             ExtensionContext extensionContext,
452             String[] coord,
453             Xpp3Dom pluginConfiguration,
454             PluginDescriptor descriptor,
455             Path pomPath)
456             throws Exception {
457         PlexusContainer plexusContainer = getContainer(extensionContext);
458 
459         MavenExecutionRequest request = setupMavenExecutionRequest(extensionContext);
460         plexusContainer.lookup(MavenExecutionRequestPopulator.class).populateDefaults(request);
461         setupRepositorySession(extensionContext, request);
462 
463         // pluginkey = groupId : artifactId : version : goal
464         Mojo mojo = plexusContainer.lookup(Mojo.class, coord[0] + ":" + coord[1] + ":" + coord[2] + ":" + coord[3]);
465 
466         Optional<MojoDescriptor> mojoDescriptor = descriptor.getMojos().stream()
467                 .filter(md ->
468                         Objects.equals(md.getImplementation(), mojo.getClass().getName()))
469                 .findFirst();
470 
471         if (mojoDescriptor.isPresent()) {
472             pluginConfiguration = finalizeConfig(pluginConfiguration, mojoDescriptor.get());
473         }
474 
475         MavenSession session = plexusContainer.lookup(MavenSession.class);
476         MavenProject mavenProject = plexusContainer.lookup(MavenProject.class);
477         MojoExecution mojoExecution = plexusContainer.lookup(MojoExecution.class);
478 
479         if (mockingDetails(session).isMock()) {
480             lenient().doReturn(mavenProject).when(session).getCurrentProject();
481             lenient().doReturn(request.getLocalRepository()).when(session).getLocalRepository();
482         }
483 
484         if (mockingDetails(mavenProject).isMock()) {
485             File pomFile = Optional.ofNullable(pomPath).map(Path::toFile).orElse(null);
486             if (mockingDetails(mavenProject).isSpy()) {
487                 // we only set the pom file
488                 // setFile also change a basedir, so should not be used here
489                 mavenProject.setPomFile(pomFile);
490             } else {
491                 lenient().doReturn(pomFile).when(mavenProject).getFile();
492             }
493         }
494 
495         if (mojoDescriptor.isPresent() && mockingDetails(mojoExecution).isMock()) {
496             if (mockingDetails(mojoExecution).isSpy()) {
497                 mojoExecution.setMojoDescriptor(mojoDescriptor.get());
498             } else {
499                 lenient().doReturn(mojoDescriptor.get()).when(mojoExecution).getMojoDescriptor();
500             }
501         }
502 
503         if (pluginConfiguration != null) {
504             ExpressionEvaluator evaluator =
505                     new WrapEvaluator(plexusContainer, new PluginParameterExpressionEvaluator(session, mojoExecution));
506             ComponentConfigurator configurator = new BasicComponentConfigurator();
507             configurator.configureComponent(
508                     mojo,
509                     new XmlPlexusConfiguration(pluginConfiguration),
510                     evaluator,
511                     plexusContainer.getContainerRealm());
512         }
513 
514         mojo.setLog(plexusContainer.lookup(Log.class));
515 
516         // clear invocations on mocks to avoid test interference
517         if (mockingDetails(session).isMock()) {
518             clearInvocations(session);
519         }
520 
521         if (mockingDetails(mavenProject).isMock()) {
522             clearInvocations(mavenProject);
523         }
524 
525         if (mockingDetails(mojoExecution).isMock()) {
526             clearInvocations(mojoExecution);
527         }
528 
529         return mojo;
530     }
531 
532     private boolean isRealRepositorySessionNotRequired(ExtensionContext context) {
533         return !AnnotationSupport.findAnnotation(
534                         context.getRequiredTestClass(), MojoTest.class, context.getEnclosingTestClasses())
535                 .map(MojoTest::realRepositorySession)
536                 .orElse(false);
537     }
538 
539     /**
540      * Create a MavenExecutionRequest if not already present in the MavenSession
541      */
542     private void createMavenExecutionRequest(ExtensionContext context) throws ComponentLookupException {
543         PlexusContainer container = getContainer(context);
544         MavenSession session = container.lookup(MavenSession.class);
545         MavenExecutionRequest request = session.getRequest();
546 
547         if (request == null && mockingDetails(session).isMock()) {
548             lenient()
549                     .doReturn(spy(new DefaultMavenExecutionRequest()))
550                     .when(session)
551                     .getRequest();
552         }
553     }
554 
555     private MavenExecutionRequest setupMavenExecutionRequest(ExtensionContext context) throws ComponentLookupException {
556         PlexusContainer container = getContainer(context);
557         MavenSession session = container.lookup(MavenSession.class);
558         MavenExecutionRequest request = session.getRequest();
559 
560         if (request == null) {
561             // user can provide own MavenSession instance without a request
562             request = new DefaultMavenExecutionRequest();
563         }
564 
565         if (request.getStartTime() == null) {
566             request.setStartTime(new Date());
567         }
568 
569         if (request.getUserProperties().isEmpty()) {
570             request.setUserProperties(session.getUserProperties());
571         }
572 
573         if (request.getSystemProperties().isEmpty()) {
574             request.setSystemProperties(session.getSystemProperties());
575         }
576 
577         // set a default local repository path if none is set
578         if (request.getLocalRepositoryPath() == null && request.getLocalRepository() == null) {
579             request.setLocalRepositoryPath(getTestBasedir(context) + "/target/local-repo");
580         }
581 
582         if (request.getBaseDirectory() == null) {
583             request.setBaseDirectory(new File(getTestBasedir(context)));
584         }
585 
586         return request;
587     }
588 
589     private void setupRepositorySession(ExtensionContext context, MavenExecutionRequest request)
590             throws ComponentLookupException {
591 
592         if (isRealRepositorySessionNotRequired(context)) {
593             return;
594         }
595 
596         PlexusContainer container = getContainer(context);
597 
598         MavenProject mavenProject = container.lookup(MavenProject.class);
599         if (mockingDetails(mavenProject).isMock()) {
600             lenient()
601                     .doReturn(request.getRemoteRepositories())
602                     .when(mavenProject)
603                     .getRemoteArtifactRepositories();
604             lenient()
605                     .doReturn(request.getPluginArtifactRepositories())
606                     .when(mavenProject)
607                     .getPluginArtifactRepositories();
608             lenient()
609                     .doReturn(RepositoryUtils.toRepos(request.getRemoteRepositories()))
610                     .when(mavenProject)
611                     .getRemoteProjectRepositories();
612             lenient()
613                     .doReturn(RepositoryUtils.toRepos(request.getPluginArtifactRepositories()))
614                     .when(mavenProject)
615                     .getRemotePluginRepositories();
616         }
617 
618         RepositorySystemSession repositorySystemSession =
619                 container.lookup(DefaultRepositorySystemSessionFactory.class).newRepositorySession(request);
620 
621         MavenSession session = container.lookup(MavenSession.class);
622         if (mockingDetails(session).isMock()) {
623             lenient().doReturn(repositorySystemSession).when(session).getRepositorySession();
624         }
625     }
626 
627     private Xpp3Dom finalizeConfig(Xpp3Dom config, MojoDescriptor mojoDescriptor) {
628         List<Xpp3Dom> children = new ArrayList<>();
629         if (mojoDescriptor != null && mojoDescriptor.getParameters() != null) {
630             Xpp3Dom defaultConfiguration = MojoDescriptorCreator.convert(mojoDescriptor);
631             for (Parameter parameter : mojoDescriptor.getParameters()) {
632                 Xpp3Dom parameterConfiguration = config.getChild(parameter.getName());
633                 if (parameterConfiguration == null) {
634                     parameterConfiguration = config.getChild(parameter.getAlias());
635                 }
636                 Xpp3Dom parameterDefaults = defaultConfiguration.getChild(parameter.getName());
637                 parameterConfiguration = Xpp3Dom.mergeXpp3Dom(parameterConfiguration, parameterDefaults, Boolean.TRUE);
638                 if (parameterConfiguration != null) {
639                     if (isEmpty(parameterConfiguration.getAttribute("implementation"))
640                             && !isEmpty(parameter.getImplementation())) {
641                         parameterConfiguration.setAttribute("implementation", parameter.getImplementation());
642                     }
643                     children.add(parameterConfiguration);
644                 }
645             }
646         }
647         Xpp3Dom c = new Xpp3Dom("configuration");
648         children.forEach(c::addChild);
649         return c;
650     }
651 
652     private boolean isEmpty(String str) {
653         return str == null || str.isEmpty();
654     }
655 
656     private static Optional<Xpp3Dom> child(Xpp3Dom element, String name) {
657         return Optional.ofNullable(element.getChild(name));
658     }
659 
660     private static Stream<Xpp3Dom> children(Xpp3Dom element) {
661         return Stream.of(element.getChildren());
662     }
663 
664     public static Xpp3Dom extractPluginConfiguration(String artifactId, Xpp3Dom pomDom) throws Exception {
665         Xpp3Dom pluginConfigurationElement = child(pomDom, "build")
666                 .flatMap(buildElement -> child(buildElement, "plugins"))
667                 .map(MojoExtension::children)
668                 .orElseGet(Stream::empty)
669                 .filter(e -> e.getChild("artifactId").getValue().equals(artifactId))
670                 .findFirst()
671                 .flatMap(buildElement -> child(buildElement, "configuration"))
672                 .orElse(Xpp3DomBuilder.build(new StringReader("<configuration/>")));
673         return pluginConfigurationElement;
674     }
675 
676     /**
677      * Convenience method to obtain the value of a variable on a mojo that might not have a getter.
678      * <br>
679      * Note: the caller is responsible for casting to what the desired type is.
680      */
681     @SuppressWarnings("unchecked")
682     public static <T> T getVariableValueFromObject(Object object, String variable) throws IllegalAccessException {
683         Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
684         field.setAccessible(true);
685         return (T) field.get(object);
686     }
687 
688     /**
689      * Convenience method to obtain all variables and values from the mojo (including its superclasses)
690      * <br>
691      * Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
692      */
693     public static Map<String, Object> getVariablesAndValuesFromObject(Object object) throws IllegalAccessException {
694         return getVariablesAndValuesFromObject(object.getClass(), object);
695     }
696 
697     /**
698      * Convenience method to obtain all variables and values from the mojo (including its superclasses)
699      * <br>
700      * Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
701      *
702      * @return map of variable names and values
703      */
704     public static Map<String, Object> getVariablesAndValuesFromObject(Class<?> clazz, Object object)
705             throws IllegalAccessException {
706         Map<String, Object> map = new HashMap<>();
707         Field[] fields = clazz.getDeclaredFields();
708         AccessibleObject.setAccessible(fields, true);
709         for (Field field : fields) {
710             map.put(field.getName(), field.get(object));
711         }
712         Class<?> superclass = clazz.getSuperclass();
713         if (!Object.class.equals(superclass)) {
714             map.putAll(getVariablesAndValuesFromObject(superclass, object));
715         }
716         return map;
717     }
718 
719     /**
720      * Gets the base directory for test resources.
721      * If not explicitly set via {@link Basedir}, returns the plugin base directory.
722      */
723     public static String getBasedir() {
724         return PlexusExtension.getBasedir();
725     }
726 
727     /**
728      * Gets the file according to base directory for test resources.
729      */
730     public static File getTestFile(String path) {
731         return PlexusExtension.getTestFile(path);
732     }
733 
734     /**
735      * Gets the path according to base directory for test resources.
736      */
737     public static String getTestPath(String path) {
738         return PlexusExtension.getTestPath(path);
739     }
740 
741     /**
742      * Convenience method to set values to variables in objects that don't have setters
743      */
744     public static void setVariableValueToObject(Object object, String variable, Object value)
745             throws IllegalAccessException {
746         Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
747         Objects.requireNonNull(field, "Field " + variable + " not found");
748         field.setAccessible(true);
749         field.set(object, value);
750     }
751 
752     private static class WrapEvaluator implements TypeAwareExpressionEvaluator {
753 
754         private final PlexusContainer container;
755 
756         private final TypeAwareExpressionEvaluator evaluator;
757 
758         WrapEvaluator(PlexusContainer container, TypeAwareExpressionEvaluator evaluator) {
759             this.container = container;
760             this.evaluator = evaluator;
761         }
762 
763         @Override
764         public Object evaluate(String expression) throws ExpressionEvaluationException {
765             return evaluate(expression, null);
766         }
767 
768         @Override
769         public Object evaluate(String expression, Class<?> type) throws ExpressionEvaluationException {
770             Object value = evaluator.evaluate(expression, type);
771             if (value == null) {
772                 String expr = stripTokens(expression);
773                 if (expr != null) {
774                     try {
775                         value = container.lookup(type, expr);
776                     } catch (ComponentLookupException e) {
777                         // nothing
778                     }
779                 }
780             }
781             return value;
782         }
783 
784         private String stripTokens(String expr) {
785             if (expr.startsWith("${") && expr.endsWith("}")) {
786                 return expr.substring(2, expr.length() - 1);
787             }
788             return null;
789         }
790 
791         @Override
792         public File alignToBaseDirectory(File path) {
793             return evaluator.alignToBaseDirectory(path);
794         }
795     }
796 
797     private static class MavenProvidesModule implements Module {
798         private final Object testInstance;
799 
800         MavenProvidesModule(Object testInstance) {
801             this.testInstance = testInstance;
802         }
803 
804         @Override
805         @SuppressWarnings("unchecked")
806         public void configure(Binder binder) {
807             List<Method> providesMethods = AnnotationSupport.findAnnotatedMethods(
808                     testInstance.getClass(), Provides.class, HierarchyTraversalMode.BOTTOM_UP);
809 
810             for (Method method : providesMethods) {
811                 if (method.getParameterCount() > 0) {
812                     throw new IllegalArgumentException("Parameterized method are not supported " + method);
813                 }
814                 try {
815                     method.setAccessible(true);
816                     Object value = method.invoke(testInstance);
817                     if (value == null) {
818                         throw new IllegalArgumentException("Provides method returned null: " + method);
819                     }
820                     binder.bind((Class<Object>) method.getReturnType()).toInstance(value);
821                 } catch (IllegalAccessException | InvocationTargetException e) {
822                     throw new IllegalArgumentException(e);
823                 }
824             }
825         }
826     }
827 }