Skip to content

#110 - Use local assets for code coverage report for offline usage #120

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 8 commits into from
Dec 27, 2020
Original file line number Diff line number Diff line change
Expand Up @@ -17,17 +17,25 @@

import java.awt.Desktop;
import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.MalformedURLException;
import java.net.URL;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.sql.Connection;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.*;
import java.util.jar.JarEntry;
import java.util.jar.JarFile;
import java.util.logging.Logger;
import java.util.stream.Collectors;

import org.springframework.core.io.Resource;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.utplsql.sqldev.dal.RealtimeReporterDao;
import org.utplsql.sqldev.dal.UtplsqlDao;
import org.utplsql.sqldev.exception.GenericDatabaseAccessException;
Expand All @@ -42,22 +50,25 @@

public class CodeCoverageReporter {
private static final Logger logger = Logger.getLogger(CodeCoverageReporter.class.getName());
private static final String ASSETS_PATH = "coverage/assets/";

private String connectionName;
private Connection conn;
private List<String> pathList;
private List<String> includeObjectList;
private final List<String> pathList;
private final List<String> includeObjectList;
private CodeCoverageReporterDialog frame;
private String schemas;
private String includeObjects;
private String excludeObjects;
private Path assetDir;

public CodeCoverageReporter(final List<String> pathList, final List<String> includeObjectList,
final String connectionName) {
this.pathList = pathList;
this.includeObjectList = includeObjectList;
setDefaultSchema();
setConnection(connectionName);
setAssetDir();
}

// constructor for testing purposes only
Expand All @@ -67,6 +78,7 @@ public CodeCoverageReporter(final List<String> pathList, final List<String> incl
this.includeObjectList = includeObjectList;
this.conn = conn;
setDefaultSchema();
setAssetDir();
}

private void setConnection(final String connectionName) {
Expand Down Expand Up @@ -105,6 +117,59 @@ private void setDefaultSchema() {
}
}

private void setAssetDir() {
try {
assetDir = Files.createTempDirectory("utplsql_assets_");
} catch (IOException e) {
throw new GenericRuntimeException("Cannot create temporary directory for code coverage report assets.", e);
}
populateCoverageAssets();
}

// public for testing purposes only
public URL getHtmlReportAssetPath() {
try {
return Paths.get(assetDir.toString()).toUri().toURL();
} catch (MalformedURLException e) {
throw new GenericRuntimeException("Cannot convert code coverage asset path to URL.", e);
}
}

private void copyStreamToFile(InputStream inputStream, Path file) throws IOException {
file.toFile().mkdirs();
Files.copy(inputStream, file, StandardCopyOption.REPLACE_EXISTING);
}

private void populateCoverageAssets() {
logger.fine(() -> "Copying code coverage report assets to " + assetDir.toString() + "...");
try {
final File file = new File(getClass().getProtectionDomain().getCodeSource().getLocation().getPath());
if (file.isFile()) {
// class loaded from a JAR file
final JarFile jar = new JarFile(file);
final List<JarEntry> entries = jar.stream().filter(entry -> !entry.isDirectory() && entry.getName().startsWith(ASSETS_PATH)).collect(Collectors.toList());
for (JarEntry entry : entries) {
Path f = Paths.get(assetDir.toString() + File.separator + entry.getName().substring(ASSETS_PATH.length()));
copyStreamToFile(jar.getInputStream(entry), f);
}
} else {
// class loaded from file system (IDE or during test/build)
ResourcePatternResolver resolver = new PathMatchingResourcePatternResolver();
Resource[] resources = resolver.getResources(ResourcePatternResolver.CLASSPATH_ALL_URL_PREFIX + "/" + ASSETS_PATH + "**");
for (Resource resource : resources) {
if (Objects.requireNonNull(resource.getFilename()).contains(".")) {
// process files but not directories, assume that directories do not contain a period
String path = resource.getURL().getPath();
Path f = Paths.get(assetDir.toString() + File.separator + path.substring(path.lastIndexOf(ASSETS_PATH) + ASSETS_PATH.length()));
copyStreamToFile(resource.getInputStream(), f);
}
}
}
} catch (IOException e) {
throw new GenericRuntimeException("Error while copying coverage report assets to temporary directory.", e);
}
}

private ArrayList<String> toStringList(final String s) {
final ArrayList<String> list = new ArrayList<>();
if (s != null && !s.isEmpty()) {
Expand Down Expand Up @@ -142,7 +207,7 @@ private void run() {

private void runCodeCoverageWithRealtimeReporter() {
final UtplsqlRunner runner = new UtplsqlRunner(pathList, toStringList(schemas), toStringList(includeObjects),
toStringList(excludeObjects), connectionName);
toStringList(excludeObjects), getHtmlReportAssetPath(), connectionName);
runner.runTestAsync();
}

Expand All @@ -152,7 +217,7 @@ private void runCodeCoverageStandalone() {
coverageConn = conn != null ? conn : DatabaseTools.cloneConnection(connectionName);
final UtplsqlDao dao = new UtplsqlDao(coverageConn);
final String html = dao.htmlCodeCoverage(pathList, toStringList(schemas),
toStringList(includeObjects), toStringList(excludeObjects));
toStringList(includeObjects), toStringList(excludeObjects), getHtmlReportAssetPath());
openInBrowser(html);
} finally {
try {
Expand All @@ -174,7 +239,7 @@ public static void openInBrowser(String html) {
final URL url = file.toURI().toURL();
logger.fine(() -> "Opening " + url.toExternalForm() + " in browser...");
final Desktop desktop = Desktop.isDesktopSupported() ? Desktop.getDesktop() : null;
if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE) && url != null) {
if (desktop != null && desktop.isSupported(Desktop.Action.BROWSE)) {
desktop.browse(url.toURI());
logger.fine(() -> url.toExternalForm() + " opened in browser.");
} else {
Expand Down Expand Up @@ -229,9 +294,7 @@ public void setExcludeObjects(final String excludeObjects) {
}

public Thread runAsync() {
final Thread thread = new Thread(() -> {
run();
});
final Thread thread = new Thread(this::run);
thread.setName("code coverage reporter");
thread.start();
return thread;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.io.IOException;
import java.io.StringReader;
import java.net.URL;
import java.sql.Clob;
import java.sql.Connection;
import java.sql.ResultSet;
Expand Down Expand Up @@ -108,11 +109,11 @@ public void produceReport(final String reporterId, final List<String> pathList)

public void produceReportWithCoverage(final String realtimeReporterId, final String coverageReporterId,
final List<String> pathList, final List<String> schemaList, final List<String> includeObjectList,
final List<String> excludeObjectList) {
final List<String> excludeObjectList, final URL htmlReportAssetPath) {
StringBuilder sb = new StringBuilder();
sb.append("DECLARE\n");
sb.append(" l_rt_rep ut_realtime_reporter := ut_realtime_reporter();\n");
sb.append(" l_cov_rep ut_coverage_html_reporter := ut_coverage_html_reporter();\n");
sb.append(" l_cov_rep ut_coverage_html_reporter := ut_coverage_html_reporter(a_html_report_assets_path => ?);\n");
sb.append("BEGIN\n");
sb.append(" l_rt_rep.set_reporter_id(?);\n");
sb.append(" l_rt_rep.output_buffer.init();\n");
Expand Down Expand Up @@ -143,7 +144,7 @@ public void produceReportWithCoverage(final String realtimeReporterId, final Str
sb.append(" sys.dbms_output.disable;\n");
sb.append("END;");
final String plsql = sb.toString();
final Object[] binds = { realtimeReporterId, coverageReporterId };
final Object[] binds = { htmlReportAssetPath == null ? null : htmlReportAssetPath.toExternalForm(), realtimeReporterId, coverageReporterId };
jdbcTemplate.update(plsql, binds);
}

Expand Down
14 changes: 12 additions & 2 deletions sqldev/src/main/java/org/utplsql/sqldev/dal/UtplsqlDao.java
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
*/
package org.utplsql.sqldev.dal;

import java.net.URL;
import java.sql.CallableStatement;
import java.sql.Connection;
import java.sql.SQLException;
Expand Down Expand Up @@ -907,12 +908,14 @@ public OutputLines doInCallableStatement(final CallableStatement cs) throws SQLE
* @param excludeObjectList
* list of objects to be excluded from coverage analysis. None, if
* empty
* @param htmlReportAssetPath
* path of the assets for the coverage report. Default, if null
* @return HTML code coverage report in HTML format
* @throws DataAccessException
* if there is a problem
*/
public String htmlCodeCoverage(final List<String> pathList, final List<String> schemaList,
final List<String> includeObjectList, final List<String> excludeObjectList) {
final List<String> includeObjectList, final List<String> excludeObjectList, final URL htmlReportAssetPath) {
StringBuilder sb = new StringBuilder();
sb.append("SELECT column_value\n");
sb.append(" FROM table(\n");
Expand All @@ -935,7 +938,14 @@ public String htmlCodeCoverage(final List<String> pathList, final List<String> s
sb.append(StringTools.getCSV(excludeObjectList, 16));
sb.append(" ),\n");
}
sb.append(" a_reporter => ut_coverage_html_reporter()\n");
sb.append(" a_reporter => ut_coverage_html_reporter(\n");
sb.append(" a_html_report_assets_path => '");
if (htmlReportAssetPath != null) {
// empty string is handled as NULL in Oracle Database
sb.append(htmlReportAssetPath.toExternalForm());
}
sb.append("'\n");
sb.append(" )\n");
sb.append(" )\n");
sb.append(" )");
final String sql = sb.toString();
Expand Down
17 changes: 12 additions & 5 deletions sqldev/src/main/java/org/utplsql/sqldev/runner/UtplsqlRunner.java
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@

import java.awt.Dimension;
import java.awt.Toolkit;
import java.net.URL;
import java.sql.Connection;
import java.text.SimpleDateFormat;
import java.util.Date;
Expand Down Expand Up @@ -71,24 +72,28 @@ public class UtplsqlRunner implements RealtimeReporterEventConsumer {
private Thread producerThread;
private Thread consumerThread;
private boolean debug = false;
private final URL htmlReportAssetPath;

public UtplsqlRunner(final List<String> pathList, final String connectionName) {
this.withCodeCoverage = false;
this.pathList = pathList;
this.schemaList = null;
this.includeObjectList = null;
this.excludeObjectList = null;
this.htmlReportAssetPath = null;
setConnection(connectionName);
this.context = Context.newIdeContext();
}

public UtplsqlRunner(final List<String> pathList, final List<String> schemaList,
final List<String> includeObjectList, final List<String> excludeObjectList, final String connectionName) {
final List<String> includeObjectList, final List<String> excludeObjectList,
final URL htmlReportAssetPath, final String connectionName) {
this.withCodeCoverage = true;
this.pathList = pathList;
this.schemaList = schemaList;
this.includeObjectList = includeObjectList;
this.excludeObjectList = excludeObjectList;
this.htmlReportAssetPath = htmlReportAssetPath;
setConnection(connectionName);
this.context = Context.newIdeContext();
}
Expand All @@ -102,21 +107,23 @@ public UtplsqlRunner(final List<String> pathList, final Connection producerConn,
this.schemaList = null;
this.includeObjectList = null;
this.excludeObjectList = null;
this.htmlReportAssetPath = null;
this.producerConn = producerConn;
this.consumerConn = consumerConn;
}

/**
* this constructor is intended for tests only (with code coverage)
* this constructor is intended for tests only (with code coverage and default htmlReportAssetPath)
*/
public UtplsqlRunner(final List<String> pathList, final List<String> schemaList,
final List<String> includeObjectList, final List<String> excludeObjectList, final Connection producerConn,
final Connection consumerConn) {
final List<String> includeObjectList, final List<String> excludeObjectList,
final Connection producerConn, final Connection consumerConn) {
this.withCodeCoverage = true;
this.pathList = pathList;
this.schemaList = schemaList;
this.includeObjectList = includeObjectList;
this.excludeObjectList = excludeObjectList;
this.htmlReportAssetPath = null;
this.producerConn = producerConn;
this.consumerConn = consumerConn;
}
Expand Down Expand Up @@ -314,7 +321,7 @@ private void produce() {
logger.fine(() -> "Running utPLSQL tests and producing events via reporter id " + realtimeReporterId + "...");
final RealtimeReporterDao dao = new RealtimeReporterDao(producerConn);
if (withCodeCoverage) {
dao.produceReportWithCoverage(realtimeReporterId, coverageReporterId, pathList, schemaList, includeObjectList, excludeObjectList);
dao.produceReportWithCoverage(realtimeReporterId, coverageReporterId, pathList, schemaList, includeObjectList, excludeObjectList, htmlReportAssetPath);
} else {
if (!debug) {
dao.produceReport(realtimeReporterId, pathList);
Expand Down
Loading