View Javadoc

1   package org.apache.maven.archetype.mojos;
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.commons.collections.CollectionUtils;
23  import org.apache.maven.archetype.ArchetypeGenerationRequest;
24  import org.apache.maven.archetype.ArchetypeGenerationResult;
25  import org.apache.maven.archetype.common.Constants;
26  import org.apache.maven.archetype.exception.ArchetypeNotConfigured;
27  import org.apache.maven.archetype.generator.ArchetypeGenerator;
28  import org.apache.maven.plugin.AbstractMojo;
29  import org.apache.maven.plugin.MojoExecutionException;
30  import org.apache.maven.plugin.MojoFailureException;
31  import org.apache.maven.project.MavenProject;
32  import org.apache.maven.shared.invoker.DefaultInvocationRequest;
33  import org.apache.maven.shared.invoker.InvocationRequest;
34  import org.apache.maven.shared.invoker.InvocationResult;
35  import org.apache.maven.shared.invoker.Invoker;
36  import org.apache.maven.shared.invoker.MavenInvocationException;
37  import org.codehaus.plexus.util.FileUtils;
38  import org.codehaus.plexus.util.IOUtil;
39  import org.codehaus.plexus.util.StringUtils;
40  
41  import java.io.File;
42  import java.io.FileInputStream;
43  import java.io.FileNotFoundException;
44  import java.io.IOException;
45  import java.io.InputStream;
46  import java.io.StringWriter;
47  import java.util.Arrays;
48  import java.util.List;
49  import java.util.Properties;
50  
51  /**
52   * <p>Execute the archetype integration tests, consisting in generating projects from the current archetype and
53   * optionally comparing generated projects with reference copy.</p>
54   * 
55   * <p>Each IT consists of a sub-directory in <code>src/test/resources/projects</code> containing:</p>
56   * <ul>
57   * <li>a <code>goal.txt</code> file, containing a list of goals to run against the generated project (can be empty,
58   * content ignored before maven-archetype-plugin 2.1),</li>
59   * <li>an <code>archetype.properties</code> file, containing properties for project generation,</li>
60   * <li>an optional <code>reference/</code> directory containing a reference copy of the expected project created from the IT.</li>
61   * </ul>
62   *
63   * Notice that it is expected to be run as part as of a build after the <code>package</code> phase and not directly
64   * as a goal from CLI.
65   *
66   * @author rafale
67   * @requiresProject true
68   * @goal integration-test
69   */
70  public class IntegrationTestMojo
71      extends AbstractMojo
72  {
73      /** @component */
74      private ArchetypeGenerator archetypeGenerator;
75  
76      /** @component */
77      private Invoker invoker;
78  
79      /**
80       * The archetype project to execute the integration tests on.
81       *
82       * @parameter expression="${project}"
83       * @required
84       * @readonly
85       */
86      private MavenProject project;
87  
88      /**
89       * Skip the integration test.
90       *
91       * @parameter expression="${archetype.test.skip}"
92       * @readonly
93       */
94      private boolean skip = false;
95  
96      public void execute()
97          throws MojoExecutionException, MojoFailureException
98      {
99          if ( skip )
100         {
101             return;
102         }
103 
104         File projectsDirectory = new File( project.getBasedir(), "target/test-classes/projects" );
105 
106         if ( !projectsDirectory.exists() )
107         {
108             getLog().warn( "No Archetype IT projects: root 'projects' directory not found." );
109 
110             return;
111         }
112 
113         File archetypeFile = project.getArtifact().getFile();
114 
115         if ( archetypeFile == null )
116         {
117             throw new MojoFailureException(
118                                             "Unable to get the archetypes' artifact which should have just been built:"
119                                                 + " you probably launched 'mvn archetype:integration-test' instead of"
120                                                 + " 'mvn integration-test'." );
121         }
122 
123         try
124         {
125             @SuppressWarnings( "unchecked" )
126             List<File> projectsGoalFiles = FileUtils.getFiles( projectsDirectory, "*/goal.txt", "" );
127 
128             if ( projectsGoalFiles.size() == 0 )
129             {
130                 getLog().warn( "No Archetype IT projects: no directory with goal.txt found." );
131 
132                 return;
133             }
134 
135             StringWriter errorWriter = new StringWriter();
136             for ( File goalFile : projectsGoalFiles )
137             {
138                 try
139                 {
140                     processIntegrationTest( goalFile, archetypeFile );
141                 }
142                 catch ( IntegrationTestFailure ex )
143                 {
144                     errorWriter.write( "\nArchetype IT '" + goalFile.getParentFile().getName() + "' failed: " );
145                     errorWriter.write( ex.getMessage() );
146                 }
147             }
148 
149             String errors = errorWriter.toString();
150             if ( !StringUtils.isEmpty( errors ) )
151             {
152                 throw new MojoExecutionException( errors );
153             }
154         }
155         catch ( IOException ex )
156         {
157             throw new MojoFailureException( ex, ex.getMessage(), ex.getMessage() );
158         }
159     }
160 
161     /**
162      * Checks that actual directory content is the same as reference.
163      *
164      * @param reference the reference directory
165      * @param actual the actual directory to compare with the reference
166      * @throws IntegrationTestFailure if content differs
167      */
168     private void assertDirectoryEquals( File reference, File actual )
169         throws IntegrationTestFailure, IOException
170     {
171         @SuppressWarnings( "unchecked" )
172         List<String> referenceFiles = FileUtils.getFileAndDirectoryNames( reference, "**", null, false, true, true, true );
173         getLog().debug( "reference content: " + referenceFiles );
174 
175         @SuppressWarnings( "unchecked" )
176         List<String> actualFiles = FileUtils.getFileAndDirectoryNames( actual, "**", null, false, true, true, true );
177         getLog().debug( "actual content: " + referenceFiles );
178 
179         boolean fileNamesEquals = CollectionUtils.isEqualCollection( referenceFiles, actualFiles );
180 
181         if ( !fileNamesEquals )
182         {
183             getLog().debug( "Actual list of files is not the same as reference:" );
184             int missing = 0;
185             for ( String ref : referenceFiles )
186             {
187                 if ( actualFiles.contains( ref ) )
188                 {
189                     actualFiles.remove( ref );
190                     getLog().debug( "Contained " + ref );
191                 }
192                 else
193                 {
194                     missing++;
195                     getLog().error( "Not contained " + ref );
196                 }
197             }
198             getLog().error( "Remains " + actualFiles );
199 
200             throw new IntegrationTestFailure( "Reference and generated project differs (missing: " + missing
201                 + ", unexpected: " + actualFiles.size() + ")" );
202         }
203 
204         boolean contentEquals = true;
205 
206         for ( String file : referenceFiles )
207         {
208             File referenceFile = new File( reference, file );
209             File actualFile = new File( actual, file );
210 
211             if ( referenceFile.isDirectory() )
212             {
213                 if ( actualFile.isFile() )
214                 {
215                     getLog().warn( "File " + file + " is a directory in the reference but a file in actual" );
216                     contentEquals = false;
217                 }
218             }
219             else if ( actualFile.isDirectory() )
220             {
221                 if ( referenceFile.isFile() )
222                 {
223                     getLog().warn( "File " + file + " is a file in the reference but a directory in actual" );
224                     contentEquals = false;
225                 }
226             }
227             else if ( !FileUtils.contentEquals( referenceFile, actualFile ) )
228             {
229                 getLog().warn( "Contents of file " + file + " are not equal" );
230                 contentEquals = false;
231             }
232         }
233         if ( !contentEquals )
234         {
235             throw new IntegrationTestFailure( "Some content are not equals" );
236         }
237     }
238 
239     private Properties loadProperties( final File propertiesFile )
240         throws IOException, FileNotFoundException
241     {
242         Properties properties = new Properties();
243 
244         InputStream in = null;
245         try
246         {
247             in = new FileInputStream( propertiesFile );
248 
249             properties.load( in );
250         }
251         finally
252         {
253             IOUtil.close( in );
254         }
255 
256         return properties;
257     }
258 
259     private void processIntegrationTest( File goalFile, File archetypeFile )
260         throws IntegrationTestFailure
261     {
262         getLog().info( "Processing Archetype IT project: " + goalFile.getParentFile().getName() );
263 
264         try
265         {
266             Properties properties = getProperties( goalFile );
267 
268             String basedir = goalFile.getParentFile().getPath() + "/project";
269 
270             FileUtils.deleteDirectory( basedir );
271 
272             FileUtils.mkdir( basedir );
273 
274             ArchetypeGenerationRequest request = new ArchetypeGenerationRequest()
275                 .setArchetypeGroupId( project.getGroupId() )
276                 .setArchetypeArtifactId( project.getArtifactId() )
277                 .setArchetypeVersion( project.getVersion() )
278                 .setGroupId( properties.getProperty( Constants.GROUP_ID ) )
279                 .setArtifactId( properties.getProperty( Constants.ARTIFACT_ID ) )
280                 .setVersion( properties.getProperty( Constants.VERSION ) )
281                 .setPackage( properties.getProperty( Constants.PACKAGE ) )
282                 .setOutputDirectory( basedir )
283                 .setProperties( properties );
284 
285             ArchetypeGenerationResult result = new ArchetypeGenerationResult();
286 
287             archetypeGenerator.generateArchetype( request, archetypeFile, result );
288 
289             if ( result.getCause() != null )
290             {
291                 if ( result.getCause() instanceof ArchetypeNotConfigured )
292                 {
293                     ArchetypeNotConfigured anc = (ArchetypeNotConfigured) result.getCause();
294 
295                     throw new IntegrationTestFailure( "Missing required properties in archetype.properties: "
296                         + StringUtils.join( anc.getMissingProperties().iterator(), ", " ), anc );
297                 }
298 
299                 throw new IntegrationTestFailure( result.getCause().getMessage(), result.getCause() );
300             }
301 
302             File reference = new File( goalFile.getParentFile(), "reference" );
303 
304             if ( reference.exists() )
305             {
306                 // compare generated project with reference
307                 getLog().info( "Comparing generated project with reference content: " + reference );
308 
309                 assertDirectoryEquals( reference, new File( basedir, request.getArtifactId() ) );
310             }
311 
312             String goals = FileUtils.fileRead( goalFile );
313 
314             invokePostArchetypeGenerationGoals( goals, new File( basedir, request.getArtifactId() ) );
315         }
316         catch ( IOException ioe )
317         {
318             throw new IntegrationTestFailure( ioe );
319         }
320     }
321 
322     private Properties getProperties( File goalFile )
323         throws IOException
324     {
325         File propertiesFile = new File( goalFile.getParentFile(), "archetype.properties" );
326 
327         return loadProperties( propertiesFile );
328     }
329 
330     private void invokePostArchetypeGenerationGoals( String goals, File basedir )
331         throws IntegrationTestFailure
332     {
333         if ( StringUtils.isBlank( goals ) )
334         {
335             getLog().info( "No post-archetype-generation goals to invoke." );
336 
337             return;
338         }
339 
340         getLog().info( "Invoking post-archetype-generation goals: " + goals );
341 
342         InvocationRequest request = new DefaultInvocationRequest()
343             .setBaseDirectory( basedir )
344             .setGoals( Arrays.asList( StringUtils.split( goals, "," ) ) );
345 
346         try
347         {
348             InvocationResult result = invoker.execute( request );
349 
350             getLog().info( "Post-archetype-generation invoker exit code: " + result.getExitCode() );
351 
352             if ( result.getExitCode() != 0 )
353             {
354                 throw new IntegrationTestFailure( "Execution failure: exit code = " + result.getExitCode(),
355                                                   result.getExecutionException() );
356             }
357         }
358         catch ( MavenInvocationException e )
359         {
360             throw new IntegrationTestFailure( "Cannot run additions goals.", e );
361         }
362     }
363 
364     class IntegrationTestFailure
365         extends Exception
366     {
367         IntegrationTestFailure()
368         {
369             super();
370         }
371 
372         IntegrationTestFailure( String message )
373         {
374             super( message );
375         }
376 
377         IntegrationTestFailure( Throwable cause )
378         {
379             super( cause );
380         }
381 
382         IntegrationTestFailure( String message, Throwable cause )
383         {
384             super( message, cause );
385         }
386     }
387 }