/*******************************************************************************
* Copyright (c) 2006-2008, Cloudsmith Inc.
* The code, documentation and other materials contained herein have been
* licensed under the Eclipse Public License - v 1.0 by the copyright holder
* listed above, as the Initial Contributor under such license. The text of
* such license is available at www.eclipse.org.
******************************************************************************/
package org.eclipse.buckminster.test.junit;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.net.MalformedURLException;
import java.net.URL;
import java.util.Enumeration;
import java.util.HashSet;
import java.util.LinkedList;
import java.util.List;
import java.util.Set;
import org.eclipse.buckminster.cmdline.AbstractCommand;
import org.eclipse.buckminster.cmdline.Option;
import org.eclipse.buckminster.cmdline.OptionDescriptor;
import org.eclipse.buckminster.cmdline.OptionValueType;
import org.eclipse.buckminster.cmdline.UsageException;
import org.eclipse.buckminster.runtime.MonitorUtils;
import org.eclipse.buckminster.sax.Utils;
import org.eclipse.buckminster.test.junit.TestCommand.TestLocationResolver.TestSuiteDescriptor;
import org.eclipse.buckminster.test.junit.TestRunner.TestSuiteResult;
import org.eclipse.core.runtime.IProgressMonitor;
import org.osgi.framework.Bundle;
import org.osgi.service.packageadmin.PackageAdmin;
import org.xml.sax.ContentHandler;
import org.xml.sax.SAXException;
/**
* <p>
* JUnit testing support for Buckminster.
* </p>
* <p>
* The class implements a "test" command. The command adds JUnit plug-in testing support to Buckminster.
* </p>
* <p>
* For usage instruction issue:
* <ul>
* <code>buckminster test -?</code>
* </ul>
* </p>
*
* @author Michal R��i�ka
*/
public class TestCommand extends AbstractCommand
{
/**
* <p>
* A test location resolver helper class.
* </p>
*/
protected static class TestLocationResolver
{
@SuppressWarnings("serial")
public static class ResolutionException extends Exception
{
public ResolutionException(Throwable cause)
{
super(cause);
}
}
/**
* <p>
* The resolved test description.
* </p>
*/
public interface TestSuiteDescriptor
{
Bundle getSuiteBundle() throws ResolutionException;
Class<?> getSuiteClass() throws ResolutionException;
String getSuiteName();
}
protected static class ResolvedTestSuiteDescriptor implements TestSuiteDescriptor
{
private Bundle m_suiteBundle;
private Class<?> m_suiteClass;
public ResolvedTestSuiteDescriptor(Bundle suiteBundle, Class<?> suiteClass)
{
m_suiteBundle = suiteBundle;
m_suiteClass = suiteClass;
}
public Bundle getSuiteBundle()
{
return m_suiteBundle;
}
public Class<?> getSuiteClass()
{
return m_suiteClass;
}
public String getSuiteName()
{
return getBundleResourceName(m_suiteBundle, m_suiteClass.getName());
}
}
protected static class UnresolvedTestSuiteDescriptor implements TestSuiteDescriptor
{
private String m_suiteName;
private Throwable m_cause;
public UnresolvedTestSuiteDescriptor(String suiteName, Throwable resolutionException)
{
m_suiteName = suiteName;
m_cause = resolutionException;
m_cause.fillInStackTrace();
}
public Bundle getSuiteBundle() throws ResolutionException
{
throw new ResolutionException(m_cause);
}
public Class<?> getSuiteClass() throws ResolutionException
{
throw new ResolutionException(m_cause);
}
public String getSuiteName()
{
return m_suiteName;
}
}
public static final String RUNTIME_LOCATION = "runtime";
public static final String DEAFULT_TEST_LIST_RESOURCE = "/plugin-tests.lst";
private static TestPlugin s_testPlugin = TestPlugin.getDefault();
/**
* <p>
* Checks if the supplied <code>name</code> is a valid bundle name. It is identical in function to
* {@link org.eclipse.pde.internal.core.util.IdUtil#isValidCompositeID(String name)}
* </p>
*
* @param name
* the name to check
* @return <code>true</code> if the supplied <code>name</code> is a valid bundle name, <code>false</code>
* otherwise
* @see org.eclipse.pde.internal.core.util.IdUtil#isValidCompositeID(String name)
*/
public static boolean isBundleName(String name)
{
if(name.length() == 0)
{
return false;
}
int cp = name.codePointAt(0);
if(!(cp >= 'A' && cp <= 'Z' || cp >= 'a' && cp <= 'z' || cp >= '0' && cp <= '9' || cp == '_'))
{
return false;
}
int i = Character.charCount(cp);
if(i >= name.length())
{
return true;
}
for(;;)
{
cp = name.codePointAt(i);
if(cp >= 'A' && cp <= 'Z' || cp >= 'a' && cp <= 'z' || cp >= '0' && cp <= '9' || cp == '_')
{
i += Character.charCount(cp);
if(i < name.length())
{
continue;
}
return true;
}
if(cp == '.')
{
i += Character.charCount(cp);
if(i < name.length())
{
continue;
}
}
return false;
}
}
/**
* <p>
* Checks if the supplied <code>name</code> is a valid fully qualified Java class name i.e. a sequence of one or
* more valid Java identifiers separated by dots ("<code>.</code>").
* </p>
*
* @param name
* the name to check
* @return <code>true</code> if the supplied <code>name</code> is a valid fully qualified Java class name,
* <code>false</code> otherwise
* @see (a method introduced Java 6) javax.lang.model.SourceVersion.isIdentifier(CharSequence name)
*/
public static boolean isQualifiedIdentifier(String name)
{
int cp;
for(int i = 0;; i += Character.charCount(cp))
{
if(i >= name.length())
{
return false;
}
cp = name.codePointAt(i);
if(!Character.isJavaIdentifierStart(cp))
{
return false;
}
for(;;)
{
i += Character.charCount(cp);
if(i >= name.length())
{
return true;
}
cp = name.codePointAt(i);
if(cp == '.')
{
break;
}
if(!Character.isJavaIdentifierPart(cp))
{
return false;
}
}
}
}
private static String getBundleResourceName(Bundle bundle, String resource)
{
return bundle.getSymbolicName() + ':' + resource;
}
@SuppressWarnings("unchecked")
private static Enumeration<URL> getBundleResources(Bundle bundle, String resource) throws IOException
{
return bundle.getResources(resource);
}
private PackageAdmin m_packageAdmin = s_testPlugin.getPackageAdmin();
private Set<URL> m_seenURLs = new HashSet<URL>();
private List<TestSuiteDescriptor> m_resolvedSuites;
/**
* <p>
* The sole constructor and the only publicly accessible (non static) member of this class. The resolution
* process is started immediately upon instantiation of this class. Once the resolution is done the instance is
* not useful any more. The resolved tests are stored in the supplied <code>resolvedSuites<code> list.
* </p>
*
* @param locations
* the test locations to resolve
* @param monitor
* resolution progress monitor
* @param resolvedSuites
* the list where to store the resolved tests
*/
public TestLocationResolver(String[] locations, IProgressMonitor monitor,
List<TestSuiteDescriptor> resolvedSuites)
{
monitor.beginTask("Resolving test suites", locations.length + 1);
m_resolvedSuites = resolvedSuites;
monitor.worked(1);
for(String location : locations)
{
if(monitor.isCanceled())
{
return;
}
resolveLocation(location, null);
monitor.worked(1);
}
monitor.done();
}
/**
* <p>
* Resolves (possibly recursively) the supplied <code>location</code> assuming the <code>parent</code> bundle to
* contain locations which don't have their containing bundle explicitly specified.
* </p>
*
* @param location
* the location to resolve
* @param parent
* a bundle to be assumed to contain the resources which don't have their containing bundle
* explicitly specified
*/
private void resolveLocation(String location, Bundle parent)
{
if(!location.startsWith(RUNTIME_LOCATION + ':'))
{
// try to handle the location specification as an URL first
try
{
resolveLocationList(new URL(location), null);
return;
}
catch(MalformedURLException e)
{
// fall through
}
catch(IOException e)
{
m_resolvedSuites.add(new UnresolvedTestSuiteDescriptor(location, e));
return;
}
}
else
{
location = location.substring(RUNTIME_LOCATION.length() + 1);
}
// handle the location specification as a runtime reference
Bundle bundle;
String resource;
{
int pos = location.indexOf(':');
String bundleName;
if(pos > 0 && isBundleName(bundleName = location.substring(0, pos)))
{
Bundle[] bundles = m_packageAdmin.getBundles(bundleName, null);
if(bundles == null)
{
m_resolvedSuites.add(new UnresolvedTestSuiteDescriptor(location, new RuntimeException(
"No such bundle: " + bundleName)));
return;
}
bundle = bundles[0];
resource = location.substring(pos + 1);
}
else if(parent == null)
{
m_resolvedSuites.add(new UnresolvedTestSuiteDescriptor(location, new RuntimeException(
"Illegal or no bundle specified")));
return;
}
else
{
bundle = parent;
resource = (pos == 0)
? location.substring(1)
: location;
}
}
if(isQualifiedIdentifier(resource))
{
// try to handle the resource as a class name (if it is a legal class name)
try
{
m_resolvedSuites.add(new ResolvedTestSuiteDescriptor(bundle, bundle.loadClass(resource)));
}
catch(Throwable t)
{
m_resolvedSuites.add(new UnresolvedTestSuiteDescriptor(getBundleResourceName(bundle, resource), t));
}
}
else
{
Enumeration<URL> resourceEnumerator;
if(resource.length() == 0)
{
resource = DEAFULT_TEST_LIST_RESOURCE;
}
try
{
resourceEnumerator = getBundleResources(bundle, resource);
}
catch(Throwable t)
{
m_resolvedSuites.add(new UnresolvedTestSuiteDescriptor(getBundleResourceName(bundle, resource), t));
return;
}
if(resourceEnumerator == null)
{
m_resolvedSuites.add(new UnresolvedTestSuiteDescriptor(getBundleResourceName(bundle, resource),
new RuntimeException("No such resource: " + resource)));
return;
}
while(resourceEnumerator.hasMoreElements())
{
URL resourceURL = resourceEnumerator.nextElement();
try
{
resolveLocationList(resourceURL, bundle);
}
catch(IOException e)
{
m_resolvedSuites.add(new UnresolvedTestSuiteDescriptor(getBundleResourceName(bundle, resource),
new RuntimeException("Error accessing " + resourceURL.toString(), e)));
}
}
}
}
/**
* <p>
* Resolves test locations contained in the target of the supplied <code>locationList</code> URL. The URL target
* is assumed to contain newline separated list of test locations. Assumes the <code>parent</code> bundle to
* contain locations which don't have their containing bundle explicitly specified.
* </p>
*
* @param locationList
* the URL pointing to the resource to process
* @param parent
* a bundle to be assumed to contain the resources which don't have their containing bundle
* explicitly specified
*/
private void resolveLocationList(URL locationList, Bundle parent) throws IOException
{
// prevent recursion
if(m_seenURLs.contains(locationList))
{
return;
}
m_seenURLs.add(locationList);
BufferedReader reader = new BufferedReader(new InputStreamReader(locationList.openStream()));
String location;
while((location = reader.readLine()) != null)
{
resolveLocation(location, parent);
}
reader.close();
}
}
private static final OptionDescriptor RESULT_PREFIX = new OptionDescriptor('p', "prefix", OptionValueType.REQUIRED);
private static final OptionDescriptor RESULT_DIRECTORY = new OptionDescriptor('d', "directory",
OptionValueType.REQUIRED);
private static final OptionDescriptor QUIET_FLAG = new OptionDescriptor('q', "quiet", OptionValueType.NONE);
private boolean m_quietFlag;
private String[] m_locations;
private File m_resultDirectory;
private String m_resultPrefix;
@SuppressWarnings("unchecked")
@Override
protected void getOptionDescriptors(List appendHere) throws Exception
{
super.getOptionDescriptors(appendHere);
appendHere.add(RESULT_PREFIX);
appendHere.add(RESULT_DIRECTORY);
appendHere.add(QUIET_FLAG);
}
@Override
protected void handleOption(Option option) throws Exception
{
if(option.is(RESULT_PREFIX))
{
m_resultPrefix = option.getValue();
}
else if(option.is(RESULT_DIRECTORY))
{
m_resultDirectory = new File(option.getValue());
}
else if(option.is(QUIET_FLAG))
{
m_quietFlag = true;
}
else
{
super.handleOption(option);
}
}
@Override
protected void handleUnparsed(String[] unparsed) throws Exception
{
m_locations = unparsed;
}
/**
* <p>
* The implementation of the <code>test</code> command for Buckminster. For more info see its usage instructions by
* issuing:
* <ul>
* <code>buckminster test -?</code>
* </ul>
* </p>
*/
@Override
protected int run(IProgressMonitor monitor) throws Exception
{
int len = m_locations.length;
if(len == 0)
{
throw new UsageException("No testsuites specified.");
}
monitor.beginTask("Buckminster JUnit testing", 10000);
List<TestSuiteDescriptor> resolvedLocations = new LinkedList<TestSuiteDescriptor>();
new TestLocationResolver(m_locations, MonitorUtils.subMonitor(monitor, 1000), resolvedLocations);
File reportFile = getReportFile();
ContentHandler report;
IProgressMonitor submonitor = MonitorUtils.subMonitor(monitor, 9000);
submonitor.beginTask("Executing test suites", resolvedLocations.size());
try
{
report = Utils.newSerializer(reportFile, new FileOutputStream(reportFile), "UTF-8", -1, false);
}
catch(FileNotFoundException e)
{
throw new Exception("Failed to create report file: " + reportFile.getPath(), e);
}
catch(SAXException e)
{
throw new Exception("Failed to create XML serializer of the test results", e);
}
TestSuiteResult.startReport(report);
{
TestRunner runner = new TestRunner();
TestSuiteResult result;
for(TestSuiteDescriptor testSuite : resolvedLocations)
{
try
{
submonitor.subTask(testSuite.getSuiteName());
if(!m_quietFlag)
{
System.out.println("Running test suite: " + testSuite.getSuiteName());
}
result = runner.run(testSuite, submonitor);
try
{
result.toSax(report, null, null, result.getDefaultTag());
}
catch(SAXException e)
{
throw new Exception("Failed to serialize test result", e);
}
}
finally
{
submonitor.subTask(null);
submonitor.worked(1);
if(submonitor.isCanceled())
{
break;
}
}
}
}
TestSuiteResult.endReport(report);
submonitor.done();
monitor.done();
return 0;
}
private File getReportFile()
{
String fileName = (m_resultPrefix != null
? m_resultPrefix
: "result") + '-' + System.currentTimeMillis() + ".xml";
return m_resultDirectory != null
? new File(m_resultDirectory, fileName)
: new File(fileName);
}
}