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