diff --git a/build.gradle b/build.gradle index 1dc8742bd3..07f439f4c2 100644 --- a/build.gradle +++ b/build.gradle @@ -38,6 +38,7 @@ def guiModule = project(':cuba-gui') def webToolkitModule = project(':cuba-web-toolkit') def webModule = project(':cuba-web') def desktopModule = project(':cuba-desktop') +def acceptanceModule = project(':cuba-test-ui') def servletApi = [group: 'org.apache.tomcat', name: 'servlet-api', version: '6.0.20'] def vaadin = [group: 'com.haulmont.thirdparty', name: 'vaadin', version: '6.6.1.91'] @@ -54,7 +55,7 @@ def postgres = [group: 'postgresql', name: 'postgresql', version: '8.3-603.jdbc4 def hsqldb = [group: 'hsqldb', name: 'hsqldb', version: '1.8.0.10'] configure([sharedLibModule, globalModule, coreModule, clientModule, guiModule, webToolkitModule, - webModule, desktopModule]) { + webModule, desktopModule, acceptanceModule]) { apply(plugin: 'java') apply(plugin: 'idea') apply(plugin: 'maven') @@ -64,23 +65,23 @@ configure([sharedLibModule, globalModule, coreModule, clientModule, guiModule, w compile(group: 'com.haulmont.bali', name: 'bali', version: '3.0-SNAPSHOT') compile(group: 'com.haulmont.chile', name: 'chile-core', version: '3.0-SNAPSHOT') compile(group: 'com.haulmont.chile', name: 'chile-jpa', version: '3.0-SNAPSHOT') - + compile(group: 'antlr', name: 'antlr', version: '2.7.7') - compile(group: 'aopalliance', name: 'aopalliance', version: '1.0') + compile(group: 'aopalliance', name: 'aopalliance', version: '1.0') compile(asm) compile(group: 'com.google.guava', name: 'guava', version: 'r07') compile(group: 'com.haulmont.thirdparty', name: 'openjpa', version: '1.2.2') - compile(group: 'commons-cli', name: 'commons-cli', version: '1.2') + compile(group: 'commons-cli', name: 'commons-cli', version: '1.2') compile(group: 'commons-pool', name: 'commons-pool', version: '1.5.1') compile(group: 'jasperreports', name: 'jasperreports', version: '3.5.1', transitive: false) compile(group: 'jfree', name: 'jfreechart', version: '1.0.13') - compile(group: 'net.sourceforge.serp', name: 'serp', version: '1.13.1') - compile(group: 'org.antlr', name: 'antlr-runtime', version: '3.2') - compile(group: 'org.freemarker', name: 'freemarker', version: '2.3.16') + compile(group: 'net.sourceforge.serp', name: 'serp', version: '1.13.1') + compile(group: 'org.antlr', name: 'antlr-runtime', version: '3.2') + compile(group: 'org.freemarker', name: 'freemarker', version: '2.3.16') compile(group: 'org.perf4j', name: 'perf4j', version: '0.9.10') - compile(group: 'org.springframework', name: 'spring-beans', version: '3.0.5.RELEASE') - compile(group: 'org.springframework', name: 'spring-context', version: '3.0.5.RELEASE') - compile(group: 'org.springframework', name: 'spring-web', version: '3.0.5.RELEASE') + compile(group: 'org.springframework', name: 'spring-beans', version: '3.0.5.RELEASE') + compile(group: 'org.springframework', name: 'spring-context', version: '3.0.5.RELEASE') + compile(group: 'org.springframework', name: 'spring-web', version: '3.0.5.RELEASE') testCompile(group: 'junit', name: 'junit', version: '4.5') } @@ -88,7 +89,7 @@ configure([sharedLibModule, globalModule, coreModule, clientModule, guiModule, w task sourceJar(type: Jar) { from file('src') classifier = 'sources' - } + } artifacts { archives sourceJar @@ -121,8 +122,9 @@ configure(globalModule) { configure(coreModule) { dependencies { compile(globalModule) + compile(acceptanceModule) compile(sharedLibModule) - compile(group: 'com.haulmont.thirdparty', name: 'ooo-bootstrapconnector', version: '3.0.0') + compile(group: 'com.haulmont.thirdparty', name: 'ooo-bootstrapconnector', version: '3.0.0') compile(group: 'com.haulmont.thirdparty', name: 'xstream', version: '1.3.1.20100706') compile(group: 'com.haulmont.thirdparty', name: 'core-renderer', version: 'R8') runtime(group: 'xpp3', name: 'xpp3_min', version: '1.1.4c') @@ -130,18 +132,18 @@ configure(coreModule) { exclude(group: 'bouncycastle', module: 'bcmail-jdk14') exclude(group: 'bouncycastle', module: 'bcprov-jdk14') } - compile(group: 'jgroups', name: 'jgroups', version: '2.9.0.GA') - compile(group: 'org.apache.commons', name: 'commons-compress', version: '1.1') + compile(group: 'jgroups', name: 'jgroups', version: '2.9.0.GA') + compile(group: 'org.apache.commons', name: 'commons-compress', version: '1.1') compile(poi) - compile(group: 'org.aspectj', name: 'aspectjrt', version: '1.6.6') - compile(group: 'org.aspectj', name: 'aspectjweaver', version: '1.6.6') + compile(group: 'org.aspectj', name: 'aspectjrt', version: '1.6.6') + compile(group: 'org.aspectj', name: 'aspectjweaver', version: '1.6.6') compile(group: 'org.mybatis', name: 'mybatis', version: '3.0.4') - compile(group: 'org.mybatis', name: 'mybatis-spring', version: '1.0.0') + compile(group: 'org.mybatis', name: 'mybatis-spring', version: '1.0.0') compile(group: 'org.json', name: 'json', version: '20090211') compile(group: 'org.openoffice', name: 'juh', version: '3.0.0') - compile(group: 'org.openoffice', name: 'ridl', version: '3.0.0') - compile(group: 'org.openoffice', name: 'unoil', version: '3.0.0') - compile(group: 'org.springframework', name: 'spring-context-support', version: '3.0.5.RELEASE') + compile(group: 'org.openoffice', name: 'ridl', version: '3.0.0') + compile(group: 'org.openoffice', name: 'unoil', version: '3.0.0') + compile(group: 'org.springframework', name: 'spring-context-support', version: '3.0.5.RELEASE') compile(group: 'org.springframework', name: 'spring-orm', version: '3.0.5.RELEASE') compile(group: 'org.springframework', name: 'spring-tx', version: '3.0.5.RELEASE') compile(group: 'org.springframework', name: 'spring-webmvc', version: '3.0.5.RELEASE') @@ -206,7 +208,7 @@ configure(guiModule) { groovy(groovyArtifact) groovy(asm) } - + test { scanForTestClasses = false includes = ['**/*Test.class'] @@ -259,7 +261,7 @@ configure(webModule) { provided(gwtServlets) provided(validationApi) provided(servletApi) - + gwtBuilding(validationApi + [classifier: 'sources']) } @@ -289,7 +291,7 @@ configure(webModule) { task buildGwt(dependsOn: buildThemes) << { File widgetsetsDir = new File(webOutDir, "VAADIN/widgetsets") File webToolkitSrcDir = new File(webToolkitModule.projectDir, 'src') - + def gwtBuildingArtifacts = configurations.gwtBuilding.resolvedConfiguration.getResolvedArtifacts() File validationSrcDir = gwtBuildingArtifacts.find { a-> a.name == 'validation-api' }.file @@ -344,6 +346,11 @@ configure(desktopModule) { } } +configure(acceptanceModule) { + dependencies { + } +} + task restart(dependsOn: ['stop', ':cuba-core:deploy', ':cuba-web:deploy'], description: 'Redeploys applications and restarts local Tomcat') << { ant.waitfor(maxwait: 6, maxwaitunit: 'second', checkevery: 2, checkeveryunit: 'second') { not { diff --git a/modules/test-ui/src/com/haulmont/cuba/ui/OsUtils.java b/modules/test-ui/src/com/haulmont/cuba/ui/OsUtils.java new file mode 100644 index 0000000000..1c0e1cd05f --- /dev/null +++ b/modules/test-ui/src/com/haulmont/cuba/ui/OsUtils.java @@ -0,0 +1,27 @@ +/* + * Copyright (c) 2011 Haulmont Technology Ltd. All Rights Reserved. + * Haulmont Technology proprietary and confidential. + * Use is subject to license terms. + */ + +package com.haulmont.cuba.ui; + +/** + *

$Id$

+ * + * @author artamonov + */ +public final class OsUtils { + private static String OS = null; + + public static String getOsName() { + if (OS == null) { + OS = System.getProperty("os.name"); + } + return OS; + } + + public static boolean isWindows() { + return getOsName().startsWith("Windows"); + } +} diff --git a/modules/test-ui/src/com/haulmont/cuba/ui/UITestCase.java b/modules/test-ui/src/com/haulmont/cuba/ui/UITestCase.java new file mode 100644 index 0000000000..103f20de32 --- /dev/null +++ b/modules/test-ui/src/com/haulmont/cuba/ui/UITestCase.java @@ -0,0 +1,251 @@ +/* + * Copyright (c) 2011 Haulmont Technology Ltd. All Rights Reserved. + * Haulmont Technology proprietary and confidential. + * Use is subject to license terms. + */ + +package com.haulmont.cuba.ui; + +import junit.framework.Assert; +import junit.framework.TestCase; +import org.apache.commons.io.FileUtils; +import org.apache.commons.lang.StringUtils; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.*; +import java.util.ArrayList; +import java.util.List; + +/** + * Base test case for UI tests + *

$Id$

+ * + * @author artamonov + */ +@SuppressWarnings("unused") +public abstract class UITestCase extends TestCase { + + /** + * Key in system environment variables
+ * Define custom executor script + */ + private static final String ACCEPTANCE_EXECUTOR_KEY = "ACCEPTANCE_EXECUTOR"; + + /** + * Key in system environment variables
+ * Define custom client program + */ + private static final String ACCEPTANCE_CLIENT_KEY = "ACCEPTANCE_CLIENT"; + + private static final String ACCEPTANCE_DIR = "tests/ui/"; + + private Process clientRunner; + + public UITestCase() { + super(); + } + + public UITestCase(String name) { + super(name); + } + + /** + * Run UI test by name in test directory
+ * If test fails then makes screenshot and writes log to output directory + * + * @param testFileName Test name + * @return List of test steps with log messages + * @throws Exception On IOException or while run client program + */ + protected List runUiTest(String testFileName) throws Exception { + String executorScript = getExecutorScript(); + + StringBuilder errors = new StringBuilder(); + StringBuilder info = new StringBuilder(); + executeTestScript(executorScript, testFileName, errors, info); + + List testSteps = analyzeTestSteps(info.toString(), errors.toString()); + for (UITestStep testStep : testSteps) { + if (!testStep.isSuccess()) { + String outDirPath = ACCEPTANCE_DIR + "out/"; + File outDir = new File(outDirPath); + if (!outDir.exists()) { + boolean result = outDir.mkdirs(); + if (!result) + throw new IOException("Couldn't create out directory"); + } + captureScreen(outDirPath + getName() + ".png"); + saveLog(testSteps, outDirPath + getName() + ".log"); + break; + } + } + + return testSteps; + } + + private void saveLog(List testSteps, String logFile) throws Exception { + StringBuilder logBuilder = new StringBuilder(); + for (UITestStep testStep : testSteps) { + if (testStep.isSuccess()) + logBuilder.append("[ STEP ] "); + else + logBuilder.append("[ FAILED ] "); + logBuilder.append(testStep.getName()).append("\n"); + for (UITestLogMessage logMessage : testStep.getLogMessages()) { + String level = "[ " + logMessage.getMessageLevel().toString() + " ] "; + logBuilder.append(level).append(logMessage.getMessage()).append("\n"); + + String content = logMessage.getContent().toString(); + if (content.length() > 0) + logBuilder.append(content.trim()).append("\n"); + } + } + FileUtils.writeStringToFile(new File(logFile), logBuilder.toString()); + } + + private void captureScreen(String destinationFile) throws Exception { + Robot robot = new Robot(); + BufferedImage screenShot = robot.createScreenCapture(new Rectangle(Toolkit.getDefaultToolkit().getScreenSize())); + ImageIO.write(screenShot, "PNG", new File(destinationFile)); + } + + private void executeTestScript(String scriptFile, String testFileName, + StringBuilder errorLog, StringBuilder infoLog) + throws FileNotFoundException { + // get path to test script + String testFilePath = getTestFilePath(testFileName); + File testFile = new File(testFilePath); + if (!testFile.exists()) + throw new FileNotFoundException("Couldn't found test file: " + testFile.getAbsolutePath()); + + try { + Process testRunner = Runtime.getRuntime().exec(scriptFile + " " + "\"" + testFilePath + "\""); + + BufferedReader stdOut = new BufferedReader(new InputStreamReader(testRunner.getInputStream())); + BufferedReader stdError = new BufferedReader(new InputStreamReader(testRunner.getErrorStream())); + + String s; + while ((s = stdOut.readLine()) != null) { + infoLog.append(s).append("\n"); + } + while ((s = stdError.readLine()) != null) { + errorLog.append(s).append("\n"); + } + } catch (IOException ex) { + throw new RuntimeException("Problem with execute external test"); + } + } + + private List analyzeTestSteps(String infoLog, String errorsLog) { + ArrayList uiTestSteps = new ArrayList(); + UITestStep currentStep = new UITestStep("Init"); + UITestLogMessage currentMessage = null; + String[] logStrings = infoLog.split("\n"); + for (String logEnrty : logStrings) { + UITestLogMessage logMessage = UITestLogMessage.parse(logEnrty); + if (logMessage.getMessageLevel() == UITestLogMessage.Level.STEP) { + if (currentMessage != null) + currentStep.getLogMessages().add(currentMessage); + uiTestSteps.add(currentStep); + currentStep = new UITestStep(logMessage.getMessage()); + currentMessage = null; + } else if (logMessage.getMessageLevel() != UITestLogMessage.Level.CONTENT) { + if (currentMessage != null) + currentStep.getLogMessages().add(currentMessage); + currentMessage = logMessage; + } else { + if (currentMessage == null) + currentMessage = new UITestLogMessage(UITestLogMessage.Level.CONTENT, ""); + if (!StringUtils.isWhitespace(logMessage.getMessage())) + currentMessage.getContent().append(logMessage.getMessage().trim()).append("\n"); + } + } + + if (currentMessage != null) + currentStep.getLogMessages().add(currentMessage); + uiTestSteps.add(currentStep); + + // mark failed steps + for (UITestStep step : uiTestSteps) { + for (UITestLogMessage logMessage : step.getLogMessages()) { + if (logMessage.getMessageLevel() == UITestLogMessage.Level.ERROR) { + step.setSuccess(false); + break; + } + } + } + + if (!StringUtils.isWhitespace(errorsLog)) { + UITestLogMessage logMessage = new UITestLogMessage(UITestLogMessage.Level.ERROR, "Fatal error"); + logMessage.getContent().append(errorsLog); + + UITestStep lastStep = uiTestSteps.get(uiTestSteps.size() - 1); + lastStep.getLogMessages().add(logMessage); + lastStep.setSuccess(false); + } + + return uiTestSteps; + } + + protected String getTestExecutor() { + return "ui-test"; + } + + protected String getClientProgram() { + return "chrome"; + } + + private String getExecutorScript() { + String testExecutor = System.getenv(ACCEPTANCE_EXECUTOR_KEY); + + if (StringUtils.isEmpty(testExecutor)) { + String scriptExt; + if (OsUtils.isWindows()) + scriptExt = "bat"; + else + scriptExt = ".sh"; + testExecutor = getTestExecutor() + "." + scriptExt; + } + return testExecutor; + } + + @Override + protected void setUp() throws Exception { + super.setUp(); + // start browser or another program + startProgramInstance(); + } + + @Override + protected void tearDown() throws Exception { + super.tearDown(); + // Exit from client program + stopProgramInstance(); + } + + protected void startProgramInstance() throws IOException { + String clientProgram = System.getenv(ACCEPTANCE_CLIENT_KEY); + if (StringUtils.isEmpty(clientProgram)) + clientProgram = getClientProgram(); + clientRunner = Runtime.getRuntime().exec(clientProgram); + } + + protected void stopProgramInstance() { + clientRunner.destroy(); + } + + protected String getTestFilePath(String testFileName) { + return ACCEPTANCE_DIR + "testsuite/" + testFileName; + } + + protected boolean isSuccessful(List testSteps) { + boolean result = true; + for (UITestStep testStep : testSteps) { + result = result && testStep.isSuccess(); + Assert.assertTrue("Failed on step: " + testStep.getName(), testStep.isSuccess()); + } + return result; + } +} \ No newline at end of file diff --git a/modules/test-ui/src/com/haulmont/cuba/ui/UITestLogMessage.java b/modules/test-ui/src/com/haulmont/cuba/ui/UITestLogMessage.java new file mode 100644 index 0000000000..8665a3f775 --- /dev/null +++ b/modules/test-ui/src/com/haulmont/cuba/ui/UITestLogMessage.java @@ -0,0 +1,78 @@ +/* + * Copyright (c) 2011 Haulmont Technology Ltd. All Rights Reserved. + * Haulmont Technology proprietary and confidential. + * Use is subject to license terms. + */ + +package com.haulmont.cuba.ui; + +import java.util.*; +import java.util.regex.*; + +/** + *

$Id$

+ * + * @author artamonov + */ +public class UITestLogMessage { + + public static enum Level { + INFO, + DEBUG, + STEP, + ERROR, + LOG, + CONTENT + } + + private Level messageLevel; + private StringBuilder content = new StringBuilder(); + private String message; + + private static Map logLevels = new HashMap(); + + static { + logLevels.put(Pattern.compile("(\\[info\\] )(.*)"), Level.INFO); + logLevels.put(Pattern.compile("(\\[log\\] )(.*)"), Level.LOG); + logLevels.put(Pattern.compile("(\\[step\\] )(.*)"), Level.STEP); + logLevels.put(Pattern.compile("(\\[debug\\] )(.*)"), Level.DEBUG); + logLevels.put(Pattern.compile("(\\[error\\] )(.*)"), Level.ERROR); + logLevels.put(Pattern.compile("(\\| )(.*)"), Level.CONTENT); + } + + public static UITestLogMessage parse(String logEntry) { + boolean find = false; + + String message = logEntry; + Level level = Level.CONTENT; + + Iterator patternIterator = logLevels.keySet().iterator(); + while (patternIterator.hasNext() && (!find)) { + Pattern pattern = patternIterator.next(); + Matcher matcher = pattern.matcher(logEntry); + if (matcher.matches()) { + message = matcher.group(2); + level = logLevels.get(pattern); + find = true; + } + } + return new UITestLogMessage(level, message); + } + + public UITestLogMessage(Level messageLevel, String message) { + this.messageLevel = messageLevel; + this.message = message; + } + + public Level getMessageLevel() { + return messageLevel; + } + + public String getMessage() { + return message; + } + + public StringBuilder getContent() { + return content; + } +} diff --git a/modules/test-ui/src/com/haulmont/cuba/ui/UITestStep.java b/modules/test-ui/src/com/haulmont/cuba/ui/UITestStep.java new file mode 100644 index 0000000000..0a49381c6f --- /dev/null +++ b/modules/test-ui/src/com/haulmont/cuba/ui/UITestStep.java @@ -0,0 +1,42 @@ +/* + * Copyright (c) 2011 Haulmont Technology Ltd. All Rights Reserved. + * Haulmont Technology proprietary and confidential. + * Use is subject to license terms. + */ + +package com.haulmont.cuba.ui; + +import java.util.ArrayList; +import java.util.List; + +/** + *

$Id$

+ * + * @author artamonov + */ +public final class UITestStep { + + private boolean success = true; + private String name; + private List logMessages = new ArrayList(); + + public UITestStep(String name) { + this.name = name; + } + + public String getName() { + return name; + } + + public List getLogMessages() { + return logMessages; + } + + public boolean isSuccess() { + return success; + } + + public void setSuccess(boolean success) { + this.success = success; + } +} diff --git a/settings.gradle b/settings.gradle index 7254e83c51..5624162286 100644 --- a/settings.gradle +++ b/settings.gradle @@ -5,7 +5,7 @@ */ include(':cuba-shared-lib', ':cuba-global', ':cuba-core', ':cuba-client', ':cuba-gui', ':cuba-web-toolkit', - ':cuba-web', ':cuba-desktop') + ':cuba-web', ':cuba-desktop', ':cuba-test-ui') rootProject.name = 'cuba' project(':cuba-shared-lib').projectDir = new File(settingsDir, 'modules/shared-lib') project(':cuba-global').projectDir = new File(settingsDir, 'modules/global') @@ -15,3 +15,4 @@ project(':cuba-gui').projectDir = new File(settingsDir, 'modules/gui') project(':cuba-web-toolkit').projectDir = new File(settingsDir, 'modules/web-toolkit') project(':cuba-web').projectDir = new File(settingsDir, 'modules/web') project(':cuba-desktop').projectDir = new File(settingsDir, 'modules/desktop') +project(':cuba-test-ui').projectDir = new File(settingsDir, 'modules/test-ui')