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.plugin.surefire.booterclient;
20  
21  import javax.annotation.Nonnull;
22  import javax.annotation.Nullable;
23  
24  import java.io.File;
25  import java.util.Collections;
26  import java.util.List;
27  import java.util.Map;
28  import java.util.Map.Entry;
29  import java.util.Properties;
30  
31  import org.apache.maven.plugin.surefire.JdkAttributes;
32  import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.Commandline;
33  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
34  import org.apache.maven.surefire.api.util.internal.ImmutableMap;
35  import org.apache.maven.surefire.booter.AbstractPathConfiguration;
36  import org.apache.maven.surefire.booter.Classpath;
37  import org.apache.maven.surefire.booter.StartupConfiguration;
38  import org.apache.maven.surefire.booter.SurefireBooterForkException;
39  import org.apache.maven.surefire.extensions.ForkNodeFactory;
40  import org.apache.maven.surefire.shared.utils.cli.CommandLineException;
41  
42  import static org.apache.maven.plugin.surefire.SurefireHelper.replaceForkThreadsInPath;
43  import static org.apache.maven.plugin.surefire.SurefireHelper.replaceThreadNumberPlaceholders;
44  import static org.apache.maven.plugin.surefire.util.Relocator.relocate;
45  import static org.apache.maven.surefire.booter.Classpath.join;
46  
47  /**
48   * Basic framework which constructs CLI.
49   *
50   * @author <a href="mailto:tibordigana@apache.org">Tibor Digana (tibor17)</a>
51   * @since 2.21.0.Jigsaw
52   */
53  public abstract class DefaultForkConfiguration extends ForkConfiguration {
54      @Nonnull
55      private final Classpath booterClasspath;
56  
57      @Nonnull
58      private final File tempDirectory;
59  
60      @Nullable
61      private final String debugLine;
62  
63      @Nonnull
64      private final File workingDirectory;
65  
66      @Nonnull
67      private final Properties modelProperties;
68  
69      @Nullable
70      private final String argLine;
71  
72      @Nonnull
73      private final Map<String, String> environmentVariables;
74  
75      @Nonnull
76      private final String[] excludedEnvironmentVariables;
77  
78      private final boolean debug;
79      private final int forkCount;
80      private final boolean reuseForks;
81  
82      @Nonnull
83      private final Platform pluginPlatform;
84  
85      @Nonnull
86      private final ConsoleLogger log;
87  
88      @Nonnull
89      private final ForkNodeFactory forkNodeFactory;
90  
91      @SuppressWarnings("checkstyle:parameternumber")
92      protected DefaultForkConfiguration(
93              @Nonnull Classpath booterClasspath,
94              @Nonnull File tempDirectory,
95              @Nullable String debugLine,
96              @Nonnull File workingDirectory,
97              @Nonnull Properties modelProperties,
98              @Nullable String argLine,
99              @Nonnull Map<String, String> environmentVariables,
100             @Nonnull String[] excludedEnvironmentVariables,
101             boolean debug,
102             int forkCount,
103             boolean reuseForks,
104             @Nonnull Platform pluginPlatform,
105             @Nonnull ConsoleLogger log,
106             @Nonnull ForkNodeFactory forkNodeFactory) {
107         this.booterClasspath = booterClasspath;
108         this.tempDirectory = tempDirectory;
109         this.debugLine = debugLine;
110         this.workingDirectory = workingDirectory;
111         this.modelProperties = modelProperties;
112         this.argLine = argLine;
113         this.environmentVariables = toImmutable(environmentVariables);
114         this.excludedEnvironmentVariables = excludedEnvironmentVariables;
115         this.debug = debug;
116         this.forkCount = forkCount;
117         this.reuseForks = reuseForks;
118         this.pluginPlatform = pluginPlatform;
119         this.log = log;
120         this.forkNodeFactory = forkNodeFactory;
121     }
122 
123     protected abstract void resolveClasspath(
124             @Nonnull Commandline cli,
125             @Nonnull String booterThatHasMainMethod,
126             @Nonnull StartupConfiguration config,
127             @Nonnull File dumpLogDirectory)
128             throws SurefireBooterForkException;
129 
130     @Nonnull
131     protected String extendJvmArgLine(@Nonnull String jvmArgLine) {
132         return jvmArgLine;
133     }
134 
135     @Nonnull
136     @Override
137     public final ForkNodeFactory getForkNodeFactory() {
138         return forkNodeFactory;
139     }
140 
141     /**
142      * @param config       The startup configuration
143      * @param forkNumber   index of forked JVM, to be the replacement in the argLine
144      * @param dumpLogDirectory     directory for dump log file
145      * @return CommandLine able to flush entire command going to be sent to forked JVM
146      * @throws org.apache.maven.surefire.booter.SurefireBooterForkException when unable to perform the fork
147      */
148     @Nonnull
149     @Override
150     public Commandline createCommandLine(
151             @Nonnull StartupConfiguration config, int forkNumber, @Nonnull File dumpLogDirectory)
152             throws SurefireBooterForkException {
153         try {
154             Commandline cli = new Commandline(getExcludedEnvironmentVariables());
155 
156             cli.setWorkingDirectory(getWorkingDirectory(forkNumber).getAbsolutePath());
157 
158             for (Entry<String, String> entry : getEnvironmentVariables().entrySet()) {
159                 String value = entry.getValue();
160                 if (value != null) {
161                     value = replaceThreadNumberPlaceholders(value, forkNumber);
162                 }
163                 cli.addEnvironment(entry.getKey(), value == null ? "" : value);
164             }
165 
166             cli.setExecutable(getJdkForTests().getJvmExecutable().getAbsolutePath());
167 
168             String jvmArgLine = newJvmArgLine(forkNumber);
169             if (!jvmArgLine.isEmpty()) {
170                 cli.createArg().setLine(jvmArgLine);
171             }
172 
173             if (getDebugLine() != null && !getDebugLine().isEmpty()) {
174                 cli.createArg().setLine(getDebugLine());
175             }
176 
177             resolveClasspath(cli, findStartClass(config), config, dumpLogDirectory);
178 
179             return cli;
180         } catch (CommandLineException e) {
181             throw new SurefireBooterForkException(e.getLocalizedMessage(), e);
182         }
183     }
184 
185     protected ConsoleLogger getLogger() {
186         return log;
187     }
188 
189     @Nonnull
190     protected List<String> toCompleteClasspath(@Nonnull StartupConfiguration conf) throws SurefireBooterForkException {
191         AbstractPathConfiguration pathConfig = conf.getClasspathConfiguration();
192         if (pathConfig.isClassPathConfig() == pathConfig.isModularPathConfig()) {
193             throw new SurefireBooterForkException("Could not find class-path config nor modular class-path either.");
194         }
195 
196         Classpath bootClasspath = getBooterClasspath();
197         Classpath testClasspath = pathConfig.getTestClasspath();
198         Classpath providerClasspath = pathConfig.getProviderClasspath();
199         Classpath completeClasspath = join(join(bootClasspath, testClasspath), providerClasspath);
200 
201         getLogger().debug(completeClasspath.getLogMessage("boot classpath:"));
202         getLogger().debug(completeClasspath.getCompactLogMessage("boot(compact) classpath:"));
203 
204         return completeClasspath.getClassPath();
205     }
206 
207     @Nonnull
208     private File getWorkingDirectory(int forkNumber) throws SurefireBooterForkException {
209         File cwd = replaceForkThreadsInPath(getWorkingDirectory(), forkNumber);
210 
211         if (!cwd.exists() && !cwd.mkdirs()) {
212             throw new SurefireBooterForkException("Cannot create workingDirectory " + cwd.getAbsolutePath());
213         }
214 
215         if (!cwd.isDirectory()) {
216             throw new SurefireBooterForkException(
217                     "WorkingDirectory " + cwd.getAbsolutePath() + " exists and is not a directory");
218         }
219         return cwd;
220     }
221 
222     /**
223      * Replaces expressions <pre>@{property-name}</pre> with the corresponding properties
224      * from the model. This allows late evaluation of property values when the plugin is executed (as compared
225      * to evaluation when the pom is parsed as is done with <pre>${property-name}</pre> expressions).
226      *
227      * This allows other plugins to modify or set properties with the changes getting picked up by surefire.
228      */
229     @Nonnull
230     private String interpolateArgLineWithPropertyExpressions() {
231         if (getArgLine() == null) {
232             return "";
233         }
234 
235         String resolvedArgLine = getArgLine().trim();
236 
237         if (resolvedArgLine.isEmpty()) {
238             return "";
239         }
240 
241         for (final String key : getModelProperties().stringPropertyNames()) {
242             String field = "@{" + key + "}";
243             if (getArgLine().contains(field)) {
244                 resolvedArgLine =
245                         resolvedArgLine.replace(field, getModelProperties().getProperty(key, ""));
246             }
247         }
248 
249         return resolvedArgLine;
250     }
251 
252     @Nonnull
253     private static String stripWhitespace(@Nonnull String argLine) {
254         return argLine.replaceAll("\\s", " ");
255     }
256 
257     /**
258      * Immutable map.
259      *
260      * @param map    immutable map copies elements from <code>map</code>
261      * @param <K>    key type
262      * @param <V>    value type
263      * @return never returns null
264      */
265     @Nonnull
266     private static <K, V> Map<K, V> toImmutable(@Nullable Map<K, V> map) {
267         return map == null ? Collections.<K, V>emptyMap() : new ImmutableMap<>(map);
268     }
269 
270     @Override
271     @Nonnull
272     public File getTempDirectory() {
273         return tempDirectory;
274     }
275 
276     @Override
277     @Nullable
278     protected String getDebugLine() {
279         return debugLine;
280     }
281 
282     @Override
283     @Nonnull
284     protected File getWorkingDirectory() {
285         return workingDirectory;
286     }
287 
288     @Override
289     @Nonnull
290     protected Properties getModelProperties() {
291         return modelProperties;
292     }
293 
294     @Override
295     @Nullable
296     protected String getArgLine() {
297         return argLine;
298     }
299 
300     @Override
301     @Nonnull
302     protected Map<String, String> getEnvironmentVariables() {
303         return environmentVariables;
304     }
305 
306     @Nonnull
307     @Override
308     protected String[] getExcludedEnvironmentVariables() {
309         return excludedEnvironmentVariables;
310     }
311 
312     @Override
313     protected boolean isDebug() {
314         return debug;
315     }
316 
317     @Override
318     protected int getForkCount() {
319         return forkCount;
320     }
321 
322     @Override
323     protected boolean isReuseForks() {
324         return reuseForks;
325     }
326 
327     @Override
328     @Nonnull
329     protected Platform getPluginPlatform() {
330         return pluginPlatform;
331     }
332 
333     @Override
334     @Nonnull
335     protected JdkAttributes getJdkForTests() {
336         return getPluginPlatform().getJdkExecAttributesForTests();
337     }
338 
339     @Override
340     @Nonnull
341     protected Classpath getBooterClasspath() {
342         return booterClasspath;
343     }
344 
345     @Nonnull
346     private String newJvmArgLine(int forks) {
347         String interpolatedArgs = stripWhitespace(interpolateArgLineWithPropertyExpressions());
348         String argsWithReplacedForkNumbers = replaceThreadNumberPlaceholders(interpolatedArgs, forks);
349         return extendJvmArgLine(argsWithReplacedForkNumbers);
350     }
351 
352     @Nonnull
353     private static String findStartClass(StartupConfiguration config) {
354         return config.isShadefire() ? relocate(DEFAULT_PROVIDER_CLASS) : DEFAULT_PROVIDER_CLASS;
355     }
356 }