package org.xtest.runner;
import java.net.URI;
import java.util.concurrent.TimeUnit;
import org.apache.log4j.Logger;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.core.runtime.QualifiedName;
import org.eclipse.core.runtime.SubMonitor;
import org.xtest.runner.external.DependencyAcceptor;
import org.xtest.runner.external.ITestRunner;
import org.xtest.runner.external.ITestType;
import org.xtest.runner.external.TestResult;
import org.xtest.runner.external.TestState;
import org.xtest.runner.util.SerializationUtils;
import org.xtest.runner.util.URIUtil;
import com.google.common.base.Objects;
import com.google.common.base.Optional;
import com.google.common.cache.Cache;
import com.google.common.collect.ComparisonChain;
import com.google.common.hash.BloomFilter;
import com.google.common.hash.Funnels;
/**
* Wrapper for a test file supported by a client contribution to the testType extension point
*
* The lifetime of this class is from workspace change to test run, and it is discarded after the
* test is run.
*
* @author Michael Barry
*/
public class RunnableTest implements Comparable<RunnableTest> {
private static final QualifiedName affectedByKey = new QualifiedName("org.xtest", "xaffectedBy");
private static final QualifiedName durationKey = new QualifiedName("org.xtest", "xduration");
private static final Logger logger = Logger.getLogger(RunnableTest.class);
private static final QualifiedName numAffectedByKey = new QualifiedName("org.xtest",
"xnumAffectedBy");
private static final QualifiedName pendingKey = new QualifiedName("org.xtest", "pending");
private static final QualifiedName resultKey = new QualifiedName("org.xtest", "xresult");
private final Optional<BloomFilter<String>> dependencies;
private final Optional<Long> duration;
private final IFile fFile;
private final ITestType fTestType;
private final Optional<Long> numDependencies;
private final Optional<String> pending;
private final TestResult result;
/**
* Construct a new runnable test for the test file and test type provided
*
* @param file
* The file with tests to run
* @param testType
* The contribution to the testType extension point that can handle running tests in
* this file
*/
public RunnableTest(IFile file, ITestType testType) {
fFile = file;
fTestType = testType;
dependencies = SerializationUtils.fromString(get(affectedByKey));
duration = getLong(durationKey);
numDependencies = getLong(numAffectedByKey);
Optional<String> optional = get(resultKey);
result = SerializationUtils.<TestResult> fromString(optional).or(TestResult.notRun());
pending = get(pendingKey);
}
/**
* Clear test results for this test
*/
public void clean() {
clear(resultKey);
}
@Override
public int compareTo(RunnableTest o) {
// Run failures, then not run, then passes
// Then sort by fastest first
return ComparisonChain.start().compare(result.getOrder(), o.result.getOrder())
.compare(duration.or(0L), o.duration.or(0L)).result();
}
@Override
public boolean equals(Object obj) {
if (this == obj) {
return true;
}
if (obj == null) {
return false;
}
if (getClass() != obj.getClass()) {
return false;
}
RunnableTest other = (RunnableTest) obj;
return Objects.equal(fFile, other.fFile);
}
/**
* Returns the {@link IFile} that this wraps
*
* @return The {@link IFile} that this wraps
*/
public IFile getFile() {
return fFile;
}
/**
* Returns the name of this test file
*
* @return The name of this test file
*/
public String getName() {
String name = fFile.getName();
return name;
}
/**
* Returns the state of this test
*
* @return pass, fail, or not run;
*/
public TestResult getState() {
return result;
}
@Override
public int hashCode() {
return Objects.hashCode(fFile);
}
/**
* Returns true if this test has run before, false if not
*
* @return true if this test has run before, false if not
*/
public boolean hasRun() {
return result.getState() != TestState.NOT_RUN;
}
/**
* Runs the test
*
* @param convert
* Progress monitor
* @param runnerCache
* {@link Cache} of runners for each test type
* @return The result of the test
*/
public TestResult invoke(SubMonitor convert, Cache<ITestType, ITestRunner> runnerCache) {
TestResult result = TestResult.notRun();
if (fFile.exists()) {
logger.debug("Start " + getName());
long start = System.nanoTime();
Acceptor acceptor = new Acceptor(dependencies, numDependencies);
ITestRunner testRunner = runnerCache.getUnchecked(fTestType);
result = testRunner.run(fFile, convert, acceptor);
long end = System.nanoTime();
if (logger.isDebugEnabled()) {
logger.debug("Finish " + getName() + " " + acceptor.dependencies + " dependencies "
+ TimeUnit.MILLISECONDS.convert(end - start, TimeUnit.NANOSECONDS) + " ms");
}
storeResultsPersistently(result, acceptor, end - start);
} else {
logger.info("Tried to run " + getName() + " but it didn't exist");
}
return result;
}
/**
* Returns true if this test file is affected by the file provided
*
* @param delta
* The file that has changed
* @return True if {@code delta} is a dependency of this test
*/
public boolean isAffectedBy(IFile delta) {
boolean result = false;
if (delta.equals(fFile)) {
// Changes to a test affect that test
result = true;
} else if (dependencies.isPresent()) {
URI uri = URIUtil.getURIForFile(delta);
String name = URIUtil.getStringFromURI(uri);
if (name != null) {
BloomFilter<String> bloomFilter = dependencies.get();
result = bloomFilter.mightContain(name);
}
}
return result;
}
/**
* Returns true if this test has been placed on a test-run queue but not run yet
*
* @return true if this test has been placed on a test-run queue but not run yet, false if not
*/
public boolean isPending() {
return pending.isPresent();
}
/**
* Persistently mark that this test file has been scheduled
*/
public void setPending() {
put(pendingKey, Optional.of(""));
}
@Override
public String toString() {
return getName();
}
private void clear(QualifiedName pendingkey2) {
put(pendingkey2, Optional.<String> absent());
}
private Optional<String> get(QualifiedName key) {
Optional<String> result = Optional.absent();
try {
String persistentProperty = fFile.getPersistentProperty(key);
result = Optional.fromNullable(persistentProperty);
} catch (CoreException e) {
}
return result;
}
private Optional<Long> getLong(QualifiedName key) {
Optional<String> durationString = get(key);
Optional<Long> result;
result = SerializationUtils.fromString(durationString);
return result;
}
private void put(QualifiedName key, Optional<String> string) {
try {
// null clears property
fFile.setPersistentProperty(key, string.orNull());
} catch (CoreException e) {
}
}
private void setDependencies(BloomFilter<? super String> filter) {
Optional<String> string = SerializationUtils.toString(filter);
put(affectedByKey, string);
}
private void storeResultsPersistently(TestResult result, Acceptor dependencies, long duration) {
setDependencies(dependencies.filter);
put(durationKey, SerializationUtils.toString(duration));
put(numAffectedByKey, SerializationUtils.toString(dependencies.dependencies));
put(resultKey, SerializationUtils.toString(result));
clear(pendingKey);
}
private static class Acceptor implements DependencyAcceptor {
private long dependencies = 0L;
private final BloomFilter<? super String> filter;
public Acceptor(Optional<BloomFilter<String>> other, Optional<Long> numExpected) {
dependencies = 0L;
int expectedInsertions = (int) (numExpected.isPresent() ? numExpected.get() : 100);
filter = BloomFilter.create(Funnels.stringFunnel(), Math.max(100, expectedInsertions));
}
@Override
public void accept(URI dependency) {
String value = URIUtil.getStringFromURI(dependency);
if (dependency != null && !filter.mightContain(value)) {
dependencies++;
filter.put(value);
}
}
}
}