package org.radargun.reporting.html;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.io.PrintWriter;
import java.nio.file.Files;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Collection;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Properties;
import java.util.Set;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import freemarker.cache.ClassTemplateLoader;
import freemarker.template.DefaultObjectWrapper;
import freemarker.template.DefaultObjectWrapperBuilder;
import freemarker.template.Template;
import freemarker.template.TemplateModelException;
import org.radargun.config.Configuration;
import org.radargun.config.MasterConfig;
import org.radargun.config.Property;
import org.radargun.config.PropertyDelegate;
import org.radargun.logging.Log;
import org.radargun.logging.LogFactory;
import org.radargun.reporting.Report;
import org.radargun.reporting.Reporter;
import org.radargun.reporting.Timeline;
import org.radargun.reporting.commons.TestAggregations;
/**
* Reporter presenting the statistics and timelines in form of directory
* with several linked HTML pages and image files displayed on those pages.
*
* @author Radim Vansa <rvansa@redhat.com>
*/
public class HtmlReporter implements Reporter {
private static final Log log = LogFactory.getLog(HtmlReporter.class);
/**
* Shared executor used for long-running tasks when the report is generated.
*/
public static final ExecutorService executor = Executors.newFixedThreadPool(2 * Runtime.getRuntime().availableProcessors());
@Property(doc = "Directory to put the reports. Default is results/html.")
private String targetDir = "results" + File.separator + "html";
@PropertyDelegate(prefix = "testReport.")
private ReportDocument.Configuration testReportConfig = new ReportDocument.Configuration();
@PropertyDelegate(prefix = "timeline.")
private TimelineDocument.Configuration timelineConfig = new TimelineDocument.Configuration();
private Set<String> allTests = new LinkedHashSet<>();
private Collection<Report> reports;
@Override
public void run(MasterConfig masterConfig, Collection<Report> reports) {
this.reports = reports;
Set<String> allTests = new LinkedHashSet<>();
Set<String> combinedTests = new LinkedHashSet<>();
Map<String, List<Report.Test>> testsByName = new HashMap<>();
resolveCombinedTests(allTests, combinedTests);
resolveTestsByName(reports, allTests, combinedTests, testsByName);
this.allTests = allTests;
writeIndexDocument(masterConfig, reports);
writeTimelineDocuments(reports, Timeline.Category.Type.CUSTOM);
writeTimelineDocuments(reports, Timeline.Category.Type.SYSMONITOR);
writeTestReportDocuments(combinedTests, testsByName);
writeCombinedReportDocuments(testsByName);
writeNormalizedConfigDocuments(reports);
}
private void resolveCombinedTests(Set<String> allTests, Set<String> combinedTests) {
for (List<String> combination : testReportConfig.combinedTests) {
StringBuilder sb = new StringBuilder();
for (String testName : combination) {
combinedTests.add(testName);
if (sb.length() != 0) sb.append('_');
sb.append(testName);
}
allTests.add(sb.toString());
}
}
private void resolveTestsByName(Collection<Report> reports, Set<String> allTests, Set<String> combinedTests, Map<String, List<Report.Test>> testsByName) {
for (Report report : reports) {
for (Report.Test test : report.getTests()) {
List<Report.Test> list = testsByName.get(test.name);
if (list == null) {
list = new ArrayList<>();
testsByName.put(test.name, list);
}
list.add(test);
if (!combinedTests.contains(test.name)) {
allTests.add(test.name);
}
}
}
}
private void writeNormalizedConfigDocuments(Collection<Report> reports) {
for (Report report : reports) {
for (Configuration.Setup setup : report.getConfiguration().getSetups()) {
Set<Integer> slaves = report.getCluster().getSlaves(setup.group);
Set<String> normalized = new HashSet<>();
for (Map.Entry<Integer, Map<String, Properties>> entry : report.getNormalizedServiceConfigs().entrySet()) {
if (slaves.contains(entry.getKey()) && entry.getValue() != null) {
normalized.addAll(entry.getValue().keySet());
}
}
for (String config : normalized) {
NormalizedConfigDocument document = new NormalizedConfigDocument(
targetDir, report.getConfiguration().name, setup.group, report.getCluster(), config, report.getNormalizedServiceConfigs(), slaves);
document.createReportDirectory();
Map root = new HashMap();
root.put("normalized", document);
processTemplate(root, targetDir, document.getFileName(), "normalizedReport.ftl");
}
}
}
}
private void writeCombinedReportDocuments(Map<String, List<Report.Test>> testsByName) {
for (List<String> combined : testReportConfig.combinedTests) {
List<TestAggregations> testAggregations = new ArrayList<>();
StringBuilder sb = new StringBuilder();
for (String testName : combined) {
if (sb.length() != 0) sb.append('_');
sb.append(testName);
List<Report.Test> reportedTests = testsByName.get(testName);
if (reportedTests == null) {
log.warn("Test " + testName + " was not found!");
continue;
}
TestAggregations ta = new TestAggregations(testName, reportedTests);
testAggregations.add(ta);
}
if (testAggregations.isEmpty()) {
log.warn("No tests to combine");
return;
}
CombinedReportDocument testReport = new CombinedReportDocument(testAggregations, sb.toString(), combined, targetDir, testReportConfig);
testReport.createReportDirectory();
testReport.calculateClusterSizes();
testReport.createTestCharts();
Map root = new HashMap();
root.put("testReport", testReport);
root.put("enums", DefaultObjectWrapper.getDefaultInstance().getEnumModels());
processTemplate(root, targetDir, "test_" + testReport.testName + ".html", "testReport.ftl");
}
}
private void writeTestReportDocuments(Set<String> combinedTests, Map<String, List<Report.Test>> testsByName) {
for (Map.Entry<String, List<Report.Test>> entry : testsByName.entrySet()) {
if (combinedTests.contains(entry.getKey())) {
// do not write TestReportDocument for combined test
continue;
}
TestAggregations ta = new TestAggregations(entry.getKey(), entry.getValue());
TestReportDocument testReport = new TestReportDocument(ta, targetDir, testReportConfig);
testReport.createReportDirectory();
testReport.createTestCharts();
Map root = new HashMap();
root.put("testReport", testReport);
root.put("enums", DefaultObjectWrapper.getDefaultInstance().getEnumModels());
processTemplate(root, targetDir, "test_" + testReport.testName + ".html", "testReport.ftl");
}
}
private void writeTimelineDocuments(Collection<Report> reports, Timeline.Category.Type categoryType) {
for (Report report : reports) {
String configName = report.getConfiguration().name;
TimelineDocument timelineDocument = new TimelineDocument(timelineConfig, targetDir,
configName + "_" + report.getCluster().getClusterIndex(), configName + " on " + report.getCluster(), report.getTimelines(), categoryType, report.getCluster());
timelineDocument.createReportDirectory();
timelineDocument.createTestCharts();
Map root = new HashMap();
root.put("timelineDocument", timelineDocument);
root.put("categoryType", categoryType.toString());
exposeStaticMethods(root, "java.lang.String", "String");
processTemplate(root, targetDir, timelineDocument.getFileName(), "timelineReport.ftl");
}
}
private void writeIndexDocument(MasterConfig masterConfig, Collection<Report> reports) {
IndexDocument index = new IndexDocument(targetDir);
index.createReportDirectory();
index.writeMasterConfig(masterConfig);
index.prepareServiceConfigs(reports);
try {
copyResources("html/templates/", "style.css");
copyResources("html/templates/", "script.js");
copyResources("html/icons/", "ic_arrow_drop_down_black_24dp.png");
copyResources("html/icons/", "ic_arrow_drop_up_black_24dp.png");
} catch (IOException e) {
log.error("Failed to copy resources", e);
}
Map root = new HashMap();
root.put("reporter", this);
root.put("indexDocument", index);
processTemplate(root, targetDir, "index.html", "index.ftl");
}
/**
* Allows to use static methods in the template
*
* @param root map to which String will be added
* @param className full name of class which is to be exposed to the template e.g. java.lang.String
* @param exposedName name by which the class will be accessed in the template e.g. String
*/
private void exposeStaticMethods(Map root, String className, String exposedName) {
DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_23);
try {
root.put(exposedName, builder.build().getStaticModels().get(className));
} catch (TemplateModelException e) {
log.error("Error while getting static class", e);
}
}
/**
* Creates html file from template
*
* @param root Map of objects that are exposed to the template
* @param targetDir target directory for the html file
* @param fileName name of html file e.g. index.html
* @param templateName name of template file e.g. index.ftl
*/
public static void processTemplate(Map root, String targetDir, String fileName, String templateName) {
freemarker.template.Configuration cfg = initConfig();
try (PrintWriter printWriter = new PrintWriter(targetDir + File.separator + fileName)) {
Template reportTemplate = cfg.getTemplate(templateName);
reportTemplate.process(root, printWriter);
} catch (Exception e) {
log.error("Templating exception", e);
}
}
/**
* Copies files from resources to targetDir destination
* This is used to copy css, js and icon files to target dir
*
* @param path to file
* @param file to be copied
* @throws IOException if file can not be copied
*/
private void copyResources(String path, String file) throws IOException {
ClassLoader classLoader = getClass().getClassLoader();
try (InputStream source = classLoader.getResourceAsStream(path + file)) {
File destination = new File(targetDir + File.separator + file);
Files.copy(source, destination.toPath(), StandardCopyOption.REPLACE_EXISTING);
} catch (Exception e) {
log.error("Exception while copying resources", e);
}
}
/**
* Initializes Freemarker configuration
*
* @return created configuration
*/
public static freemarker.template.Configuration initConfig() {
freemarker.template.Configuration cfg = new freemarker.template.Configuration(freemarker.template.Configuration.VERSION_2_3_23);
cfg.setTemplateLoader(new ClassTemplateLoader(HtmlReporter.class, "/html/templates"));
DefaultObjectWrapperBuilder builder = new DefaultObjectWrapperBuilder(freemarker.template.Configuration.VERSION_2_3_23);
// grants access to public fields
builder.setExposeFields(true);
DefaultObjectWrapper bw = builder.build();
// allows to use ?api construct in templates
cfg.setAPIBuiltinEnabled(true);
// setting how booleans are displayed, default results in error
cfg.setBooleanFormat("True, False");
cfg.setObjectWrapper(bw);
return cfg;
}
/**
* The following methods are used in Freemarker templates
* e.g. method getPercentiles() can be used as getPercentiles() or percentiles in template
*/
public String getSystemProperty(String property) {
return System.getProperty(property);
}
public Set<String> getAllTests() {
return allTests;
}
public Collection<Report> getReports() {
return reports;
}
public boolean hasReportsWithValuesOfType(Timeline.Category.Type type) {
return reports.stream().anyMatch(r -> r.hasTimelineWithValuesOfType(type));
}
}