Skip to content
Merged
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