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.HashMap;
39  import java.util.HashSet;
40  import java.util.List;
41  import java.util.Map;
42  import java.util.Objects;
43  import java.util.Optional;
44  import java.util.Properties;
45  import java.util.Set;
46  import java.util.function.Supplier;
47  import java.util.stream.Collectors;
48  import java.util.stream.Stream;
49  
50  import com.google.inject.Binder;
51  import com.google.inject.Module;
52  import com.google.inject.internal.ProviderMethodsModule;
53  import org.apache.maven.api.di.Provides;
54  import org.apache.maven.execution.MavenSession;
55  import org.apache.maven.execution.scope.internal.MojoExecutionScope;
56  import org.apache.maven.lifecycle.internal.MojoDescriptorCreator;
57  import org.apache.maven.plugin.Mojo;
58  import org.apache.maven.plugin.MojoExecution;
59  import org.apache.maven.plugin.PluginParameterExpressionEvaluator;
60  import org.apache.maven.plugin.descriptor.MojoDescriptor;
61  import org.apache.maven.plugin.descriptor.Parameter;
62  import org.apache.maven.plugin.descriptor.PluginDescriptor;
63  import org.apache.maven.plugin.descriptor.PluginDescriptorBuilder;
64  import org.apache.maven.plugin.logging.Log;
65  import org.apache.maven.plugin.testing.MojoLogWrapper;
66  import org.apache.maven.project.MavenProject;
67  import org.apache.maven.session.scope.internal.SessionScope;
68  import org.codehaus.plexus.DefaultPlexusContainer;
69  import org.codehaus.plexus.PlexusContainer;
70  import org.codehaus.plexus.component.configurator.BasicComponentConfigurator;
71  import org.codehaus.plexus.component.configurator.ComponentConfigurator;
72  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluationException;
73  import org.codehaus.plexus.component.configurator.expression.ExpressionEvaluator;
74  import org.codehaus.plexus.component.configurator.expression.TypeAwareExpressionEvaluator;
75  import org.codehaus.plexus.component.repository.ComponentDescriptor;
76  import org.codehaus.plexus.component.repository.exception.ComponentLookupException;
77  import org.codehaus.plexus.configuration.xml.XmlPlexusConfiguration;
78  import org.codehaus.plexus.testing.PlexusExtension;
79  import org.codehaus.plexus.util.InterpolationFilterReader;
80  import org.codehaus.plexus.util.ReflectionUtils;
81  import org.codehaus.plexus.util.xml.XmlStreamReader;
82  import org.codehaus.plexus.util.xml.Xpp3Dom;
83  import org.codehaus.plexus.util.xml.Xpp3DomBuilder;
84  import org.junit.jupiter.api.extension.ExtensionContext;
85  import org.junit.jupiter.api.extension.ParameterContext;
86  import org.junit.jupiter.api.extension.ParameterResolutionException;
87  import org.junit.jupiter.api.extension.ParameterResolver;
88  import org.junit.platform.commons.support.AnnotationSupport;
89  import org.junit.platform.commons.support.HierarchyTraversalMode;
90  import org.mockito.Mockito;
91  import org.slf4j.LoggerFactory;
92  
93  import static org.mockito.Mockito.clearInvocations;
94  import static org.mockito.Mockito.lenient;
95  import static org.mockito.Mockito.mockingDetails;
96  
97  /**
98   * JUnit Jupiter extension that provides support for testing Maven plugins (Mojos).
99   * This extension handles the lifecycle of Mojo instances in tests, including instantiation,
100  * configuration, and dependency injection.
101  *
102  * <p>The extension is automatically registered when using the {@link MojoTest} annotation
103  * on a test class. It provides the following features:</p>
104  * <ul>
105  *   <li>Automatic Mojo instantiation based on {@link InjectMojo} annotations</li>
106  *   <li>Parameter injection using {@link MojoParameter} annotations</li>
107  *   <li>POM configuration handling</li>
108  *   <li>Project stub creation and configuration</li>
109  *   <li>Maven session and build context setup</li>
110  *   <li>Component dependency injection</li>
111  * </ul>
112  *
113  * <p>Example usage in a test class:</p>
114  * <pre>
115  * {@code
116  * @MojoTest
117  * class MyMojoTest {
118  *     @Test
119  *     @InjectMojo(goal = "my-goal")
120  *     @MojoParameter(name = "outputDirectory", value = "${project.build.directory}/generated")
121  *     void testMojoExecution(MyMojo mojo) throws Exception {
122  *         mojo.execute();
123  *         // verify execution results
124  *     }
125  * }
126  * }
127  * </pre>
128  **
129  * <p>For custom POM configurations, you can specify a POM file using the {@link InjectMojo#pom()}
130  * attribute. The extension will merge this configuration with default test project settings.</p>*
131  *
132  * @see MojoTest
133  * @see InjectMojo
134  * @see MojoParameter
135  * @see Basedir
136  * @since 3.4.0
137  */
138 public class MojoExtension extends PlexusExtension implements ParameterResolver {
139 
140     // Namespace for storing/retrieving data related to MojoExtension
141     private static final ExtensionContext.Namespace MOJO_EXTENSION = ExtensionContext.Namespace.create("MojoExtension");
142 
143     public static final String BASEDIR_IS_SET_KEY = "basedirIsSet";
144 
145     @Override
146     public boolean supportsParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
147             throws ParameterResolutionException {
148         return parameterContext.isAnnotated(InjectMojo.class)
149                 || parameterContext.getDeclaringExecutable().isAnnotationPresent(InjectMojo.class);
150     }
151 
152     @Override
153     public Object resolveParameter(ParameterContext parameterContext, ExtensionContext extensionContext)
154             throws ParameterResolutionException {
155         try {
156             InjectMojo injectMojo = parameterContext
157                     .findAnnotation(InjectMojo.class)
158                     .orElseGet(() -> parameterContext.getDeclaringExecutable().getAnnotation(InjectMojo.class));
159 
160             Set<MojoParameter> mojoParameters =
161                     new HashSet<>(parameterContext.findRepeatableAnnotations(MojoParameter.class));
162 
163             Optional.ofNullable(parameterContext.getDeclaringExecutable().getAnnotation(MojoParameter.class))
164                     .ifPresent(mojoParameters::add);
165 
166             Optional.ofNullable(parameterContext.getDeclaringExecutable().getAnnotation(MojoParameters.class))
167                     .map(MojoParameters::value)
168                     .map(Arrays::asList)
169                     .ifPresent(mojoParameters::addAll);
170 
171             Class<?> holder = parameterContext.getTarget().get().getClass();
172             PluginDescriptor descriptor =
173                     extensionContext.getStore(MOJO_EXTENSION).get(PluginDescriptor.class, PluginDescriptor.class);
174             return lookupMojo(extensionContext, holder, injectMojo, mojoParameters, descriptor);
175         } catch (Exception e) {
176             throw new ParameterResolutionException("Unable to resolve parameter", e);
177         }
178     }
179 
180     @Override
181     public void beforeEach(ExtensionContext context) throws Exception {
182         String basedir = AnnotationSupport.findAnnotation(context.getElement().get(), Basedir.class)
183                 .map(Basedir::value)
184                 .orElse(null);
185 
186         if (basedir == null) {
187             basedir = getBasedir();
188         } else {
189             context.getStore(MOJO_EXTENSION).put(BASEDIR_IS_SET_KEY, Boolean.TRUE);
190         }
191 
192         URL resource = context.getRequiredTestClass().getResource(basedir);
193         if (resource != null) {
194             basedir = Paths.get(resource.toURI()).toString();
195         }
196 
197         // as PluginParameterExpressionEvaluator changes the basedir to absolute path, we need to normalize it here too
198         basedir = new File(basedir).getAbsolutePath();
199 
200         setTestBasedir(basedir, context);
201 
202         PlexusContainer plexusContainer = getContainer(context);
203 
204         ((DefaultPlexusContainer) plexusContainer).addPlexusInjector(Collections.emptyList(), binder -> {
205             binder.install(ProviderMethodsModule.forObject(context.getRequiredTestInstance()));
206             binder.install(new MavenProvidesModule(context.getRequiredTestInstance()));
207         });
208 
209         addMock(plexusContainer, Log.class, () -> new MojoLogWrapper(LoggerFactory.getLogger("anonymous")));
210         MavenProject mavenProject = addMock(plexusContainer, MavenProject.class, this::mockMavenProject);
211         MojoExecution mojoExecution = addMock(plexusContainer, MojoExecution.class, this::mockMojoExecution);
212         MavenSession mavenSession = addMock(plexusContainer, MavenSession.class, this::mockMavenSession);
213 
214         SessionScope sessionScope = plexusContainer.lookup(SessionScope.class);
215         sessionScope.enter();
216         sessionScope.seed(MavenSession.class, mavenSession);
217 
218         MojoExecutionScope executionScope = plexusContainer.lookup(MojoExecutionScope.class);
219         executionScope.enter();
220         executionScope.seed(MavenProject.class, mavenProject);
221         executionScope.seed(MojoExecution.class, mojoExecution);
222 
223         ((DefaultPlexusContainer) plexusContainer).addPlexusInjector(Collections.emptyList(), binder -> {
224             binder.requestInjection(context.getRequiredTestInstance());
225         });
226 
227         Map<Object, Object> map = plexusContainer.getContext().getContextData();
228 
229         ClassLoader classLoader = context.getRequiredTestClass().getClassLoader();
230         try (InputStream is = Objects.requireNonNull(
231                         classLoader.getResourceAsStream(getPluginDescriptorLocation()),
232                         "Unable to find plugin descriptor: " + getPluginDescriptorLocation());
233                 Reader reader = new BufferedReader(new XmlStreamReader(is));
234                 InterpolationFilterReader interpolationReader = new InterpolationFilterReader(reader, map, "${", "}")) {
235 
236             PluginDescriptor pluginDescriptor = new PluginDescriptorBuilder().build(interpolationReader);
237 
238             context.getStore(MOJO_EXTENSION).put(PluginDescriptor.class, pluginDescriptor);
239 
240             for (ComponentDescriptor<?> desc : pluginDescriptor.getComponents()) {
241                 plexusContainer.addComponentDescriptor(desc);
242             }
243         }
244     }
245 
246     private <T> T addMock(PlexusContainer container, Class<T> role, Supplier<T> supplier)
247             throws ComponentLookupException {
248         if (!container.hasComponent(role)) {
249             T mock = supplier.get();
250             container.addComponent(mock, role, "default");
251             return mock;
252         } else {
253             return container.lookup(role);
254         }
255     }
256 
257     @Override
258     public void afterEach(ExtensionContext context) throws Exception {
259         SessionScope sessionScope = getContainer(context).lookup(SessionScope.class);
260         sessionScope.exit();
261 
262         MojoExecutionScope executionScope = getContainer(context).lookup(MojoExecutionScope.class);
263         executionScope.exit();
264 
265         super.afterEach(context);
266     }
267 
268     /**
269      * Default MojoExecution mock
270      *
271      * @return a MojoExecution mock
272      */
273     private MojoExecution mockMojoExecution() {
274         return Mockito.mock(MojoExecution.class);
275     }
276 
277     /**
278      * Default MavenSession mock
279      *
280      * @return a MavenSession mock
281      */
282     private MavenSession mockMavenSession() {
283         MavenSession session = Mockito.mock(MavenSession.class);
284         lenient().when(session.getUserProperties()).thenReturn(new Properties());
285         lenient().when(session.getSystemProperties()).thenReturn(new Properties());
286         return session;
287     }
288 
289     /**
290      * Default MavenProject mock
291      *
292      * @return a MavenProject mock
293      */
294     private MavenProject mockMavenProject() {
295         MavenProject mavenProject = Mockito.mock(MavenProject.class);
296         lenient().when(mavenProject.getProperties()).thenReturn(new Properties());
297         return mavenProject;
298     }
299 
300     protected String getPluginDescriptorLocation() {
301         return "META-INF/maven/plugin.xml";
302     }
303 
304     private Mojo lookupMojo(
305             ExtensionContext extensionContext,
306             Class<?> holder,
307             InjectMojo injectMojo,
308             Collection<MojoParameter> mojoParameters,
309             PluginDescriptor descriptor)
310             throws Exception {
311         String goal = injectMojo.goal();
312         String pom = injectMojo.pom();
313         Path basedir = Paths.get(getTestBasedir(extensionContext));
314         String[] coord = mojoCoordinates(goal, descriptor);
315         Xpp3Dom pomDom;
316         if (pom.startsWith("file:")) {
317             Path path = basedir.resolve(pom.substring("file:".length()));
318             pomDom = Xpp3DomBuilder.build(new XmlStreamReader(path.toFile()));
319         } else if (pom.startsWith("classpath:")) {
320             URL url = holder.getResource(pom.substring("classpath:".length()));
321             if (url == null) {
322                 throw new IllegalStateException("Unable to find pom on classpath: " + pom);
323             }
324             pomDom = Xpp3DomBuilder.build(new XmlStreamReader(url.openStream()));
325         } else if (pom.contains("<project>")) {
326             pomDom = Xpp3DomBuilder.build(new StringReader(pom));
327         } else if (!pom.isEmpty()) {
328             Path path = basedir.resolve(pom);
329             pomDom = Xpp3DomBuilder.build(new XmlStreamReader(path.toFile()));
330         } else if (isBasedirSet(extensionContext)) {
331             // only look for a pom.xml if basedir is explicitly set
332             Path path = basedir.resolve("pom.xml");
333             if (Files.exists(path)) {
334                 pomDom = Xpp3DomBuilder.build(new XmlStreamReader(path.toFile()));
335             } else {
336                 pomDom = new Xpp3Dom("");
337             }
338         } else {
339             pomDom = new Xpp3Dom("");
340         }
341         Xpp3Dom pluginConfiguration = extractPluginConfiguration(coord[1], pomDom);
342         if (!mojoParameters.isEmpty()) {
343             List<Xpp3Dom> children = mojoParameters.stream()
344                     .map(mp -> {
345                         Xpp3Dom c = new Xpp3Dom(mp.name());
346                         c.setValue(mp.value());
347                         return c;
348                     })
349                     .collect(Collectors.toList());
350             Xpp3Dom config = new Xpp3Dom("configuration");
351             children.forEach(config::addChild);
352             pluginConfiguration = Xpp3Dom.mergeXpp3Dom(config, pluginConfiguration);
353         }
354         return lookupMojo(extensionContext, coord, pluginConfiguration, descriptor);
355     }
356 
357     private boolean isBasedirSet(ExtensionContext extensionContext) {
358         return extensionContext.getStore(MOJO_EXTENSION).getOrDefault(BASEDIR_IS_SET_KEY, Boolean.class, Boolean.FALSE);
359     }
360 
361     protected String[] mojoCoordinates(String goal, PluginDescriptor pluginDescriptor) throws Exception {
362         if (goal.matches(".*:.*:.*:.*")) {
363             return goal.split(":");
364         } else {
365             String artifactId = pluginDescriptor.getArtifactId();
366             String groupId = pluginDescriptor.getGroupId();
367             String version = pluginDescriptor.getVersion();
368             return new String[] {groupId, artifactId, version, goal};
369         }
370     }
371 
372     /**
373      * lookup the mojo while we have all the relevent information
374      */
375     protected Mojo lookupMojo(
376             ExtensionContext extensionContext, String[] coord, Xpp3Dom pluginConfiguration, PluginDescriptor descriptor)
377             throws Exception {
378         PlexusContainer plexusContainer = getContainer(extensionContext);
379         // pluginkey = groupId : artifactId : version : goal
380         Mojo mojo = plexusContainer.lookup(Mojo.class, coord[0] + ":" + coord[1] + ":" + coord[2] + ":" + coord[3]);
381 
382         Optional<MojoDescriptor> mojoDescriptor = descriptor.getMojos().stream()
383                 .filter(md ->
384                         Objects.equals(md.getImplementation(), mojo.getClass().getName()))
385                 .findFirst();
386 
387         if (mojoDescriptor.isPresent()) {
388             pluginConfiguration = finalizeConfig(pluginConfiguration, mojoDescriptor.get());
389         }
390 
391         MavenSession session = plexusContainer.lookup(MavenSession.class);
392         MavenProject mavenProject = plexusContainer.lookup(MavenProject.class);
393         MojoExecution mojoExecution = plexusContainer.lookup(MojoExecution.class);
394 
395         if (mockingDetails(session).isMock()) {
396             lenient().when(session.getCurrentProject()).thenReturn(mavenProject);
397         }
398 
399         if (mockingDetails(mavenProject).isMock()) {
400             lenient().when(mavenProject.getBasedir()).thenReturn(new File(getTestBasedir(extensionContext)));
401         }
402 
403         if (mojoDescriptor.isPresent() && mockingDetails(mojoExecution).isMock()) {
404             lenient().when(mojoExecution.getMojoDescriptor()).thenReturn(mojoDescriptor.get());
405         }
406 
407         if (pluginConfiguration != null) {
408             ExpressionEvaluator evaluator =
409                     new WrapEvaluator(plexusContainer, new PluginParameterExpressionEvaluator(session, mojoExecution));
410             ComponentConfigurator configurator = new BasicComponentConfigurator();
411             configurator.configureComponent(
412                     mojo,
413                     new XmlPlexusConfiguration(pluginConfiguration),
414                     evaluator,
415                     plexusContainer.getContainerRealm());
416         }
417 
418         mojo.setLog(plexusContainer.lookup(Log.class));
419 
420         // clear invocations on mocks to avoid test interference
421         if (mockingDetails(session).isMock()) {
422             clearInvocations(session);
423         }
424 
425         if (mockingDetails(mavenProject).isMock()) {
426             clearInvocations(mavenProject);
427         }
428 
429         if (mockingDetails(mojoExecution).isMock()) {
430             clearInvocations(mojoExecution);
431         }
432 
433         return mojo;
434     }
435 
436     private Xpp3Dom finalizeConfig(Xpp3Dom config, MojoDescriptor mojoDescriptor) {
437         List<Xpp3Dom> children = new ArrayList<>();
438         if (mojoDescriptor != null && mojoDescriptor.getParameters() != null) {
439             Xpp3Dom defaultConfiguration = MojoDescriptorCreator.convert(mojoDescriptor);
440             for (Parameter parameter : mojoDescriptor.getParameters()) {
441                 Xpp3Dom parameterConfiguration = config.getChild(parameter.getName());
442                 if (parameterConfiguration == null) {
443                     parameterConfiguration = config.getChild(parameter.getAlias());
444                 }
445                 Xpp3Dom parameterDefaults = defaultConfiguration.getChild(parameter.getName());
446                 parameterConfiguration = Xpp3Dom.mergeXpp3Dom(parameterConfiguration, parameterDefaults, Boolean.TRUE);
447                 if (parameterConfiguration != null) {
448                     if (isEmpty(parameterConfiguration.getAttribute("implementation"))
449                             && !isEmpty(parameter.getImplementation())) {
450                         parameterConfiguration.setAttribute("implementation", parameter.getImplementation());
451                     }
452                     children.add(parameterConfiguration);
453                 }
454             }
455         }
456         Xpp3Dom c = new Xpp3Dom("configuration");
457         children.forEach(c::addChild);
458         return c;
459     }
460 
461     private boolean isEmpty(String str) {
462         return str == null || str.isEmpty();
463     }
464 
465     private static Optional<Xpp3Dom> child(Xpp3Dom element, String name) {
466         return Optional.ofNullable(element.getChild(name));
467     }
468 
469     private static Stream<Xpp3Dom> children(Xpp3Dom element) {
470         return Stream.of(element.getChildren());
471     }
472 
473     public static Xpp3Dom extractPluginConfiguration(String artifactId, Xpp3Dom pomDom) throws Exception {
474         Xpp3Dom pluginConfigurationElement = child(pomDom, "build")
475                 .flatMap(buildElement -> child(buildElement, "plugins"))
476                 .map(MojoExtension::children)
477                 .orElseGet(Stream::empty)
478                 .filter(e -> e.getChild("artifactId").getValue().equals(artifactId))
479                 .findFirst()
480                 .flatMap(buildElement -> child(buildElement, "configuration"))
481                 .orElse(Xpp3DomBuilder.build(new StringReader("<configuration/>")));
482         return pluginConfigurationElement;
483     }
484 
485     /**
486      * Convenience method to obtain the value of a variable on a mojo that might not have a getter.
487      * <br>
488      * Note: the caller is responsible for casting to what the desired type is.
489      */
490     public static Object getVariableValueFromObject(Object object, String variable) throws IllegalAccessException {
491         Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
492         field.setAccessible(true);
493         return field.get(object);
494     }
495 
496     /**
497      * Convenience method to obtain all variables and values from the mojo (including its superclasses)
498      * <br>
499      * Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
500      */
501     public static Map<String, Object> getVariablesAndValuesFromObject(Object object) throws IllegalAccessException {
502         return getVariablesAndValuesFromObject(object.getClass(), object);
503     }
504 
505     /**
506      * Convenience method to obtain all variables and values from the mojo (including its superclasses)
507      * <br>
508      * Note: the values in the map are of type Object so the caller is responsible for casting to desired types.
509      *
510      * @return map of variable names and values
511      */
512     public static Map<String, Object> getVariablesAndValuesFromObject(Class<?> clazz, Object object)
513             throws IllegalAccessException {
514         Map<String, Object> map = new HashMap<>();
515         Field[] fields = clazz.getDeclaredFields();
516         AccessibleObject.setAccessible(fields, true);
517         for (Field field : fields) {
518             map.put(field.getName(), field.get(object));
519         }
520         Class<?> superclass = clazz.getSuperclass();
521         if (!Object.class.equals(superclass)) {
522             map.putAll(getVariablesAndValuesFromObject(superclass, object));
523         }
524         return map;
525     }
526 
527     /**
528      * Gets the base directory for test resources.
529      * If not explicitly set via {@link Basedir}, returns the plugin base directory.
530      */
531     public static String getBasedir() {
532         return PlexusExtension.getBasedir();
533     }
534 
535     /**
536      * Convenience method to set values to variables in objects that don't have setters
537      */
538     public static void setVariableValueToObject(Object object, String variable, Object value)
539             throws IllegalAccessException {
540         Field field = ReflectionUtils.getFieldByNameIncludingSuperclasses(variable, object.getClass());
541         Objects.requireNonNull(field, "Field " + variable + " not found");
542         field.setAccessible(true);
543         field.set(object, value);
544     }
545 
546     private static class WrapEvaluator implements TypeAwareExpressionEvaluator {
547 
548         private final PlexusContainer container;
549 
550         private final TypeAwareExpressionEvaluator evaluator;
551 
552         WrapEvaluator(PlexusContainer container, TypeAwareExpressionEvaluator evaluator) {
553             this.container = container;
554             this.evaluator = evaluator;
555         }
556 
557         @Override
558         public Object evaluate(String expression) throws ExpressionEvaluationException {
559             return evaluate(expression, null);
560         }
561 
562         @Override
563         public Object evaluate(String expression, Class<?> type) throws ExpressionEvaluationException {
564             Object value = evaluator.evaluate(expression, type);
565             if (value == null) {
566                 String expr = stripTokens(expression);
567                 if (expr != null) {
568                     try {
569                         value = container.lookup(type, expr);
570                     } catch (ComponentLookupException e) {
571                         // nothing
572                     }
573                 }
574             }
575             return value;
576         }
577 
578         private String stripTokens(String expr) {
579             if (expr.startsWith("${") && expr.endsWith("}")) {
580                 return expr.substring(2, expr.length() - 1);
581             }
582             return null;
583         }
584 
585         @Override
586         public File alignToBaseDirectory(File path) {
587             return evaluator.alignToBaseDirectory(path);
588         }
589     }
590 
591     private static class MavenProvidesModule implements Module {
592         private final Object testInstance;
593 
594         MavenProvidesModule(Object testInstance) {
595             this.testInstance = testInstance;
596         }
597 
598         @Override
599         @SuppressWarnings("unchecked")
600         public void configure(Binder binder) {
601             List<Method> providesMethods = AnnotationSupport.findAnnotatedMethods(
602                     testInstance.getClass(), Provides.class, HierarchyTraversalMode.BOTTOM_UP);
603 
604             for (Method method : providesMethods) {
605                 if (method.getParameterCount() > 0) {
606                     throw new IllegalArgumentException("Parameterized method are not supported " + method);
607                 }
608                 try {
609                     method.setAccessible(true);
610                     Object value = method.invoke(testInstance);
611                     if (value == null) {
612                         throw new IllegalArgumentException("Provides method returned null: " + method);
613                     }
614                     binder.bind((Class<Object>) method.getReturnType()).toInstance(value);
615                 } catch (IllegalAccessException | InvocationTargetException e) {
616                     throw new IllegalArgumentException(e);
617                 }
618             }
619         }
620     }
621 }