package org.xtest.junit; import java.io.BufferedReader; import java.io.FileNotFoundException; import java.io.FileReader; import java.io.IOException; import java.text.DecimalFormat; import java.util.HashSet; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import org.eclipse.emf.common.util.URI; import org.eclipse.emf.ecore.resource.ResourceSet; import org.eclipse.xtext.diagnostics.Severity; import org.eclipse.xtext.resource.XtextResourceSet; import org.eclipse.xtext.validation.Issue; import org.junit.runner.Description; import org.junit.runner.Runner; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; import org.xtest.Xtest; import org.xtest.results.XTestResult; import org.xtest.results.XTestState; import org.xtest.xTest.Body; import com.google.common.collect.Iterables; import com.google.common.collect.Sets; import com.google.inject.Injector; /** * Custom Junit runner to report results of invoking an Xtest file * * @author Michael Barry */ public class XtestJunitRunner extends Runner { private static double cumulative = 0.0; /** * Returns the cumulative time spent running xtest files throughout the lifetime of this class * * @return The cumulative time spent running xtest files */ public static double getCumulative() { return cumulative; } private final Class<?> clazz; private final String file; private final Injector injector; private final Set<String> names = Sets.newHashSet(); private Throwable preEvalException = null; private XTestResult run; private Description top; /** * Constructor for Junit * * @param clazz * The Xtest-invoking test class */ public XtestJunitRunner(Class<?> clazz) { file = getXtestFile(clazz); injector = getInjector(clazz); this.clazz = clazz; } @Override public Description getDescription() { // TODO link descriptions and exceptions to actual files if (top == null) { top = Description.createSuiteDescription(clazz); try { // TODO can run() dynamically add descriptions as it runs the tests? If so move // running out of this step run = runTest(); if (run.getErrorMessages().isEmpty()) { mark(run, top); } else { preEvalException = exceptionForSyntaxErrors(run.getErrorMessages()); } } catch (FileNotFoundException e) { preEvalException = e; } catch (IOException e) { preEvalException = e; } } return top; } @Override public void run(RunNotifier notifier) { if (preEvalException != null) { notifier.fireTestStarted(top); notifier.fireTestFailure(new Failure(top, preEvalException)); } else { markTest(notifier, run, top); } } private Description createDescription(XTestResult result) { String name = uniqueName(result.getName()); return result.getSubTests().isEmpty() ? Description.createTestDescription(clazz, name) : Description.createSuiteDescription(name); } private Throwable exceptionForSyntaxErrors(List<String> errorMessages) { StringBuilder builder = new StringBuilder("Validation errors occurred before running test:"); for (String message : errorMessages) { builder.append('\n'); builder.append(message); } Throwable result = new SyntaxError(builder.toString()); return result; } private Injector getInjector(Class<?> clazz) { RunsXtest annotation = clazz.getAnnotation(RunsXtest.class); Injector injector = null; if (annotation != null) { Class<? extends InjectorProvider> injectorClass = annotation.injector(); InjectorProvider newInstance; try { newInstance = injectorClass.newInstance(); injector = newInstance.getInjector(); } catch (InstantiationException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } return injector; } private Set<Class<?>> getRunnerDependencies(Class<?> xtestRunner) { DependsOnXtest annotation = xtestRunner.getAnnotation(DependsOnXtest.class); Set<Class<?>> classes = Sets.newHashSet(); if (annotation != null) { Class<?>[] value = annotation.value(); classes = Sets.newHashSet(value); } return classes; } private String getXtestFile(Class<?> clazz) { RunsXtest annotation = clazz.getAnnotation(RunsXtest.class); return annotation == null ? null : annotation.value(); } private void mark(XTestResult run2, Description top) { List<XTestResult> subTests = run2.getSubTests(); for (XTestResult result : subTests) { Description next = createDescription(result); top.addChild(next); mark(result, next); } } private void markTest(RunNotifier notifier, XTestResult run2, Description top2) { notifier.fireTestStarted(top2); for (int i = 0; i < run2.getSubTests().size() && i < top2.getChildren().size(); i++) { markTest(notifier, run2.getSubTests().get(i), top2.getChildren().get(i)); } if (!run2.getEvaluationException().isEmpty()) { Throwable first = Iterables.getFirst(run2.getEvaluationException(), null).getCause(); notifier.fireTestFailure(new Failure(top2, first)); } else { notifier.fireTestFinished(top2); } } private void parseDependencies(Class<?> xtestRunner, ResourceSet set, Set<Class<?>> visited) throws FileNotFoundException, IOException { if (!visited.contains(xtestRunner)) { Set<Class<?>> classes = getRunnerDependencies(xtestRunner); for (Class<?> dependency : classes) { String file = getXtestFile(dependency); if (file != null) { String fileDependedOn = readFile(file); try { Xtest.parse(fileDependedOn, URI.createURI(file), set, injector); } catch (Exception e) { } } parseDependencies(dependency, set, visited); } visited.add(xtestRunner); } } private String readFile(String file) throws FileNotFoundException, IOException { BufferedReader in = new BufferedReader(new FileReader(file)); StringBuilder builder = new StringBuilder(); try { String line; while ((line = in.readLine()) != null) { if (builder.length() > 0) { builder.append('\n'); } builder.append(line); } } finally { in.close(); } return builder.toString(); } private XTestResult runTest() throws FileNotFoundException, IOException { String fileBeingRun = readFile(file); ResourceSet set = injector.getInstance(XtextResourceSet.class); Class<?> claz = clazz; HashSet<Class<?>> visited = Sets.newHashSet(); parseDependencies(claz, set, visited); // profiling stats long start = System.nanoTime(); XTestResult run = null; try { Body parse = Xtest.parse(fileBeingRun, URI.createURI(file), set, injector); run = new XTestResult(parse); List<Issue> validate = Xtest.validate(parse, injector); for (Issue issue : validate) { if (issue.getSeverity() == Severity.ERROR) { run.addSyntaxError(issue.getLineNumber() + ": " + issue.getMessage()); } } if (run.getState() != XTestState.FAIL) { run = Xtest.run(parse, injector); } } catch (Exception e) { } long end = System.nanoTime(); double d = (end - start) / (double) TimeUnit.NANOSECONDS.convert(1, TimeUnit.SECONDS); cumulative += d; DecimalFormat decimalFormat = new DecimalFormat("#.###"); System.out.println(decimalFormat.format(d) + "s " + file.replaceAll("^.*/", "")); return run; } private String uniqueName(String name) { if (names.contains(name)) { String newName; int i = 2; do { newName = name + "-" + i++; } while (names.contains(newName)); name = newName; } names.add(name); return name; } /** * Dummy exception for wrapping an Xtest file syntax error to point Junit to * * @author Michael Barry */ private static class SyntaxError extends Exception { /** * Serial version UID */ private static final long serialVersionUID = 7636482455156418516L; public SyntaxError(String string) { super(string); setStackTrace(new StackTraceElement[0]); } } }