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.di.testing;
20
21 import java.io.File;
22
23 import org.apache.maven.di.Injector;
24 import org.apache.maven.di.Key;
25 import org.apache.maven.di.impl.DIException;
26 import org.junit.jupiter.api.extension.AfterEachCallback;
27 import org.junit.jupiter.api.extension.BeforeEachCallback;
28 import org.junit.jupiter.api.extension.ExtensionContext;
29
30 /**
31 * JUnit Jupiter extension that provides dependency injection support for Maven tests.
32 * This extension manages the lifecycle of a DI container for each test method execution,
33 * automatically performing injection into test instances and cleanup.
34 *
35 * <p>This is a modernized version of the original Plexus test support, adapted for
36 * Maven's new DI framework and JUnit Jupiter.</p>
37 *
38 * <p>Usage example:</p>
39 * <pre>
40 * {@code
41 * @ExtendWith(MavenDIExtension.class)
42 * class MyTest {
43 * @Inject
44 * private MyComponent component;
45 *
46 * @Test
47 * void testSomething() {
48 * // component is automatically injected
49 * }
50 * }
51 * }
52 * </pre>
53 */
54 public class MavenDIExtension implements BeforeEachCallback, AfterEachCallback {
55 protected static ExtensionContext context;
56 protected Injector injector;
57 protected static String basedir;
58
59 /**
60 * Initializes the test environment before each test method execution.
61 * Sets up the base directory and DI container, then performs injection into the test instance.
62 *
63 * @param context The extension context provided by JUnit
64 * @throws Exception if initialization fails
65 */
66 @Override
67 public void beforeEach(ExtensionContext context) throws Exception {
68 basedir = getBasedir();
69 setContext(context);
70 getInjector().bindInstance((Class<Object>) context.getRequiredTestClass(), context.getRequiredTestInstance());
71 getInjector().injectInstance(context.getRequiredTestInstance());
72 }
73
74 /**
75 * Stores the extension context for use during test execution.
76 *
77 * @param context The extension context to store
78 */
79 protected void setContext(ExtensionContext context) {
80 MavenDIExtension.context = context;
81 }
82
83 /**
84 * Creates and configures the DI container for test execution.
85 * Performs component discovery and sets up basic bindings.
86 *
87 * @throws IllegalStateException if the ExtensionContext is null, the required test class is unavailable,
88 * the required test instance is unavailable, or if container setup fails
89 */
90 protected void setupContainer() {
91 if (context == null) {
92 throw new IllegalStateException("ExtensionContext must not be null");
93 }
94 final Class<?> testClass = context.getRequiredTestClass();
95 if (testClass == null) {
96 throw new IllegalStateException("Required test class is not available in ExtensionContext");
97 }
98 final Object testInstance = context.getRequiredTestInstance();
99 if (testInstance == null) {
100 throw new IllegalStateException("Required test instance is not available in ExtensionContext");
101 }
102
103 try {
104 injector = Injector.create();
105 injector.bindInstance(ExtensionContext.class, context);
106 injector.discover(testClass.getClassLoader());
107 injector.bindInstance(Injector.class, injector);
108 injector.bindInstance(testClass.asSubclass(Object.class), (Object) testInstance); // Safe generics handling
109 } catch (final Exception e) {
110 throw new IllegalStateException(
111 String.format(
112 "Failed to set up DI injector for test class '%s': %s",
113 testClass.getName(), e.getMessage()),
114 e);
115 }
116 }
117
118 /**
119 * Cleans up resources after each test method execution.
120 * Currently a placeholder for future cleanup implementation.
121 *
122 * @param context The extension context provided by JUnit
123 */
124 @Override
125 public void afterEach(ExtensionContext context) throws Exception {
126 if (injector != null) {
127 injector.dispose();
128 injector = null;
129 }
130 }
131
132 /**
133 * Returns the DI injector, creating it if necessary.
134 *
135 * @return The configured injector instance
136 */
137 public Injector getInjector() {
138 if (injector == null) {
139 setupContainer();
140 }
141 return injector;
142 }
143
144 /**
145 * Looks up a component of the specified type from the container.
146 *
147 * @param <T> The component type
148 * @param componentClass The class of the component to look up
149 * @return The component instance
150 * @throws DIException if lookup fails
151 */
152 protected <T> T lookup(Class<T> componentClass) throws DIException {
153 return getInjector().getInstance(componentClass);
154 }
155
156 /**
157 * Looks up a component of the specified type and role hint from the container.
158 *
159 * @param <T> The component type
160 * @param componentClass The class of the component to look up
161 * @param roleHint The role hint for the component
162 * @return The component instance
163 * @throws DIException if lookup fails
164 */
165 protected <T> T lookup(Class<T> componentClass, String roleHint) throws DIException {
166 return getInjector().getInstance(Key.ofType(componentClass, roleHint));
167 }
168
169 /**
170 * Looks up a component of the specified type and qualifier from the container.
171 *
172 * @param <T> The component type
173 * @param componentClass The class of the component to look up
174 * @param qualifier The qualifier for the component
175 * @return The component instance
176 * @throws DIException if lookup fails
177 */
178 protected <T> T lookup(Class<T> componentClass, Object qualifier) throws DIException {
179 return getInjector().getInstance(Key.ofType(componentClass, qualifier));
180 }
181
182 /**
183 * Releases a component back to the container.
184 * Currently a placeholder for future implementation.
185 *
186 * @param component The component to release
187 * @throws DIException if release fails
188 */
189 protected void release(Object component) throws DIException {
190 // TODO: implement
191 // getInjector().release(component);
192 }
193
194 /**
195 * Creates a File object for a path relative to the base directory.
196 *
197 * @param path The relative path
198 * @return A File object representing the path
199 */
200 public static File getTestFile(String path) {
201 return new File(getBasedir(), path);
202 }
203
204 /**
205 * Creates a File object for a path relative to a specified base directory.
206 *
207 * @param basedir The base directory path
208 * @param path The relative path
209 * @return A File object representing the path
210 */
211 public static File getTestFile(String basedir, String path) {
212 File basedirFile = new File(basedir);
213
214 if (!basedirFile.isAbsolute()) {
215 basedirFile = getTestFile(basedir);
216 }
217
218 return new File(basedirFile, path);
219 }
220
221 /**
222 * Returns the absolute path for a path relative to the base directory.
223 *
224 * @param path The relative path
225 * @return The absolute path
226 */
227 public static String getTestPath(String path) {
228 return getTestFile(path).getAbsolutePath();
229 }
230
231 /**
232 * Returns the absolute path for a path relative to a specified base directory.
233 *
234 * @param basedir The base directory path
235 * @param path The relative path
236 * @return The absolute path
237 */
238 public static String getTestPath(String basedir, String path) {
239 return getTestFile(basedir, path).getAbsolutePath();
240 }
241
242 /**
243 * Returns the base directory for test execution.
244 * Uses the "basedir" system property if set, otherwise uses the current directory.
245 *
246 * @return The base directory path
247 */
248 public static String getBasedir() {
249 if (basedir != null) {
250 return basedir;
251 }
252
253 basedir = System.getProperty("basedir");
254
255 if (basedir == null) {
256 basedir = new File("").getAbsolutePath();
257 }
258
259 return basedir;
260 }
261 }