1package org.antlr.mojo.antlr3; 2 3import java.util.List; 4import java.util.Set; 5import java.util.HashSet; 6import java.util.ArrayList; 7import java.util.Collections; 8import java.io.File; 9import java.io.IOException; 10import java.io.Writer; 11import java.io.FileWriter; 12import java.io.BufferedWriter; 13import java.net.URL; 14import java.net.MalformedURLException; 15import java.net.URLClassLoader; 16 17import org.apache.maven.plugin.AbstractMojo; 18import org.apache.maven.plugin.MojoExecutionException; 19import org.apache.maven.plugin.MojoFailureException; 20import org.apache.maven.project.MavenProject; 21import org.apache.maven.artifact.Artifact; 22import org.apache.maven.artifact.DependencyResolutionRequiredException; 23import org.apache.maven.artifact.versioning.ArtifactVersion; 24import org.apache.maven.artifact.versioning.DefaultArtifactVersion; 25import org.apache.maven.artifact.versioning.OverConstrainedVersionException; 26import org.codehaus.plexus.util.StringUtils; 27import org.codehaus.plexus.util.FileUtils; 28import org.codehaus.plexus.compiler.util.scan.mapping.SourceMapping; 29import org.codehaus.plexus.compiler.util.scan.mapping.SuffixMapping; 30import org.codehaus.plexus.compiler.util.scan.SourceInclusionScanner; 31import org.codehaus.plexus.compiler.util.scan.SimpleSourceInclusionScanner; 32import org.codehaus.plexus.compiler.util.scan.InclusionScanException; 33import org.antlr.runtime.ANTLRFileStream; 34import org.antlr.runtime.RecognitionException; 35import org.antlr.gunit.GrammarInfo; 36import org.antlr.gunit.gUnitExecutor; 37import org.antlr.gunit.AbstractTest; 38import org.antlr.gunit.Interp; 39 40/** 41 * Takes gUnit scripts and directly performs testing. 42 * 43 * @goal gunit 44 * 45 * @phase test 46 * @requiresDependencyResolution test 47 * @requiresProject true 48 * 49 * @author Steve Ebersole 50 */ 51public class GUnitExecuteMojo extends AbstractMojo { 52 public static final String ANTLR_GROUP_ID = "org.antlr"; 53 public static final String ANTLR_ARTIFACT_NAME = "antlr"; 54 public static final String ANTLR_RUNTIME_ARTIFACT_NAME = "antlr-runtime"; 55 56 /** 57 * INTERNAL : The Maven Project to which we are attached 58 * 59 * @parameter expression="${project}" 60 * @required 61 */ 62 private MavenProject project; 63 64 /** 65 * INTERNAL : The artifacts associated to the dependencies defined as part 66 * of our configuration within the project to which we are being attached. 67 * 68 * @parameter expression="${plugin.artifacts}" 69 * @required 70 * @readonly 71 */ 72 private List<Artifact> pluginArtifacts; 73 74 /** 75 * Specifies the directory containing the gUnit testing files. 76 * 77 * @parameter expression="${basedir}/src/test/gunit" 78 * @required 79 */ 80 private File sourceDirectory; 81 82 /** 83 * A set of patterns for matching files from the sourceDirectory that 84 * should be included as gUnit source files. 85 * 86 * @parameter 87 */ 88 private Set includes; 89 90 /** 91 * A set of exclude patterns. 92 * 93 * @parameter 94 */ 95 private Set excludes; 96 97 /** 98 * Specifies directory to which gUnit reports should get written. 99 * 100 * @parameter expression="${basedir}/target/gunit-report" 101 * @required 102 */ 103 private File reportDirectory; 104 105 /** 106 * Should gUnit functionality be completely by-passed? 107 * <p/> 108 * By default we skip gUnit tests if the user requested that all testing be skipped using 'maven.test.skip' 109 * 110 * @parameter expression="${maven.test.skip}" 111 */ 112 private boolean skip; 113 114 public Set getIncludePatterns() { 115 return includes == null || includes.isEmpty() 116 ? Collections.singleton( "**/*.testsuite" ) 117 : includes; 118 } 119 120 public Set getExcludePatterns() { 121 return excludes == null 122 ? Collections.emptySet() 123 : excludes; 124 } 125 126 127 public final void execute() throws MojoExecutionException, MojoFailureException { 128 if ( skip ) { 129 getLog().info( "Skipping gUnit processing" ); 130 return; 131 } 132 Artifact pluginAntlrArtifact = determinePluginAntlrArtifact(); 133 134 validateProjectsAntlrVersion( determineArtifactVersion( pluginAntlrArtifact ) ); 135 136 performExecution( determineProjectCompileScopeClassLoader( pluginAntlrArtifact ) ); 137 } 138 139 private Artifact determinePluginAntlrArtifact() throws MojoExecutionException { 140 for ( Artifact artifact : pluginArtifacts ) { 141 boolean match = ANTLR_GROUP_ID.equals( artifact.getGroupId() ) 142 && ANTLR_ARTIFACT_NAME.equals( artifact.getArtifactId() ); 143 if ( match ) { 144 return artifact; 145 } 146 } 147 throw new MojoExecutionException( 148 "Unexpected state : could not locate " + ANTLR_GROUP_ID + ':' + ANTLR_ARTIFACT_NAME + 149 " in plugin dependencies" 150 ); 151 } 152 153 private ArtifactVersion determineArtifactVersion(Artifact artifact) throws MojoExecutionException { 154 try { 155 return artifact.getVersion() != null 156 ? new DefaultArtifactVersion( artifact.getVersion() ) 157 : artifact.getSelectedVersion(); 158 } 159 catch ( OverConstrainedVersionException e ) { 160 throw new MojoExecutionException( "artifact [" + artifact.getId() + "] defined an overly constrained version range" ); 161 } 162 } 163 164 private void validateProjectsAntlrVersion(ArtifactVersion pluginAntlrVersion) throws MojoExecutionException { 165 Artifact antlrArtifact = null; 166 Artifact antlrRuntimeArtifact = null; 167 168 if ( project.getCompileArtifacts() != null ) { 169 for ( Object o : project.getCompileArtifacts() ) { 170 final Artifact artifact = ( Artifact ) o; 171 if ( ANTLR_GROUP_ID.equals( artifact.getGroupId() ) ) { 172 if ( ANTLR_ARTIFACT_NAME.equals( artifact.getArtifactId() ) ) { 173 antlrArtifact = artifact; 174 break; 175 } 176 if ( ANTLR_RUNTIME_ARTIFACT_NAME.equals( artifact.getArtifactId() ) ) { 177 antlrRuntimeArtifact = artifact; 178 } 179 } 180 } 181 } 182 183 validateBuildTimeArtifact( antlrArtifact, pluginAntlrVersion ); 184 validateRunTimeArtifact( antlrRuntimeArtifact, pluginAntlrVersion ); 185 } 186 187 @SuppressWarnings(value = "unchecked") 188 protected void validateBuildTimeArtifact(Artifact antlrArtifact, ArtifactVersion pluginAntlrVersion) 189 throws MojoExecutionException { 190 if ( antlrArtifact == null ) { 191 validateMissingBuildtimeArtifact(); 192 return; 193 } 194 195 // otherwise, lets make sure they match... 196 ArtifactVersion projectAntlrVersion = determineArtifactVersion( antlrArtifact ); 197 if ( pluginAntlrVersion.compareTo( projectAntlrVersion ) != 0 ) { 198 getLog().warn( 199 "Encountered " + ANTLR_GROUP_ID + ':' + ANTLR_ARTIFACT_NAME + ':' + projectAntlrVersion.toString() + 200 " which did not match Antlr version used by plugin [" + pluginAntlrVersion.toString() + "]" 201 ); 202 } 203 } 204 205 protected void validateMissingBuildtimeArtifact() { 206 // generally speaking, its ok for the project to not define a dep on the build-time artifact... 207 } 208 209 @SuppressWarnings(value = "unchecked") 210 protected void validateRunTimeArtifact(Artifact antlrRuntimeArtifact, ArtifactVersion pluginAntlrVersion) 211 throws MojoExecutionException { 212 if ( antlrRuntimeArtifact == null ) { 213 // its possible, if the project instead depends on the build-time (or full) artifact. 214 return; 215 } 216 217 ArtifactVersion projectAntlrVersion = determineArtifactVersion( antlrRuntimeArtifact ); 218 if ( pluginAntlrVersion.compareTo( projectAntlrVersion ) != 0 ) { 219 getLog().warn( 220 "Encountered " + ANTLR_GROUP_ID + ':' + ANTLR_RUNTIME_ARTIFACT_NAME + ':' + projectAntlrVersion.toString() + 221 " which did not match Antlr version used by plugin [" + pluginAntlrVersion.toString() + "]" 222 ); 223 } 224 } 225 226 /** 227 * Builds the classloader to pass to gUnit. 228 * 229 * @param antlrArtifact The plugin's (our) Antlr dependency artifact. 230 * 231 * @return The classloader for gUnit to use 232 * 233 * @throws MojoExecutionException Problem resolving artifacts to {@link java.net.URL urls}. 234 */ 235 private ClassLoader determineProjectCompileScopeClassLoader(Artifact antlrArtifact) 236 throws MojoExecutionException { 237 ArrayList<URL> classPathUrls = new ArrayList<URL>(); 238 getLog().info( "Adding Antlr artifact : " + antlrArtifact.getId() ); 239 classPathUrls.add( resolveLocalURL( antlrArtifact ) ); 240 241 for ( String path : classpathElements() ) { 242 try { 243 getLog().info( "Adding project compile classpath element : " + path ); 244 classPathUrls.add( new File( path ).toURI().toURL() ); 245 } 246 catch ( MalformedURLException e ) { 247 throw new MojoExecutionException( "Unable to build path URL [" + path + "]" ); 248 } 249 } 250 251 return new URLClassLoader( classPathUrls.toArray( new URL[classPathUrls.size()] ), getClass().getClassLoader() ); 252 } 253 254 protected static URL resolveLocalURL(Artifact artifact) throws MojoExecutionException { 255 try { 256 return artifact.getFile().toURI().toURL(); 257 } 258 catch ( MalformedURLException e ) { 259 throw new MojoExecutionException( "Unable to resolve artifact url : " + artifact.getId(), e ); 260 } 261 } 262 263 @SuppressWarnings( "unchecked" ) 264 private List<String> classpathElements() throws MojoExecutionException { 265 try { 266 // todo : should we combine both compile and test scoped elements? 267 return ( List<String> ) project.getTestClasspathElements(); 268 } 269 catch ( DependencyResolutionRequiredException e ) { 270 throw new MojoExecutionException( "Call to Project#getCompileClasspathElements required dependency resolution" ); 271 } 272 } 273 274 private void performExecution(ClassLoader projectCompileScopeClassLoader) throws MojoExecutionException { 275 getLog().info( "gUnit report directory : " + reportDirectory.getAbsolutePath() ); 276 if ( !reportDirectory.exists() ) { 277 boolean directoryCreated = reportDirectory.mkdirs(); 278 if ( !directoryCreated ) { 279 getLog().warn( "mkdirs() reported problem creating report directory" ); 280 } 281 } 282 283 Result runningResults = new Result(); 284 ArrayList<String> failureNames = new ArrayList<String>(); 285 286 System.out.println(); 287 System.out.println( "-----------------------------------------------------------" ); 288 System.out.println( " G U N I T R E S U L T S" ); 289 System.out.println( "-----------------------------------------------------------" ); 290 291 for ( File script : collectIncludedSourceGrammars() ) { 292 final String scriptPath = script.getAbsolutePath(); 293 System.out.println( "Executing script " + scriptPath ); 294 try { 295 String scriptBaseName = StringUtils.chompLast( FileUtils.basename( script.getName() ), "." ); 296 297 ANTLRFileStream antlrStream = new ANTLRFileStream( scriptPath ); 298 GrammarInfo grammarInfo = Interp.parse( antlrStream ); 299 gUnitExecutor executor = new gUnitExecutor( 300 grammarInfo, 301 projectCompileScopeClassLoader, 302 script.getParentFile().getAbsolutePath() 303 ); 304 305 String report = executor.execTest(); 306 writeReportFile( new File( reportDirectory, scriptBaseName + ".txt" ), report ); 307 308 Result testResult = new Result(); 309 testResult.tests = executor.numOfTest; 310 testResult.failures = executor.numOfFailure; 311 testResult.invalids = executor.numOfInvalidInput; 312 313 System.out.println( testResult.render() ); 314 315 runningResults.add( testResult ); 316 for ( AbstractTest test : executor.failures ) { 317 failureNames.add( scriptBaseName + "#" + test.getHeader() ); 318 } 319 } 320 catch ( IOException e ) { 321 throw new MojoExecutionException( "Could not open specified script file", e ); 322 } 323 catch ( RecognitionException e ) { 324 throw new MojoExecutionException( "Could not parse gUnit script", e ); 325 } 326 } 327 328 System.out.println(); 329 System.out.println( "Summary :" ); 330 if ( ! failureNames.isEmpty() ) { 331 System.out.println( " Found " + failureNames.size() + " failures" ); 332 for ( String name : failureNames ) { 333 System.out.println( " - " + name ); 334 } 335 } 336 System.out.println( runningResults.render() ); 337 System.out.println(); 338 339 if ( runningResults.failures > 0 ) { 340 throw new MojoExecutionException( "Found gUnit test failures" ); 341 } 342 343 if ( runningResults.invalids > 0 ) { 344 throw new MojoExecutionException( "Found invalid gUnit tests" ); 345 } 346 } 347 348 private Set<File> collectIncludedSourceGrammars() throws MojoExecutionException { 349 SourceMapping mapping = new SuffixMapping( "g", Collections.EMPTY_SET ); 350 SourceInclusionScanner scan = new SimpleSourceInclusionScanner( getIncludePatterns(), getExcludePatterns() ); 351 scan.addSourceMapping( mapping ); 352 try { 353 Set scanResults = scan.getIncludedSources( sourceDirectory, null ); 354 Set<File> results = new HashSet<File>(); 355 for ( Object result : scanResults ) { 356 if ( result instanceof File ) { 357 results.add( ( File ) result ); 358 } 359 else if ( result instanceof String ) { 360 results.add( new File( ( String ) result ) ); 361 } 362 else { 363 throw new MojoExecutionException( "Unexpected result type from scanning [" + result.getClass().getName() + "]" ); 364 } 365 } 366 return results; 367 } 368 catch ( InclusionScanException e ) { 369 throw new MojoExecutionException( "Error determining gUnit sources", e ); 370 } 371 } 372 373 private void writeReportFile(File reportFile, String results) { 374 try { 375 Writer writer = new FileWriter( reportFile ); 376 writer = new BufferedWriter( writer ); 377 try { 378 writer.write( results ); 379 writer.flush(); 380 } 381 finally { 382 try { 383 writer.close(); 384 } 385 catch ( IOException ignore ) { 386 } 387 } 388 } 389 catch ( IOException e ) { 390 getLog().warn( "Error writing gUnit report file", e ); 391 } 392 } 393 394 private static class Result { 395 private int tests = 0; 396 private int failures = 0; 397 private int invalids = 0; 398 399 public String render() { 400 return String.format( "Tests run: %d, Failures: %d, Invalid: %d", tests, failures, invalids ); 401 } 402 403 public void add(Result result) { 404 this.tests += result.tests; 405 this.failures += result.failures; 406 this.invalids += result.invalids; 407 } 408 } 409 410} 411