View Javadoc
1   package org.apache.maven.plugin.surefire.booterclient;
2   
3   /*
4    * Licensed to the Apache Software Foundation (ASF) under one
5    * or more contributor license agreements.  See the NOTICE file
6    * distributed with this work for additional information
7    * regarding copyright ownership.  The ASF licenses this file
8    * to you under the Apache License, Version 2.0 (the
9    * "License"); you may not use this file except in compliance
10   * with the License.  You may obtain a copy of the License at
11   *
12   *     http://www.apache.org/licenses/LICENSE-2.0
13   *
14   * Unless required by applicable law or agreed to in writing,
15   * software distributed under the License is distributed on an
16   * "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
17   * KIND, either express or implied.  See the License for the
18   * specific language governing permissions and limitations
19   * under the License.
20   */
21  
22  import org.apache.maven.plugin.surefire.JdkAttributes;
23  import org.apache.maven.plugin.surefire.booterclient.lazytestprovider.Commandline;
24  import org.apache.maven.plugin.surefire.log.api.ConsoleLogger;
25  import org.apache.maven.surefire.booter.AbstractPathConfiguration;
26  import org.apache.maven.surefire.booter.Classpath;
27  import org.apache.maven.surefire.booter.StartupConfiguration;
28  import org.apache.maven.surefire.booter.SurefireBooterForkException;
29  import org.apache.maven.surefire.extensions.ForkNodeFactory;
30  import org.apache.maven.surefire.api.util.internal.ImmutableMap;
31  import org.apache.maven.surefire.shared.utils.cli.CommandLineException;
32  
33  import javax.annotation.Nonnull;
34  import javax.annotation.Nullable;
35  import java.io.File;
36  import java.util.Collections;
37  import java.util.List;
38  import java.util.Map;
39  import java.util.Map.Entry;
40  import java.util.Properties;
41  
42  import static org.apache.maven.plugin.surefire.SurefireHelper.replaceForkThreadsInPath;
43  import static org.apache.maven.plugin.surefire.util.Relocator.relocate;
44  import static org.apache.maven.plugin.surefire.SurefireHelper.replaceThreadNumberPlaceholders;
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
54          extends ForkConfiguration
55  {
56      @Nonnull private final Classpath booterClasspath;
57      @Nonnull private final File tempDirectory;
58      @Nullable
59      private final String debugLine;
60      @Nonnull private final File workingDirectory;
61      @Nonnull private final Properties modelProperties;
62      @Nullable private final String argLine;
63      @Nonnull private final Map<String, String> environmentVariables;
64      @Nonnull private final String[] excludedEnvironmentVariables;
65      private final boolean debug;
66      private final int forkCount;
67      private final boolean reuseForks;
68      @Nonnull private final Platform pluginPlatform;
69      @Nonnull private final ConsoleLogger log;
70      @Nonnull private final ForkNodeFactory forkNodeFactory;
71  
72      @SuppressWarnings( "checkstyle:parameternumber" )
73      protected DefaultForkConfiguration( @Nonnull Classpath booterClasspath,
74                                       @Nonnull File tempDirectory,
75                                       @Nullable String debugLine,
76                                       @Nonnull File workingDirectory,
77                                       @Nonnull Properties modelProperties,
78                                       @Nullable String argLine,
79                                       @Nonnull Map<String, String> environmentVariables,
80                                       @Nonnull String[] excludedEnvironmentVariables,
81                                       boolean debug,
82                                       int forkCount,
83                                       boolean reuseForks,
84                                       @Nonnull Platform pluginPlatform,
85                                       @Nonnull ConsoleLogger log,
86                                       @Nonnull ForkNodeFactory forkNodeFactory )
87      {
88          this.booterClasspath = booterClasspath;
89          this.tempDirectory = tempDirectory;
90          this.debugLine = debugLine;
91          this.workingDirectory = workingDirectory;
92          this.modelProperties = modelProperties;
93          this.argLine = argLine;
94          this.environmentVariables = toImmutable( environmentVariables );
95          this.excludedEnvironmentVariables = excludedEnvironmentVariables;
96          this.debug = debug;
97          this.forkCount = forkCount;
98          this.reuseForks = reuseForks;
99          this.pluginPlatform = pluginPlatform;
100         this.log = log;
101         this.forkNodeFactory = forkNodeFactory;
102     }
103 
104     protected abstract void resolveClasspath( @Nonnull Commandline cli,
105                                               @Nonnull String booterThatHasMainMethod,
106                                               @Nonnull StartupConfiguration config,
107                                               @Nonnull File dumpLogDirectory )
108             throws SurefireBooterForkException;
109 
110     @Nonnull
111     protected String extendJvmArgLine( @Nonnull String jvmArgLine )
112     {
113         return jvmArgLine;
114     }
115 
116     @Nonnull
117     @Override
118     public final ForkNodeFactory getForkNodeFactory()
119     {
120         return forkNodeFactory;
121     }
122 
123     /**
124      * @param config       The startup configuration
125      * @param forkNumber   index of forked JVM, to be the replacement in the argLine
126      * @param dumpLogDirectory     directory for dump log file
127      * @return CommandLine able to flush entire command going to be sent to forked JVM
128      * @throws org.apache.maven.surefire.booter.SurefireBooterForkException when unable to perform the fork
129      */
130     @Nonnull
131     @Override
132     public Commandline createCommandLine( @Nonnull StartupConfiguration config,
133                                           int forkNumber,
134                                           @Nonnull File dumpLogDirectory )
135             throws SurefireBooterForkException
136     {
137         try
138         {
139             Commandline cli =
140                     new Commandline( getExcludedEnvironmentVariables() );
141 
142             cli.setWorkingDirectory( getWorkingDirectory( forkNumber ).getAbsolutePath() );
143 
144             for ( Entry<String, String> entry : getEnvironmentVariables().entrySet() )
145             {
146                 String value = entry.getValue();
147                 cli.addEnvironment( entry.getKey(), value == null ? "" : value );
148             }
149 
150             cli.setExecutable( getJdkForTests().getJvmExecutable().getAbsolutePath() );
151 
152             String jvmArgLine = newJvmArgLine( forkNumber );
153             if ( !jvmArgLine.isEmpty() )
154             {
155                 cli.createArg()
156                         .setLine( jvmArgLine );
157             }
158 
159             if ( getDebugLine() != null && !getDebugLine().isEmpty() )
160             {
161                 cli.createArg()
162                         .setLine( getDebugLine() );
163             }
164 
165             resolveClasspath( cli, findStartClass( config ), config, dumpLogDirectory );
166 
167             return cli;
168         }
169         catch ( CommandLineException e )
170         {
171             throw new SurefireBooterForkException( e.getLocalizedMessage(), e );
172         }
173     }
174 
175     protected ConsoleLogger getLogger()
176     {
177         return log;
178     }
179 
180     @Nonnull
181     protected List<String> toCompleteClasspath( @Nonnull StartupConfiguration conf ) throws SurefireBooterForkException
182     {
183         AbstractPathConfiguration pathConfig = conf.getClasspathConfiguration();
184         if ( pathConfig.isClassPathConfig() == pathConfig.isModularPathConfig() )
185         {
186             throw new SurefireBooterForkException( "Could not find class-path config nor modular class-path either." );
187         }
188 
189         Classpath bootClasspath = getBooterClasspath();
190         Classpath testClasspath = pathConfig.getTestClasspath();
191         Classpath providerClasspath = pathConfig.getProviderClasspath();
192         Classpath completeClasspath = join( join( bootClasspath, testClasspath ), providerClasspath );
193 
194         getLogger().debug( completeClasspath.getLogMessage( "boot classpath:" ) );
195         getLogger().debug( completeClasspath.getCompactLogMessage( "boot(compact) classpath:" ) );
196 
197         return completeClasspath.getClassPath();
198     }
199 
200     @Nonnull
201     private File getWorkingDirectory( int forkNumber )
202             throws SurefireBooterForkException
203     {
204         File cwd = replaceForkThreadsInPath( getWorkingDirectory(), forkNumber );
205 
206         if ( !cwd.exists() && !cwd.mkdirs() )
207         {
208             throw new SurefireBooterForkException( "Cannot create workingDirectory " + cwd.getAbsolutePath() );
209         }
210 
211         if ( !cwd.isDirectory() )
212         {
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     {
229         if ( getArgLine() == null )
230         {
231             return "";
232         }
233 
234         String resolvedArgLine = getArgLine().trim();
235 
236         if ( resolvedArgLine.isEmpty() )
237         {
238             return "";
239         }
240 
241         for ( final String key : getModelProperties().stringPropertyNames() )
242         {
243             String field = "@{" + key + "}";
244             if ( getArgLine().contains( field ) )
245             {
246                 resolvedArgLine = resolvedArgLine.replace( field, getModelProperties().getProperty( key, "" ) );
247             }
248         }
249 
250         return resolvedArgLine;
251     }
252 
253     @Nonnull
254     private static String stripNewLines( @Nonnull String argLine )
255     {
256         return argLine.replace( "\n", " " ).replace( "\r", " " );
257     }
258 
259     /**
260      * Immutable map.
261      *
262      * @param map    immutable map copies elements from <code>map</code>
263      * @param <K>    key type
264      * @param <V>    value type
265      * @return never returns null
266      */
267     @Nonnull
268     private static <K, V> Map<K, V> toImmutable( @Nullable Map<K, V> map )
269     {
270         return map == null ? Collections.<K, V>emptyMap() : new ImmutableMap<>( map );
271     }
272 
273     @Override
274     @Nonnull
275     public File getTempDirectory()
276     {
277         return tempDirectory;
278     }
279 
280     @Override
281     @Nullable
282     protected String getDebugLine()
283     {
284         return debugLine;
285     }
286 
287     @Override
288     @Nonnull
289     protected File getWorkingDirectory()
290     {
291         return workingDirectory;
292     }
293 
294     @Override
295     @Nonnull
296     protected Properties getModelProperties()
297     {
298         return modelProperties;
299     }
300 
301     @Override
302     @Nullable
303     protected String getArgLine()
304     {
305         return argLine;
306     }
307 
308     @Override
309     @Nonnull
310     protected Map<String, String> getEnvironmentVariables()
311     {
312         return environmentVariables;
313     }
314 
315     @Nonnull
316     @Override
317     protected String[] getExcludedEnvironmentVariables()
318     {
319         return excludedEnvironmentVariables;
320     }
321 
322     @Override
323     protected boolean isDebug()
324     {
325         return debug;
326     }
327 
328     @Override
329     protected int getForkCount()
330     {
331         return forkCount;
332     }
333 
334     @Override
335     protected boolean isReuseForks()
336     {
337         return reuseForks;
338     }
339 
340     @Override
341     @Nonnull
342     protected Platform getPluginPlatform()
343     {
344         return pluginPlatform;
345     }
346 
347     @Override
348     @Nonnull
349     protected JdkAttributes getJdkForTests()
350     {
351         return getPluginPlatform().getJdkExecAttributesForTests();
352     }
353 
354     @Override
355     @Nonnull
356     protected Classpath getBooterClasspath()
357     {
358         return booterClasspath;
359     }
360 
361     @Nonnull
362     private String newJvmArgLine( int forks )
363     {
364         String interpolatedArgs = stripNewLines( interpolateArgLineWithPropertyExpressions() );
365         String argsWithReplacedForkNumbers = replaceThreadNumberPlaceholders( interpolatedArgs, forks );
366         return extendJvmArgLine( argsWithReplacedForkNumbers );
367     }
368 
369     @Nonnull
370     private static String findStartClass( StartupConfiguration config )
371     {
372         return config.isShadefire() ? relocate( DEFAULT_PROVIDER_CLASS ) : DEFAULT_PROVIDER_CLASS;
373     }
374 }