/*
* Copyright 2015 Trento Rise (trentorise.eu)
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package eu.trentorise.opendata.jackan.test.ckan;
import com.google.common.base.Optional;
import com.google.common.base.Throwables;
import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.escape.Escaper;
import com.google.common.html.HtmlEscapers;
import eu.trentorise.opendata.commons.BuildInfo;
import eu.trentorise.opendata.commons.TodConfig;
import eu.trentorise.opendata.jackan.CkanClient;
import eu.trentorise.opendata.jackan.test.JackanTestConfig;
import eu.trentorise.opendata.commons.TodUtils;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileNotFoundException;
import java.io.IOException;
import java.io.InputStream;
import java.io.InputStreamReader;
import java.io.PrintWriter;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Map.Entry;
import java.util.logging.Level;
import java.util.logging.Logger;
import org.apache.commons.io.FileUtils;
import static org.rendersnake.HtmlAttributesFactory.class_;
import static org.rendersnake.HtmlAttributesFactory.href;
import static org.rendersnake.HtmlAttributesFactory.style;
import org.rendersnake.HtmlAttributes;
import org.rendersnake.HtmlCanvas;
/**
* Little app to test a list of catalogs and produce an HTML report.
*
* @author David Leoni
*/
public class CkanTestReporter {
private static final Logger logger = Logger.getLogger(CkanTestReporter.class.getName());
/**
* Tests names from {@link ReadCkanIT}
*/
public static final List<String> ALL_TEST_NAMES = ImmutableList.of("testApiVersionSupported", "testDatasetList",
"testDatasetListWithLimit", "testSearchDatasetsByText", "testDatasetAndResource", "testLicenseList",
"testTagList", "testTagNameList", "testUserList", "testUser", "testGroupList", "testGroupNames",
"testGroup", "testOrganizationList", "testOrganization", "testOrganizationNames", "testFormatList",
"testSearchDatasetsByGroups", "testSearchDatasetsByOrganization", "testSearchDatasetsByTags",
"testSearchDatasetsByLicenseIds");
private static final String ERROR_CLASS = "jackan-error";
private static final String PASSED_CLASS = "jackan-passed";
private static final String JACKAN_TABLE_CLASS = "jackan-table";
/**
* Takes as first argument the catalog list files to be used. If not
* provided, by default test/resources/ckan-instances.txt file is used.
*/
public static void main(String[] args) {
JackanTestConfig.of()
.loadConfig();
String catFilename;
if (args.length == 2) {
catFilename = args[1];
logger.log(Level.INFO, "Using provided catalogs file {0}", catFilename);
} else {
catFilename = "ckan-instances.txt";
logger.log(Level.INFO,
"Using default catalogs file {0}. If you wish to provide yours pass filename as first argument.",
catFilename);
}
Map<String, String> catalogsNames = readCatalogsList(catFilename);
List<String> testNames = ALL_TEST_NAMES;
RunSuite testResults = runTests(catalogsNames, testNames);
String content = renderRunSuite(catalogsNames, testNames, testResults);
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd--HH-mm-ss");
saveToDirectory(new File("reports/" + REPORT_PREFIX + "-" + formatter.format(new Date())), content,
testResults);
File latestDir = new File("reports/" + REPORT_PREFIX + "-latest");
FileUtils.deleteQuietly(latestDir);
saveToDirectory(latestDir, content, testResults);
}
/**
* @param catalogListFilepath
* absolute file path
*/
public static Map<String, String> readCatalogsList(String catalogListFilepath) {
// catalog url, name
ImmutableMap.Builder<String, String> catalogsBuilder = ImmutableMap.builder();
InputStream is;
try {
is = new FileInputStream(catalogListFilepath);
} catch (FileNotFoundException fex) {
logger.log(Level.INFO, "Trying to take file {0} from test resources", catalogListFilepath);
is = CkanTestReporter.class.getClassLoader()
.getResourceAsStream(catalogListFilepath);
if (is == null) {
throw new RuntimeException("Couldn't find file " + catalogListFilepath);
}
}
try {
String str;
BufferedReader reader = new BufferedReader(new InputStreamReader(is));
boolean readingName = true;
String name = "";
while ((str = reader.readLine()) != null) {
if (readingName) {
name = str;
} else {
catalogsBuilder.put(TodUtils.removeTrailingSlash(str), name);
}
readingName = !readingName;
}
} catch (IOException ex) {
Logger.getLogger(CkanTestReporter.class.getName())
.log(Level.SEVERE, null, ex);
} finally {
try {
if (is != null) {
is.close();
}
} catch (Throwable ignore) {
}
}
/*
* TODO validate urls: URL catURL; try { catURL = new URL(catalogURL); }
* catch (MalformedURLException ex) { html.tr() .td(class_(ERROR_CLASS))
* // todo we are skipping columns.... .write("Bad catalog URL: " +
* catalogURL + " for catalog " + catalogs.get(catalogURL)) ._td()
* ._tr(); continue; }
*/
return catalogsBuilder.build();
}
public static final String REPORT_PREFIX = "jackan-scan";
public static final String TEST_RESULT_PREFIX = "test-result-";
private static TestResult runTest(int testId, CkanClient client, String catalogName, String testName) {
Optional<Throwable> error;
ReadCkanIT ckanTests = new ReadCkanIT();
try {
java.lang.reflect.Method method;
method = ReadCkanIT.class.getMethod(testName, CkanClient.class);
method.invoke(ckanTests, client);
error = Optional.absent();
} catch (Throwable t) {
error = Optional.of(t);
}
return new TestResult(testId, testName, client.getCatalogUrl(), catalogName, error);
}
public static class RunSuite {
private Date startTime;
private Date endTime;
private ImmutableList<TestResult> results;
public RunSuite(Date startTime, Date endTime, List<TestResult> results) {
this.startTime = startTime;
this.endTime = endTime;
this.results = ImmutableList.copyOf(results);
}
/**
* Returns the number of passed tests under the given name.
*/
public List<TestResult> getPassedTestsByName(String testName) {
ImmutableList.Builder<TestResult> retb = ImmutableList.builder();
for (TestResult tr : results) {
if (tr.passed() && testName.equals(tr.getTestName())) {
retb.add(tr);
}
}
return retb.build();
}
/**
* Returns the number of passed tests of provided catalog.
*/
public List<TestResult> getPassedTestsByCatalogUrl(String catalogUrl) {
String cat = TodUtils.removeTrailingSlash(catalogUrl);
ImmutableList.Builder<TestResult> retb = ImmutableList.builder();
for (TestResult tr : results) {
if (tr.passed() && cat.equals(tr.getCatalogURL())) {
retb.add(tr);
}
}
return retb.build();
}
public Date getStartTime() {
return startTime;
}
public Date getEndTime() {
return endTime;
}
public ImmutableList<TestResult> getResults() {
return results;
}
}
public static RunSuite runTests(Map<String, String> catalogNames, List<String> testNames) {
Date startTime = new Date();
Map<String, CkanClient> clients = new HashMap();
for (Entry<String, String> e : catalogNames.entrySet()) {
clients.put(e.getKey(), new CkanClient(e.getKey()));
}
ImmutableList.Builder<TestResult> results = ImmutableList.builder();
int testCounter = 0;
for (String testName : testNames) { // so we don't stress one catalog
// with all tests in sequence
for (String catalogUrl : catalogNames.keySet()) {
results.add(runTest(testCounter, clients.get(catalogUrl), catalogNames.get(catalogUrl), testName));
testCounter += 1;
}
}
return new RunSuite(startTime, new Date(), results.build());
}
/**
* Formats date time up to the day, in English format
*/
private static String formatDateUpToDay(Date date) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy-MM-dd");
return formatter.format(date);
}
/**
* Formats date time up to the second, in English format
*/
private static String formatDateUpToSecond(Date date) {
SimpleDateFormat formatter = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
return formatter.format(date);
}
private static HtmlAttributes errClass(int passed, int total) {
if (passed == total) {
return class_(PASSED_CLASS);
} else {
return class_(ERROR_CLASS);
}
}
/**
* Puts in bold label name and a value after it
*/
private static HtmlCanvas labelValue(HtmlCanvas html, String label, String value) {
try {
return html.br()
.b()
.write(label)
._b()
.span()
.write(value)
._span();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
/**
* Puts in bold label name and a span after it. Remember to close tag with
* _span()
*/
private static HtmlCanvas labelValue(HtmlCanvas html, String label) {
try {
return html.br()
.b()
.write(label)
._b()
.span();
} catch (IOException e) {
throw new RuntimeException(e);
}
}
public static String renderRunSuite(Map<String, String> catalogs, List<String> testNames, RunSuite runSuite) {
String outputFileContent;
BuildInfo buildInfo = TodConfig.of(JackanTestConfig.class)
.getBuildInfo();
try {
HtmlCanvas html = new HtmlCanvas();
html.html()
.head()
.title()
.content("Jackan Test Results")
// .meta(name("description").add("content","Jackan test
// analyzer",false))
// .macros().stylesheet("htdocs/style-01.css"))
// .render(JQueryLibrary.core("1.4.3"))
// .render(JQueryLibrary.ui("1.8.6"))
// .render(JQueryLibrary.baseTheme("1.8"))
.style()
.write("." + ERROR_CLASS + " {color:red}")
.write("." + PASSED_CLASS + " {color:black}")
.write("." + JACKAN_TABLE_CLASS + " { border-collapse:collapse; table-layout: fixed; width: 100%; }")
.write("." + JACKAN_TABLE_CLASS
+ " td, th { border: 1px solid black; vertical-align: top; padding:10px; width:100px;}")
.write("." + JACKAN_TABLE_CLASS + " th { position:absolute; left:0; width:230px;}")
.write(".outer {position:relative}")
.write(".inner {\n" + " overflow-x:scroll;\n" + " overflow-y:visible;\n" + " margin-left:250px;\n"
+ "}")
._style()
._head();
html.body()
.h1()
.a(href("https://github.com/opendatatrentino/jackan").target("_blank"))
.write("Jackan")
._a()
.write(" Report - " + formatDateUpToDay(runSuite.getStartTime()))
._h1()
.b()
.write("NOTE: ")
._b()
.span()
.write("Some tests might fail due to missing items in the target catalog (i.e. catalog has no tags or no organizations). "
+ "Also, some catalogs might only be ckan-compatible and support a subset of ckan functionality (i.e. DKAN). ")
._span()
.br();
labelValue(html, "Jackan Version: ").write(buildInfo.getVersion() + " ")
.a(href("https://github.com/opendatatrentino/jackan/commit/"
+ buildInfo.getGitSha()).target("_blank"))
.write("Git commit")
._a()
._span();
labelValue(html, "Started: ", formatDateUpToSecond(runSuite.getStartTime()));
labelValue(html, "Finished: ", formatDateUpToSecond(runSuite.getEndTime()));
labelValue(html, "Catalogs scanned: ", Integer.toString(catalogs.keySet() .size()));
labelValue(html, "Tests per catalog executed: ", Integer.toString(testNames.size()));
html.br()
.br();
Escaper escaper = HtmlEscapers.htmlEscaper();
html.div(class_("outer"))
.div(class_("inner"));
html.table(class_(JACKAN_TABLE_CLASS))
.tr()
.th(style("height:100%"))
.write("test")
._th();
html.td()
.write("Passed")
._td();
for (String catalogUrl : catalogs.keySet()) {
html.td()
.a(href(catalogUrl).target("_blank"))
.write(escaper.escape(catalogs.get(catalogUrl)))
._a()
._td();
}
html._tr();
html.tr();
html.th()
.write("Passed")
._th()
.td()
._td();
for (String catalogUrl : catalogs.keySet()) {
int passedByCat = runSuite.getPassedTestsByCatalogUrl(catalogUrl)
.size();
html.td(errClass(passedByCat, testNames.size()))
.write(passedByCat + "/" + testNames.size())
._td();
}
html._tr();
Iterator<TestResult> resultIterator = runSuite.getResults()
.iterator();
for (String testName : testNames) {
html.tr();
html.th()
.b()
.write(testName)
._b()
._th();
int passedByName = runSuite.getPassedTestsByName(testName)
.size();
html.td()
.b()
.write(passedByName + "/" + catalogs.size())
._b()
._td();
for (String catalogURL : catalogs.keySet()) {
TestResult result = resultIterator.next();
if (result.passed()) {
html.td()
// .a(href(TEST_RESULT_PREFIX + result.getId() +
// ".html").target("_blank"))
.write("PASSED")
._td();
} else {
html.td()
.a(href(TEST_RESULT_PREFIX + result.getId() + ".html").target("_blank")
.class_(ERROR_CLASS))
.write("ERROR")
._a()
._td();
}
}
html._tr();
}
html._table()
._div()
._div()
._body()
._html();
outputFileContent = html.toHtml();
} catch (IOException ex) {
outputFileContent = "HTML generation problem!" + ex;
}
return outputFileContent;
}
private static int catalogsWithErrors(RunSuite runSuite) {
Map<String, Integer> map = new HashMap();
for (TestResult tr : runSuite.getResults()){
if (!tr.passed()){
String key = tr.getCatalogURL();
if (map.containsKey(key)){
map.put(key, map.get(key) + 1);
} else {
map.put(key, 1);
}
}
}
return map.size();
}
/**
* Returns a new html page with test result.
*/
public static String renderTestResult(TestResult result) {
HtmlCanvas html = new HtmlCanvas();
try {
html.html()
.head()
.title()
.content("Jackan Test #" + result.getId())
// .meta(name("description").add("content","Jackan test
// anal",false))
// .macros().stylesheet("htdocs/style-01.css"))
// .render(JQueryLibrary.core("1.4.3"))
// .render(JQueryLibrary.ui("1.8.6"))
// .render(JQueryLibrary.baseTheme("1.8"))
.style()
.write("." + ERROR_CLASS + " {color:red}")
._style()
._head();
html.body()
.h1()
.write("Jackan Test #" + result.getId())
._h1();
if (result.passed()) {
html.h2()
.write("Test passed!")
._h2();
} else {
html.h2()
.write("Test didn't pass!")
._h2();
html.pre()
.write(Throwables.getStackTraceAsString(result.getError()))
._pre();
}
html._body();
return html.toHtml();
} catch (IOException ex) {
logger.log(Level.SEVERE, "Error while rendering Jackan Test " + result, ex);
return "<html><body>Error while rendering Jackan Test #" + result.getId() + " </body></html>";
}
}
public static void saveToDirectory(File outputDirectory, String indexContent, RunSuite runSuite) {
outputDirectory.mkdirs();
PrintWriter outIndex;
try {
outIndex = new PrintWriter(outputDirectory + "/index.html");
outIndex.write(indexContent);
outIndex.close();
for (TestResult result : runSuite.getResults()) {
PrintWriter outResult;
String resultHtml = renderTestResult(result);
outResult = new PrintWriter(outputDirectory + "/" + TEST_RESULT_PREFIX + result.getId() + ".html");
outResult.write(resultHtml);
outResult.close();
}
logger.log(Level.INFO, "Report is now available at {0}{1}index.html",
new Object[] { outputDirectory.getAbsolutePath(), File.separator });
} catch (FileNotFoundException ex) {
logger.log(Level.SEVERE, null, ex);
}
}
}