changeset 173:6b6ac47a811f

Added new reporter (generating report from test logs) slightly modified existing templates and stylesheet (foldable details)
author Zdenek Zambersky <zzambers@redhat.com>
date Thu, 19 Mar 2015 12:44:59 +0100
parents 661ca2d7795f
children 4d8930e19c50
files Makefile src/org/thermostat/qa/reporter/ResultsGraphGenerator.java src/org/thermostat/qa2/framework/ThermostatQAConfig.java src/org/thermostat/qa2/framework/utils/FileUtilities.java src/org/thermostat/qa2/reporter/Generator.java src/org/thermostat/qa2/reporter/LogParser.java src/org/thermostat/qa2/reporter/Reporter.java src/org/thermostat/qa2/reporter/result/TestClassResult.java src/org/thermostat/qa2/reporter/result/TestMethodResult.java src/org/thermostat/qa2/reporter/result/TestRunResult.java templates/graph.html templates/hist.html templates/index.html templates/log.html templates/style.css
diffstat 15 files changed, 910 insertions(+), 31 deletions(-) [+]
line wrap: on
line diff
--- a/Makefile	Thu Mar 19 12:25:06 2015 +0100
+++ b/Makefile	Thu Mar 19 12:44:59 2015 +0100
@@ -187,27 +187,16 @@
 	perl $(JAPI_COMPILANCE_CHECKER_DIR)/japi-compliance-checker.pl -lib Thermostat -old $(BUILD_DIR)/thermostat-other.xml -new $(BUILD_DIR)/thermostat-tested.xml || true
 
 #### reports & javadoc ####
-	
-report:	$(CLASSES_DIR) flotr javadoc
+
+report:	$(CLASSES_DIR) flotr
 	mkdir -p $(REPORT_DIR)
 	cp -u $(TEMPLATE_DIR)/style.css $(REPORT_DIR)/style.css
 	cp -r $(FLOTR_DIR) $(REPORT_DIR)
-	$(JAVA) -cp $(CLASSES_DIR) org.thermostat.qa.reporter.Reporter -template-dir=$(TEMPLATE_DIR) -log-dir=$(LOGS_DIR) -report-dir=$(REPORT_DIR) -date=$(DATE) -tests="$(TESTS)"
-	sed -i 's/ignored/not applicable/g' $(REPORT_DIR)/log_$(DATE).html 
-	sed -i 's/IGNORED/NOT APPLICABLE/g' $(REPORT_DIR)/log_$(DATE).html 
-	links -dump $(REPORT_DIR)/index.html 2>/dev/null > $(REPORT_DIR)/$(DAILY_REPORT_NAME) || \
-	w3m   -dump $(REPORT_DIR)/index.html 2>/dev/null > $(REPORT_DIR)/$(DAILY_REPORT_NAME) || \
-	lynx  -dump $(REPORT_DIR)/index.html -force_html 2>/dev/null > $(REPORT_DIR)/$(DAILY_REPORT_NAME)
-	echo "" >> $(REPORT_DIR)/$(DAILY_REPORT_NAME)
-	echo "" >> $(REPORT_DIR)/$(DAILY_REPORT_NAME)
-	links -dump $(REPORT_DIR)/log_$(DATE).html 2>/dev/null >> $(REPORT_DIR)/$(DAILY_REPORT_NAME) || \
-	w3m   -dump $(REPORT_DIR)/log_$(DATE).html 2>/dev/null >> $(REPORT_DIR)/$(DAILY_REPORT_NAME) || \
-	lynx  -dump $(REPORT_DIR)/log_$(DATE).html -force_html 2>/dev/null >> $(REPORT_DIR)/$(DAILY_REPORT_NAME)
-	echo "" >> $(REPORT_DIR)/$(DAILY_REPORT_NAME)
-	echo "" >> $(REPORT_DIR)/$(DAILY_REPORT_NAME)
-	links -dump $(REPORT_DIR)/hist_10.html 2>/dev/null >> $(REPORT_DIR)/$(DAILY_REPORT_NAME) || \
-	w3m   -dump $(REPORT_DIR)/hist_10.html 2>/dev/null >> $(REPORT_DIR)/$(DAILY_REPORT_NAME) || \
-	lynx  -dump $(REPORT_DIR)/hist_10.html -force_html 2>/dev/null >> $(REPORT_DIR)/$(DAILY_REPORT_NAME)
+	$(JAVA) -cp $(CLASSES_DIR) \
+	-Dthermostat.version=$(THERMOSTAT_VERSION) \
+	-Dlogs.path=$(LOGS_DIR) \
+	-Dreports.path=$(REPORT_DIR) \
+	org.thermostat.qa2.reporter.Reporter
 	
 javadoc:
 	mkdir -p $(JAVADOC_DIR)
--- a/src/org/thermostat/qa/reporter/ResultsGraphGenerator.java	Thu Mar 19 12:25:06 2015 +0100
+++ b/src/org/thermostat/qa/reporter/ResultsGraphGenerator.java	Thu Mar 19 12:44:59 2015 +0100
@@ -84,7 +84,7 @@
      * @param string
      * @throws IOException 
      */
-    private void generateGraph(TestResult testResult, Dimension graphSize, String fileName) throws IOException {
+    public void generateGraph(TestResult testResult, Dimension graphSize, String fileName) throws IOException {
         setupGeometry(graphSize);
         BufferedImage bitmap = createBitmap();
         setGraphics(bitmap);
--- a/src/org/thermostat/qa2/framework/ThermostatQAConfig.java	Thu Mar 19 12:25:06 2015 +0100
+++ b/src/org/thermostat/qa2/framework/ThermostatQAConfig.java	Thu Mar 19 12:44:59 2015 +0100
@@ -47,6 +47,8 @@
     public static int webStoragePort = 8080;
     public static String gnomeKeyringConfigDir;
     public static String backupDir;
+    public static String logsDir;
+    public static String reportDir;
 
     public static Login mongoLogin = new Login("mongodevuser", "mongodevpassword");
     public static Login agentLogin = new Login("agent-tester", "heslo1");
@@ -79,6 +81,9 @@
 
         gnomeKeyringConfigDir = System.getProperty("gnome-keyring.config.path");
         backupDir = System.getProperty("backup.path");
+        
+        logsDir = System.getProperty("logs.path");
+        reportDir = System.getProperty("reports.path");
     }
 
     static String getPatternsDir() {
--- a/src/org/thermostat/qa2/framework/utils/FileUtilities.java	Thu Mar 19 12:25:06 2015 +0100
+++ b/src/org/thermostat/qa2/framework/utils/FileUtilities.java	Thu Mar 19 12:44:59 2015 +0100
@@ -33,8 +33,10 @@
 import java.io.BufferedReader;
 import java.io.File;
 import java.io.FileNotFoundException;
+import java.io.FileOutputStream;
 import java.io.FileReader;
 import java.io.IOException;
+import java.io.PrintStream;
 import java.util.ArrayList;
 import java.util.List;
 import java.util.logging.Level;
@@ -94,4 +96,22 @@
         }
     }
 
