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                 cli.addEnvironment(entry.getKey(), value == null ? "" : value);
161             }
162 
163             cli.setExecutable(getJdkForTests().getJvmExecutable().getAbsolutePath());
164 
165             String jvmArgLine = newJvmArgLine(forkNumber);
166             if (!jvmArgLine.isEmpty()) {
167                 cli.createArg().setLine(jvmArgLine);
168             }
169 
170             if (getDebugLine() != null && !getDebugLine().isEmpty()) {
171                 cli.createArg().setLine(getDebugLine());
172             }
173 
174             resolveClasspath(cli, findStartClass(config), config, dumpLogDirectory);
175 
176             return cli;
177         } catch (CommandLineException e) {
178             throw new SurefireBooterForkException(e.getLocalizedMessage(), e);
179         }
180     }
181 
182     protected ConsoleLogger getLogger() {
183         return log;
184     }
185 
186     @Nonnull
187     protected List<String> toCompleteClasspath(@Nonnull StartupConfiguration conf) throws SurefireBooterForkException {
188         AbstractPathConfiguration pathConfig = conf.getClasspathConfiguration();
189         if (pathConfig.isClassPathConfig() == pathConfig.isModularPathConfig()) {
190             throw new SurefireBooterForkException("Could not find class-path config nor modular class-path either.");
191         }
192 
193         Classpath bootClasspath = getBooterClasspath();
194         Classpath testClasspath = pathConfig.getTestClasspath();
195         Classpath providerClasspath = pathConfig.getProviderClasspath();
196         Classpath completeClasspath = join(join(bootClasspath, testClasspath), providerClasspath);
197 
198         getLogger().debug(completeClasspath.getLogMessage("boot classpath:"));
199         getLogger().debug(completeClasspath.getCompactLogMessage("boot(compact) classpath:"));
200 
201         return completeClasspath.getClassPath();
202     }
203 
204     @Nonnull
205     private File getWorkingDirectory(int forkNumber) throws SurefireBooterForkException {
206         File cwd = replaceForkThreadsInPath(getWorkingDirectory(), forkNumber);
207 
208         if (!cwd.exists() && !cwd.mkdirs()) {
209             throw new SurefireBooterForkException("Cannot create workingDirectory " + cwd.getAbsolutePath());
210         }
211 
212         if (!cwd.isDirectory()) {
213             throw new SurefireBooterForkException(
214                     "WorkingDirectory " + cwd.getAbsolutePath() + " exists and is not a directory");
215         }
216         return cwd;
217     }
218 
219     /**
220      * Replaces expressions <pre>@{property-name}</pre> with the corresponding properties
221      * from the model. This allows late evaluation of property values when the plugin is executed (as compared
222      * to evaluation when the pom is parsed as is done with <pre>${property-name}</pre> expressions).
223      *
224      * This allows other plugins to modify or set properties with the changes getting picked up by surefire.
225      */
226     @Nonnull
227     private String interpolateArgLineWithPropertyExpressions() {
228         if (getArgLine() == null) {
229             return "";
230         }
231 
232         String resolvedArgLine = getArgLine().trim();
233 
234         if (resolvedArgLine.isEmpty()) {
235             return "";
236         }
237 
238         for (final String key : getModelProperties().stringPropertyNames()) {
239             String field = "@{" + key + "}";
240             if (getArgLine().contains(field)) {
241                 resolvedArgLine =
242                         resolvedArgLine.replace(field, getModelProperties().getProperty(key, ""));
243             }
244         }
245 
246         return resolvedArgLine;
247     }
248 
249     @Nonnull
250     private static String stripWhitespace(@Nonnull String argLine) {
251         return argLine.replaceAll("\\s", " ");
252     }
253 
254     /**
255      * Immutable map.
256      *
257      * @param map    immutable map copies elements from <code>map</code>
258      * @param <K>    key type
259      * @param <V>    value type
260      * @return never returns null
261      */
262     @Nonnull
263     private static <K, V> Map<K, V> toImmutable(@Nullable Map<K, V> map) {
264         return map == null ? Collections.<K, V>emptyMap() : new ImmutableMap<>(map);
265     }
266 
267     @Override
268     @Nonnull
269     public File getTempDirectory() {
270         return tempDirectory;
271     }
272 
273     @Override
274     @Nullable
275     protected String getDebugLine() {
276         return debugLine;
277     }
278 
279     @Override
280     @Nonnull
281     protected File getWorkingDirectory() {
282         return workingDirectory;
283     }
284 
285     @Override
286     @Nonnull
287     protected Properties getModelProperties() {
288         return modelProperties;
289     }
290 
291     @Override
292     @Nullable
293     protected String getArgLine() {
294         return argLine;
295     }
296 
297     @Override
298     @Nonnull
299     protected Map<String, String> getEnvironmentVariables() {
300         return environmentVariables;
301     }
302 
303     @Nonnull
304     @Override
305     protected String[] getExcludedEnvironmentVariables() {
306         return excludedEnvironmentVariables;
307     }
308 
309     @Override
310     protected boolean isDebug() {
311         return debug;
312     }
313 
314     @Override
315     protected int getForkCount() {
316         return forkCount;
317     }
318 
319     @Override
320     protected boolean isReuseForks() {
321         return reuseForks;
322     }
323 
324     @Override
325     @Nonnull
326     protected Platform getPluginPlatform() {
327         return pluginPlatform;
328     }
329 
330     @Override
331     @Nonnull
332     protected JdkAttributes getJdkForTests() {
333         return getPluginPlatform().getJdkExecAttributesForTests();
334     }
335 
336     @Override
337     @Nonnull
338     protected Classpath getBooterClasspath() {
339         return booterClasspath;
340     }
341 
342     @Nonnull
343     private String newJvmArgLine(int forks) {
344         String interpolatedArgs = stripWhitespace(interpolateArgLineWithPropertyExpressions());
345         String argsWithReplacedForkNumbers = replaceThreadNumberPlaceholders(interpolatedArgs, forks);
346         return extendJvmArgLine(argsWithReplacedForkNumbers);
347     }
348 
349     @Nonnull
350     private static String findStartClass(StartupConfiguration config) {
351         return config.isShadefire() ? relocate(DEFAULT_PROVIDER_CLASS) : DEFAULT_PROVIDER_CLASS;
352     }
353 }