| /******************************************************************************* |
| * Copyright (c) 2015 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 |
| * Bernhard Wedl - added some Variable functions |
| *******************************************************************************/ |
| package org.eclipse.ease.modules.unittest.modules; |
| |
| import java.io.IOException; |
| import java.lang.reflect.Method; |
| |
| import org.eclipse.core.resources.IFile; |
| import org.eclipse.core.runtime.CoreException; |
| import org.eclipse.core.runtime.IPath; |
| import org.eclipse.core.runtime.Path; |
| import org.eclipse.ease.AbstractScriptEngine; |
| import org.eclipse.ease.IDebugEngine; |
| import org.eclipse.ease.IScriptEngine; |
| import org.eclipse.ease.modules.AbstractScriptModule; |
| import org.eclipse.ease.modules.IScriptFunctionModifier; |
| import org.eclipse.ease.modules.ScriptParameter; |
| import org.eclipse.ease.modules.WrapToScript; |
| import org.eclipse.ease.modules.platform.IFileHandle; |
| import org.eclipse.ease.modules.platform.ResourcesModule; |
| import org.eclipse.ease.modules.unittest.components.Test; |
| import org.eclipse.ease.modules.unittest.components.TestComposite; |
| import org.eclipse.ease.modules.unittest.components.TestEntity; |
| import org.eclipse.ease.modules.unittest.components.TestFile; |
| import org.eclipse.ease.modules.unittest.components.TestStatus; |
| import org.eclipse.ease.modules.unittest.components.TestSuite; |
| import org.eclipse.ease.modules.unittest.components.TestSuiteModel; |
| import org.eclipse.ease.modules.unittest.components.TestSuiteModel.Variable; |
| import org.eclipse.ease.modules.unittest.reporters.IReportGenerator; |
| import org.eclipse.ease.modules.unittest.reporters.ReportTools; |
| import org.eclipse.ease.tools.ResourceTools; |
| |
| /** |
| * Library providing UnitTest functionality. Unit tests are parts of a JavaScript file which are embedded between a startTest() and an endTest() function call. |
| * Every function result returned inbetween will be checked for a response of type {@link IAssertion}. If such a response is detected its result will |
| * automatically be applied to the current unit test. |
| */ |
| public class UnitTestModule extends AbstractScriptModule implements IScriptFunctionModifier { |
| |
| public static final String MODULE_NAME = "Unittest"; |
| private static final String ASSERTION_FUNCION_NAME = "assertion"; |
| public static final String INJECTED_MAIN = "injected_code_"; |
| public static final String FAIL_ON_ERROR_VARIABLE = "__FAIL_ON_ERROR"; |
| |
| private boolean fAssertionEnablement = true; |
| private int fAssertionsToBeIgnored = 0; |
| |
| /** |
| * Run a unit test. If the file addressed is a suite, then the whole suite is started. If the file is a simple JavaScript file, then a dynamic suite is |
| * created that contains just the one file. |
| * |
| * @param filename |
| * location of testsuite or testfile. Must be a file from the workspace |
| * @return {@link TestSuite} definition or <code>null</code> |
| */ |
| @WrapToScript |
| public TestSuite runUnitTest(Object executable) { |
| if (executable instanceof TestSuite) { |
| ((TestSuite) executable).run(); |
| return (TestSuite) executable; |
| } |
| |
| if (!(executable instanceof IFile)) |
| executable = ResourceTools.resolveFile(executable.toString(), getScriptEngine().getExecutedFile(), true); |
| |
| if ((executable instanceof IFile) && (((IFile) executable).exists())) { |
| try { |
| if ("suite".equalsIgnoreCase(((IFile) executable).getFileExtension())) { |
| // we have a test suite |
| final TestSuiteModel description = new TestSuiteModel((IFile) executable); |
| final TestSuite testSuite = new TestSuite(description); |
| testSuite.run(); |
| return testSuite; |
| |
| } else if ("js".equalsIgnoreCase(((IFile) executable).getFileExtension())) { |
| // we have a JavaScript file |
| final TestSuiteModel description = new TestSuiteModel(); |
| // FIXME re-implement |
| // description.addTestFile((IFile) executable); |
| |
| final TestSuite testSuite = new TestSuite(description); |
| testSuite.run(); |
| return testSuite; |
| } |
| |
| } catch (final Exception e) { |
| getScriptEngine().getErrorStream().println("Error executing unit test suite: " + e.getLocalizedMessage()); |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Open a testsuite without executing it. |
| * |
| * @param filename |
| * location of testsuite file. Must be a file from the workspace |
| * @return {@link TestSuite} definition or <code>null</code> |
| * @throws IOException |
| * @throws CoreException |
| */ |
| @WrapToScript |
| public TestSuite openTestSuite(final String filename) throws IOException, CoreException { |
| final Object file = ResourceTools.resolveFile(filename, getScriptEngine().getExecutedFile(), true); |
| if ((file instanceof IFile) && (((IFile) file).exists())) { |
| if ("suite".equalsIgnoreCase(((IFile) file).getFileExtension())) { |
| // we have a test suite |
| final TestSuite suite = new TestSuite(new TestSuiteModel((IFile) file)); |
| suite.setOutputStream(getScriptEngine().getOutputStream()); |
| suite.setErrorStream(getScriptEngine().getErrorStream()); |
| |
| return suite; |
| } |
| } |
| |
| return null; |
| } |
| |
| /** |
| * Create a test report file. |
| * |
| * @param reportType |
| * type of report; see getReportTypes() for values |
| * @param suite |
| * {@link TestSuite} to be reported |
| * @param fileLocation |
| * location where report should be stored |
| * @param title |
| * report title |
| * @param description |
| * report description (ignored by some reports) |
| * @return <code>true</code> when report was created successfully |
| * @throws Exception |
| * thrown on file write errors |
| */ |
| @WrapToScript |
| public boolean createReport(final String reportType, final TestSuite suite, final String fileLocation, final String title, final String description) |
| throws Exception { |
| |
| final Object file = ResourceTools.resolveFile(fileLocation, getScriptEngine().getExecutedFile(), false); |
| |
| final IReportGenerator report = ReportTools.getReport(reportType); |
| if (report != null) { |
| final String reportData = report.createReport(title, description, suite); |
| final IFileHandle handle = getEnvironment().getModule(ResourcesModule.class).writeFile(file, reportData, ResourcesModule.WRITE); |
| ResourcesModule.closeFile(handle); |
| return true; |
| } |
| |
| return false; |
| } |
| |
| /** |
| * Get a list of available report types. |
| * |
| * @return String array containing available report types |
| */ |
| @WrapToScript |
| public static String[] getReportTypes() { |
| return ReportTools.getReportTemplates().toArray(new String[0]); |
| } |
| |
| @Override |
| public String getPreExecutionCode(final Method method) { |
| return ""; |
| } |
| |
| @Override |
| public String getPostExecutionCode(final Method method) { |
| if (returnsAssertion(method)) |
| return "\t" + ASSERTION_FUNCION_NAME + "(" + IScriptFunctionModifier.RESULT_NAME + ");\n\n"; |
| |
| return ""; |
| } |
| |
| /** |
| * Check whether a method returns an assertion. |
| * |
| * @param method |
| * method to verify |
| * @return <code>true</code> when method returns an assertion |
| */ |
| private static boolean returnsAssertion(final Method method) { |
| final Class<?> returnType = method.getReturnType(); |
| |
| // check for return type "IAssertion" |
| return (IAssertion.class.isAssignableFrom(returnType)); |
| } |
| |
| /** |
| * 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) { |
| fAssertionsToBeIgnored = 0; |
| fAssertionEnablement = true; |
| |
| executeUserCode(TestSuiteModel.CODE_LOCATION_TEST_SETUP); |
| |
| final TestComposite testObject = getTestObject(); |
| if (testObject != null) { |
| final Test test = new Test(testObject, title, description); |
| final IScriptEngine engine = getScriptEngine(); |
| if (engine instanceof IDebugEngine) |
| test.setTestLocation(((IDebugEngine) engine).getStackTrace()); |
| else if (engine instanceof AbstractScriptEngine) |
| test.setTestLocation(((AbstractScriptEngine) engine).getStackTrace()); |
| |
| testObject.addTest(test); |
| } |
| } |
| |
| /** |
| * End the current test. |
| */ |
| @WrapToScript |
| public final void endTest() { |
| final TestComposite testObject = getTestObject(); |
| if (testObject != null) |
| testObject.endTest(); |
| |
| executeUserCode(TestSuiteModel.CODE_LOCATION_TEST_TEARDOWN); |
| } |
| |
| /** |
| * 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 |
| */ |
| @WrapToScript |
| public final void assertion(final IAssertion reason) { |
| if (fAssertionEnablement) { |
| if (fAssertionsToBeIgnored == 0) { |
| if (!reason.isValid()) { |
| // TODO check if we need this any longer as mergedassertion will return all errors |
| if (reason instanceof MergedAssertion) { |
| // we might have multiple errors here, create an error marker for each of them |
| for (final IAssertion mergedReason : ((MergedAssertion) reason).getAssertions()) { |
| if (!mergedReason.isValid()) |
| error(mergedReason.toString()); |
| } |
| |
| } |
| |
| error(reason.toString()); |
| } |
| |
| } else |
| fAssertionsToBeIgnored--; |
| } |
| } |
| |
| /** |
| * Add a new error to the current testcase. |
| * |
| * @param message |
| * error message |
| */ |
| @WrapToScript |
| public final void error(final String message) { |
| final TestComposite testObject = getTestObject(); |
| if (testObject != null) { |
| final Object failOnError = getScriptEngine().getVariable(FAIL_ON_ERROR_VARIABLE); |
| if ((failOnError instanceof Boolean) && (((Boolean) failOnError).booleanValue())) { |
| testObject.addTestResult(TestStatus.FAILURE, message); |
| |
| throw new RuntimeException("Unit Failure: " + message); |
| |
| } else |
| testObject.addTestResult(TestStatus.ERROR, message); |
| } |
| } |
| |
| /** |
| * Report a new failure for the current UnitTest. |
| * |
| * @param message |
| * failure message |
| */ |
| @WrapToScript |
| public final void failure(final String message) { |
| final TestComposite testObject = getTestObject(); |
| if (testObject != null) |
| testObject.addTestResult(TestStatus.FAILURE, message); |
| |
| throw new RuntimeException("Unit Failure: " + message); |
| } |
| |
| /** |
| * Get the current {@link TestFile} instance. |
| * |
| * @return test file instance |
| */ |
| @WrapToScript |
| public TestFile getTestFile() { |
| final TestComposite testObject = getTestObject(); |
| if (testObject instanceof TestFile) |
| return (TestFile) testObject; |
| |
| return null; |
| } |
| |
| /** |
| * Get a {@link Test} instance. if no <i>name</i> is provided, the current test instance is returned. When provided, this method searches for a test with |
| * the given title. |
| * |
| * @param name |
| * test name to look for |
| * @return current test or <code>null</code> |
| */ |
| @WrapToScript |
| public Test getTest(@ScriptParameter(defaultValue = ScriptParameter.NULL) final String name) { |
| if (name == null) |
| return getTestFile().getCurrentTest(); |
| |
| for (final TestEntity test : getTestFile().getChildren()) { |
| if ((test instanceof Test) && (name.equals(((Test) test).getTitle()))) |
| return (Test) test; |
| } |
| |
| return null; |
| } |
| |
| private TestComposite getTestObject() { |
| final Object testObject = getScriptEngine().getVariable(TestComposite.CURRENT_TESTCOMPOSITE); |
| if (testObject instanceof TestComposite) |
| return (TestComposite) testObject; |
| |
| return null; |
| } |
| |
| /** |
| * Get the current {@link TestSuite} instance. |
| * |
| * @return test suite instance |
| */ |
| @WrapToScript |
| public TestSuite getTestSuite() { |
| final TestComposite testObject = getTestObject(); |
| if (testObject instanceof TestSuite) |
| return (TestSuite) testObject; |
| |
| if (testObject instanceof TestFile) |
| return ((TestFile) testObject).getTestSuite(); |
| |
| return null; |
| } |
| |
| /** |
| * Insert additional test code at a given code location. Some code locations are predefined like start test file or start test case. Others may be freely |
| * defined by the unit test execution target. Those other tests need to be invoked by the unit tests by calling <code>executeUserCode(identifier);</code>. |
| * |
| * @param identifier |
| * code identifier |
| */ |
| @WrapToScript |
| public final void executeUserCode(final String identifier) { |
| if (getTestObject() != null) { |
| final String code = getTestSuite().getModel().getCodeFragment(identifier); |
| if ((code != null) && (!code.isEmpty())) |
| getScriptEngine().inject(code); |
| } |
| } |
| |
| /** |
| * 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); |
| } |
| |
| /** |
| * 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()); |
| } |
| |
| /** |
| * Disables automatic assertions in tests. When set user generated errors, warnings, failures will still be reported, but assert functions will not report |
| * errors. Assertions are automatically enabled on a {@link #startTest(String, String)} command. |
| */ |
| @WrapToScript |
| public void disableAssertions() { |
| fAssertionEnablement = false; |
| } |
| |
| /** |
| * Enables automatic assertions in tests. This is the default setting for any testfile. |
| */ |
| @WrapToScript |
| public void enableAssertions() { |
| fAssertionEnablement = true; |
| fAssertionsToBeIgnored = 0; |
| } |
| |
| /** |
| * Define amount of upcoming assertions to be ignored. Allows to ignore <code>count</code> upcoming assertions (not depending on actual assertion status). |
| * Assertions are enabled again once the counter reaches 0,when {@link #enableAssertions()} is called or a new test is started using |
| * {@link #startTest(String, String)}. |
| * |
| * @param count |
| * assertions to be ignored |
| */ |
| @WrapToScript |
| public void ignoreAssertions(final int count) { |
| assert (count >= 0); |
| fAssertionsToBeIgnored = count; |
| } |
| |
| /** |
| * Wait until a test entity is completed. If the entity is not scheduled this method might stall forever! |
| * |
| * @param testObject |
| * {@link TestSuite} or {@link Test} to wait for |
| * @return <code>true</code> on success, <code>false</code> if test was interrupted |
| */ |
| @WrapToScript |
| public static boolean waitForCompletion(final TestComposite testObject) { |
| try { |
| while ((testObject.getStatus() == TestStatus.NOT_RUN) || (testObject.getStatus() == TestStatus.RUNNING)) |
| Thread.sleep(1000); |
| } catch (final InterruptedException e) { |
| return false; |
| } |
| |
| return true; |
| } |
| |
| /** |
| * Add metadata to the current test object. Metadata is stored as a Map, so setting with an already existing keyword will override the previous setting. |
| * |
| * @param key |
| * metadata keyword |
| * @param data |
| * metadata |
| */ |
| @WrapToScript |
| public void addMetaData(final String key, final String data) { |
| final TestComposite testObject = getTestObject(); |
| testObject.getCurrentTest().addMetaData(key, data); |
| } |
| |
| /** |
| * Array of available variables. |
| * |
| * @param testSuite |
| * opened TestSuite |
| * @return array of available variables |
| */ |
| @WrapToScript |
| public Variable[] getVariables(final TestSuite testSuite) { |
| return testSuite.getModel().getVariables().toArray(new Variable[0]); |
| } |
| |
| /** |
| * Get the content of the variable by its <i>identifier</i>. If the variable does not exist <code>null</code> is returned. |
| * |
| * @param testSuite |
| * opened TestSuite |
| * @param identifier |
| * unique identifier |
| * @return the variable or <code>null</code> |
| */ |
| @WrapToScript |
| public Variable getVariable(final TestSuite testSuite, final String identifier) { |
| return testSuite.getModel().getVariable(identifier); |
| } |
| |
| /** |
| * Set the content of a variable defined by its <i>identifier</i>. If the variable with the given <i>identifier</i> does not exist a new one is created. |
| * |
| * @param testSuite |
| * opened TestSuite |
| * @param identifier |
| * unique identifier |
| * @param content |
| * @param description |
| * @param path |
| * path of the variable |
| */ |
| @WrapToScript |
| public void setVariable(final TestSuite testSuite, final String identifier, final String content, |
| @ScriptParameter(defaultValue = ScriptParameter.NULL) final String description, |
| @ScriptParameter(defaultValue = ScriptParameter.NULL) final String path) { |
| final IPath validPath = (path == null) ? Path.ROOT : new Path(path); |
| testSuite.getModel().setVariable(identifier, content, description, validPath); |
| } |
| |
| /** |
| * Remove the variable with the given <i>identifier</i>. After the variable is removed it will not be available during the {@link TestSuite} execution. |
| * |
| * @param testSuite |
| * opened TestSuite |
| * @param identifier |
| * unique identifier of the variable |
| */ |
| @WrapToScript |
| public void removeVariable(final TestSuite testSuite, final String identifier) { |
| testSuite.getModel().removeVariable(testSuite.getModel().getVariable(identifier)); |
| } |
| } |