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 // TODO: implement
128 // injector.dispose();
129 injector = null;
130 }
131 }
132
133 /**
134 * Returns the DI injector, creating it if necessary.
135 *
136 * @return The configured injector instance
137 */
138 public Injector getInjector() {
139 if (injector == null) {
140 setupContainer();
141 }
142 return injector;
143 }
144
145 /**
146 * Looks up a component of the specified type from the container.
147 *
148 * @param <T> The component type
149 * @param componentClass The class of the component to look up
150 * @return The component instance
151 * @throws DIException if lookup fails
152 */
153 protected <T> T lookup(Class<T> componentClass) throws DIException {
154 return getInjector().getInstance(componentClass);
155 }
156
157 /**
158 * Looks up a component of the specified type and role hint from the container.
159 *
160 * @param <T> The component type
161 * @param componentClass The class of the component to look up
162 * @param roleHint The role hint for the component
163 * @return The component instance
164 * @throws DIException if lookup fails
165 */
166 protected <T> T lookup(Class<T> componentClass, String roleHint) throws DIException {
167 return getInjector().getInstance(Key.ofType(componentClass, roleHint));
168 }
169
170 /**
171 * Looks up a component of the specified type and qualifier from the container.
172 *
173 * @param <T> The component type
174 * @param componentClass The class of the component to look up
175 * @param qualifier The qualifier for the component
176 * @return The component instance
177 * @throws DIException if lookup fails
178 */
179 protected <T> T lookup(Class<T> componentClass, Object qualifier) throws DIException {
180 return getInjector().getInstance(Key.ofType(componentClass, qualifier));
181 }
182
183 /**
184 * Releases a component back to the container.
185 * Currently a placeholder for future implementation.
186 *
187 * @param component The component to release
188 * @throws DIException if release fails
189 */
190 protected void release(Object component) throws DIException {
191 // TODO: implement
192 // getInjector().release(component);
193 }
194
195 /**
196 * Creates a File object for a path relative to the base directory.
197 *
198 * @param path The relative path
199 * @return A File object representing the path
200 */
201 public static File getTestFile(String path) {
202 return new File(getBasedir(), path);
203 }
204
205 /**
206 * Creates a File object for a path relative to a specified base directory.
207 *
208 * @param basedir The base directory path
209 * @param path The relative path
210 * @return A File object representing the path
211 */
212 public static File getTestFile(String basedir, String path) {
213 File basedirFile = new File(basedir);
214
215 if (!basedirFile.isAbsolute()) {
216 basedirFile = getTestFile(basedir);
217 }
218
219 return new File(basedirFile, path);
220 }
221
222 /**
223 * Returns the absolute path for a path relative to the base directory.
224 *
225 * @param path The relative path
226 * @return The absolute path
227 */
228 public static String getTestPath(String path) {
229 return getTestFile(path).getAbsolutePath();
230 }
231
232 /**
233 * Returns the absolute path for a path relative to a specified base directory.
234 *
235 * @param basedir The base directory path
236 * @param path The relative path
237 * @return The absolute path
238 */
239 public static String getTestPath(String basedir, String path) {
240 return getTestFile(basedir, path).getAbsolutePath();
241 }
242
243 /**
244 * Returns the base directory for test execution.
245 * Uses the "basedir" system property if set, otherwise uses the current directory.
246 *
247 * @return The base directory path
248 */
249 public static String getBasedir() {
250 if (basedir != null) {
251 return basedir;
252 }
253
254 basedir = System.getProperty("basedir");
255
256 if (basedir == null) {
257 basedir = new File("").getAbsolutePath();
258 }
259
260 return basedir;
261 }
262 }