blob: 84bfc9c96157add89d66678c12b83194611d66db [file] [log] [blame]
/*******************************************************************************
* Copyright (c) 2017 Christian Pontesegger and others.
* All rights reserved. This program and the accompanying materials
* are made available under the terms of the Eclipse Public License v1.0
* which accompanies this distribution, and is available at
* http://www.eclipse.org/legal/epl-v10.html
*
* Contributors:
* Christian Pontesegger - initial API and implementation
*******************************************************************************/
package org.eclipse.ease.lang.unittest;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.io.IOException;
import java.lang.reflect.Method;
import java.util.regex.Pattern;
import org.eclipse.core.resources.IFile;
import org.eclipse.core.resources.IResource;
import org.eclipse.core.runtime.CoreException;
import org.eclipse.ease.IDebugEngine;
import org.eclipse.ease.IScriptEngine;
import org.eclipse.ease.Script;
import org.eclipse.ease.debugging.ScriptStackTrace;
import org.eclipse.ease.lang.unittest.definition.Flag;
import org.eclipse.ease.lang.unittest.definition.ICode;
import org.eclipse.ease.lang.unittest.definition.ITestSuiteDefinition;
import org.eclipse.ease.lang.unittest.reporters.IReportGenerator;
import org.eclipse.ease.lang.unittest.reporters.ReportTools;
import org.eclipse.ease.lang.unittest.runtime.IMetadata;
import org.eclipse.ease.lang.unittest.runtime.IRuntimeFactory;
import org.eclipse.ease.lang.unittest.runtime.ITest;
import org.eclipse.ease.lang.unittest.runtime.ITestClass;
import org.eclipse.ease.lang.unittest.runtime.ITestContainer;
import org.eclipse.ease.lang.unittest.runtime.ITestEntity;
import org.eclipse.ease.lang.unittest.runtime.ITestFile;
import org.eclipse.ease.lang.unittest.runtime.ITestResult;
import org.eclipse.ease.lang.unittest.runtime.ITestSuite;
import org.eclipse.ease.lang.unittest.runtime.TestStatus;
import org.eclipse.ease.modules.AbstractScriptModule;
import org.eclipse.ease.modules.IEnvironment;
import org.eclipse.ease.modules.IModuleCallbackProvider;
import org.eclipse.ease.modules.ScriptParameter;
import org.eclipse.ease.modules.WrapToScript;
import org.eclipse.ease.tools.ResourceTools;
/**
* Support methods for scripted unit tests. Provides several assertion methods and utility functions to manipulate the current test instances and states.
*/
public class UnitTestModule extends AbstractScriptModule {
private ITestClass fCurrentTestClass = null;
private ITest fCurrentTest = null;
private boolean fThrowOnFailure = false;
@Override
public void initialize(IScriptEngine engine, IEnvironment environment) {
super.initialize(engine, environment);
environment.addModuleCallback(new IModuleCallbackProvider() {
@Override
public void preExecutionCallback(Method method, Object[] parameters) {
}
@Override
public void postExecutionCallback(Method method, Object result) {
if (result instanceof IAssertion)
assertion((IAssertion) result);
}
@Override
public boolean hasPreExecutionCallback(Method method) {
return false;
}
@Override
public boolean hasPostExecutionCallback(Method method) {
return IAssertion.class.isAssignableFrom(method.getReturnType());
}
});
}
/**
* Start a specific unit test. Started tests should be terminated by an {@link #endTest()}.
*
* @param title
* name of test
* @param description
* short test description
*/
@WrapToScript
public final void startTest(final String title, @ScriptParameter(defaultValue = "") final String description) {
ITestContainer container = fCurrentTestClass;
if (container == null) {
final ITestFile testFile = getTestFile();
if (testFile != null)
container = testFile;
else
// temporary container to allow to run tests as normal scripts
container = IRuntimeFactory.eINSTANCE.createTestFile();
}
fCurrentTest = IRuntimeFactory.eINSTANCE.createTest();
fCurrentTest.setName(title);
fCurrentTest.setDescription(description);
fCurrentTest.setEntityStatus(TestStatus.RUNNING);
if (getScriptEngine() instanceof IDebugEngine)
fCurrentTest.setStackTrace(((IDebugEngine) getScriptEngine()).getStackTrace());
container.getChildren().add(fCurrentTest);
}
/**
* End the current test. Does nothing if no test was started.
*/
@WrapToScript
public final void endTest() {
if (fCurrentTest != null) {
fCurrentTest.setEntityStatus(TestStatus.PASS);
fCurrentTest = null;
}
}
/**
* Get the current test suite.
*
* @return test suite instance
*/
@WrapToScript
public ITestSuite getTestSuite() {
if (getTestFile() != null)
return getTestFile().getTestSuite();
final Object testSuite = getScriptEngine().getVariable(TestSuiteScriptEngine.TEST_SUITE_VARIABLE);
return (testSuite instanceof ITestSuite) ? (ITestSuite) testSuite : null;
}
/**
* Get the currently executed test file instance. The test file is not a file instance but the runtime representation of a testsuite test file.
*
* @return test file instance
*/
@WrapToScript
public ITestFile getTestFile() {
final Object testFile = getScriptEngine().getVariable(TestSuiteScriptEngine.TEST_FILE_VARIABLE);
return (testFile instanceof ITestFile) ? (ITestFile) testFile : null;
}
/**
* Append generic data to the current test, testfile or test suite.
*
* @param name
* key to use. Has to be unique for this test object
* @param value
* data to be stored
*/
@WrapToScript
public void addMetaData(String name, Object value) {
final IMetadata metadata = IRuntimeFactory.eINSTANCE.createMetadata();
metadata.setKey(name);
metadata.setValue(value);
if (getScriptEngine() instanceof IDebugEngine)
metadata.setStackTrace(((IDebugEngine) getScriptEngine()).getStackTrace());
if (fCurrentTest != null)
fCurrentTest.getMetadata().add(metadata);
else if (getTestFile() != null)
getTestFile().getMetadata().add(metadata);
else if (getTestSuite() != null)
getTestSuite().getMetadata().add(metadata);
else
throw new RuntimeException("No test entity availabe. Nowhere to store metadata to");
}
/**
* Expect two objects to be equal.
*
* @param expected
* expected result
* @param actual
* actual result
* @param errorDescription
* optional error text to be displayed when not equal
* @return assertion containing comparison result
*/
@WrapToScript
public static IAssertion assertEquals(final Object expected, final Object actual,
@ScriptParameter(defaultValue = ScriptParameter.NULL) final Object errorDescription) {
if (expected != null)
return new DefaultAssertion(expected.equals(actual),
(errorDescription == null) ? "Objects do not match: expected <" + expected + ">, actual <" + actual + ">" : errorDescription.toString());
return assertNull(actual, errorDescription);
}
/**
* Asserts when provided value does not match to a given regular expression pattern.
*
* @param pattern
* pattern to match
* @param candidate
* text to be matched
* @param errorMessage
* error message in case of a mismatch
* @return assertion depending on <code>actual</code> value
*/
@WrapToScript
public static IAssertion assertMatch(String pattern, String candidate, @ScriptParameter(defaultValue = ScriptParameter.NULL) String errorMessage) {
if (Pattern.matches(pattern, candidate))
return IAssertion.VALID;
return new DefaultAssertion((errorMessage != null) ? errorMessage : "\"" + candidate + "\" does not match pattern \"" + pattern + "\"");
}
/**
* Expect two objects not to be equal.
*
* @param expected
* unexpected result
* @param actual
* actual result
* @param errorDescription
* optional error text to be displayed when equal
* @return assertion containing comparison result
*/
@WrapToScript
public static IAssertion assertNotEquals(final Object expected, final Object actual,
@ScriptParameter(defaultValue = ScriptParameter.NULL) final Object errorDescription) {
if (expected != null)
return new DefaultAssertion(!expected.equals(actual), (errorDescription == null) ? "Objects match" : errorDescription.toString());
return assertNotNull(actual, errorDescription);
}
/**
* Asserts when provided value is not <code>null</code>.
*
* @param actual
* value to verify
* @param errorDescription
* optional error description
* @return assertion depending on <code>actual</code> value
*/
@WrapToScript
public static IAssertion assertNull(final Object actual, @ScriptParameter(defaultValue = ScriptParameter.NULL) final Object errorDescription) {
return new DefaultAssertion(actual == null, (errorDescription == null) ? "Object is not null, actual <" + actual + ">" : errorDescription.toString());
}
/**
* Asserts when provided value is <code>null</code>.
*
* @param actual
* value to verify
* @param errorDescription
* optional error description
* @return assertion depending on <code>actual</code> value
*/
@WrapToScript
public static IAssertion assertNotNull(final Object actual, @ScriptParameter(defaultValue = ScriptParameter.NULL) final Object errorDescription) {
return new DefaultAssertion(actual != null, (errorDescription == null) ? "Object is null" : errorDescription.toString());
}
/**
* Asserts when provided value is <code>false</code>.
*
* @param actual
* value to verify
* @param errorDescription
* optional error description
* @return assertion depending on <code>actual</code> value
*/
@WrapToScript
public static IAssertion assertTrue(final Boolean actual, @ScriptParameter(defaultValue = ScriptParameter.NULL) final Object errorDescription) {
return new DefaultAssertion(actual, (errorDescription == null) ? "Value is false" : errorDescription.toString());
}
/**
* Asserts when provided value is <code>true</code>.
*
* @param actual
* value to verify
* @param errorDescription
* optional error description
* @return assertion depending on <code>actual</code> value
*/
@WrapToScript
public static IAssertion assertFalse(final Boolean actual, @ScriptParameter(defaultValue = ScriptParameter.NULL) final Object errorDescription) {
return new DefaultAssertion(!actual, (errorDescription == null) ? "Value is true" : errorDescription.toString());
}
/**
* Changes behavior for assertion handling. By default assertions do not cause an exception to be thrown, which supports classic (functional) test mode.
* Object oriented test mode enhances exceptions to be thrown on assertions.
*
* @param throwOnFailure
* <code>true</code> to thow exceptions on assertions
*/
public void setThrowOnFailure(boolean throwOnFailure) {
fThrowOnFailure = throwOnFailure;
}
/**
* Create a new assertion for the current test. According to the assertion status an error might be added to the current testcase.
*
* @param reason
* assertion to be checked
* @throws AssertionException
* in case {@link #setThrowOnFailure(boolean)} is enabled
*/
@WrapToScript
public final void assertion(final IAssertion reason) throws AssertionException {
if (!reason.isValid()) {
if (fThrowOnFailure)
throw new AssertionException(reason.toString());
else
failure(reason.toString());
}
}
public ITestContainer addTestClass(String className) {
if (fCurrentTestClass != null) {
// testclass finished
fCurrentTestClass.setEntityStatus(TestStatus.FINISHED);
fCurrentTestClass = null;
}
if (className != null) {
// new testclass created
final ITestFile testFile = getTestFile();
fCurrentTestClass = IRuntimeFactory.eINSTANCE.createTestClass();
fCurrentTestClass.setName(className);
fCurrentTestClass.setEntityStatus(TestStatus.RUNNING);
if (getScriptEngine() instanceof IDebugEngine)
fCurrentTestClass.setStackTrace(((IDebugEngine) getScriptEngine()).getStackTrace());
testFile.getChildren().add(fCurrentTestClass);
}
return fCurrentTestClass;
}
/**
* Ignore the current test.
*
* @param reason
* message why the test got ignored.
*/
@WrapToScript
public void ignoreTest(@ScriptParameter(defaultValue = "") String reason) {
if (fCurrentTest != null)
addResult(TestStatus.DISABLED, reason, null);
}
/**
* Force a failure (=assertion) for the current test entity (test/testclass/testfile/testsuite).
*
* @param message
* failure message
*/
@WrapToScript
public void failure(String message) {
failure(message, null);
}
/**
* Called from the javascript runner.
*
* @param message
* failure message
* @param stackTrace
* stacktrace of failure event
*/
public void failure(String message, ScriptStackTrace stackTrace) {
final ITestSuiteDefinition definition = (getTestSuite() != null) ? getTestSuite().getDefinition() : null;
if ((definition != null) && (definition.getFlag(Flag.PROMOTE_FAILURE_TO_ERROR, false)))
error(message, stackTrace);
else
addResult(TestStatus.FAILURE, message, stackTrace);
}
private void addResult(TestStatus status, String message, ScriptStackTrace stackTrace) {
final ITestResult result = IRuntimeFactory.eINSTANCE.createTestResult();
result.setStatus(status);
result.setMessage(message);
if (stackTrace != null)
result.setStackTrace(stackTrace);
else if (getScriptEngine() instanceof IDebugEngine)
result.setStackTrace(((IDebugEngine) getScriptEngine()).getStackTrace());
getTest().getResults().add(result);
}
/**
* Get the current unit test.
*
* @return the current test or a generic global test scope if called outside of a valid testcase
*/
@WrapToScript
public ITest getTest() {
if (fCurrentTest == null) {
if (getTestFile() != null)
return getTestFile().getTest(ITestEntity.GLOBAL_SCOPE_TEST);
if (getTestSuite() != null)
return getTestSuite().getTest(ITestEntity.GLOBAL_SCOPE_TEST);
// executed outside of unit test framework, return dummy test instance
return IRuntimeFactory.eINSTANCE.createTest();
}
return fCurrentTest;
}
/**
* Force an error for the current test entity (test/testclass/testfile/testsuite).
*
* @param message
* error message
* @throws AssertionException
* containing the provided message
*/
@WrapToScript
public void error(String message) throws AssertionException {
throw new AssertionException(message);
}
/**
* Called from the javascript runner.
*
* @param message
* error message
* @param stackTrace
* stacktrace of error event
*/
public void error(String message, ScriptStackTrace stackTrace) {
addResult(TestStatus.ERROR, message, stackTrace);
}
/**
* Set the timeout for the current test. If test execution takes longer than the timeout, the test is marked as failed.
*
* @param timeout
* timeout in [ms]
*/
@WrapToScript
public void setTestTimeout(long timeout) {
if (fCurrentTest != null)
getTest().setDurationLimit(timeout);
}
/**
* Execute code registered in the testsuite.
*
* @param location
* name of the code fragment to execute.
* @return execution result
* @throws Exception
* when no user specific code can be found or the injected code throws
*/
@WrapToScript
public Object executeUserCode(String location) throws Exception {
final ITestSuiteDefinition definition = (getTestSuite() != null) ? getTestSuite().getDefinition() : null;
if (definition != null) {
final ICode customCode = definition.getCustomCode(location);
if (customCode != null)
return getScriptEngine().inject(new Script(customCode.getLocation(), customCode.getContent()));
else
throw new Exception("No user specific code for \"" + location + "\" found.");
} else
throw new Exception("No user specific code for \"" + location + "\" found.");
}
/**
* Get a list of available test report types.
*
* @return String array containing available report types
*/
@WrapToScript
public static String[] getReportTypes() {
return ReportTools.getReportTemplates().toArray(new String[0]);
}
/**
* Create a test report file.
*
* @param reportType
* type of report; see getReportTypes() for values
* @param suite
* {@link ITestEntity} to be reported
* @param fileLocation
* location where report should be stored
* @param title
* report title
* @param description
* report description (ignored by some reports)
* @param reportData
* additional report data. Specific to report type
* @throws CoreException
* when we could not write to a workspace file
* @throws IOException
* when we could not write to the file system
*/
@WrapToScript
public void createReport(final String reportType, final ITestEntity suite, final Object fileLocation, final String title, final String description,
@ScriptParameter(defaultValue = ScriptParameter.NULL) Object reportData) throws IOException, CoreException {
final IReportGenerator report = ReportTools.getReport(reportType);
if (report != null) {
final String reportOutput = report.createReport(title, description, suite, reportData);
saveDataToFile(reportOutput.getBytes(), fileLocation);
} else
throw new IOException("Report does not contain any data");
}
/**
* Load a test suite definition from a given resource.
*
* @param location
* location to load from
* @return test suite definition
* @throws IOException
* when reading of definition fails
*/
@WrapToScript
public ITestSuiteDefinition loadTestSuiteDefinition(Object location) throws IOException {
final Object resolvedLocation = ResourceTools.resolve(location, getScriptEngine().getExecutedFile());
return UnitTestHelper.loadTestSuite(ResourceTools.getInputStream(resolvedLocation));
}
/**
* Save a test suite definition to a file.
*
* @param testsuite
* definition to save
* @param fileLocation
* location to save file to (file system or workspace)
* @throws CoreException
* when we could not write to a workspace file
* @throws IOException
* when we could not write to the file system
*/
@WrapToScript
public void saveTestSuiteDefinition(ITestSuiteDefinition testsuite, Object fileLocation) throws IOException, CoreException {
final byte[] testSuiteData = UnitTestHelper.serializeTestSuite(testsuite);
saveDataToFile(testSuiteData, fileLocation);
}
private void saveDataToFile(byte[] data, Object fileLocation) throws IOException, CoreException {
final Object file = ResourceTools.resolve(fileLocation, getScriptEngine().getExecutedFile());
if (file instanceof IFile) {
if (((IFile) file).exists())
((IFile) file).setContents(new ByteArrayInputStream(data), IResource.FORCE | IResource.KEEP_HISTORY, null);
else {
ResourceTools.createFolder(((IFile) file).getParent());
((IFile) file).create(new ByteArrayInputStream(data), true, null);
}
} else if (file instanceof File) {
final FileOutputStream outputStream = new FileOutputStream((File) file);
outputStream.write(data);
outputStream.close();
} else
throw new IOException("Could not find file: " + fileLocation);
}
}