package com.sugarcrm.candybean.runner; import java.awt.GraphicsConfiguration; import java.awt.GraphicsEnvironment; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.nio.ByteBuffer; import java.nio.charset.Charset; import java.nio.file.Files; import java.nio.file.Paths; import java.util.ArrayList; import java.util.List; import java.util.Locale; import java.util.Map; import java.util.logging.Logger; import javax.xml.bind.JAXBContext; import javax.xml.bind.JAXBException; import javax.xml.bind.Marshaller; import javax.xml.bind.UnmarshalException; import javax.xml.bind.Unmarshaller; import com.sugarcrm.candybean.exceptions.CandybeanException; import org.apache.maven.plugins.surefire.report.ReportTestCase; import org.apache.maven.plugins.surefire.report.ReportTestSuite; import org.apache.maven.plugins.surefire.report.SurefireReportParser; import org.apache.maven.reporting.MavenReportException; import org.junit.Test; import org.junit.runner.Description; import org.junit.runner.Result; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunListener; import com.sugarcrm.candybean.automation.Candybean; import com.sugarcrm.candybean.configuration.Configuration; import com.sugarcrm.candybean.utilities.CandybeanLogger; import com.sugarcrm.candybean.utilities.SpecializedScreenRecorder; import com.sugarcrm.candybean.utilities.Utils; import com.sugarcrm.candybean.utilities.reporting.FailedTests; import com.sugarcrm.candybean.utilities.reporting.TestFailure; /** * A custom {@link RunListener} which includes callback routines for when any * {@link Test} annotated with {@link Record} is started. The listener will * record any failing tests. * * @author Shehryar Farooq */ public class TestRecorder extends RunListener { /* * A custom {@link ScreenRecorder} used to capture a video of the screen. * The {@link SpecializedScreenRecorder} can be used to configure specific * details of the recorded files such as location and name. */ private SpecializedScreenRecorder screenRecorder; /* * Singleton TestRecorder instance */ private static TestRecorder testRecorder; /* * Failed state of the current test */ private boolean testFailed = false; /* * Recording state */ private boolean recordInProgress = false; /* * Logger of the current test class */ private Logger logger; /* * The last failed test */ private Failure failure; /* * The JAXBContext entry point for marshaling TestFailure objects to XML */ private JAXBContext context; /* * File containing the details of the failed tests */ private File xmlFile; /* * Candybean configuration */ private Configuration config; /* * The default file location to store test results in XML */ private static final String FAILED_TEST_RESULTS_XML = "./target/candybean-reports/results/failedTestResults.xml"; /* * The default file location of the surefire results directory */ private static final String SUREFIRE_RESULTS_DIRECTORY = "./target/surefire-reports"; /* * The default path of where the candybean test results report will be generated */ private static final String CANDYBEAN_REPORT_PATH = "./target/candybean-reports/reports/CandybeanTestResults.html"; private static final String TEST_TEMPLATE_PATH = "./resources/html/testTemplate.html"; private static final String PACKAGE_TEMPLATE_PATH = "./resources/html/packageTemplate.html"; private static final String TEST_RESULTS_TEMPLATE_PATH = "./resources/html/testResultsTemplate.html"; private static final String PACKAGE_TABLE_TEMPLATE_PATH = "./resources/html/packageTableTemplate.html"; private static final String DATA_TABLE_INIT_TEMPLATE_PATH = "./resources/html/dataTableInitTemplate.html"; private TestRecorder() throws SecurityException, IOException, JAXBException, CandybeanException { super(); logger = Logger.getLogger(Candybean.class.getSimpleName()); this.context = JAXBContext.newInstance(FailedTests.class); String candybeanConfigStr = System.getProperty(Candybean.CONFIG_KEY, Candybean.getDefaultConfigFile()); this.config = new Configuration(new File(candybeanConfigStr)); this.xmlFile = createFile(config.getValue("testResultsXMLPath", FAILED_TEST_RESULTS_XML), false); } public static TestRecorder getInstance() throws SecurityException, IOException, JAXBException, CandybeanException { if(testRecorder == null) { testRecorder = new TestRecorder(); } return testRecorder; } @Override public void testStarted(Description description) throws Exception { Record record = description.getAnnotation(Record.class); this.testFailed = false; // Check to see if this test is annotated with Record if (record != null) { if(!recordInProgress){ logger.info("Recording started: " + description.getClassName() + "." + description.getMethodName()); // Start the recording startRecording(description.getClassName() + "-" + description.getMethodName()); recordInProgress = true; }else{ logger.info("Record already in progress"); } } } @Override public void testFinished(Description description) throws Exception { Record record = description.getAnnotation(Record.class); if (record != null && recordInProgress) { Duration duration = record.duration(); logger.info("Recording ended: " + description.getClassName() + "." + description.getMethodName()); // Stop the recording stopRecording(); recordInProgress = false; // Last recorded test List<File> recordedTests = this.screenRecorder .getCreatedMovieFiles(); File createdVideoFile = recordedTests.get(recordedTests.size() - 1); // Create failure object containing all the details for this failed // test if (testFailed) { TestFailure failedTest = new TestFailure(); Unmarshaller unmarshaller = context.createUnmarshaller(); FailedTests failedTests; try { failedTests = (FailedTests) unmarshaller.unmarshal(xmlFile); } catch (UnmarshalException e) { // The file doesn't contain any previous results, so we will // start with a clean file. failedTests = new FailedTests(); } failedTest.setClassName(description.getClassName()); failedTest.setMethodName(description.getMethodName()); failedTest.setPathToVideo(createdVideoFile.getCanonicalPath()); failedTest.setTestHeader(failure.getTestHeader()); failedTest.setTrace(failure.getTrace()); failedTest.setFailMessage(failure.getMessage()); if(!failedTests.getFailures().containsKey(failedTest.getTestHeader())){ failedTests.getFailures().put(failedTest.getTestHeader(),failedTest); } Marshaller marshal = context.createMarshaller(); marshal.marshal(failedTests, xmlFile); } // If the test failed, and user configured to record final moments, // cut the recording if (duration.equals(Duration.FINAL) || (duration.equals(Duration.FINAL_FAILED) && testFailed)) { logger.info("TODO: Cut the recording to its final few seconds"); // TODO: Cut the recording to final moments } else if (duration.equals(Duration.FINAL_FAILED) && !testFailed) { logger.info("Test passed, but we are only recording failed tests, deleting recording"); // If the test didn't fail and we are only recording final // moments of a failed test, delete the recording. if (recordedTests.size() > 0) { createdVideoFile.delete(); } } } } @Override public void testFailure(Failure failure) throws Exception { this.testFailed = true; this.failure = failure; super.testFailure(failure); } /** * Starts a recording of the screen * * @param testFileName * The name of the video file to create * @throws Exception */ private void startRecording(String testFileName) throws Exception { GraphicsConfiguration gc = GraphicsEnvironment .getLocalGraphicsEnvironment().getDefaultScreenDevice() .getDefaultConfiguration(); this.screenRecorder = new SpecializedScreenRecorder(gc, testFileName, config); this.screenRecorder.start(); } /** * Stops the current recording * * @throws Exception */ private void stopRecording() throws Exception { this.screenRecorder.stop(); } @Override public void testRunFinished(Result result) throws Exception { generateTestResultsReport(); super.testRunFinished(result); } /** * Generates the custom candybean report for all test results * @throws MavenReportException * @throws IOException * @throws JAXBException */ private void generateTestResultsReport() throws MavenReportException, IOException, JAXBException { File reportsDirectory = new File(config.getValue("surefireResultsDirectory", SUREFIRE_RESULTS_DIRECTORY)); if(reportsDirectory.exists()){ Unmarshaller unmarshaller = context.createUnmarshaller(); FailedTests failedTestList; try { failedTestList = (FailedTests) unmarshaller.unmarshal(xmlFile); } catch (UnmarshalException e) { // The file doesn't contain any previous results, so we will start // with a clean file. failedTestList = new FailedTests(); } List<File> reportsDirectories = new ArrayList<File>(); reportsDirectories.add(reportsDirectory); SurefireReportParser report = new SurefireReportParser( reportsDirectories, Locale.ENGLISH ); List<ReportTestSuite> suites = report.parseXMLReportFiles(); Map<String, String> summary = report.getSummary(suites); Map<String, List<ReportTestSuite>> packages = report.getSuitesGroupByPackage(suites); String baseTemplate = readFile(TEST_RESULTS_TEMPLATE_PATH, Charset.defaultCharset()); baseTemplate = baseTemplate.replace("${title}", config.getValue("testResultsReport.title", "")); baseTemplate = baseTemplate.replace("${summary.tests}", summary.get("totalTests")); baseTemplate = baseTemplate.replace("${summary.errors}", summary.get("totalErrors")); baseTemplate = baseTemplate.replace("${summary.failures}", summary.get("totalFailures")); baseTemplate = baseTemplate.replace("${summary.skipped}", summary.get("totalSkipped")); baseTemplate = baseTemplate.replace("${summary.success}", summary.get("totalPercentage")+"%"); baseTemplate = baseTemplate.replace("${summary.time}", Utils.calculateTime(Float.parseFloat(summary.get("totalElapsedTime").replace(",","")))); baseTemplate = baseTemplate.replace("${testsPerPage}", config.getValue("testResultsReport.testsPerPage")); StringBuilder packageMarkup = new StringBuilder(); StringBuilder packageTableMarkup = new StringBuilder(); StringBuilder summaryTests = new StringBuilder(); StringBuilder tableInits = new StringBuilder(); String packageTemplate = readFile(PACKAGE_TEMPLATE_PATH, Charset.defaultCharset()); String packageTableTemplate = readFile(PACKAGE_TABLE_TEMPLATE_PATH, Charset.defaultCharset()); String dataTableInitTempalte = readFile(DATA_TABLE_INIT_TEMPLATE_PATH, Charset.defaultCharset()); String entryTemplate = readFile("./resources/html/videoEntryTemplate.html", Charset.defaultCharset()); String entryFailTemplate = readFile("./resources/html/failedEntryTemplate.html", Charset.defaultCharset()); StringBuilder completeClassTableMarkup = new StringBuilder(); StringBuilder completeClassMarkup = new StringBuilder(); StringBuilder completeFailedTestPillsMarkup = new StringBuilder(); StringBuilder completeFailedTestContentMarkup = new StringBuilder(); for(String pkg: packages.keySet()){ String pkgTemplate = packageTemplate; String pkgTableTemplate = packageTableTemplate; String dataTblInitTempalte = dataTableInitTempalte; List<ReportTestSuite> packageSuites = packages.get(pkg); Map<String, String> summaryForPackage = report.getSummary(packageSuites); StringBuilder packageTests = new StringBuilder(); dataTblInitTempalte = dataTblInitTempalte.replace("${tableId}", pkg.replace(".", "")); dataTblInitTempalte = dataTblInitTempalte.replace("${testsPerPage}", config.getValue("testResultsReport.testsPerPage")); pkgTableTemplate = pkgTableTemplate.replace("${packageId}", pkg.replace(".", "")); pkgTableTemplate = pkgTableTemplate.replace("${package.tests}", summaryForPackage.get("totalTests")); pkgTableTemplate = pkgTableTemplate.replace("${package.errors}", summaryForPackage.get("totalErrors")); pkgTableTemplate = pkgTableTemplate.replace("${package.failures}", summaryForPackage.get("totalFailures")); pkgTableTemplate = pkgTableTemplate.replace("${package.skipped}", summaryForPackage.get("totalSkipped")); pkgTableTemplate = pkgTableTemplate.replace("${package.success}", summaryForPackage.get("totalPercentage")+"%"); pkgTableTemplate = pkgTableTemplate.replace("${package.time}", Utils.calculateTime(Float.parseFloat(summaryForPackage.get("totalElapsedTime").replace(",","")))); pkgTemplate = pkgTemplate.replace("${packageId}", pkg.replace(".", "")); pkgTemplate = pkgTemplate.replace("${package.name}", pkg); StringBuilder classTableInitMarkup = new StringBuilder(); String classTemplate = readFile(PACKAGE_TEMPLATE_PATH, Charset.defaultCharset()); String classTableTemplate = readFile(PACKAGE_TABLE_TEMPLATE_PATH, Charset.defaultCharset()); String classDataTableInitTempalte = readFile(DATA_TABLE_INIT_TEMPLATE_PATH, Charset.defaultCharset()); for(ReportTestSuite testSuite: packageSuites){ String clsTemplate = classTemplate; String clsTableTemplate = classTableTemplate; String clsDataTblInitTempalte = classDataTableInitTempalte; int numberOfTests = testSuite.getNumberOfTests(); int numberOfErrors = testSuite.getNumberOfErrors(); int numberOfFailures = testSuite.getNumberOfFailures(); int numberOfSkipped = testSuite.getNumberOfSkipped(); clsDataTblInitTempalte = clsDataTblInitTempalte.replace("${tableId}", testSuite.getFullClassName().replace(".", "")); clsDataTblInitTempalte = clsDataTblInitTempalte.replace("${testsPerPage}", config.getValue("testResultsReport.testsPerPage")); clsTableTemplate = clsTableTemplate.replace("${packageId}", testSuite.getFullClassName().replace(".", "")); clsTableTemplate = clsTableTemplate.replace("${package.tests}", String.valueOf(testSuite.getNumberOfTests())); clsTableTemplate = clsTableTemplate.replace("${package.errors}", String.valueOf(testSuite.getNumberOfErrors())); clsTableTemplate = clsTableTemplate.replace("${package.failures}", String.valueOf(testSuite.getNumberOfFailures())); clsTableTemplate = clsTableTemplate.replace("${package.skipped}", String.valueOf(testSuite.getNumberOfSkipped())); clsTableTemplate = clsTableTemplate.replace("${package.success}", String.valueOf(report.computePercentage(numberOfTests, numberOfErrors, numberOfFailures, numberOfSkipped))+"%"); clsTableTemplate = clsTableTemplate.replace("${package.time}", Utils.calculateTime(testSuite.getTimeElapsed())); clsTemplate = clsTemplate.replace("${packageId}", testSuite.getFullClassName().replace(".", "")); clsTemplate = clsTemplate.replace("${package.name}", testSuite.getName()); StringBuilder testMarkup = new StringBuilder(); String testTemplate = readFile(TEST_TEMPLATE_PATH, Charset.defaultCharset()); String baseListItemTemplate = readFile(PACKAGE_TEMPLATE_PATH, Charset.defaultCharset()); for(ReportTestCase testCase: testSuite.getTestCases()){ String listItemTemplate = baseListItemTemplate; boolean testPassed = true; String tstTemplate = testTemplate; Map<String, Object> result = testCase.getFailure(); if(result == null){ tstTemplate = tstTemplate.replace("${test.result}", "Success"); tstTemplate = tstTemplate.replace("${result}", "pass"); }else if(result.get("type").equals("skipped")){ tstTemplate = tstTemplate.replace("${test.result}", "Skipped"); tstTemplate = tstTemplate.replace("${result}", "skip"); }else{ tstTemplate = tstTemplate.replace("${test.result}", "Failed!"); tstTemplate = tstTemplate.replace("${result}", "fail"); testPassed = false; } tstTemplate = tstTemplate.replace("${test.name}", testCase.getName()); tstTemplate = tstTemplate.replace("${test.class}", testCase.getClassName()); tstTemplate = tstTemplate.replace("${test.package}", testCase.getFullClassName().replace(testCase.getClassName(), "")); float time = testCase.getTime(); if(time == 0){ tstTemplate = tstTemplate.replace("${test.runtime}", "Did not run"); }else{ tstTemplate = tstTemplate.replace("${test.runtime}", Utils.calculateTime(testSuite.getTimeElapsed())); } testMarkup.append(tstTemplate); listItemTemplate = listItemTemplate.replace("${packageId}", "failed" + testCase.getName() + testCase.getClassName()); listItemTemplate = listItemTemplate.replace("${package.name}", testCase.getFullName()); if(!testPassed){ if(failedTestList.getFailures().containsKey(testCase.getName() + "(" + testCase.getFullClassName() + ")")) { String entryVideoTemplate = entryTemplate; TestFailure videoInformation = failedTestList.getFailures().get(testCase.getName() + "(" + testCase.getFullClassName() + ")"); entryVideoTemplate = entryVideoTemplate.replace("${failure.header}", videoInformation.getPathToVideo()); entryVideoTemplate = entryVideoTemplate.replace("${failure.pathToVideo}", videoInformation.getPathToVideo()); entryVideoTemplate = entryVideoTemplate.replace("${failure.stacktrace}", videoInformation.getTrace()); entryVideoTemplate = entryVideoTemplate.replace("${packageId}", "failed"+testCase.getName()+testCase.getClassName()); completeFailedTestContentMarkup.append(entryVideoTemplate); } else { //Video for this failed test was not recorded String entryFailureTemplate = entryFailTemplate; entryFailureTemplate = entryFailureTemplate.replace("${packageId}", "failed"+testCase.getName()+testCase.getClassName()); entryFailureTemplate = entryFailureTemplate.replace("${failure.exception}", testCase.getFailure().get("type") == null?"Not Specified":testCase.getFailure().get("type").toString()); entryFailureTemplate = entryFailureTemplate.replace("${failure.message}", testCase.getFailure().get("message") == null?"Not Specified":testCase.getFailure().get("message").toString()); entryFailureTemplate = entryFailureTemplate.replace("${failure.detail}", testCase.getFailure().get("detail") == null?"Not Specified":testCase.getFailure().get("detail").toString()); completeFailedTestContentMarkup.append(entryFailureTemplate); } completeFailedTestPillsMarkup.append(listItemTemplate); } } summaryTests.append(testMarkup.toString()); packageTests.append(testMarkup.toString()); clsTableTemplate = clsTableTemplate.replace("${packageTests}", testMarkup.toString()); completeClassTableMarkup.append(clsTableTemplate.toString()); //classMarkup.append(clsTemplate); completeClassMarkup.append(clsTemplate); classTableInitMarkup.append(clsDataTblInitTempalte); } pkgTableTemplate = pkgTableTemplate.replace("${packageTests}", packageTests.toString()); packageTableMarkup.append(pkgTableTemplate); packageMarkup.append(pkgTemplate); tableInits.append(dataTblInitTempalte); tableInits.append(classTableInitMarkup.toString()); } baseTemplate = baseTemplate.replace("${summaryTests}", summaryTests.toString()); baseTemplate = baseTemplate.replace("${packageList}", packageMarkup.toString()); baseTemplate = baseTemplate.replace("${packageTableList}", packageTableMarkup.toString()); baseTemplate = baseTemplate.replace("${classList}", completeClassMarkup.toString()); baseTemplate = baseTemplate.replace("${classTableList}", completeClassTableMarkup.toString()); baseTemplate = baseTemplate.replace("${failedTestList}", completeFailedTestPillsMarkup.toString()); baseTemplate = baseTemplate.replace("${failedTestInfoList}", completeFailedTestContentMarkup.toString()); baseTemplate = baseTemplate.replace("${table.init}", tableInits.toString()); FileWriter fstream = new FileWriter(createFile(config.getValue( "testResultsReportPath", CANDYBEAN_REPORT_PATH), true)); BufferedWriter out = new BufferedWriter(fstream); out.write(baseTemplate); out.close(); }else{ logger.warning("No surefire XML reports found in the surefire results directory, skipping html report generation"); return; } } /** * Reads the contents of a file * @param path Path to file * @param encoding Encoding of the file * @return The contents of the file * @throws IOException */ private String readFile(String path, Charset encoding) throws IOException { byte[] encoded = Files.readAllBytes(Paths.get(path)); return encoding.decode(ByteBuffer.wrap(encoded)).toString(); } /** * Creates a file at the given path * @param path * @return * @throws IOException */ private File createFile(String path, boolean replace) throws IOException{ File f = new File(path); if (!f.getParentFile().exists()) { f.getParentFile().mkdirs(); } if (f.exists()) { if (replace) { f.delete(); f.createNewFile(); } } else { f.createNewFile(); } return f; } }