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