+    public static void printLineListToFile(String file, List<String> list) throws FileNotFoundException  {
+        FileOutputStream fos = null;
+        PrintStream ps = null;
+
+        try {
+            fos = new FileOutputStream(file);
+            ps = new PrintStream(fos);
+            for (String s : list) {
+                ps.println(s);
+            }
+            ps.flush();
+        } finally {
+            if (ps != null) {
+                ps.close();
+            }
+        }
+    }
+
 }
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/thermostat/qa2/reporter/Generator.java	Thu Mar 19 12:44:59 2015 +0100
@@ -0,0 +1,355 @@
+/*
+ ThermostatQA - test framework for Thermostat Monitoring Tool
+
+ Copyright 2015 Red Hat, Inc.
+
+ This file is part of ThermostatQA
+
+ ThermostatQA is distributed under the GNU General Public License,
+ version 2 or any later version (with a special exception described
+ below, commonly known as the "Classpath Exception").
+
+ A copy of GNU General Public License (GPL) is included in this
+ distribution, in the file COPYING.
+
+ Linking ThermostatQA code with other modules is making a combined work
+ based on ThermostatQA.  Thus, the terms and conditions of the GPL
+ cover the whole combination.
+
+ As a special exception, the copyright holders of ThermostatQA give you
+ permission to link this code with independent modules to produce an
+ executable, regardless of the license terms of these independent
+ modules, and to copy and distribute the resulting executable under
+ terms of your choice, provided that you also meet, for each linked
+ independent module, the terms and conditions of the license of that
+ module.  An independent module is a module which is not derived from
+ or based on ThermostatQA code.  If you modify ThermostatQA, you may
+ extend this exception to your version of the software, but you are
+ not obligated to do so.  If you do not wish to do so, delete this
+ exception statement from your version.
+ */
+package org.thermostat.qa2.reporter;
+
+import org.thermostat.qa2.reporter.result.TestMethodResult;
+import org.thermostat.qa2.reporter.result.TestClassResult;
+import org.thermostat.qa2.reporter.result.TestRunResult;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.thermostat.qa2.framework.TestRunner;
+import org.thermostat.qa2.framework.TestResult;
+import org.thermostat.qa2.framework.utils.FileUtilities;
+
+/**
+ *
+ * @author Zdeněk Žamberský
+ */
+public class Generator {
+
+    public static final String resultsUrlPattern = "${RESULTS_URL}";
+    public static final String datePattern = "${DATE}";
+    public static final String testCountPattern = "${TEST_COUNT}";
+
+    public static final String passedPattern = "${PASSED}";
+    public static final String failedPattern = "${FAILED}";
+    public static final String errorPattern = "${ERROR}";
+    public static final String ignoredPattern = "${IGNORED}";
+
+    public static final String osNamePattern = "${OS_NAME}";
+    public static final String osVersionPattern = "${OS_VER}";
+    public static final String osArchPattern = "${OS_ARCH}";
+
+    public static final String javaVersionPattern = "${JAVA_VERSION}";
+    public static final String vmNamePattern = "${VM_NAME}";
+    public static final String vmVersionPattern = "${VM_VERSION}";
+
+    public static final String sumaryPattern = "${SUMMARY}";
+    public static final String resultsPattern = "${RESULTS}";
+    public static final String tableDataPattern = "${TABLE_DATA}";
+
+    public static final String graphDataNonePattern = "${GRAPH_DATA_NONE}";
+    public static final String graphDataPassedPattern = "${GRAPH_DATA_PASSED}";
+    public static final String graphDataFailedPattern = "${GRAPH_DATA_FAILED}";
+    public static final String graphDataErrorPattern = "${GRAPH_DATA_ERROR}";
+    public static final String graphDataIgnoredPattern = "${GRAPH_DATA_IGNORED}";
+
+    public static final String indexPageTemplate = "templates/index.html";
+    public static final String logPageTemplate = "templates/log.html";
+    public static final String historyPageTemplate = "templates/hist.html";
+    public static final String graphPageTemplate = "templates/graph.html";
+
+    ////////////////////
+    //// INDEX PAGE ////
+    ////////////////////
+    public static void generateIndexPage(List<String> output, TestRunResult run) throws IOException {
+        Map<String, List<String>> replacements = new HashMap();
+        addReplacementToMap(replacements, resultsUrlPattern, "log_" + run.date + ".xhtml");
+        addReplacementToMap(replacements, datePattern, run.date);
+        addReplacementToMap(replacements, passedPattern, Integer.toString(run.passedCount));
+        addReplacementToMap(replacements, failedPattern, Integer.toString(run.failedCount));
+        addReplacementToMap(replacements, errorPattern, Integer.toString(run.errorCount));
+        addReplacementToMap(replacements, ignoredPattern, Integer.toString(run.ignoredCount));
+        addReplacementToMap(replacements, osNamePattern, System.getProperty("os.name"));
+        addReplacementToMap(replacements, osVersionPattern, System.getProperty("os.version"));
+        addReplacementToMap(replacements, osArchPattern, System.getProperty("os.arch"));
+        addReplacementToMap(replacements, javaVersionPattern, System.getProperty("java.version"));
+        addReplacementToMap(replacements, vmNamePattern, System.getProperty("java.vm.name"));
+        addReplacementToMap(replacements, vmVersionPattern, System.getProperty("java.vm.version"));
+        List<String> template = FileUtilities.getLineListFromFile(indexPageTemplate);
+        replacePatterns(template, output, replacements);
+    }
+
+    //////////////////
+    //// LOG PAGE ////
+    //////////////////
+    public static void generateLogPage(List<String> output, TestRunResult run) throws IOException {
+        List<String> dateText = new ArrayList();
+        dateText.add(run.date);
+        List<String> summaryText = new ArrayList();
+        generateLogSummary(summaryText, run);
+        List<String> resultsText = new ArrayList();
+        generateLogTable(resultsText, run);
+        Map<String, List<String>> replacements = new HashMap();
+        replacements.put(datePattern, dateText);
+        replacements.put(sumaryPattern, summaryText);
+        replacements.put(resultsPattern, resultsText);
+        List<String> template = FileUtilities.getLineListFromFile(logPageTemplate);
+        replacePatterns(template, output, replacements);
+    }
+
+    public static void generateLogSummary(List<String> output, TestRunResult run) {
+        for (TestClassResult test : run.results) {
+            output.add("<tr><td><a href='#" + test.name + "'>" + test.name + "</a></td></tr>");
+        }
+    }
+
+    public static void generateLogTable(List<String> output, TestRunResult run) {
+        for (TestClassResult test : run.results) {
+            String testName = test.name;
+            output.add("<tr id='" + testName + "' ><td class='table-header' colspan='2'>" + testName + "</td></tr>");
+            for (TestMethodResult method : test.methods) {
+                TestResult result = method.result;
+                String resultString = result.toString();
+                String resultLine = "<tr id='" + testName + "." + method.name + "' >";
+                resultLine += result == TestResult.PASSED ? "<td>" : "<td class='" + resultString.toLowerCase() + "-header'" + ">";
+                resultLine += "<span class='test-name-prefix'>" + testName + ".</span><span class='test-name-postfix'>" + method.name + "</span>";
+                if (method.reason != null) {
+                    resultLine += "<span class='test-name-postfix'>:&nbsp;&nbsp;</span><small class='reason'>" + method.reason + "</small>";
+                }
+                resultLine += "</td><td class='" + resultString.toLowerCase() + "-text'>" + resultString + "</td></tr>";
+                output.add(resultLine);
+                List<String> stackTrace = method.stackTrace;
+                if (stackTrace != null) {
+                    output.add("<tr><td>");
+                    output.add("<input id=\"log-toggle-" + testName + "-" + method.name + "\" type=\"checkbox\" class=\"log-toggle\" />");
+                    output.add("<label for=\"log-toggle-" + testName + "-" + method.name + "\"></label><br />");
+                    output.add("<div>");
+                    output.add("<h5>stack trace:</h5>");
+                    output.add("<pre class='stack-trace'><![CDATA[" + (stackTrace.size() > 0 ? stackTrace.get(0) : ""));
+                    for (int i = 1; i < stackTrace.size(); ++i) {
+                        output.add(stackTrace.get(i));
+                    }
+                    output.add("]]></pre>");
+                    output.add("<h5>log:</h5>");
+                    List<String> log = method.getLog();
+                    output.add("<pre class='stack-trace'><![CDATA[" + (log.size() > 0 ? log.get(0) : ""));
+                    for (int i = 1; i < log.size(); ++i) {
+                        output.add(log.get(i));
+                    }
+                    output.add("]]></pre>");
+                    output.add("</div>");
+                    output.add("</td></tr>");
+                }
+            }
+            output.add("<tr><td class='error-text' colspan='2'>SUMMARY: " + testName + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;total: " + test.methods.size() + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;passed: " + test.passedCount + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;failed: " + test.failedCount + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;error: " + test.errorCount + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;not applicable: " + test.ignoredCount + "&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;duration: " + test.duration + "</td></tr>");
+            output.add("<tr><td colspan='2'>&nbsp;</td></tr>");
+        }
+    }
+
+    //////////////////////
+    //// HISTORY PAGE ////
+    //////////////////////
+    public static void generateHistoryPage(List<String> output, List<TestRunResult> runs, String resultsType, int count, boolean onlyFail) throws IOException {
+        List<String> resultTypeText = new ArrayList();
+        resultTypeText.add(resultsType);
+        List<String> historyTableText = new ArrayList();
+        generateHistoryTable(historyTableText, runs, count, onlyFail);
+        Map<String, List<String>> replacements = new HashMap();
+        replacements.put(resultsPattern, resultTypeText);
+        replacements.put(tableDataPattern, historyTableText);
+        List<String> template = FileUtilities.getLineListFromFile(historyPageTemplate);
+        replacePatterns(template, output, replacements);
+    }
+
+    public static void generateHistoryTable(List<String> output, List<TestRunResult> runs, int count, boolean onlyFail) {
+        int size = runs.size();
+        int start = Math.max(0, size - count);
+        TestRunResult lastRun = runs.get(size - 1);
+        output.add("<tr><td class='group-id'>Test name/Date:</td>");
+        for (int i = start; i < size; ++i) {
+            String name = runs.get(i).date;
+            int index = name.indexOf("-");
+            String year = name.substring(0, index);
+            String date = name.substring(index + 1, name.length());
+            output.add("<td>" + year + "<br />" + date + "</td>");
+        }
+        output.add("</tr>");
+        List<TestClassResult> lastResults = lastRun.results;
+        List<TestClassResult> testHistory = new ArrayList();
+        for (TestClassResult result : lastResults) {
+            String name = result.name;
+            for (int i = start; i < size; ++i) {
+                Map<String, TestClassResult> testMap = runs.get(i).resultsMap;
+                testHistory.add(testMap.get(name));
+            }
+            generateHistoryTableRow(output, runs, testHistory, count, onlyFail);
+            testHistory.clear();
+        }
+    }
+
+    public static void generateHistoryTableRow(List<String> output, List<TestRunResult> runs, List<TestClassResult> testHistory, int count, boolean onlyFail) {
+        int size = testHistory.size();
+        TestRunResult lastRun = runs.get(size - 1);
+        TestClassResult current = testHistory.get(size - 1);
+        List<TestMethodResult> allMethods = new ArrayList();
+        for (TestMethodResult method : current.methods) {
+            String name = method.getName();
+            boolean fail = false;
+            for (TestClassResult previous : testHistory) {
+                if (previous != null) {
+                    TestMethodResult previousMethod = previous.getMethod(name);
+                    if (previousMethod != null && previousMethod.getResult() != TestResult.PASSED) {
+                        fail = true;
+                    }
+                    allMethods.add(previousMethod);
+                } else {
+                    allMethods.add(null);
+                }
+            }
+            if (!onlyFail || fail) {
+                String lastTestFile = "log_" + lastRun.date + ".xhtml";
+                String methodref = "#" + current.name + "." + method.name;
+                output.add("<tr><td>" + current.name + ".<a href='" + lastTestFile + methodref + "'>" + method.getName() + "</a></td>");
+                int i = 0;
+                for (TestMethodResult printedMethod : allMethods) {
+                    if (printedMethod != null) {
+                        TestResult result = printedMethod.getResult();
+                        String resultString = result.toString().toLowerCase();
+                        String printedResultString = result == TestResult.PASSED ? "ok" : result == TestResult.FAILED ? "fail" : resultString;
+                        output.add("<td class='" + resultString + "-header'><a href='" + "log_" + runs.get(i).date + ".xhtml" + methodref + "'>" + printedResultString + "</a></td>");
+                    } else {
+                        output.add("<td> </td>");
+                    }
+                    ++i;
+                }
+                output.add("</tr>");
+            }
+            allMethods.clear();
+        }
+    }
+
+    ////////////////////
+    //// GRAPH PAGE ////
+    ////////////////////
+    public static void generateGraphPage(List<String> output, List<TestRunResult> runs, int count) throws IOException {
+        List<String> testCountText = new ArrayList();
+        testCountText.add(Integer.toString(count));
+        List<String> passedDataText = new ArrayList();
+        generateGraphData(passedDataText, runs, TestResult.PASSED, count);
+        List<String> failedDataText = new ArrayList();
+        generateGraphData(failedDataText, runs, TestResult.FAILED, count);
+        List<String> errorDataText = new ArrayList();
+        generateGraphData(errorDataText, runs, TestResult.ERROR, count);
+        List<String> ignorredDataText = new ArrayList();
+        generateGraphData(ignorredDataText, runs, TestResult.IGNORED, count);
+        List<String> noneDataText = new ArrayList();
+        generateGraphData(noneDataText, runs, null, count);
+        List<String> tableText = new ArrayList();
+        generateGraphTable(tableText, runs, count);
+        Map<String, List<String>> replacements = new HashMap();
+        replacements.put(testCountPattern, testCountText);
+        replacements.put(graphDataPassedPattern, passedDataText);
+        replacements.put(graphDataFailedPattern, failedDataText);
+        replacements.put(graphDataErrorPattern, errorDataText);
+        replacements.put(graphDataIgnoredPattern, ignorredDataText);
+        replacements.put(graphDataNonePattern, noneDataText);
+        replacements.put(tableDataPattern, tableText);
+        List<String> template = FileUtilities.getLineListFromFile(graphPageTemplate);
+        replacePatterns(template, output, replacements);
+    }
+
+    public static void generateGraphTable(List<String> output, List<TestRunResult> results, int count) {
+        int size = results.size();
+        int start = Math.max(0, size - count);
+        for (int i = start; i < results.size(); ++i) {
+            TestRunResult result = results.get(i);
+            output.add("<tr><td>" + i + "</td><td><a href='log_" + result.date + ".xhtml'>&nbsp;" + result.date + "&nbsp;</a></td>");
+            output.add("<td style='color:#006000;text-align:right;'>" + result.passedCount + "</td>");
+            output.add("<td style='color:#800000;text-align:right;'>" + result.failedCount + "</td>");
+            output.add("<td style='color:#000080;text-align:right;'>" + result.errorCount + "</td>");
+            output.add("<td style='color:#606060;text-align:right;'>" + result.ignoredCount + "</td>");
+            output.add("</tr>");
+        }
+    }
+
+    public static void generateGraphData(List<String> output, List<TestRunResult> results, TestResult result, int count) {
+        int size = results.size();
+        int start = Math.max(0, size - count);
+        for (int i = start; i < results.size(); ++i) {
+            TestRunResult run = results.get(i);
+            int targetResultCount = result == TestResult.PASSED ? run.passedCount : result == TestResult.FAILED ? run.failedCount : result == TestResult.ERROR ? run.errorCount : result == TestResult.IGNORED ? run.ignoredCount : 0;
+            output.add("[" + (i + 1) + ", " + targetResultCount + "],");
+        }
+    }
+
+    ////////////////
+    //// COMMON ////
+    ////////////////
+    public static void replacePatterns(List<String> template, List<String> output, Map<String, List<String>> patterns) {
+        int patternsCount = patterns.size();
+        Map.Entry<String, List<String>>[] entries = new Map.Entry[patternsCount];
+        patterns.entrySet().toArray(entries);
+        for (String line : template) {
+            String restOfLine = line;
+            String toPrint = null;
+            Map.Entry<String, List<String>> entryFound;
+            do {
+                entryFound = null;
+                int index = -1;
+                for (int i = 0; i < patternsCount; ++i) {
+                    Map.Entry<String, List<String>> entry = entries[i];
+                    String pattern = entry.getKey();
+                    int foundIndex = restOfLine.indexOf(pattern);
+                    if (foundIndex >= 0 && (entryFound == null || foundIndex < index)) {
+                        index = foundIndex;
+                        entryFound = entry;
+                    }
+                }
+                if (entryFound != null) {
+                    String pattern = entryFound.getKey();
+                    List<String> replacement = entryFound.getValue();
+                    int replacementSize = replacement.size();
+                    toPrint = line.substring(0, index);
+                    for (int j = 0; j < replacementSize - 1; ++j) {
+                        String replacementLine = replacement.get(j);
+                        output.add(toPrint == null ? replacementLine : toPrint + replacementLine);
+                        toPrint = null;
+                    }
+                    String lastReplacementLine = replacementSize > 0 ? replacement.get(replacementSize - 1) : "";
+                    toPrint = toPrint == null ? lastReplacementLine : toPrint + lastReplacementLine;
+                    restOfLine = line.substring(index + pattern.length());
+                }
+            } while (entryFound != null);
+            output.add(toPrint == null ? restOfLine : toPrint + restOfLine);
+        }
+    }
+
+    public static void addReplacementToMap(Map<String, List<String>> map, String pattern, String replacement) {
+        List<String> replacementText = new ArrayList();
+        replacementText.add(replacement);
+        map.put(pattern, replacementText);
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/thermostat/qa2/reporter/LogParser.java	Thu Mar 19 12:44:59 2015 +0100
@@ -0,0 +1,142 @@
+/*
+ ThermostatQA - test framework for Thermostat Monitoring Tool
+
+ Copyright 2015 Red Hat, Inc.
+
+ This file is part of ThermostatQA
+
+ ThermostatQA is distributed under the GNU General Public License,
+ version 2 or any later version (with a special exception described
+ below, commonly known as the "Classpath Exception").
+
+ A copy of GNU General Public License (GPL) is included in this
+ distribution, in the file COPYING.
+
+ Linking ThermostatQA code with other modules is making a combined work
+ based on ThermostatQA.  Thus, the terms and conditions of the GPL
+ cover the whole combination.
+
+ As a special exception, the copyright holders of ThermostatQA give you
+ permission to link this code with independent modules to produce an
+ executable, regardless of the license terms of these independent
+ modules, and to copy and distribute the resulting executable under
+ terms of your choice, provided that you also meet, for each linked
+ independent module, the terms and conditions of the license of that
+ module.  An independent module is a module which is not derived from
+ or based on ThermostatQA code.  If you modify ThermostatQA, you may
+ extend this exception to your version of the software, but you are
+ not obligated to do so.  If you do not wish to do so, delete this
+ exception statement from your version.
+ */
+package org.thermostat.qa2.reporter;
+
+import org.thermostat.qa2.reporter.result.TestMethodResult;
+import org.thermostat.qa2.reporter.result.TestClassResult;
+import org.thermostat.qa2.reporter.result.TestRunResult;
+import java.io.File;
+import java.io.IOException;
+import java.text.Collator;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.List;
+import static org.thermostat.qa2.framework.TestResult.*;
+import org.thermostat.qa2.framework.TestResult;
+import org.thermostat.qa2.framework.utils.FileUtilities;
+
+/**
+ *
+ * @author Zdeněk Žamberský
+ */
+public class LogParser {
+
+    public static TestRunResult parseLogDir(File dir) throws IOException {
+        List<TestClassResult> testClasses = new ArrayList();
+        for (File f : dir.listFiles()) {
+            testClasses.add(parseLog(f));
+        }
+        Collections.sort(testClasses, new Comparator<TestClassResult>() {
+            Collator collator = Collator.getInstance();
+            
+            @Override
+            public int compare(TestClassResult o1, TestClassResult o2) {
+                return collator.compare(o1.name, o2.name);
+            }
+
+        });
+        return new TestRunResult(dir.getName(), testClasses);
+    }
+
+    public static TestClassResult parseLog(File file) throws IOException {
+        List<String> lineList = FileUtilities.getLineListFromFile(file.getAbsolutePath());
+        List<TestMethodResult> methodList = new ArrayList();
+        long duration = 0;
+        List<String> log = new ArrayList();
+        for (int i = 0; i < lineList.size(); ++i) {
+            String line = lineList.get(i);
+            TestResult result = getResult(line);
+            if (result != null) {
+                String methodLine = line.substring(result.toString().length() + 1);
+                String methodName = getMethodName(methodLine);
+                TestMethodResult testMethod = new TestMethodResult(methodName, result);
+                if (result == FAILED) {
+                    String reason = getReason(methodLine);
+                    List<String> stackTrace = new ArrayList();
+                    for (;;) {
+                        line = lineList.get(++i);
+                        if (line.startsWith("INFO:") || line.startsWith("SUMMARY:")) {
+                            --i;
+                            break;
+                        }
+                        stackTrace.add(line);
+                    }
+                    for (String s : stackTrace) {
+                        String pattern = "Caused by: java.lang.AssertionError:";
+                        if (s.startsWith(pattern)) {
+                            testMethod.setReason(s.substring(pattern.length()));
+                        }
+                    }
+                    testMethod.setStackTrace(stackTrace);
+                    testMethod.setLog(log);
+                    log = new ArrayList();
+                } else {
+                    log.clear();
+                }
+                methodList.add(testMethod);
+            } else if (line.startsWith("INFO:")) {
+                log.add(line);
+            } else if (line.startsWith("SUMMARY")) {
+                int index = line.indexOf("duration:");
+                if (index >= 0) {
+                    String durationString = line.substring(line.indexOf(":", index) + 1).trim();
+                    duration = Long.parseLong(durationString);
+                }
+            }
+        }
+        String testName = file.getName();
+        testName = testName.substring(0, testName.length() - 4);
+        TestClassResult test = new TestClassResult(testName, methodList, duration);
+        return test;
+    }
+
+    public static TestResult getResult(String s) {
+        return s.startsWith(PASSED.toString()) ? PASSED
+                : s.startsWith(FAILED.toString()) ? FAILED
+                        : s.startsWith(IGNORED.toString()) ? IGNORED
+                                : s.startsWith(ERROR.toString()) ? ERROR
+                                        : null;
+    }
+
+    public static String getMethodName(String line) {
+        int index1 = line.indexOf(':');
+        String tmp = index1 >= 0 ? line.substring(0, index1) : line;
+        int index2 = tmp.lastIndexOf('.');
+        return tmp.substring(index2 + 1).trim();
+    }
+
+    public static String getReason(String line) {
+        int index = line.indexOf(':');
+        return index >= 0 ? line.substring(index + 1).trim() : "";
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/thermostat/qa2/reporter/Reporter.java	Thu Mar 19 12:44:59 2015 +0100
@@ -0,0 +1,112 @@
+/*
+ ThermostatQA - test framework for Thermostat Monitoring Tool
+
+ Copyright 2015 Red Hat, Inc.
+
+ This file is part of ThermostatQA
+
+ ThermostatQA is distributed under the GNU General Public License,
+ version 2 or any later version (with a special exception described
+ below, commonly known as the "Classpath Exception").
+
+ A copy of GNU General Public License (GPL) is included in this
+ distribution, in the file COPYING.
+
+ Linking ThermostatQA code with other modules is making a combined work
+ based on ThermostatQA.  Thus, the terms and conditions of the GPL
+ cover the whole combination.
+
+ As a special exception, the copyright holders of ThermostatQA give you
+ permission to link this code with independent modules to produce an
+ executable, regardless of the license terms of these independent
+ modules, and to copy and distribute the resulting executable under
+ terms of your choice, provided that you also meet, for each linked
+ independent module, the terms and conditions of the license of that
+ module.  An independent module is a module which is not derived from
+ or based on ThermostatQA code.  If you modify ThermostatQA, you may
+ extend this exception to your version of the software, but you are
+ not obligated to do so.  If you do not wish to do so, delete this
+ exception statement from your version.
+ */
+package org.thermostat.qa2.reporter;
+
+import java.awt.Dimension;
+import org.thermostat.qa2.reporter.result.TestRunResult;
+import java.io.File;
+import java.io.IOException;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import org.thermostat.qa.reporter.ResultsGraphGenerator;
+import org.thermostat.qa.reporter.TestResult;
+import org.thermostat.qa2.framework.ThermostatQAConfig;
+import org.thermostat.qa2.framework.utils.FileUtilities;
+
+/**
+ *
+ * @author Zdeněk Žamberský
+ */
+public class Reporter {
+
+    public static void generateReport() throws IOException {
+        File logsDir = new File(ThermostatQAConfig.logsDir);
+        File[] runDirs = logsDir.listFiles();
+        Arrays.sort(runDirs);
+
+        int runsCount = runDirs.length;
+        int limit = 30;
+        if (runsCount > 0) {
+            List<TestRunResult> runs = new ArrayList();
+            for (int i = Math.max(0, runsCount - limit); i < runsCount; ++i) {
+                TestRunResult run = LogParser.parseLogDir(runDirs[i]);
+                runs.add(run);
+            }
+            TestRunResult lastRun = runs.get(runs.size() - 1);
+
+            // index page
+            String indexPagePath = ThermostatQAConfig.reportDir + File.separator + "index.xhtml";
+            List<String> indexPageText = new ArrayList();
+            Generator.generateIndexPage(indexPageText, lastRun);
+            FileUtilities.printLineListToFile(indexPagePath, indexPageText);
+
+            // log page
+            String logPagePath = ThermostatQAConfig.reportDir + File.separator + "log_" + lastRun.date + ".xhtml";
+            List<String> logPageText = new ArrayList();
+            Generator.generateLogPage(logPageText, lastRun);
+            FileUtilities.printLineListToFile(logPagePath, logPageText);
+
+            // history pages
+            String[] fileNames = {"hist_all_tests", "hist_failed_tests", "hist_10", "hist_20", "hist_30"};
+            String[] labels = {"all tests", "failed tests", "last 10 results", "last 20 results", "last 30 results"};
+            int[] counts = {30, 30, 10, 20, 30};
+            boolean[] onlyFailed = {false, true, true, true, true};
+
+            for (int i = 0; i < fileNames.length; ++i) {
+                String historyPagePath = ThermostatQAConfig.reportDir + File.separator + fileNames[i] + ".xhtml";
+                List<String> historyPageText = new ArrayList();
+                Generator.generateHistoryPage(historyPageText, runs, labels[i], counts[i], onlyFailed[i]);
+                FileUtilities.printLineListToFile(historyPagePath, historyPageText);
+            }
+
+            // graphs pages
+            fileNames = new String[]{"graph_all", "graph_10", "graph_20", "graph_30"};
+            counts = new int[]{30, 10, 20, 30};
+            for (int i = 0; i < fileNames.length; ++i) {
+                String graphsPagePath = ThermostatQAConfig.reportDir + File.separator + fileNames[i] + ".xhtml";
+                List<String> graphPageText = new ArrayList();
+                Generator.generateGraphPage(graphPageText, runs, counts[i]);
+                FileUtilities.printLineListToFile(graphsPagePath, graphPageText);
+            }
+
+            // generate image using old reporter
+            Dimension graphSize = new Dimension(256, 256);
+            TestResult testResult = new TestResult(lastRun.passedCount, lastRun.failedCount, lastRun.errorCount, lastRun.ignoredCount);
+            (new ResultsGraphGenerator()).generateGraph(testResult, graphSize, ThermostatQAConfig.reportDir + File.separator + "graph.png");
+        }
+    }
+
+    public static void main(String[] args) throws IOException {
+        generateReport();
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/thermostat/qa2/reporter/result/TestClassResult.java	Thu Mar 19 12:44:59 2015 +0100
@@ -0,0 +1,85 @@
+/*
+ ThermostatQA - test framework for Thermostat Monitoring Tool
+
+ Copyright 2015 Red Hat, Inc.
+
+ This file is part of ThermostatQA
+
+ ThermostatQA is distributed under the GNU General Public License,
+ version 2 or any later version (with a special exception described
+ below, commonly known as the "Classpath Exception").
+
+ A copy of GNU General Public License (GPL) is included in this
+ distribution, in the file COPYING.
+
+ Linking ThermostatQA code with other modules is making a combined work
+ based on ThermostatQA.  Thus, the terms and conditions of the GPL
+ cover the whole combination.
+
+ As a special exception, the copyright holders of ThermostatQA give you
+ permission to link this code with independent modules to produce an
+ executable, regardless of the license terms of these independent
+ modules, and to copy and distribute the resulting executable under
+ terms of your choice, provided that you also meet, for each linked
+ independent module, the terms and conditions of the license of that
+ module.  An independent module is a module which is not derived from
+ or based on ThermostatQA code.  If you modify ThermostatQA, you may
+ extend this exception to your version of the software, but you are
+ not obligated to do so.  If you do not wish to do so, delete this
+ exception statement from your version.
+ */
+package org.thermostat.qa2.reporter.result;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+import org.thermostat.qa2.framework.TestResult;
+import static org.thermostat.qa2.framework.TestResult.*;
+
+/**
+ *
+ * @author Zdeněk Žamberský
+ */
+public class TestClassResult {
+
+    public String name;
+    public List<TestMethodResult> methods;
+    public Map<String, TestMethodResult> methodsMap = new HashMap();
+
+    public int methodCount;
+    public int passedCount;
+    public int failedCount;
+    public int errorCount;
+    public int ignoredCount;
+
+    public long duration;
+
+    public TestClassResult(String name, List<TestMethodResult> methods, long duration) {
+        this.name = name;
+        this.methods = methods;
+        this.duration = duration;
+        for (TestMethodResult method : methods) {
+            methodsMap.put(method.name, method);
+            TestResult result = method.getResult();
+            switch (result) {
+                case PASSED:
+                    ++passedCount;
+                    break;
+                case FAILED:
+                    ++failedCount;
+                    break;
+                case ERROR:
+                    ++errorCount;
+                    break;
+                case IGNORED:
+                    ++ignoredCount;
+                    break;
+            }
+            ++methodCount;
+        }
+    }
+
+    public TestMethodResult getMethod(String name) {
+        return methodsMap.get(name);
+    }
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/thermostat/qa2/reporter/result/TestMethodResult.java	Thu Mar 19 12:44:59 2015 +0100
@@ -0,0 +1,86 @@
+/*
+ ThermostatQA - test framework for Thermostat Monitoring Tool
+
+ Copyright 2015 Red Hat, Inc.
+
+ This file is part of ThermostatQA
+
+ ThermostatQA is distributed under the GNU General Public License,
+ version 2 or any later version (with a special exception described
+ below, commonly known as the "Classpath Exception").
+
+ A copy of GNU General Public License (GPL) is included in this
+ distribution, in the file COPYING.
+
+ Linking ThermostatQA code with other modules is making a combined work
+ based on ThermostatQA.  Thus, the terms and conditions of the GPL
+ cover the whole combination.
+
+ As a special exception, the copyright holders of ThermostatQA give you
+ permission to link this code with independent modules to produce an
+ executable, regardless of the license terms of these independent
+ modules, and to copy and distribute the resulting executable under
+ terms of your choice, provided that you also meet, for each linked
+ independent module, the terms and conditions of the license of that
+ module.  An independent module is a module which is not derived from
+ or based on ThermostatQA code.  If you modify ThermostatQA, you may
+ extend this exception to your version of the software, but you are
+ not obligated to do so.  If you do not wish to do so, delete this
+ exception statement from your version.
+ */
+package org.thermostat.qa2.reporter.result;
+
+import java.util.List;
+import org.thermostat.qa2.framework.TestResult;
+
+/**
+ *
+ * @author Zdeněk Žamberský
+ */
+public class TestMethodResult {
+
+    public String name;
+    public TestResult result;
+    //
+    public String reason;
+    public List<String> stackTrace;
+    public List<String> log;
+
+    public TestMethodResult(String name, TestResult result) {
+        this.name = name;
+        this.result = result;
+    }
+
+    public String getName() {
+        return name;
+    }
+
+    public TestResult getResult() {
+        return result;
+    }
+
+    public String getReason() {
+        return reason;
+    }
+
+    public List<String> getLog() {
+        return log;
+    }
+
+    public List<String> getStackTrace() {
+        return stackTrace;
+    }
+
+    public void setReason(String reason) {
+        this.reason = reason;
+    }
+
+    public void setStackTrace(List<String> stackTrace) {
+        this.stackTrace = stackTrace;
+    }
+
+    public void setLog(List<String> log) {
+        this.log = log;
+    }
+
+}
--- /dev/null	Thu Jan 01 00:00:00 1970 +0000
+++ b/src/org/thermostat/qa2/reporter/result/TestRunResult.java	Thu Mar 19 12:44:59 2015 +0100
@@ -0,0 +1,68 @@
+/*
+ ThermostatQA - test framework for Thermostat Monitoring Tool
+
+ Copyright 2015 Red Hat, Inc.
+
+ This file is part of ThermostatQA
+
+ ThermostatQA is distributed under the GNU General Public License,
+ version 2 or any later version (with a special exception described
+ below, commonly known as the "Classpath Exception").
+
+ A copy of GNU General Public License (GPL) is included in this
+ distribution, in the file COPYING.
+
+ Linking ThermostatQA code with other modules is making a combined work
+ based on ThermostatQA.  Thus, the terms and conditions of the GPL
+ cover the whole combination.
+
+ As a special exception, the copyright holders of ThermostatQA give you
+ permission to link this code with independent modules to produce an
+ executable, regardless of the license terms of these independent
+ modules, and to copy and distribute the resulting executable under
+ terms of your choice, provided that you also meet, for each linked
+ independent module, the terms and conditions of the license of that
+ module.  An independent module is a module which is not derived from
+ or based on ThermostatQA code.  If you modify ThermostatQA, you may
+ extend this exception to your version of the software, but you are
+ not obligated to do so.  If you do not wish to do so, delete this
+ exception statement from your version.
+ */
+package org.thermostat.qa2.reporter.result;
+
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ *
+ * @author Zdeněk Žamberský
+ */
+public class TestRunResult {
+
+    public String date;
+
+    public List<TestClassResult> results;
+    public Map<String, TestClassResult> resultsMap = new HashMap();
+
+    public int methodCount;
+    public int passedCount;
+    public int failedCount;
+    public int errorCount;
+    public int ignoredCount;
+
+    public TestRunResult(String date, List<TestClassResult> results) {
+        this.date = date;
+        this.results = results;
+        for (TestClassResult result : results) {
+            methodCount += result.methodCount;
+            passedCount += result.passedCount;
+            failedCount += result.failedCount;
+            errorCount += result.errorCount;
+            ignoredCount += result.ignoredCount;
+            
+            resultsMap.put(result.name, result);
+        }
+    }
+
+}
--- a/templates/graph.html	Thu Mar 19 12:25:06 2015 +0100
+++ b/templates/graph.html	Thu Mar 19 12:44:59 2015 +0100
@@ -51,6 +51,7 @@
 
     </head>
 <body>
+<div>
     <h1>ThermostatQA tests report - graph for ${TEST_COUNT} tests</h1>
 
     <table border="0">
@@ -166,6 +167,7 @@
             </tr>
         </table>
     <hr />
+    </div>
     </body>
 </html>
 
--- a/templates/hist.html	Thu Mar 19 12:25:06 2015 +0100
+++ b/templates/hist.html	Thu Mar 19 12:44:59 2015 +0100
@@ -41,6 +41,7 @@
         <link type="text/css" rel="StyleSheet" href="style.css" />
     </head>
 <body>
+<div>
     <h1>ThermostatQA tests history -  ${RESULTS}</h1>
 
     <br />
@@ -48,6 +49,7 @@
     <table border='2' frame='border' rules='all' cellspacing='1' cellpadding='1' class='forms' summary='' style='white-space:nowrap;margin-left:0px'>
 ${TABLE_DATA}
     </table>
+</div>
 </body>
 </html>
 
--- a/templates/index.html	Thu Mar 19 12:25:06 2015 +0100
+++ b/templates/index.html	Thu Mar 19 12:44:59 2015 +0100
@@ -37,10 +37,11 @@
         <title>ThermostatQA tests report: JDK ${JAVA_VERSION}</title>
         <meta name="Author" content="Pavel Tisnovsky" />
         <meta name="Generator" content="org.thermostat.qa.reporter.Reporter" />
-        <meta http-equiv="content-type" content="text/html; charset=utf-8" />
+        <meta http-equiv="content-type" content="application/xhtml+xml; charset=utf-8" />
         <link type="text/css" rel="StyleSheet" href="style.css" />
     </head>
 <body>
+<div>
     <h1>ThermostatQA tests report: JDK ${JAVA_VERSION}</h1>
     
     <p>
@@ -48,16 +49,15 @@
     <a href="${RESULTS_URL}">
     ${RESULTS_URL}</a>
     </p>
-
-    <br />
+    
 
     <table border='2' frame='border' rules='all' cellspacing='1' cellpadding='1' class='forms' style='width:1000px' summary=''>
         <tr><td colspan='5' class='table-header'>Daily report</td></tr>
         <tr>
             <td rowspan='3' class='group-id'>${DATE}</td>
             <td>Log location:</td>
-            <td colspan='2'><a href='log_${DATE}.html'>
-            log_${DATE}.html</a></td>
+            <td colspan='2'><a href='log_${DATE}.xhtml'>
+            log_${DATE}.xhtml</a></td>
         </tr>
         <tr>
             <td style='width:20%' class='passed-header'>Passed</td>
@@ -74,25 +74,25 @@
         <tr>
             <td colspan='5'>&nbsp;</td>
         </tr>
-        <tr><td colspan='4' class='table-header'>Full history</td><td rowspan="18"><img src="graph.png" title="test results in graphical form"></td></tr>
+        <tr><td colspan='4' class='table-header'>Full history</td><td rowspan="18"><img src="graph.png" title="test results in graphical form" alt="missing image" /></td></tr>
         <tr>
-            <td>&nbsp;</td><td colspan='3'><a href='hist_all_tests.html'>All tests (huge page!)</a></td>
+            <td>&nbsp;</td><td colspan='3'><a href='hist_all_tests.xhtml'>All tests (huge page!)</a></td>
         </tr>
         <tr>
-            <td>&nbsp;</td><td colspan='3'><a href='hist_failed_tests.html'>Failed tests (more useful)</a></td>
+            <td>&nbsp;</td><td colspan='3'><a href='hist_failed_tests.xhtml'>Failed tests (more useful)</a></td>
         </tr>
         <tr>
-            <td>&nbsp;</td><td colspan='3'>Last <i>n</i> results: <a href='hist_10.html'>[10]</a><a href='hist_20.html'>[20]</a><a href='hist_30.html'>[30]</a></td>
+            <td>&nbsp;</td><td colspan='3'>Last <i>n</i> results: <a href='hist_10.xhtml'>[10]</a><a href='hist_20.xhtml'>[20]</a><a href='hist_30.xhtml'>[30]</a></td>
         </tr>
         <tr>
             <td colspan='4'>&nbsp;</td>
         </tr>
         <tr><td colspan='4' class='table-header'>Graphs</td></tr>
         <tr>
-            <td>&nbsp;</td><td colspan='3'><a href='graph_all.html'>All tests</a></td>
+            <td>&nbsp;</td><td colspan='3'><a href='graph_all.xhtml'>All tests</a></td>
         </tr>
         <tr>
-            <td>&nbsp;</td><td colspan='3'>Last <i>n</i> results: <a href='graph_10.html'>[10]</a><a href='graph_20.html'>[20]</a><a href='graph_30.html'>[30]</a></td>
+            <td>&nbsp;</td><td colspan='3'>Last <i>n</i> results: <a href='graph_10.xhtml'>[10]</a><a href='graph_20.xhtml'>[20]</a><a href='graph_30.xhtml'>[30]</a></td>
         </tr>
         <tr>
             <td colspan='4'>&nbsp;</td>
@@ -127,6 +127,7 @@
             <td colspan='3'>${VM_VERSION}</td>
         </tr>
     </table>
+</div>
 </body>
 </html>
 
--- a/templates/log.html	Thu Mar 19 12:25:06 2015 +0100
+++ b/templates/log.html	Thu Mar 19 12:44:59 2015 +0100
@@ -41,6 +41,7 @@
         <link type="text/css" rel="StyleSheet" href="style.css" />
     </head>
 <body>
+<div>
     <h1>ThermostatQA tests log for date ${DATE}</h1>
 
     <br />
@@ -59,6 +60,7 @@
 
     <!-- it's needed to be able to display specific test on the top of browser window -->
     <div style='height:2000px;'>&nbsp;</div>
+</div>
 </body>
 </html>
 
--- a/templates/style.css	Thu Mar 19 12:25:06 2015 +0100
+++ b/templates/style.css	Thu Mar 19 12:44:59 2015 +0100
@@ -65,8 +65,18 @@
 
 .stack-trace    {color:#604040}
 
-.test-name-prefix  {color:#808080}
+.test-name-prefix  {color:#606060}
 .test-name-postfix {color:#000000}
 
 .empty         {background-color:#ffffff}
 
+label { color:#0000ff; }
+label:hover { text-decoration: underline; }
+.log-toggle + label::after { content: "show details"; }
+.log-toggle:checked + label::after { content: "hide details"; }
+.log-toggle { display: none; }
+.log-toggle ~ div { display: none; }
+.log-toggle:checked ~ div { display: initial; }
+
+.reason {color:#303030; }
+