/* * Copyright Technophobia Ltd 2012 * * This file is part of Substeps. * * Substeps is free software: you can redistribute it and/or modify * it under the terms of the GNU Lesser General Public License as published by * the Free Software Foundation, either version 3 of the License, or * (at your option) any later version. * * Substeps is distributed in the hope that it will be useful, * but WITHOUT ANY WARRANTY; without even the implied warranty of * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the * GNU Lesser General Public License for more details. * * You should have received a copy of the GNU Lesser General Public License * along with Substeps. If not, see <http://www.gnu.org/licenses/>. */ package com.technophobia.substeps.report; import java.io.BufferedWriter; import java.io.File; import java.io.FileWriter; import java.io.IOException; import java.io.InputStream; import java.io.Writer; import java.net.JarURLConnection; import java.net.URISyntaxException; import java.net.URL; import java.net.URLConnection; import java.nio.charset.Charset; import java.text.SimpleDateFormat; import java.util.Collections; import java.util.Date; import java.util.HashMap; import java.util.Map; import java.util.Properties; import java.util.jar.JarEntry; import java.util.jar.JarFile; import org.apache.commons.io.FileUtils; import org.apache.commons.io.IOUtils; import org.apache.commons.lang3.StringUtils; import org.apache.velocity.VelocityContext; import org.apache.velocity.app.VelocityEngine; import org.apache.velocity.exception.MethodInvocationException; import org.apache.velocity.exception.ParseErrorException; import org.apache.velocity.exception.ResourceNotFoundException; import org.junit.Assert; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import sun.net.www.protocol.file.FileURLConnection; import com.google.common.io.Files; import com.technophobia.substeps.execution.ExecutionResult; import com.technophobia.substeps.execution.node.ExecutionNode; import com.technophobia.substeps.execution.node.RootNode; /** * @author ian */ public class DefaultExecutionReportBuilder extends ExecutionReportBuilder { private final Logger log = LoggerFactory.getLogger(DefaultExecutionReportBuilder.class); private final Properties velocityProperties = new Properties(); public static final String FEATURE_REPORT_FOLDER = "feature_report"; private static final String SCREENSHOT_FOLDER = "screenshots"; public static final String JSON_DATA_FILENAME = "report_data.json"; public static final String JSON_DETAIL_DATA_FILENAME = "detail_data.js"; private static final String DEFAULT_REPORT_TITLE = "Substep Test Execution Report"; private static final String JSON_STATS_DATA_FILENAME = "susbteps-stats.js"; private static Map<ExecutionResult, String> resultToImageMap = new HashMap<ExecutionResult, String>(); private final ReportData data = new ReportData(); static { resultToImageMap.put(ExecutionResult.PASSED, "img/PASSED.png"); resultToImageMap.put(ExecutionResult.NOT_RUN, "img/NOT_RUN.png"); resultToImageMap.put(ExecutionResult.PARSE_FAILURE, "img/PARSE_FAILURE.png"); resultToImageMap.put(ExecutionResult.FAILED, "img/FAILED.png"); } /** * @parameter default-value = ${project.build.directory} */ private File outputDirectory; /** * @parameter default-value = "Substeps report" */ private String reportTitle; public DefaultExecutionReportBuilder() { this.velocityProperties.setProperty("resource.loader", "class"); this.velocityProperties.setProperty("class.resource.loader.class", "org.apache.velocity.runtime.resource.loader.ClasspathResourceLoader"); } @Override public void setOutputDirectory(final File outputDirectory) { this.outputDirectory = outputDirectory; } @Override public void buildReport() { this.log.debug("Build report in: " + this.outputDirectory.getAbsolutePath()); final File reportDir = new File(this.outputDirectory + File.separator + FEATURE_REPORT_FOLDER); final File screenshotDirectory = new File(reportDir, SCREENSHOT_FOLDER); try { this.log.debug("trying to create: " + reportDir.getAbsolutePath()); if (reportDir.exists()) { FileUtils.deleteDirectory(reportDir); } Assert.assertTrue("failed to create directory: " + reportDir, reportDir.mkdirs()); copyStaticResources(reportDir); buildMainReport(this.data, reportDir); TreeJsonBuilder.writeTreeJson(this.data, new File(reportDir, JSON_DATA_FILENAME)); DetailedJsonBuilder.writeDetailJson(this.data, SCREENSHOT_FOLDER, new File(reportDir, JSON_DETAIL_DATA_FILENAME)); buildStatsJSON(this.data, reportDir); for (final ExecutionNode rootNode : this.data.getRootNodes()) { ScreenshotWriter.writeScreenshots(screenshotDirectory, rootNode); } } catch (final IOException ex) { this.log.error("IOException: ", ex); } catch (final URISyntaxException ex) { this.log.error("URISyntaxException: ", ex); } } /** * @param data * @param reportDir */ private void buildStatsJSON(final ReportData data, final File reportDir) throws IOException { final File jsonFile = new File(reportDir, JSON_STATS_DATA_FILENAME); final ExecutionStats stats = new ExecutionStats(); stats.buildStats(data); final BufferedWriter writer = Files.newWriter(jsonFile, Charset.defaultCharset()); try { buildStatsJSON(stats, writer); } finally { writer.close(); } } /** * @param stats * @param writer */ private void buildStatsJSON(final ExecutionStats stats, final BufferedWriter writer) throws IOException { writer.append("var featureStatsData = ["); boolean first = true; for (final TestCounterSet stat : stats.getSortedList()) { if (!first) { writer.append(",\n"); } writer.append("[\"").append(stat.getTag()).append("\","); writer.append("\"").append(Integer.toString(stat.getFeatureStats().getCount())).append("\","); writer.append("\"").append(Integer.toString(stat.getFeatureStats().getRun())).append("\","); writer.append("\"").append(Integer.toString(stat.getFeatureStats().getPassed())).append("\","); writer.append("\"").append(Integer.toString(stat.getFeatureStats().getFailed())).append("\","); writer.append("\"").append(Double.toString(stat.getFeatureStats().getSuccessPc())).append("\"]"); first = false; } writer.append("];\n"); writer.append("var scenarioStatsData = ["); first = true; for (final TestCounterSet stat : stats.getSortedList()) { if (!first) { writer.append(",\n"); } writer.append("[\"").append(stat.getTag()).append("\","); writer.append("\"").append(Integer.toString(stat.getScenarioStats().getCount())).append("\","); writer.append("\"").append(Integer.toString(stat.getScenarioStats().getRun())).append("\","); writer.append("\"").append(Integer.toString(stat.getScenarioStats().getPassed())).append("\","); writer.append("\"").append(Integer.toString(stat.getScenarioStats().getFailed())).append("\","); writer.append("\"").append(Double.toString(stat.getScenarioStats().getSuccessPc())).append("\"") .append("]"); first = false; } writer.append("];\n"); } /** * @param reportDir * @throws IOException */ private void copyStaticResources(final File reportDir) throws URISyntaxException, IOException { this.log.debug("Copying static resources to: " + reportDir.getAbsolutePath()); final URL staticURL = getClass().getResource("/static"); if (staticURL == null) { throw new IllegalStateException("Failed to copy static resources for report. URL for resources is null."); } copyResourcesRecursively(staticURL, reportDir); } private void buildMainReport(final ReportData data, final File reportDir) throws IOException { this.log.debug("Building main report file."); final VelocityContext vCtx = new VelocityContext(); final String vml = "report_frame.vm"; final ExecutionStats stats = new ExecutionStats(); stats.buildStats(data); final SimpleDateFormat sdf = new SimpleDateFormat("EEE dd MMM yyyy HH:mm"); final String dateTimeStr = sdf.format(new Date()); vCtx.put("stats", stats); vCtx.put("dateTimeStr", dateTimeStr); if (StringUtils.isEmpty(this.reportTitle)) { this.reportTitle = DEFAULT_REPORT_TITLE; } vCtx.put("reportTitle", this.reportTitle); renderAndWriteToFile(reportDir, vCtx, vml, "report_frame.html"); } private void renderAndWriteToFile(final File reportDir, final VelocityContext vCtx, final String vm, final String targetFilename) throws IOException { final Writer writer = new BufferedWriter(new FileWriter(new File(reportDir, targetFilename))); final VelocityEngine velocityEngine = new VelocityEngine(); try { velocityEngine.init(this.velocityProperties); velocityEngine.getTemplate("templates/" + vm).merge(vCtx, writer); } catch (final ResourceNotFoundException e) { throw new RuntimeException(e); } catch (final ParseErrorException e) { throw new RuntimeException(e); } catch (final MethodInvocationException e) { throw new RuntimeException(e); } catch (final IOException e) { throw new RuntimeException(e); } catch (final Exception e) { throw new RuntimeException(e); } finally { try { if (writer != null) { writer.close(); } } catch (final IOException e) { this.log.error("IOException: ", e); } } } public void copyResourcesRecursively(final URL originUrl, final File destination) throws IOException { final URLConnection urlConnection = originUrl.openConnection(); if (urlConnection instanceof JarURLConnection) { copyJarResourcesRecursively(destination, (JarURLConnection) urlConnection); } else if (urlConnection instanceof FileURLConnection) { FileUtils.copyDirectory(new File(originUrl.getPath()), destination); } else { throw new RuntimeException("URLConnection[" + urlConnection.getClass().getSimpleName() + "] is not a recognized/implemented connection type."); } } public void copyJarResourcesRecursively(final File destination, final JarURLConnection jarConnection) throws IOException { final JarFile jarFile = jarConnection.getJarFile(); for (final JarEntry entry : Collections.list(jarFile.entries())) { if (entry.getName().startsWith(jarConnection.getEntryName())) { final String fileName = StringUtils.removeStart(entry.getName(), jarConnection.getEntryName()); if (!entry.isDirectory()) { InputStream entryInputStream = null; try { entryInputStream = jarFile.getInputStream(entry); FileUtils.copyInputStreamToFile(entryInputStream, new File(destination, fileName)); } finally { IOUtils.closeQuietly(entryInputStream); } } else { new File(destination, fileName).mkdirs(); } } } } @Override public void addRootExecutionNode(final RootNode node) { this.data.addRootExecutionNode(node); } }