package org.xtest.interpreter; import java.lang.reflect.Array; import java.lang.reflect.Field; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.Stack; import java.util.concurrent.Callable; import org.eclipse.emf.common.util.WrappedException; import org.eclipse.emf.ecore.EObject; import org.eclipse.xtend.core.xtend.XtendFunction; import org.eclipse.xtend.core.xtend.XtendParameter; import org.eclipse.xtext.common.types.JvmDeclaredType; import org.eclipse.xtext.common.types.JvmExecutable; import org.eclipse.xtext.common.types.JvmOperation; import org.eclipse.xtext.common.types.JvmParameterizedTypeReference; import org.eclipse.xtext.common.types.JvmType; import org.eclipse.xtext.common.types.JvmTypeParameter; import org.eclipse.xtext.common.types.JvmTypeReference; import org.eclipse.xtext.common.types.JvmVoid; import org.eclipse.xtext.common.types.TypesFactory; import org.eclipse.xtext.common.types.access.impl.ClassFinder; import org.eclipse.xtext.common.types.impl.JvmTypeParameterImplCustom; import org.eclipse.xtext.common.types.util.RawTypeHelper; import org.eclipse.xtext.naming.QualifiedName; import org.eclipse.xtext.util.CancelIndicator; import org.eclipse.xtext.xbase.XAssignment; import org.eclipse.xtext.xbase.XClosure; import org.eclipse.xtext.xbase.XExpression; import org.eclipse.xtext.xbase.XFeatureCall; import org.eclipse.xtext.xbase.XReturnExpression; import org.eclipse.xtext.xbase.interpreter.IEvaluationContext; import org.eclipse.xtext.xbase.interpreter.IEvaluationResult; import org.eclipse.xtext.xbase.interpreter.impl.DefaultEvaluationResult; import org.eclipse.xtext.xbase.interpreter.impl.EvaluationException; import org.eclipse.xtext.xbase.interpreter.impl.InterpreterCanceledException; import org.eclipse.xtext.xbase.interpreter.impl.XbaseInterpreter; import org.xtest.XTestAssertionFailure; import org.xtest.XTestEvaluationException; import org.xtest.XtestUtil; import org.xtest.jvmmodel.XtestJvmModelAssociator; import org.xtest.results.XTestResult; import org.xtest.results.XTestState; import org.xtest.xTest.Body; import org.xtest.xTest.UniqueName; import org.xtest.xTest.XAssertExpression; import org.xtest.xTest.XMethodDef; import org.xtest.xTest.XMethodDefExpression; import org.xtest.xTest.XTestExpression; import com.google.common.collect.Lists; import com.google.common.collect.Maps; import com.google.common.collect.Sets; import com.google.inject.Inject; /** * Xtest interpreter, inherits behavior from Xbase, but adds handling for running tests and keeps * track of the tests being run, returning the final root test * * @author Michael Barry */ @SuppressWarnings("restriction") public class XTestInterpreter extends XbaseInterpreter { private Map<XExpression, Object> assertionDiagnostics = null; @Inject private XtestJvmModelAssociator assocations; private final Stack<XExpression> callStack = new Stack<XExpression>(); private ClassLoader classLoader; // TODO move some of this stuff into custom context private final Set<XExpression> executedExpressions = Sets.newHashSet(); private final Map<XtendFunction, IEvaluationContext> localMethodContexts = Maps.newHashMap(); @Inject private AssertionMessageBuilder messageBuilder; @Inject private RawTypeHelper rawTypeHelper; private XTestResult result; private StackTraceElement[] startTrace = null; private final Stack<XTestResult> testStack = new Stack<XTestResult>(); @Inject private TypesFactory typesFactory; @Override public XtestEvaluationResult evaluate(XExpression expression, IEvaluationContext context, CancelIndicator indicator) { boolean isTopLevel = false; try { IEvaluationResult evaluate; if (expression.eContainer() instanceof XClosure || expression.eContainer() instanceof XMethodDef) { evaluate = evaluateInsideOfClosure(expression, context, indicator); } else { isTopLevel = true; startTrace = Thread.currentThread().getStackTrace(); evaluate = super.evaluate(expression, context, indicator); } HashSet<XExpression> copy = Sets.newHashSet(executedExpressions); XtestEvaluationResult toReturn = new XtestEvaluationResult(evaluate, copy, result); return toReturn; } catch (ReturnValue e) { HashSet<XExpression> copy2 = Sets.newHashSet(executedExpressions); DefaultEvaluationResult other = new DefaultEvaluationResult(e.returnValue, null); XtestEvaluationResult toReturn = new XtestEvaluationResult(other, copy2, result); return toReturn; } finally { // release references held by this object if (isTopLevel) { callStack.clear(); executedExpressions.clear(); localMethodContexts.clear(); result = null; startTrace = null; assertionDiagnostics = null; testStack.clear(); } } } /** * Returns this interpreters classloader * * @return The classloader for this interpreter */ public ClassLoader getClassLoader() { return classLoader; } @Override @Inject public void setClassLoader(ClassLoader classLoader) { this.classLoader = classLoader; super.setClassLoader(classLoader); // HACK to work avoid duplicating all of XbaseInterpreter to get at classFinder since it is // private not protected ClassFinder finder = new ClassFinder(classLoader) { @Override public Class<?> forName(String name) throws ClassNotFoundException { String erasedType = calculateTypeParemeterErasure(name); return super.forName(erasedType); } }; Field declaredField; try { declaredField = XbaseInterpreter.class.getDeclaredField("classFinder"); declaredField.setAccessible(true); declaredField.set(this, finder); } catch (SecurityException e) { e.printStackTrace(); } catch (NoSuchFieldException e) { e.printStackTrace(); } catch (IllegalArgumentException e) { e.printStackTrace(); } catch (IllegalAccessException e) { e.printStackTrace(); } } /** * Evaluates an assert expression. Throws an {@link XTestAssertionFailure} if the assert does * not succeed * * @param assertExpression * The expression to evaluate * @param context * The evaluation context * @param indicator * The cancel indicator * @return null */ protected Object _evaluateAssertExpression(XAssertExpression assertExpression, IEvaluationContext context, CancelIndicator indicator) { boolean wasFirst = assertionDiagnostics == null; if (wasFirst) { assertionDiagnostics = Maps.newHashMap(); } XExpression resultExp = assertExpression.getActual(); JvmTypeReference expected = assertExpression.getThrows(); boolean returnVal = true; if (expected == null) { // normal assert try { Object result = internalEvaluate(resultExp, context, indicator); if (!(result instanceof Boolean) || !(Boolean) result) { handleAssertionFailure(assertExpression); returnVal = false; } } finally { if (wasFirst) { assertionDiagnostics = null; } } } else { // assert exception String qualifiedName = expected.getType().getQualifiedName(); try { Class<?> expectedExceptionClass = getClassFinder().forName(qualifiedName); try { internalEvaluate(resultExp, context, indicator); fail("Expected <" + expectedExceptionClass.getName() + "> but no exception was thrown"); } catch (XTestEvaluationException exception) { Throwable throwable = exception.getCause(); if (!expectedExceptionClass.isInstance(throwable)) { fail("Expected <" + expectedExceptionClass.getName() + "> but threw <" + throwable.getClass().getName() + "> instead"); } } } catch (ClassNotFoundException e) { throw new WrappedException(e); } } return returnVal; } @Override protected Object _evaluateAssignment(XAssignment assignment, IEvaluationContext context, CancelIndicator indicator) { if (assignment.getAssignable() instanceof XFeatureCall && assignment.getFeature() == null) { assignment.setFeature(((XFeatureCall) assignment.getAssignable()).getFeature()); } return super._evaluateAssignment(assignment, context, indicator); } /** * Evaluates the xtest script body. Catches any exceptions thrown. * * @param main * The main body to evaluate * @param context * The evaluation context * @param indicator * The cancel indicator * @return null */ protected Object _evaluateBody(Body main, IEvaluationContext context, CancelIndicator indicator) { if (result == null) { // In case this is called outside of the context of XtestDiagnostician result = new XTestResult(main); } testStack.push(result); Object toReturn = null; try { toReturn = super._evaluateBlockExpression(main, context, indicator); if (result.getState() != XTestState.FAIL) { result.pass(); } } catch (ReturnValue e) { toReturn = e.returnValue; result.pass(); } catch (XTestEvaluationException e) { result.addEvaluationException(e); } finally { testStack.pop(); } return toReturn; } /** * Evaluate a method definition. Store its context if local and return null (since methods are * void) * * @param method * The method * @param context * The context * @param indicator * The cancel indicator * @return Null */ protected Object _evaluateMethodDef(XMethodDefExpression method, IEvaluationContext context, CancelIndicator indicator) { if (!method.getMethod().isStatic()) { localMethodContexts.put(method.getMethod(), context); } return null; } @Override protected Object _evaluateReturnExpression(XReturnExpression returnExpr, IEvaluationContext context, CancelIndicator indicator) { // Need to reimplement xbase's return expression since I need to catch all exceptions and // handle return exception but I don't have access to package-protected ReturnValue Object returnValue = internalEvaluate(returnExpr.getExpression(), context, indicator); throw new ReturnValue(returnValue); } /** * Evaluates the test. Catches any evaluation exceptions thrown and adds them to the test. * * @param test * The test to evaluate * @param context * The evaluation context * @param indicator * The cancel indicator * @return null */ protected Object _evaluateTestExpression(XTestExpression test, IEvaluationContext context, CancelIndicator indicator) { UniqueName name = test.getName(); String nameStr = getName(name, test, context, indicator); XExpression expression = test.getExpression(); XTestResult peek = testStack.peek(); XTestResult subTest = peek.subTest(nameStr, test); testStack.push(subTest); try { internalEvaluate(expression, context, indicator); if (subTest.getState() != XTestState.FAIL) { subTest.pass(); } } catch (ReturnValue e) { subTest.pass(); } catch (XTestEvaluationException e) { subTest.addEvaluationException(e); } testStack.pop(); return null; } /** * Handles a feature call to retrieve the class of a class * * @param jvmVoid * Void because feature is null in this case * @param featureCall * The feature call expression * @param receiver * The receiver, null in this case * @param context * The context * @param indicator * The cancellation indicator * @return The class of the declared type from {@code featureCall} */ protected Object _featureCallVoid(JvmVoid jvmVoid, XFeatureCall featureCall, Object receiver, IEvaluationContext context, CancelIndicator indicator) { JvmDeclaredType declaringType = featureCall.getDeclaringType(); ClassFinder classFinder = getClassFinder(); Class<?> clazz = null; try { clazz = declaringType == null ? null : classFinder.forName(declaringType .getQualifiedName()); } catch (ClassNotFoundException e) { } return clazz; } @Override protected List<Object> evaluateArgumentExpressions(JvmExecutable executable, List<XExpression> expressions, IEvaluationContext context, CancelIndicator indicator) { // Same as XbaseInterpreter.evaluateArgumentExpressions() ... XMethodDef methodDef = assocations.getMethodDef(executable); List<Object> result; if (methodDef != null) { result = Lists.newArrayList(); int paramCount = executable.getParameters().size(); if (executable.isVarArgs()) { paramCount--; } for (int i = 0; i < paramCount; i++) { XExpression arg = expressions.get(i); Object argResult = internalEvaluate(arg, context, indicator); JvmTypeReference parameterType = executable.getParameters().get(i) .getParameterType(); Object argumentValue = coerceArgumentType(argResult, parameterType); result.add(argumentValue); } if (executable.isVarArgs()) { JvmTypeReference parameterType = methodDef.getParameters().get(paramCount) .getParameterType(); // ... except get the var-arg class from the XMethodDef JvmType type = parameterType.getType(); String typeName; if (type instanceof JvmTypeParameterImplCustom) { JvmTypeParameterImplCustom param = (JvmTypeParameterImplCustom) type; typeName = calculateTypeParemeterErasure(param); } else { typeName = type.getQualifiedName(); } Class<?> componentType; try { componentType = getClassFinder().forName(typeName); } catch (ClassNotFoundException e) { throw new WrappedException(e); } // end diff if (expressions.size() == executable.getParameters().size()) { XExpression arg = expressions.get(paramCount); Object lastArgResult = internalEvaluate(arg, context, indicator); if (componentType.isInstance(lastArgResult) && !lastArgResult.getClass().isArray()) { Object array = Array.newInstance(componentType, 1); Array.set(array, 0, lastArgResult); result.add(array); } else { result.add(lastArgResult); } } else { Object array = Array .newInstance(componentType, expressions.size() - paramCount); for (int i = paramCount; i < expressions.size(); i++) { XExpression arg = expressions.get(i); Object argValue = internalEvaluate(arg, context, indicator); Array.set(array, i - paramCount, argValue); } result.add(array); } } } else { result = super.evaluateArgumentExpressions(executable, expressions, context, indicator); } return result; } /** * Returns the Xtest expression call stack * * @return The Xtest expression call stack */ protected Stack<XExpression> getCallStack() { return callStack; } /* * Override default expression evaluator to wrap thrown exceptions with an xtest evaluation * exception wrapper that contains the expression that threw the exception */ @Override protected Object internalEvaluate(XExpression expression, IEvaluationContext context, CancelIndicator indicator) throws EvaluationException { Object internalEvaluate; executedExpressions.add(expression); if (assertionDiagnostics != null) { assertionDiagnostics.put(expression, null); } XExpression previous = null; if (!callStack.empty()) { previous = callStack.pop(); } callStack.push(expression); try { // replace top of stack with current expression internalEvaluate = super.internalEvaluate(expression, context, indicator); executedExpressions.add(expression); if (assertionDiagnostics != null) { assertionDiagnostics.put(expression, internalEvaluate); } } catch (ReturnValue value) { throw value; } catch (InterpreterCanceledException e) { throw e; } catch (Throwable e) { if (e instanceof XTestEvaluationException) { throw (RuntimeException) e; } else { Throwable cause = e; while (cause instanceof RuntimeException && !(cause instanceof XTestEvaluationException) && cause.getCause() != null) { cause = cause.getCause(); } if (cause instanceof XTestEvaluationException) { throw (RuntimeException) cause; } internalEvaluate = null; handleEvaluationException(cause, expression); } } finally { callStack.pop(); callStack.push(previous != null ? previous : expression); } return internalEvaluate; } @Override protected Object invokeOperation(JvmOperation operation, Object receiver, List<Object> argumentValues) { XMethodDef method = assocations.getMethodDef(operation); if (method != null) { return invokeXtestMethod(method, argumentValues); } else { return super.invokeOperation(operation, receiver, argumentValues); } } /** * Invokes a method declared in an Xtest file * * @param method * The method * @param argumentValues * The argument values * @return The result of invoking that operation */ protected Object invokeXtestMethod(XMethodDef method, List<Object> argumentValues) { IEvaluationContext context = localMethodContexts.get(method); if (context == null) { context = createContext(); } else { context = context.fork(); } if (argumentValues.size() != method.getParameters().size()) { throw new IllegalStateException("Number of arguments did not match. Expected: " + method.getParameters().size() + " but was: " + argumentValues.size()); } int i = 0; for (XtendParameter param : method.getParameters()) { Object value; value = argumentValues.get(i); context.newValue(QualifiedName.create(param.getName()), value); i++; } IEvaluationResult evaluate = evaluate(method.getExpression(), context, CancelIndicator.NullImpl); return evaluate.getResult(); } private String calculateTypeParemeterErasure(JvmTypeParameter param) { JvmParameterizedTypeReference demandCreated = typesFactory .createJvmParameterizedTypeReference(); demandCreated.setType(param); JvmTypeReference result = rawTypeHelper.getRawTypeReference(demandCreated, param.eResource()); return result.getType().getQualifiedName(); } private String calculateTypeParemeterErasure(String name) { for (EObject obj = callStack.peek(); obj != null; obj = obj.eContainer()) { if (obj instanceof XMethodDef) { XMethodDef def = (XMethodDef) obj; JvmOperation op = assocations.getJvmOperation(def); if (op != null) { for (JvmTypeParameter param : op.getTypeParameters()) { if (param.getQualifiedName().equals(name)) { String calculateTypeParemeterErasure = calculateTypeParemeterErasure(param); return calculateTypeParemeterErasure; } } } if (def.isStatic()) { return name; } } } return name; } private IEvaluationResult evaluateInsideOfClosure(final XExpression expression, final IEvaluationContext context, final CancelIndicator indicator) { // Same as super.internalEvaluate ... // push this layer onto the call stack callStack.push(expression); try { Object result = XtestUtil.runOnNewLevelOfXtestStack(new Callable<Object>() { @Override public Object call() throws Exception { return internalEvaluate(expression, context, indicator != null ? indicator : CancelIndicator.NullImpl); } }); return new DefaultEvaluationResult(result, null); } catch (ReturnValue e) { return new DefaultEvaluationResult(e.returnValue, null); } catch (XTestEvaluationException e) { // ... except throw Xtest evaluation exceptions to be handled by outer expression throw e; } catch (EvaluationException e) { return new DefaultEvaluationResult(null, e.getCause()); } catch (InterpreterCanceledException e) { return null; } catch (Exception e) { return new DefaultEvaluationResult(null, e); } finally { callStack.pop(); } } private void fail(String failure) { throw new XTestAssertionFailure(failure); } /** * Evaluates a {@link UniqueName} and returns the result * * @param uniqueName * The {@link UniqueName} object of the test * @param test * The test expression for if no name is given * @param context * The evaluation context * @param indicator * The cancel indicator * @return The name derived from uniqueName */ private String getName(UniqueName uniqueName, XTestExpression test, IEvaluationContext context, CancelIndicator indicator) { String name = uniqueName.getName(); XExpression uidExp = uniqueName.getIdentifier(); if (uidExp != null) { Object nameObj = internalEvaluate(uidExp, context, indicator); if (nameObj != null) { if (name == null) { // no ID specified name = nameObj.toString(); } else { name += " (" + nameObj.toString() + ")"; } } } if (name == null) { XExpression expression = test.getExpression(); name = XtestUtil.getTextOfFirstLine(expression, 40); } return name; } private void handleAssertionFailure(XAssertExpression assertExpression) { String message = messageBuilder.buildMessage(assertExpression.getActual(), assertionDiagnostics); fail(message); } private void handleEvaluationException(Throwable toWrap, XExpression expression) { // Start from the root of the stack and work down until a call has been made that jumps // locations in the file, want to drill down to the most specific expression contained // within the top-level expression that caused the exception XExpression cause = callStack.firstElement(); for (XExpression element : callStack) { if (org.eclipse.xtext.EcoreUtil2.isAncestor(cause, element)) { cause = element; } } toWrap = XtestUtil.getRootCause(toWrap); XTestEvaluationException toThrow = new XTestEvaluationException(toWrap, cause); StackTraceElement[] generatedStack = XtestUtil.generateXtestStackTrace(startTrace, toWrap.getStackTrace(), callStack); toWrap.setStackTrace(generatedStack); throw toThrow; } private static class ReturnValue extends RuntimeException { private static final long serialVersionUID = 7864448463694945628L; public Object returnValue; public ReturnValue(Object value) { super(); this.returnValue = value; } } }