diff --git a/images/runner_model.png b/images/runner_model.png index f807ea78..1ca8e819 100644 Binary files a/images/runner_model.png and b/images/runner_model.png differ diff --git a/sqldev/src/main/java/org/utplsql/sqldev/model/StringTools.java b/sqldev/src/main/java/org/utplsql/sqldev/model/StringTools.java index 23fc20f9..23323043 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/model/StringTools.java +++ b/sqldev/src/main/java/org/utplsql/sqldev/model/StringTools.java @@ -15,9 +15,14 @@ */ package org.utplsql.sqldev.model; +import java.text.ParseException; +import java.text.SimpleDateFormat; import java.util.Collections; +import java.util.Date; import java.util.List; +import org.utplsql.sqldev.exception.GenericRuntimeException; + public class StringTools { // do not instantiate this class private StringTools() { @@ -38,11 +43,11 @@ public static String getCSV(List list, String indent) { sb.append("\n"); return sb.toString(); } - + public static String getCSV(List list, int indentSpaces) { return getCSV(list, repeat(" ", indentSpaces)); } - + public static String getSimpleCSV(List list) { final StringBuilder sb = new StringBuilder(); for (final String item : list) { @@ -74,4 +79,45 @@ public static String formatDateTime(final String dateTime) { } } } + + public static String millisToDateTimeString(long millis) { + final Date dateTime = new Date(millis); + final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'000'"); + return df.format(dateTime); + } + + public static String getSysdate() { + return millisToDateTimeString(System.currentTimeMillis()); + } + + public static long dateTimeStringToMillis(final String dateTime) { + // handle milliseconds separately since they get lost (rounded) when converted to date + final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss"); + Date date; + try { + date = df.parse(dateTime.substring(0, 20)); + } catch (ParseException e) { + throw new GenericRuntimeException("cannot parse datetime string " + dateTime + ".", e); + } + long millis = Long.parseLong(dateTime.substring(20, 23)); + return date.getTime() + millis; + } + + public static double elapsedTime(String startDateTime, String endDateTime) { + double start = (double) dateTimeStringToMillis(startDateTime); + double end = (double) dateTimeStringToMillis(endDateTime); + return (end - start) / 1000; + } + + public static boolean isNotBlank(String value) { + return value != null && !value.trim().isEmpty(); + } + + public static String trim(String value) { + if (value == null) { + return null; + } + return value.trim(); + } + } diff --git a/sqldev/src/main/java/org/utplsql/sqldev/model/preference/PreferenceModel.java b/sqldev/src/main/java/org/utplsql/sqldev/model/preference/PreferenceModel.java index 33a3d190..5cba1557 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/model/preference/PreferenceModel.java +++ b/sqldev/src/main/java/org/utplsql/sqldev/model/preference/PreferenceModel.java @@ -53,6 +53,7 @@ public static PreferenceModel getInstance(final PropertyStorage prefs) { private static final String KEY_SHOW_DISABLED_TESTS = "showDisabledTests"; private static final String KEY_SHOW_TEST_DESCRIPTION = "showTestDescription"; private static final String KEY_SYNC_DETAIL_TAB = "syncDetailTab"; + private static final String KEY_SHOW_SUITES = "showSuites"; private static final String KEY_TEST_PACKAGE_PREFIX = "testPackagePrefix"; private static final String KEY_TEST_PACKAGE_SUFFIX = "testPackageSuffix"; private static final String KEY_TEST_UNIT_PREFIX = "testUnitPrefix"; @@ -88,6 +89,7 @@ public String toString() { .append(KEY_SHOW_DISABLED_TESTS, isShowDisabledTests()) .append(KEY_SHOW_TEST_DESCRIPTION, isShowTestDescription()) .append(KEY_SYNC_DETAIL_TAB, isSyncDetailTab()) + .append(KEY_SHOW_SUITES, isShowSuites()) .append(KEY_TEST_PACKAGE_PREFIX, getTestPackagePrefix()) .append(KEY_TEST_PACKAGE_SUFFIX, getTestPackageSuffix()) .append(KEY_TEST_UNIT_PREFIX, getTestUnitPrefix()) @@ -241,6 +243,14 @@ public void setSyncDetailTab(final boolean syncDetailTab) { getHashStructure().putBoolean(KEY_SYNC_DETAIL_TAB, syncDetailTab); } + public boolean isShowSuites() { + return getHashStructure().getBoolean(KEY_SHOW_SUITES, true); + } + + public void setShowSuites(final boolean showSuites) { + getHashStructure().putBoolean(KEY_SHOW_SUITES, showSuites); + } + public String getTestPackagePrefix() { return getHashStructure().getString(KEY_TEST_PACKAGE_PREFIX, "test_"); } diff --git a/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Item.java b/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Item.java index 5f578ca2..76b859c2 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Item.java +++ b/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Item.java @@ -15,11 +15,16 @@ */ package org.utplsql.sqldev.model.runner; +import javax.swing.Icon; + import org.springframework.core.style.ToStringCreator; import org.utplsql.sqldev.model.JsonToStringStyler; +import org.utplsql.sqldev.resources.UtplsqlResources; public abstract class Item { private String id; + private String name; + private String description; private String startTime; private String endTime; private Double executionTime; @@ -36,6 +41,8 @@ public Item() { public String toString() { return new ToStringCreator(this, JsonToStringStyler.getInstance()) .append("id", id) + .append("name", name) + .append("description", description) .append("startTime", startTime) .append("endTime", endTime) .append("executionTime", executionTime) @@ -43,8 +50,66 @@ public String toString() { .append("errorStack", errorStack) .append("serverOutput", serverOutput) .append("warnings", warnings) + .append("parentId", getParentId()) + .append("statusIcon", getStatusIcon()) + .append("warningIcon", getWarningIcon()) + .append("infoIcon", getInfoIcon()) .toString(); } + + public String getParentId() { + // Works only if id (suitepath) is build based on names delimited with a period + // that's expected for real utPLSQL runs, but may fail for artificial runs. + // Returning null is valid, it means this item has no parent and as a + // consequence it will be shown on the top level in the runner. + // A key is required to identify an item since suites can be delivered + // multiple times, e.g. when running a chosen list of tests. This way + // the tests will shown at the right position in the tree, regardless of the call + // parameters. + if (name != null && id != null && name.length() < id.length() && id.endsWith(name)) { + return id.substring(0, id.length() - name.length() - 1); + } + return null; + } + + public Icon getStatusIcon() { + Icon icon = null; + if (getStartTime() != null && getEndTime() == null) { + icon = UtplsqlResources.getIcon("PROGRESS_ICON"); + } else { + if (getCounter() != null) { + // Escalation logic as for the color of the progress bar. + // A suite with errors or failed tests cannot be considered successful, + // even if some tests completed successfully. + if (getCounter().getError() > 0) { + icon = UtplsqlResources.getIcon("ERROR_ICON"); + } else if (getCounter().getFailure() > 0) { + icon = UtplsqlResources.getIcon("FAILURE_ICON"); + } else if (getCounter().getSuccess() > 0) { + icon = UtplsqlResources.getIcon("SUCCESS_ICON"); + } else if (getCounter().getDisabled() > 0) { + icon = UtplsqlResources.getIcon("DISABLED_ICON"); + } + } + } + return icon; + } + + public Icon getWarningIcon() { + Icon icon = null; + if (getCounter() != null && getCounter().getWarning() > 0) { + icon = UtplsqlResources.getIcon("WARNING_ICON"); + } + return icon; + } + + public Icon getInfoIcon() { + Icon icon = null; + if (getServerOutput() != null && getServerOutput().length() > 0) { + icon = UtplsqlResources.getIcon("INFO_ICON"); + } + return icon; + } public String getId() { return id; @@ -54,6 +119,22 @@ public void setId(final String id) { this.id = id; } + public String getName() { + return name; + } + + public void setName(final String name) { + this.name = name; + } + + public String getDescription() { + return description; + } + + public void setDescription(final String description) { + this.description = description; + } + public String getStartTime() { return startTime; } diff --git a/sqldev/src/main/java/org/utplsql/sqldev/model/runner/ItemNode.java b/sqldev/src/main/java/org/utplsql/sqldev/model/runner/ItemNode.java new file mode 100644 index 00000000..591e1b3f --- /dev/null +++ b/sqldev/src/main/java/org/utplsql/sqldev/model/runner/ItemNode.java @@ -0,0 +1,201 @@ +/* + * Copyright 2021 Philipp Salvisberg + * + * 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 org.utplsql.sqldev.model.runner; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.Enumeration; +import java.util.HashSet; +import java.util.List; +import java.util.Set; + +import javax.swing.Icon; +import javax.swing.tree.DefaultMutableTreeNode; + +import org.utplsql.sqldev.resources.UtplsqlResources; + +public class ItemNode extends DefaultMutableTreeNode implements Comparable { + + private static final long serialVersionUID = -4053143673822661743L; + + public ItemNode(Item userObject) { + super(userObject, userObject instanceof Suite); + } + + @Override + public int compareTo(ItemNode other) { + return getId().compareTo(other.getId()); + } + + public String getId() { + return ((Item) getUserObject()).getId(); + } + + public String getName() { + return ((Item) getUserObject()).getName(); + } + + public String getDescription() { + return ((Item) getUserObject()).getDescription(); + } + + public Double getExecutionTime() { + return ((Item) getUserObject()).getExecutionTime(); + } + + public Set getTestPackages() { + HashSet testPackages = new HashSet<>(); + Enumeration orderedNodes = preorderEnumeration(); + while (orderedNodes.hasMoreElements()) { + ItemNode node = (ItemNode) orderedNodes.nextElement(); + if (node.getUserObject() instanceof Test) { + Test test = (Test) node.getUserObject(); + testPackages.add(test.getOwnerName() + "." + test.getObjectName()); + } + } + return testPackages; + } + + public Set getOwners() { + HashSet owners = new HashSet<>(); + Enumeration children = children(); + while (children.hasMoreElements()) { + ItemNode child = (ItemNode) children.nextElement(); + owners.add(child.getOwnerName()); + } + return owners; + } + + public String getOwnerName() { + String ownerName = null; + Enumeration orderedNodes = preorderEnumeration(); + while (orderedNodes.hasMoreElements()) { + ItemNode node = (ItemNode) orderedNodes.nextElement(); + if (node.getUserObject() instanceof Test) { + Test test = (Test) node.getUserObject(); + if (ownerName == null) { + ownerName = test.getOwnerName(); + } else if (!ownerName.equals(test.getOwnerName())) { + ownerName = "***"; + break; + } + } + } + return ownerName; + } + + public String getPackageName() { + String packageName = null; + Enumeration orderedNodes = preorderEnumeration(); + while (orderedNodes.hasMoreElements()) { + ItemNode node = (ItemNode) orderedNodes.nextElement(); + if (node.getUserObject() instanceof Test) { + Test test = (Test) node.getUserObject(); + if (packageName == null) { + packageName = test.getObjectName(); + } else if (!packageName.equals(test.getObjectName())) { + packageName = "***"; + break; + } + } + } + return packageName; + } + + public String getProcedureName() { + String procedureName = null; + Enumeration orderedNodes = preorderEnumeration(); + while (orderedNodes.hasMoreElements()) { + ItemNode node = (ItemNode) orderedNodes.nextElement(); + if (node.getUserObject() instanceof Test) { + Test test = (Test) node.getUserObject(); + if (procedureName == null) { + procedureName = test.getProcedureName(); + } else if (!procedureName.equals(test.getProcedureName())) { + procedureName = "***"; + break; + } + } + } + return procedureName; + } + + public Icon getStatusIcon() { + Item item = (Item) getUserObject(); + Icon icon = item.getStatusIcon(); + if (icon == null) { + if (item.getId() != null) { + if (item instanceof Test) { + icon = UtplsqlResources.getIcon("PROCEDURE_ICON"); + } else if (item.getId().contains("context_#")) { + icon = UtplsqlResources.getIcon("PROCEDURE_FOLDER_ICON"); + } else { + if (item.getName().equals(getPackageName())) { + icon = UtplsqlResources.getIcon("PACKAGE_ICON"); + } else { + icon = UtplsqlResources.getIcon("PACKAGE_FOLDER_ICON"); + } + } + } + } + return icon; + } + + public Icon getWarningIcon() { + return ((Item) getUserObject()).getWarningIcon(); + } + + public Icon getInfoIcon() { + return ((Item) getUserObject()).getInfoIcon(); + } + + /** + * Calculates non-overlapping items. + * + * This can be used to build a list of suites to be started by utPLSQL while ensuring that + * + * - all requested tests are executed, but not more than once + * - the test execution is efficient by ensuring that the list is as short as possible + * + * This means if all tests of a suite shall be executed that the suit should be + * part of the result list and not all of its tests. + * + * In other words, top-level nodes are preferred to produce an optimal result. + * + * @param selectedNodes all selected nodes must be part of the same tree + * @return non-overlapping set of nodes + */ + public static Set createNonOverlappingSet(List selectedNodes) { + HashSet result = new HashSet<>(); + if (selectedNodes != null && selectedNodes.size() > 0) { + HashSet expandedResult = new HashSet<>(); + List sortedNodes = new ArrayList<>(selectedNodes); + Collections.sort(sortedNodes); + for (ItemNode sortedNode : sortedNodes) { + if (!expandedResult.contains(sortedNode)) { + result.add(sortedNode); + Enumeration expandedNodes = sortedNode.preorderEnumeration(); + while (expandedNodes.hasMoreElements()) { + ItemNode expandedNode = (ItemNode) expandedNodes.nextElement(); + expandedResult.add(expandedNode); + } + } + } + } + return result; + } + +} diff --git a/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Run.java b/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Run.java index 94d7c172..09d38e2f 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Run.java +++ b/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Run.java @@ -17,7 +17,10 @@ import java.sql.Connection; import java.util.LinkedHashMap; +import java.util.LinkedHashSet; import java.util.List; +import java.util.Map; +import java.util.Set; import org.springframework.core.style.ToStringCreator; import org.utplsql.sqldev.model.JsonToStringStyler; @@ -37,7 +40,9 @@ public class Run { private Integer infoCount; private String errorStack; private String serverOutput; - private LinkedHashMap tests; + private final Set items; + private Map tests; + private Map itemNodes; private String status; private Long start; // to abort connections, producerConn is handled by UtplsqlRunner @@ -60,6 +65,7 @@ public String toString() { .append("errorStack", errorStack) .append("serverOutput", serverOutput) .append("tests", tests) + .append("rootNode", itemNodes.get(reporterId)) .append("status", status) .append("start", start) .append("endTime", endTime) @@ -72,7 +78,10 @@ public Run(final String reporterId, final String connectionName, final List(); tests = new LinkedHashMap<>(); + itemNodes = new LinkedHashMap<>(); + createRootNode(); } public void setStartTime(final String startTime) { @@ -86,17 +95,54 @@ public String getName() { return time + " (" + conn + ")"; } + /** + * Is called after consuming the pre-run event to populate all items of a run. + * It's expected to be called only once. + * + * @param items items of a run, to be shown in the runner right after starting a run. + */ public void put(final List items) { + populateItems(items); + populateItemNodes(); + populateItemNodeChildren(); + } + + private void createRootNode() { + // Create pseudo root node as suite. + // The TreeTableModel requires a single root node, but it will not be displayed. + final Suite rootSuite = new Suite(); + rootSuite.setId(getReporterId()); + rootSuite.setName(getReporterId()); + ItemNode rootNode = new ItemNode(rootSuite); + itemNodes.put(rootSuite.getId(), rootNode); + } + + private void populateItems(List items) { for (final Item item : items) { - if (item instanceof Test) { - tests.put(item.getId(), (Test) item); - } + this.items.add(item); if (item instanceof Suite) { - put(((Suite) item).getItems()); + populateItems(((Suite) item).getItems()); + } else if (item instanceof Test) { + this.tests.put(item.getId(), (Test) item); } } } - + + private void populateItemNodes() { + for (final Item item : items) { + itemNodes.put(item.getId(), new ItemNode(item)); + } + } + + private void populateItemNodeChildren() { + for (Item item : items) { + String parentId = item.getParentId(); + ItemNode node = itemNodes.get(item.getId()); + ItemNode parent = itemNodes.get(parentId == null ? reporterId : parentId); + parent.add(node); + } + } + public Test getTest(final String id) { return tests.get(id); } @@ -216,12 +262,20 @@ public void setServerOutput(final String serverOutput) { } public LinkedHashMap getTests() { - return tests; + return (LinkedHashMap) tests; } public void setTests(final LinkedHashMap tests) { this.tests = tests; } + + public LinkedHashMap getItemNodes() { + return (LinkedHashMap) itemNodes; + } + + public void setItemNodes(LinkedHashMap itemNodes) { + this.itemNodes = itemNodes; + } public String getStatus() { return status; @@ -246,4 +300,5 @@ public Connection getConsumerConn() { public void setConsumerConn(Connection consumerConn) { this.consumerConn = consumerConn; } + } diff --git a/sqldev/src/main/java/org/utplsql/sqldev/model/runner/RunnerModel.mgc b/sqldev/src/main/java/org/utplsql/sqldev/model/runner/RunnerModel.mgc index 61fb3090..e4ec1ec8 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/model/runner/RunnerModel.mgc +++ b/sqldev/src/main/java/org/utplsql/sqldev/model/runner/RunnerModel.mgc @@ -94,7 +94,7 @@ default-accepted="false" static-accepted="false" accessors-accepted="false" constructors-accepted="false"/> - + @@ -102,13 +102,27 @@ private-accepted="false" protected-accepted="false" default-accepted="false" static-accepted="false" accessors-accepted="false" constructors-accepted="false"/> + + + endpointName="currentTest" kind="Simple"> + + + + + + + + + + + + + items; public Suite() { @@ -35,6 +33,8 @@ public String toString() { return new ToStringCreator(this, JsonToStringStyler.getInstance()) // ancestor .append("id", getId()) + .append("name", getName()) + .append("description", getDescription()) .append("startTime", getStartTime()) .append("endTime", getEndTime()) .append("executionTime", getExecutionTime()) @@ -42,29 +42,15 @@ public String toString() { .append("errorStack", getErrorStack()) .append("serverOutput", getServerOutput()) .append("warnings", getWarnings()) + .append("parentId", getParentId()) + .append("statusIcon", getStatusIcon()) + .append("warningIcon", getWarningIcon()) + .append("infoIcon", getInfoIcon()) // local - .append("name", name) - .append("description", description) .append("items", items) .toString(); } - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(final String description) { - this.description = description; - } - public List getItems() { return items; } diff --git a/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Test.java b/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Test.java index 79e8a3fe..0df46eba 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Test.java +++ b/sqldev/src/main/java/org/utplsql/sqldev/model/runner/Test.java @@ -17,11 +17,8 @@ import java.util.List; -import javax.swing.Icon; - import org.springframework.core.style.ToStringCreator; import org.utplsql.sqldev.model.JsonToStringStyler; -import org.utplsql.sqldev.resources.UtplsqlResources; public class Test extends Item { private String executableType; @@ -29,8 +26,6 @@ public class Test extends Item { private String objectName; private String procedureName; private Boolean disabled; - private String name; - private String description; private Integer testNumber; private List failedExpectations; @@ -39,6 +34,8 @@ public String toString() { return new ToStringCreator(this, JsonToStringStyler.getInstance()) // ancestor .append("id", getId()) + .append("name", getName()) + .append("description", getDescription()) .append("startTime", getStartTime()) .append("endTime", getEndTime()) .append("executionTime", getExecutionTime()) @@ -46,58 +43,21 @@ public String toString() { .append("errorStack", getErrorStack()) .append("serverOutput", getServerOutput()) .append("warnings", getWarnings()) + .append("parentId", getParentId()) + .append("statusIcon", getStatusIcon()) + .append("warningIcon", getWarningIcon()) + .append("infoIcon", getInfoIcon()) // local .append("executableType", executableType) .append("ownerName", ownerName) .append("objectName", objectName) .append("procedureName", procedureName) .append("disabled", disabled) - .append("name", name) - .append("description", description) .append("testNumber", testNumber) .append("failedExpectations", failedExpectations) - .append("statusIcon", getStatusIcon()) - .append("warningIcon", getWarningIcon()) - .append("infoIcon", getInfoIcon()) .toString(); } - public Icon getStatusIcon() { - Icon icon = null; - if (getStartTime() != null && getEndTime() == null) { - icon = UtplsqlResources.getIcon("PROGRESS_ICON"); - } else { - if (getCounter() != null) { - if (getCounter().getSuccess() > 0) { - icon = UtplsqlResources.getIcon("SUCCESS_ICON"); - } else if (getCounter().getError() > 0) { - icon = UtplsqlResources.getIcon("ERROR_ICON"); - } else if (getCounter().getFailure() > 0) { - icon = UtplsqlResources.getIcon("FAILURE_ICON"); - } else if (getCounter().getDisabled() > 0) { - icon = UtplsqlResources.getIcon("DISABLED_ICON"); - } - } - } - return icon; - } - - public Icon getWarningIcon() { - Icon icon = null; - if (getCounter() != null && getCounter().getWarning() > 0) { - icon = UtplsqlResources.getIcon("WARNING_ICON"); - } - return icon; - } - - public Icon getInfoIcon() { - Icon icon = null; - if (getServerOutput() != null && getServerOutput().length() > 0) { - icon = UtplsqlResources.getIcon("INFO_ICON"); - } - return icon; - } - public String getExecutableType() { return executableType; } @@ -138,22 +98,6 @@ public void setDisabled(final Boolean disabled) { this.disabled = disabled; } - public String getName() { - return name; - } - - public void setName(final String name) { - this.name = name; - } - - public String getDescription() { - return description; - } - - public void setDescription(final String description) { - this.description = description; - } - public Integer getTestNumber() { return testNumber; } diff --git a/sqldev/src/main/java/org/utplsql/sqldev/runner/UtplsqlRunner.java b/sqldev/src/main/java/org/utplsql/sqldev/runner/UtplsqlRunner.java index fbd0ab47..5f7c783d 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/runner/UtplsqlRunner.java +++ b/sqldev/src/main/java/org/utplsql/sqldev/runner/UtplsqlRunner.java @@ -19,24 +19,21 @@ import java.awt.Toolkit; import java.net.URL; import java.sql.Connection; -import java.text.SimpleDateFormat; -import java.util.Date; import java.util.List; import java.util.UUID; +import java.util.function.Supplier; import java.util.logging.Logger; import javax.swing.JFrame; -import oracle.dbtools.raptor.runner.DBStarterFactory; -import oracle.ide.Context; -import oracle.jdevimpl.runner.debug.DebuggingProcess; -import oracle.jdevimpl.runner.run.JRunner; import org.utplsql.sqldev.coverage.CodeCoverageReporter; import org.utplsql.sqldev.dal.RealtimeReporterDao; import org.utplsql.sqldev.dal.RealtimeReporterEventConsumer; import org.utplsql.sqldev.exception.GenericRuntimeException; import org.utplsql.sqldev.model.DatabaseTools; +import org.utplsql.sqldev.model.StringTools; import org.utplsql.sqldev.model.SystemTools; +import org.utplsql.sqldev.model.runner.ItemNode; import org.utplsql.sqldev.model.runner.PostRunEvent; import org.utplsql.sqldev.model.runner.PostSuiteEvent; import org.utplsql.sqldev.model.runner.PostTestEvent; @@ -45,12 +42,18 @@ import org.utplsql.sqldev.model.runner.PreTestEvent; import org.utplsql.sqldev.model.runner.RealtimeReporterEvent; import org.utplsql.sqldev.model.runner.Run; +import org.utplsql.sqldev.model.runner.Suite; import org.utplsql.sqldev.model.runner.Test; import org.utplsql.sqldev.resources.UtplsqlResources; import org.utplsql.sqldev.ui.runner.RunnerFactory; import org.utplsql.sqldev.ui.runner.RunnerPanel; import org.utplsql.sqldev.ui.runner.RunnerView; +import oracle.dbtools.raptor.runner.DBStarterFactory; +import oracle.ide.Context; +import oracle.jdevimpl.runner.debug.DebuggingProcess; +import oracle.jdevimpl.runner.run.JRunner; + public class UtplsqlRunner implements RealtimeReporterEventConsumer { private static final Logger logger = Logger.getLogger(UtplsqlRunner.class.getName()); private static final int DEBUG_TIMEOUT_SECONDS = 60*60; @@ -165,7 +168,7 @@ public void process(final RealtimeReporterEvent event) { } else if (event instanceof PreRunEvent) { doProcess((PreRunEvent) event); } else if (event instanceof PreSuiteEvent) { - // not processed + doProcess((PreSuiteEvent) event); } else if (event instanceof PreTestEvent) { doProcess((PreTestEvent) event); } else { @@ -173,19 +176,13 @@ public void process(final RealtimeReporterEvent event) { } } - public static String getSysdate() { - final Date dateTime = new Date(System.currentTimeMillis()); - final SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSS'000'"); - return df.format(dateTime); - } - public boolean isRunning() { return run != null && run.getEndTime() == null; } private void initRun() { run = new Run(realtimeReporterId, connectionName, pathList); - run.setStartTime(getSysdate()); + run.setStartTime(StringTools.getSysdate()); run.getCounter().setDisabled(0); run.getCounter().setSuccess(0); run.getCounter().setFailure(0); @@ -200,6 +197,13 @@ private void initRun() { panel.update(realtimeReporterId); } + private boolean logFalseCondition(boolean condition, Supplier msgToLog) { + if (!condition) { + logger.severe(msgToLog); + } + return condition; + } + private void doProcess(final PreRunEvent event) { run.setTotalNumberOfTests(event.getTotalNumberOfTests()); run.put(event.getItems()); @@ -216,8 +220,59 @@ private void doProcess(final PostRunEvent event) { run.setStatus(UtplsqlResources.getString("RUNNER_FINISHED_TEXT")); panel.update(realtimeReporterId); } + + private void doProcess(final PreSuiteEvent event) { + final ItemNode node = run.getItemNodes().get(event.getId()); + assert logFalseCondition(node != null, () -> "Could not find suite id \"" + event.getId() + + "\" when processing PreSuiteEvent " + event.toString() + "."); + final Suite suite = (Suite) node.getUserObject(); + suite.setStartTime(StringTools.getSysdate()); + panel.update(realtimeReporterId); + } private void doProcess(final PostSuiteEvent event) { + final ItemNode node = run.getItemNodes().get(event.getId()); + assert logFalseCondition(node != null, () -> "Could not find suite id \"" + event.getId() + + "\" when processing PostSuiteEvent " + event.toString() + "."); + final Suite suite = (Suite) node.getUserObject(); + if (suite.getEndTime() == null) { + // first occurrence, multiple possible, e.g. ut_tester and ut_user in utPLSQL project + suite.setStartTime(event.getStartTime()); + suite.setEndTime(event.getEndTime()); + suite.setExecutionTime(event.getExecutionTime()); + suite.setCounter(event.getCounter()); + suite.setErrorStack(event.getErrorStack()); + suite.setWarnings(event.getWarnings()); + suite.setServerOutput(event.getServerOutput()); + } else { + // subsequent occurrence, aggregate + suite.setEndTime(event.getEndTime()); + suite.setExecutionTime(suite.getExecutionTime() + event.getExecutionTime()); + suite.getCounter().setWarning(suite.getCounter().getWarning() + event.getCounter().getWarning()); + suite.getCounter().setDisabled(suite.getCounter().getDisabled() + event.getCounter().getDisabled()); + suite.getCounter().setSuccess(suite.getCounter().getSuccess() + event.getCounter().getSuccess()); + suite.getCounter().setFailure(suite.getCounter().getFailure() + event.getCounter().getFailure()); + suite.getCounter().setError(suite.getCounter().getError() + event.getCounter().getError()); + if (event.getWarnings() != null) { + StringBuilder sb = new StringBuilder(); + if (suite.getWarnings() != null) { + sb.append(suite.getWarnings()); + sb.append("\n\n"); + } + sb.append(event.getWarnings()); + suite.setWarnings(sb.toString()); + } + if (event.getServerOutput() != null) { + StringBuilder sb = new StringBuilder(); + if (suite.getServerOutput() != null) { + sb.append(suite.getServerOutput()); + sb.append("\n\n"); + } + sb.append(event.getServerOutput()); + suite.setServerOutput(sb.toString()); + } + } + final Test test = run.getCurrentTest(); // Errors on suite levels are reported as warnings by the utPLSQL framework, // since an error on suite level does not affect a status of a test. @@ -254,52 +309,46 @@ private void doProcess(final PostSuiteEvent event) { sb.append(event.getServerOutput()); test.setServerOutput(sb.toString()); } - panel.update(realtimeReporterId); + panel.update(realtimeReporterId, suite); } private void doProcess(final PreTestEvent event) { final Test test = run.getTest(event.getId()); - if (test == null) { - logger.severe(() -> "Could not find test id \"" + event.getId() + "\" when processing PreTestEvent " - + event.toString() + "."); - } else { - test.setStartTime(getSysdate()); - } + assert logFalseCondition(test != null, () -> "Could not find test id \"" + event.getId() + + "\" when processing PreTestEvent " + event.toString() + "."); + test.setStartTime(StringTools.getSysdate()); run.setStatus(event.getId() + "..."); run.setCurrentTestNumber(event.getTestNumber()); run.setCurrentTest(test); - panel.update(realtimeReporterId); + panel.update(realtimeReporterId, test); } private void doProcess(final PostTestEvent event) { final Test test = run.getTest(event.getId()); - if (test == null) { - logger.severe(() -> "Could not find test id \"" + event.getId() + "\" when processing PostTestEvent " - + event.toString() + "."); + assert logFalseCondition(test != null, () -> "Could not find test id \"" + event.getId() + + "\" when processing PostTestEvent " + event.toString() + "."); + test.setStartTime(event.getStartTime()); + test.setEndTime(event.getEndTime()); + test.setExecutionTime(event.getExecutionTime()); + test.setCounter(event.getCounter()); + test.setErrorStack(event.getErrorStack()); + test.setServerOutput(event.getServerOutput()); + if (test.getServerOutput() != null) { + run.setInfoCount(run.getInfoCount() + 1); + } + test.setFailedExpectations(event.getFailedExpectations()); + test.setWarnings(event.getWarnings()); + if (test.getWarnings() != null) { + test.getCounter().setWarning(1); } else { - test.setStartTime(event.getStartTime()); - test.setEndTime(event.getEndTime()); - test.setExecutionTime(event.getExecutionTime()); - test.setCounter(event.getCounter()); - test.setErrorStack(event.getErrorStack()); - test.setServerOutput(event.getServerOutput()); - if (test.getServerOutput() != null) { - run.setInfoCount(run.getInfoCount() + 1); - } - test.setFailedExpectations(event.getFailedExpectations()); - test.setWarnings(event.getWarnings()); - if (test.getWarnings() != null) { - test.getCounter().setWarning(1); - } else { - test.getCounter().setWarning(0); - } - run.getCounter().setWarning(run.getCounter().getWarning() + test.getCounter().getWarning()); + test.getCounter().setWarning(0); } + run.getCounter().setWarning(run.getCounter().getWarning() + test.getCounter().getWarning()); run.getCounter().setDisabled(run.getCounter().getDisabled() + event.getCounter().getDisabled()); run.getCounter().setSuccess(run.getCounter().getSuccess() + event.getCounter().getSuccess()); run.getCounter().setFailure(run.getCounter().getFailure() + event.getCounter().getFailure()); run.getCounter().setError(run.getCounter().getError() + event.getCounter().getError()); - panel.update(realtimeReporterId); + panel.update(realtimeReporterId, test); } private void produceReportWithDebugger(String anonymousPlsqlBlock) { @@ -356,7 +405,7 @@ private void consume() { if (run.getTotalNumberOfTests() < 0) { run.setStatus(UtplsqlResources.getString("RUNNER_NO_TESTS_FOUND_TEXT")); run.setExecutionTime((System.currentTimeMillis() - Double.valueOf(run.getStart())) / 1000); - run.setEndTime(getSysdate()); + run.setEndTime(StringTools.getSysdate()); run.setTotalNumberOfTests(0); panel.update(realtimeReporterId); } diff --git a/sqldev/src/main/java/org/utplsql/sqldev/ui/preference/PreferencePanel.java b/sqldev/src/main/java/org/utplsql/sqldev/ui/preference/PreferencePanel.java index 8a0bf227..d56a1176 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/ui/preference/PreferencePanel.java +++ b/sqldev/src/main/java/org/utplsql/sqldev/ui/preference/PreferencePanel.java @@ -63,6 +63,7 @@ public class PreferencePanel extends DefaultTraversablePanel { private final JCheckBox showDisabledTestsCheckBox = new JCheckBox(); private final JCheckBox showTestDescriptionCheckBox = new JCheckBox(); private final JCheckBox syncDetailTabCheckBox = new JCheckBox(); + private final JCheckBox showSuitesCheckBox = new JCheckBox(); private final JPanel generateTestPanel = new JPanel(); private final JTextField testPackagePrefixTextField = new JTextField(); private final JTextField testPackageSuffixTextField = new JTextField(); @@ -135,6 +136,8 @@ private void layoutControls() { .component(showTestDescriptionCheckBox)); rrTab.add(runTab.field().label().withText(UtplsqlResources.getString("PREF_SYNC_DETAIL_TAB_LABEL")) .component(syncDetailTabCheckBox)); + rrTab.add(runTab.field().label().withText(UtplsqlResources.getString("PREF_SHOW_SUITES_LABEL")) + .component(showSuitesCheckBox)); rrTab.addVerticalSpring(); // generate test group @@ -376,6 +379,7 @@ public void onEntry(final TraversableContext traversableContext) { showDisabledTestsCheckBox.setSelected(info.isShowDisabledTests()); showTestDescriptionCheckBox.setSelected(info.isShowTestDescription()); syncDetailTabCheckBox.setSelected(info.isSyncDetailTab()); + showSuitesCheckBox.setSelected(info.isShowSuites()); testPackagePrefixTextField.setText(info.getTestPackagePrefix()); testPackageSuffixTextField.setText(info.getTestPackageSuffix()); testUnitPrefixTextField.setText(info.getTestUnitPrefix()); @@ -414,6 +418,7 @@ public void onExit(final TraversableContext traversableContext) throws Traversal info.setShowDisabledTests(showDisabledTestsCheckBox.isSelected()); info.setShowTestDescription(showTestDescriptionCheckBox.isSelected()); info.setSyncDetailTab(syncDetailTabCheckBox.isSelected()); + info.setShowSuites(showSuitesCheckBox.isSelected()); info.setTestPackagePrefix(testPackagePrefixTextField.getText()); info.setTestPackageSuffix(testPackageSuffixTextField.getText()); info.setTestUnitPrefix(testUnitPrefixTextField.getText()); diff --git a/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/RunnerPanel.java b/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/RunnerPanel.java index a49e91d8..145dde29 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/RunnerPanel.java +++ b/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/RunnerPanel.java @@ -31,6 +31,7 @@ import java.util.HashSet; import java.util.List; import java.util.Map; +import java.util.logging.Logger; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.stream.Collectors; @@ -38,6 +39,7 @@ import javax.swing.BorderFactory; import javax.swing.Box; import javax.swing.DefaultComboBoxModel; +import javax.swing.Icon; import javax.swing.JCheckBoxMenuItem; import javax.swing.JComboBox; import javax.swing.JComponent; @@ -51,6 +53,7 @@ import javax.swing.JSplitPane; import javax.swing.JTabbedPane; import javax.swing.JTable; +import javax.swing.JTree; import javax.swing.LookAndFeel; import javax.swing.RepaintManager; import javax.swing.RowFilter; @@ -65,6 +68,8 @@ import javax.swing.table.TableCellRenderer; import javax.swing.table.TableColumn; import javax.swing.table.TableRowSorter; +import javax.swing.tree.DefaultTreeCellRenderer; +import javax.swing.tree.TreePath; import org.springframework.web.util.HtmlUtils; import org.utplsql.sqldev.coverage.CodeCoverageReporter; @@ -76,6 +81,8 @@ import org.utplsql.sqldev.model.preference.PreferenceModel; import org.utplsql.sqldev.model.runner.Counter; import org.utplsql.sqldev.model.runner.Expectation; +import org.utplsql.sqldev.model.runner.Item; +import org.utplsql.sqldev.model.runner.ItemNode; import org.utplsql.sqldev.model.runner.Run; import org.utplsql.sqldev.model.runner.Test; import org.utplsql.sqldev.parser.UtplsqlParser; @@ -86,8 +93,10 @@ import oracle.dbtools.raptor.controls.grid.DefaultDrillLink; import oracle.ide.config.Preferences; import oracle.javatools.ui.table.ToolbarButton; +import oracle.javatools.ui.treetable.JFastTreeTable; public class RunnerPanel { + private static final Logger logger = Logger.getLogger(RunnerPanel.class.getName()); private static final Color GREEN = new Color(0, 153, 0); private static final Color RED = new Color(153, 0, 0); private static final int INDICATOR_WIDTH = 20; @@ -114,7 +123,10 @@ public class RunnerPanel { private JCheckBoxMenuItem showInfoCounterCheckBoxMenuItem; private JProgressBar progressBar; private TestOverviewTableModel testOverviewTableModel; + private TestOverviewTreeTableModel testOverviewTreeTableModel; private JTable testOverviewTable; + private JFastTreeTable testOverviewTreeTable; + private JScrollPane testOverviewScrollPane; private JMenuItem testOverviewRunMenuItem; private JMenuItem testOverviewRunWorksheetMenuItem; private JMenuItem testOverviewDebugMenuItem; @@ -125,6 +137,7 @@ public class RunnerPanel { private JCheckBoxMenuItem showSuccessfulTestsCheckBoxMenuItem; private JCheckBoxMenuItem showDisabledTestsCheckBoxMenuItem; private JCheckBoxMenuItem syncDetailTabCheckBoxMenuItem; + private JCheckBoxMenuItem showSuitesCheckBoxMenuItem; private RunnerTextField testOwnerTextField; private RunnerTextField testPackageTextField; private RunnerTextField testProcedureTextField; @@ -153,14 +166,14 @@ public Component getTableCellRendererComponent(final JTable table, final Object label.setIcon(UtplsqlResources.getIcon("STATUS_ICON")); label.setHorizontalAlignment(JLabel.CENTER); } else if (col == 1) { + label.setIcon(null); + label.setHorizontalAlignment(JLabel.LEFT); + } else if (col == 2) { label.setIcon(UtplsqlResources.getIcon("WARNING_ICON")); label.setHorizontalAlignment(JLabel.CENTER); - } else if (col == 2) { + } else if (col == 3) { label.setIcon(UtplsqlResources.getIcon("INFO_ICON")); label.setHorizontalAlignment(JLabel.CENTER); - } else if (col == 3) { - label.setIcon(null); - label.setHorizontalAlignment(JLabel.LEFT); } else if (col == 4) { label.setIcon(null); label.setHorizontalAlignment(JLabel.RIGHT); @@ -169,6 +182,33 @@ public Component getTableCellRendererComponent(final JTable table, final Object } } + // used in multiple components, therefore an inner class + private static class TestTreeTableHeaderRenderer extends DefaultTableCellRenderer { + private static final long serialVersionUID = -1784754761029185815L; + + @Override + public Component getTableCellRendererComponent(final JTable table, final Object value, final boolean isSelected, + final boolean hasFocus, final int row, final int col) { + final TableCellRenderer renderer = table.getTableHeader().getDefaultRenderer(); + final JLabel label = ((JLabel) renderer.getTableCellRendererComponent(table, value, isSelected, hasFocus, + row, col)); + if (col == 0) { + label.setIcon(null); + label.setHorizontalAlignment(JLabel.LEFT); + } else if (col == 1) { + label.setIcon(UtplsqlResources.getIcon("WARNING_ICON")); + label.setHorizontalAlignment(JLabel.CENTER); + } else if (col == 2) { + label.setIcon(UtplsqlResources.getIcon("INFO_ICON")); + label.setHorizontalAlignment(JLabel.CENTER); + } else if (col == 3) { + label.setIcon(null); + label.setHorizontalAlignment(JLabel.RIGHT); + } + return label; + } + } + // used in multiple components, therefore an inner class private static class FailuresTableHeaderRenderer extends DefaultTableCellRenderer { private static final long serialVersionUID = 5059401447983514596L; @@ -257,12 +297,14 @@ private void applyShowWarningsCounter() { private void applyShowInfoCounter() { infoCounterValueLabel.getParent().setVisible(showInfoCounterCheckBoxMenuItem.isSelected()); } - + private void applyShowTestDescription() { + // table testOverviewTableModel.updateModel(showTestDescriptionCheckBoxMenuItem.isSelected()); - final TableColumn idColumn = testOverviewTable.getColumnModel().getColumn(3); - idColumn.setHeaderValue(testOverviewTableModel.getTestIdColumnName()); - testOverviewTable.getTableHeader().repaint(); + fixColumnHeader(testOverviewTableModel.getTestIdColumnName(), testOverviewTable, 1); + // tree-table + testOverviewTreeTableModel.updateModel(showTestDescriptionCheckBoxMenuItem.isSelected()); + fixColumnHeader(testOverviewTreeTableModel.getTreeColumnName(), testOverviewTreeTable, 0); } private void showColumn(final boolean show, TableColumn col) { @@ -280,35 +322,89 @@ private void showColumn(final boolean show, TableColumn col) { } private void applyShowWarningIndicator(final boolean show) { - showColumn(show, testOverviewTable.getColumnModel().getColumn(1)); + showColumn(show, testOverviewTable.getColumnModel().getColumn(2)); + showColumn(show, testOverviewTreeTable.getColumnModel().getColumn(1)); } private void applyShowInfoIndicator(final boolean show) { - showColumn(show, testOverviewTable.getColumnModel().getColumn(2)); + showColumn(show, testOverviewTable.getColumnModel().getColumn(3)); + showColumn(show, testOverviewTreeTable.getColumnModel().getColumn(2)); + } + + private void selectTestInTestOverviewTable(Test test) { + if (test != null) { + for (int i=0; i sorter = ((TableRowSorter) testOverviewTable.getRowSorter()); - final RowFilter filter = new RowFilter() { - @Override - public boolean include(final RowFilter.Entry entry) { - final Test test = entry.getModel().getTest((entry.getIdentifier()).intValue()); - final Counter counter = test.getCounter(); - if (counter != null) { - if (counter.getSuccess() > 0 && !showSuccessfulTests) { - return false; - } - if (counter.getDisabled() > 0 && !showDisabledTests) { - return false; - } + private void applyShowSuites() { + applyFilter(showSuccessfulTestsCheckBoxMenuItem.isSelected(), showDisabledTestsCheckBoxMenuItem.isSelected()); + if (showSuitesCheckBoxMenuItem.isSelected()) { + testOverviewScrollPane.setViewportView(testOverviewTreeTable); + // sync in tree-table - just first selected test + final int rowIndex = testOverviewTable.getSelectedRow(); + if (rowIndex != -1) { + final int row = testOverviewTable.convertRowIndexToModel(rowIndex); + final Test test = testOverviewTableModel.getTest(row); + ItemNode itemNode = testOverviewTreeTableModel.getItemNode(test.getId()); + if (itemNode != null) { + testOverviewTreeTable.getTree().setSelectionPath(new TreePath(itemNode.getPath())); } - return true; } - }; - sorter.setRowFilter(filter); + } else { + testOverviewScrollPane.setViewportView(testOverviewTable); + // sync in table - just first test in selected item + TreePath path = testOverviewTreeTable.getTree().getSelectionPath(); + if (path != null) { + ItemNode itemNode = (ItemNode) path.getLastPathComponent(); + Item item = (Item) itemNode.getUserObject(); + Test test; + if (item instanceof Test) { + test = (Test) item; + } else { + test = testOverviewTreeTableModel.getTestOf(itemNode); + } + selectTestInTestOverviewTable(test); + } + } + showSelectedRow(); } + private void applyFilter(final boolean showSuccessfulTests, final boolean showDisabledTests) { + if (!showSuitesCheckBoxMenuItem.isSelected()) { + // table + @SuppressWarnings("unchecked") + final TableRowSorter sorter = ((TableRowSorter) testOverviewTable.getRowSorter()); + final RowFilter filter = new RowFilter() { + @Override + public boolean include(final RowFilter.Entry entry) { + final Test test = entry.getModel().getTest((entry.getIdentifier()).intValue()); + final Counter counter = test.getCounter(); + if (counter != null) { + if (counter.getSuccess() > 0 && !showSuccessfulTests) { + return false; + } + if (counter.getDisabled() > 0 && !showDisabledTests) { + return false; + } + } + return true; + } + }; + sorter.setRowFilter(filter); + } else { + // tree-table + testOverviewTreeTableModel.updateModel(showSuccessfulTests, showDisabledTests); + expandAllNodes(testOverviewTreeTable.getTree(), 0); + } + } + private void openTest(final Test test) { final UtplsqlDao dao = new UtplsqlDao(DatabaseTools.getConnection(currentRun.getConnectionName())); final String source = dao.getSource(test.getOwnerName(), "PACKAGE", test.getObjectName().toUpperCase()).trim(); @@ -387,30 +483,46 @@ private void openEditor(final String owner, final String type, final String name "oracle.dbtools.raptor.controls.grid.DefaultDrillLink" }); drillLink.performDrill(); } + + private void syncDetailTab(Item item) { + int tabIndex = 0; + if (item != null) { + if (failuresTableModel.getRowCount() > 0) { + tabIndex = 1; + } else if (StringTools.isNotBlank(item.getErrorStack())) { + tabIndex = 2; + } else if (StringTools.isNotBlank(item.getWarnings())) { + tabIndex = 3; + } else if (StringTools.isNotBlank(item.getServerOutput())) { + tabIndex = 4; + } + } + testDetailTabbedPane.setSelectedIndex(tabIndex); + } private void syncDetailTab() { if (syncDetailTabCheckBoxMenuItem.isSelected()) { - final int rowIndex = testOverviewTable.getSelectedRow(); - if (rowIndex != -1) { - final int row = testOverviewTable.convertRowIndexToModel(rowIndex); - final Test test = testOverviewTableModel.getTest(row); - int tabIndex = 0; - if (test != null && test.getCounter() != null) { - if (test.getCounter().getFailure() != null && test.getCounter().getFailure() > 0) { - tabIndex = 1; - } else if (test.getCounter().getError() != null && test.getCounter().getError() > 0) { - tabIndex = 2; - } else if (test.getCounter().getWarning() != null && test.getCounter().getWarning() > 0) { - tabIndex = 3; - } else if (test.getServerOutput() != null && test.getServerOutput().length() > 0) { - tabIndex = 4; - } + if (!showSuitesCheckBoxMenuItem.isSelected()) { + // table + final int rowIndex = testOverviewTable.getSelectedRow(); + if (rowIndex != -1) { + final int row = testOverviewTable.convertRowIndexToModel(rowIndex); + final Test test = testOverviewTableModel.getTest(row); + syncDetailTab(test); + } + } else { + // tree-table + TreePath path = testOverviewTreeTable.getTree().getSelectionPath(); + if (path != null) { + ItemNode itemNode = (ItemNode) path.getLastPathComponent(); + Item item = (Item) itemNode.getUserObject(); + syncDetailTab(item); } - testDetailTabbedPane.setSelectedIndex(tabIndex); } + } } - + private PreferenceModel getPreferenceModel() { try { return PreferenceModel.getInstance(Preferences.getPreferences()); @@ -445,96 +557,206 @@ private void applyPreferences() { showDisabledTestsCheckBoxMenuItem.setSelected(preferences.isShowDisabledTests()); fixCheckBoxMenuItem(showDisabledTestsCheckBoxMenuItem); applyFilter(showSuccessfulTestsCheckBoxMenuItem.isSelected(), showDisabledTestsCheckBoxMenuItem.isSelected()); + testOverviewTreeTableModel.updateModel(showSuccessfulTestsCheckBoxMenuItem.isSelected(), showDisabledTestsCheckBoxMenuItem.isSelected()); fixCheckBoxMenuItem(showInfoIndicatorCheckBoxMenuItem); syncDetailTabCheckBoxMenuItem.setSelected(preferences.isSyncDetailTab()); fixCheckBoxMenuItem(syncDetailTabCheckBoxMenuItem); + showSuitesCheckBoxMenuItem.setSelected(preferences.isShowSuites()); + fixCheckBoxMenuItem(showSuitesCheckBoxMenuItem); + applyShowSuites(); useSmartTimes = preferences.isUseSmartTimes(); } public void setModel(final Run run) { + assert run != null && run.getReporterId() != null : "Cannot run without reporterId"; + setModel(run, false); + } + + private void setModel(final Run run, boolean force) { runs.put(run.getReporterId(), run); refreshRunsComboBox(); - setCurrentRun(run); + setCurrentRun(run, force); + } + + private void expandAllNodes(JTree tree, int startingRow) { + int rowCount = tree.getRowCount(); + for (int i = startingRow; i < rowCount; i++) { + tree.expandRow(i); + } + // recursive call until all nodes are expanded + if (tree.getRowCount() != rowCount) { + expandAllNodes(tree, rowCount); + } + } + + private void fixColumnHeader(String columnHeader, JTable table, int columnIndex) { + final TableColumn column = table.getColumnModel().getColumn(columnIndex); + if (!column.getHeaderValue().equals(columnHeader)) { + column.setHeaderValue(columnHeader); + table.getTableHeader().repaint(); + } } - private void setCurrentRun(final Run run) { - if (run != currentRun) { - currentRun = run; - testOverviewTableModel.setModel(run.getTests(), showTestDescriptionCheckBoxMenuItem.isSelected(), - useSmartTimes); - final String header = testOverviewTableModel.getTimeColumnName(); - final TableColumn timeColumn = testOverviewTable.getColumnModel().getColumn(4); - if (!timeColumn.getHeaderValue().equals(header)) { - timeColumn.setHeaderValue(header); - testOverviewTable.getTableHeader().repaint(); + /** + * Sets the current run. This can be forced with the force parameter. + * + * However, as long as a run is in progress you will technically not be able + * to switch to another run because a subsequent {@link #update(String)} call will + * switch back to the currently executing run. This behavior is intentional. + */ + private void setCurrentRun(final Run run, boolean force) { + boolean switched = false; + // Multiple, parallel runs are supported. Ensure that the runner does not switch back and forth. + if (force // choosing the run via the combo box + || currentRun == null // the very first run + || currentRun.getEndTime() != null // the current run is not running + || run.getTotalNumberOfTests() == -1) // when initializing a new run (newest wins once) + { + if (run != currentRun) { + currentRun = run; + // table + testOverviewTableModel.setModel(run.getTests(), showTestDescriptionCheckBoxMenuItem.isSelected(), + useSmartTimes); + fixColumnHeader(testOverviewTableModel.getTimeColumnName(), testOverviewTable, 4); + // tree-table + testOverviewTreeTableModel.setModel(run, showTestDescriptionCheckBoxMenuItem.isSelected(), useSmartTimes, + showSuccessfulTestsCheckBoxMenuItem.isSelected(), showDisabledTestsCheckBoxMenuItem.isSelected()); + fixColumnHeader(testOverviewTreeTableModel.getTimeColumnName(), testOverviewTreeTable, 3); + testOverviewTreeTableModel.reload(); + // common + resetDerived(); + final ComboBoxItem item = new ComboBoxItem<>(currentRun.getReporterId(), + currentRun.getName()); + runComboBox.setSelectedItem(item); + elapsedTimeTimer.start(); + switched = true; } - resetDerived(); + } + if (switched || !testOverviewTreeTableModel.isComplete()) { + // table + testOverviewTableModel.fireTableDataChanged(); + // tree-table + testOverviewTreeTableModel.updateModel(); + testOverviewTreeTableModel.reload(); + expandAllNodes(testOverviewTreeTable.getTree(), 0); + } + // ensure that the runComboBox shows always the currentRun + @SuppressWarnings("unchecked") + final ComboBoxItem currentItem = (ComboBoxItem) runComboBox.getSelectedItem(); + if (currentItem != null && !currentItem.getKey().equals(currentRun.getReporterId())) { final ComboBoxItem item = new ComboBoxItem<>(currentRun.getReporterId(), currentRun.getName()); runComboBox.setSelectedItem(item); - elapsedTimeTimer.start(); } } private void enableOrDisableStopButton() { stopButton.setEnabled(currentRun.getEndTime() == null); } - + public synchronized void update(final String reporterId) { - enableOrDisableStopButton(); - setCurrentRun(runs.get(reporterId)); - final int row = currentRun.getCurrentTestNumber() - 1; - final CharSequence header = testOverviewTableModel.getTestIdColumnName(); - final TableColumn idColumn = testOverviewTable.getColumnModel().getColumn(3); - if (!idColumn.getHeaderValue().equals(header)) { - idColumn.setHeaderValue(header); - testOverviewTable.getTableHeader().repaint(); - } - if (row < 0) { - testOverviewTableModel.fireTableDataChanged(); - } else { - if (testOverviewTableModel.getRowCount() > row) { - final Rectangle positionOfCurrentTest = testOverviewTable - .getCellRect(testOverviewTable.convertRowIndexToView(row), 0, true); - testOverviewTable.scrollRectToVisible(positionOfCurrentTest); - testOverviewTableModel.fireTableRowsUpdated(row, row); - SystemTools.sleep(5); - if (!showSuccessfulTestsCheckBoxMenuItem.isSelected() - || !showDisabledTestsCheckBoxMenuItem.isSelected()) { - applyFilter(showSuccessfulTestsCheckBoxMenuItem.isSelected(), - showDisabledTestsCheckBoxMenuItem.isSelected()); + update(reporterId, null); + } + + public synchronized void update(final String reporterId, Item item) { + try { + setCurrentRun(runs.get(reporterId), false); + if (!currentRun.getReporterId().equals(reporterId)) { + // this run is currently not active in the runner + return; + } + enableOrDisableStopButton(); + fixColumnHeader(testOverviewTableModel.getTestIdColumnName(), testOverviewTable, 1); + fixColumnHeader(testOverviewTreeTableModel.getTreeColumnName(), testOverviewTreeTable, 0); + if (!showSuitesCheckBoxMenuItem.isSelected()) { + // table + if (item instanceof Test) { + final int row = ((Test) item).getTestNumber() - 1; + if (row >= 0 && testOverviewTableModel.getRowCount() > row) { + final Rectangle positionOfCurrentTest = testOverviewTable + .getCellRect(testOverviewTable.convertRowIndexToView(row), 0, true); + testOverviewTable.scrollRectToVisible(positionOfCurrentTest); + testOverviewTableModel.fireTableRowsUpdated(row, row); + SystemTools.sleep(5); + if (!showSuccessfulTestsCheckBoxMenuItem.isSelected() + || !showDisabledTestsCheckBoxMenuItem.isSelected()) { + applyFilter(showSuccessfulTestsCheckBoxMenuItem.isSelected(), + showDisabledTestsCheckBoxMenuItem.isSelected()); + } + testOverviewTable.scrollRectToVisible(positionOfCurrentTest); + } + } + } + if (showSuitesCheckBoxMenuItem.isSelected()) { + // tree-table + if (item != null && testOverviewTreeTableModel.isComplete()) { + if (item instanceof Test) { + final Test test = (Test) item; + int treeRow = testOverviewTreeTableModel.getRow(test.getId()); + if (treeRow >= 0) { + final Rectangle positionOfCurrentTestInTree = testOverviewTreeTable + .getCellRect(testOverviewTreeTable.convertRowIndexToView(treeRow), 0, true); + testOverviewTreeTable.scrollRectToVisible(positionOfCurrentTestInTree); + testOverviewTreeTableModel.updateModel(test.getId()); + } + } else { + testOverviewTreeTableModel.updateModel(item.getId()); + } } - testOverviewTable.scrollRectToVisible(positionOfCurrentTest); } - } - statusLabel.setText(currentRun.getStatus()); - testCounterValueLabel.setText(currentRun.getTotalNumberOfCompletedTests() - + (currentRun.getTotalNumberOfTests() >= 0 ? "/" + currentRun.getTotalNumberOfTests() : "")); - errorCounterValueLabel.setText(String.valueOf(currentRun.getCounter().getError())); - failureCounterValueLabel.setText(String.valueOf(currentRun.getCounter().getFailure())); - disabledCounterValueLabel.setText(String.valueOf(currentRun.getCounter().getDisabled())); - warningsCounterValueLabel.setText(String.valueOf(currentRun.getCounter().getWarning())); - infoCounterValueLabel.setText(String.valueOf(currentRun.getInfoCount())); - if (currentRun.getTotalNumberOfTests() == 0) { - progressBar.setValue(100); - } else { - progressBar - .setValue(100 * currentRun.getTotalNumberOfCompletedTests() / currentRun.getTotalNumberOfTests()); - } - if (currentRun.getCounter().getError() > 0 || (currentRun.getCounter().getFailure() > 0)) { - progressBar.setForeground(RED); - } else { - progressBar.setForeground(GREEN); + statusLabel.setText(currentRun.getStatus()); + testCounterValueLabel.setText(currentRun.getTotalNumberOfCompletedTests() + + (currentRun.getTotalNumberOfTests() >= 0 ? "/" + currentRun.getTotalNumberOfTests() : "")); + errorCounterValueLabel.setText(String.valueOf(currentRun.getCounter().getError())); + failureCounterValueLabel.setText(String.valueOf(currentRun.getCounter().getFailure())); + disabledCounterValueLabel.setText(String.valueOf(currentRun.getCounter().getDisabled())); + warningsCounterValueLabel.setText(String.valueOf(currentRun.getCounter().getWarning())); + infoCounterValueLabel.setText(String.valueOf(currentRun.getInfoCount())); + if (currentRun.getTotalNumberOfTests() == 0) { + progressBar.setValue(100); + } else { + progressBar + .setValue(100 * currentRun.getTotalNumberOfCompletedTests() / currentRun.getTotalNumberOfTests()); + } + if (currentRun.getCounter().getError() > 0 || (currentRun.getCounter().getFailure() > 0)) { + progressBar.setForeground(RED); + } else { + progressBar.setForeground(GREEN); + } + } catch (Exception e) { + logger.warning(() -> "Ignored exception " + (e.getMessage() == null ? e.getClass().getSimpleName() + : e.getMessage()) + " while processing reporterId " + reporterId + (item == null ? "." + : " for item id " + item.getId() + ".")); } } private ArrayList getPathListFromSelectedTests() { final ArrayList pathList = new ArrayList<>(); - for (final int rowIndex : testOverviewTable.getSelectedRows()) { - final int row = testOverviewTable.convertRowIndexToModel(rowIndex); - final Test test = testOverviewTableModel.getTest(row); - final String path = test.getOwnerName() + "." + test.getObjectName() + "." + test.getProcedureName(); - pathList.add(path); + if (!showSuitesCheckBoxMenuItem.isSelected()) { + // table + for (final int rowIndex : testOverviewTable.getSelectedRows()) { + final int row = testOverviewTable.convertRowIndexToModel(rowIndex); + final Test test = testOverviewTableModel.getTest(row); + final String path = test.getOwnerName() + "." + test.getObjectName() + "." + test.getProcedureName(); + pathList.add(path); + } + } else { + // tree-table + TreePath[] selectionPaths = testOverviewTreeTable.getTree().getSelectionPaths(); + ArrayList selectedNodes = new ArrayList<>(); + if (selectionPaths != null) { + for (final TreePath path : selectionPaths) { + selectedNodes.add((ItemNode) path.getLastPathComponent()); + } + for (final ItemNode node : ItemNode.createNonOverlappingSet(selectedNodes)) { + if (node.getOwnerName().equals("***")) { + // process children, which must be owners only. + pathList.addAll(node.getOwners()); + } else { + pathList.add(node.getOwnerName() + ":" + node.getId()); + } + } + } } return pathList; } @@ -561,6 +783,53 @@ private void fixCheckBoxMenuItem(final JCheckBoxMenuItem item) { } } + private void showFirstRow() { + // table + final Rectangle positionOfCurrentTest = testOverviewTable + .getCellRect(testOverviewTable.convertRowIndexToView(0), 0, true); + testOverviewTable.scrollRectToVisible(positionOfCurrentTest); + // tree-table + final Rectangle positionOfCurrentTestInTree = testOverviewTreeTable + .getCellRect(testOverviewTreeTable.convertRowIndexToView(0), 0, true); + testOverviewTreeTable.scrollRectToVisible(positionOfCurrentTestInTree); + } + + private void showSelectedRow() { + if (!showSuitesCheckBoxMenuItem.isSelected()) { + // table + final int rowIndex = testOverviewTable.getSelectedRow(); + final int row = testOverviewTable.convertRowIndexToModel(rowIndex); + final Rectangle position = testOverviewTable + .getCellRect(testOverviewTable.convertRowIndexToView(row), 0, true); + testOverviewTable.scrollRectToVisible(position); + } else { + // tree-table + TreePath path = testOverviewTreeTable.getTree().getSelectionPath(); + if (path != null) { + ItemNode itemNode = (ItemNode) path.getLastPathComponent(); + Item item = (Item) itemNode.getUserObject(); + int treeRow = testOverviewTreeTableModel.getRow(item.getId()); + if (treeRow >= 0) { + final Rectangle position = testOverviewTreeTable + .getCellRect(testOverviewTreeTable.convertRowIndexToView(treeRow), 0, true); + testOverviewTreeTable.scrollRectToVisible(position); + } + } + } + } + + private void refreshAction() { + // table + testOverviewTableModel.fireTableDataChanged(); + // tree-table + testOverviewTreeTableModel.updateModel(); + expandAllNodes(testOverviewTreeTable.getTree(), 0); + // common + showFirstRow(); + resetDerived(); + testDetailTabbedPane.setSelectedIndex(0); + } + private void comboBoxAction() { if (currentRun != null) { @SuppressWarnings("unchecked") @@ -569,7 +838,7 @@ private void comboBoxAction() { if (currentRun.getReporterId() != null && comboBoxItem != null) { if (!currentRun.getReporterId().equals(comboBoxItem.getKey())) { update(comboBoxItem.getKey()); - testDetailTabbedPane.setSelectedIndex(0); + refreshAction(); } } } @@ -666,12 +935,26 @@ private void runCodeCoverage(boolean selectedOnly) { final HashSet testPackages = new HashSet<>(); if (selectedOnly) { // pathList and unique testPackages based on selected tests - for (final int rowIndex : testOverviewTable.getSelectedRows()) { - final int row = testOverviewTable.convertRowIndexToModel(rowIndex); - final Test test = testOverviewTableModel.getTest(row); - final String path = test.getOwnerName() + "." + test.getObjectName() + "." + test.getProcedureName(); - pathList.add(path); - testPackages.add(test.getOwnerName() + "." + test.getObjectName()); + if (!showSuitesCheckBoxMenuItem.isSelected()) { + // table + for (final int rowIndex : testOverviewTable.getSelectedRows()) { + final int row = testOverviewTable.convertRowIndexToModel(rowIndex); + final Test test = testOverviewTableModel.getTest(row); + final String path = test.getOwnerName() + "." + test.getObjectName() + "." + test.getProcedureName(); + pathList.add(path); + testPackages.add(test.getOwnerName() + "." + test.getObjectName()); + } + } else { + // tree-table + TreePath[] selectionPaths = testOverviewTreeTable.getTree().getSelectionPaths(); + if (selectionPaths != null) { + for (final TreePath path : selectionPaths) { + final ItemNode node = (ItemNode) path.getLastPathComponent(); + final Item item = (Item) node.getUserObject(); + pathList.add(":" + item.getId()); + testPackages.addAll(node.getTestPackages()); + } + } } } else { // pathList and unique testPackages based on currentRun @@ -700,12 +983,11 @@ private void fixCountersAndUpdate() { List incompleteTests = currentRun.getTests().values().stream() .filter(it -> it.getEndTime() == null && !it.isDisabled()).collect(Collectors.toList()); if (!incompleteTests.isEmpty()) { - final Double now = (double) System.currentTimeMillis(); - final String sysdate = UtplsqlRunner.getSysdate(); + final String sysdate = StringTools.getSysdate(); for (Test test : incompleteTests) { // fix incomplete tests, see https://github.com/utPLSQL/utPLSQL-SQLDeveloper/issues/107 test.setEndTime(sysdate); - test.setExecutionTime((now - currentRun.getStart()) / 1000); + test.setExecutionTime(StringTools.elapsedTime(test.getStartTime(), test.getEndTime())); test.setErrorStack(UtplsqlResources.getString("RUNNER_MISSING_TEST_RESULT_MESSAGE")); test.getCounter().setError(1); } @@ -733,9 +1015,8 @@ private void fixCountersAndUpdate() { currentRun.getCounter().setWarning(currentRun.getCounter().getWarning() + test.getCounter().getWarning()); } // terminate run - currentRun.setEndTime(UtplsqlRunner.getSysdate()); - double now = (double) System.currentTimeMillis(); - currentRun.setExecutionTime((now - currentRun.getStart()) / 1000); + currentRun.setEndTime(StringTools.getSysdate()); + currentRun.setExecutionTime(StringTools.elapsedTime(currentRun.getStartTime(), currentRun.getEndTime())); currentRun.setCurrentTestNumber(0); // update run in GUI update(currentRun.getReporterId()); @@ -751,14 +1032,19 @@ private void initializeGUI() { final GradientToolbar toolbar = new GradientToolbar(); toolbar.setFloatable(false); final EmptyBorder buttonBorder = new EmptyBorder(new Insets(2, 4, 2, 4)); // insets: top, left, bottom, right + final ToolbarButton showSuitesButton = new ToolbarButton(UtplsqlResources.getIcon("PACKAGE_FOLDER_ICON")); + showSuitesButton.setToolTipText(UtplsqlResources.getString("RUNNER_SHOW_SUITES_BUTTON")); + showSuitesButton.setBorder(buttonBorder); + showSuitesButton.addActionListener(event -> { + showSuitesCheckBoxMenuItem.setSelected(!showSuitesCheckBoxMenuItem.isSelected()); + applyShowSuites(); + fixCheckBoxMenuItem(showSuitesCheckBoxMenuItem); + }); + toolbar.add(showSuitesButton); final ToolbarButton refreshButton = new ToolbarButton(UtplsqlResources.getIcon("REFRESH_ICON")); refreshButton.setToolTipText(UtplsqlResources.getString("RUNNER_REFRESH_TOOLTIP")); refreshButton.setBorder(buttonBorder); - refreshButton.addActionListener(event -> { - resetDerived(); - testDetailTabbedPane.setSelectedIndex(0); - testOverviewTableModel.fireTableDataChanged(); - }); + refreshButton.addActionListener(event -> refreshAction()); toolbar.add(refreshButton); final ToolbarButton rerunButton = new ToolbarButton(UtplsqlResources.getIcon("RUN_ICON")); rerunButton.setToolTipText(UtplsqlResources.getString("RUNNER_RERUN_TOOLTIP")); @@ -798,15 +1084,35 @@ private void initializeGUI() { if (currentRun.getConsumerConn() != null) { // Aborts JDBC Connection. Connection might still run in the background. That's expected. DatabaseTools.abortConnection(currentRun.getConsumerConn()); - List notCompletedTests = currentRun.getTests().values().stream() - .filter(it -> it.getTestNumber() >= currentRun.getCurrentTestNumber() && it.getEndTime() == null && !it.isDisabled()) + List notCompletedItems = currentRun.getItemNodes().values().stream() + .map(node -> (Item) node.getUserObject()) + .filter(item -> item.getEndTime() == null && !(item instanceof Test && ((Test) item).isDisabled())) .collect(Collectors.toList()); - for (Test test : notCompletedTests) { - test.setDisabled(true); - test.getCounter().setDisabled(1); - test.getCounter().setWarning(1); - test.setWarnings(UtplsqlResources.getString("RUNNER_STOP_TEST_MESSAGE")); - test.setStartTime(null); + String sysdate = StringTools.getSysdate(); + for (Item item : notCompletedItems) { + item.getCounter().setDisabled(1); + if (item instanceof Test) { + Test test = (Test) item; + test.setDisabled(true); + test.getCounter().setWarning(1); + test.setWarnings(UtplsqlResources.getString("RUNNER_STOP_TEST_MESSAGE")); + test.setStartTime(null); + } else { + if (item.getStartTime() != null) { + item.setEndTime(sysdate); + if (testOverviewTreeTableModel.ItemNodeHasErrors(item.getId())) { + item.getCounter().setError(1); + } + if (testOverviewTreeTableModel.ItemNodeHasFailedTests(item.getId())) { + item.getCounter().setFailure(1); + } + if (testOverviewTreeTableModel.ItemNodeHasSuccessfulTests(item.getId())) { + item.getCounter().setSuccess(1); + } + item.setExecutionTime(StringTools.elapsedTime(item.getStartTime(), item.getEndTime())); + } + } + testOverviewTreeTableModel.nodeChanged(item.getId()); } currentRun.setStatus(UtplsqlResources.getString("RUNNER_STOP_RUN_MESSAGE")); fixCountersAndUpdate(); @@ -829,7 +1135,7 @@ private void initializeGUI() { final Run run = currentRun; runs.clear(); currentRun = null; - setModel(run); + setModel(run, true); update(run.getReporterId()); }); toolbar.add(clearButton); @@ -952,7 +1258,7 @@ private void initializeGUI() { fixCheckBoxMenuItem(showInfoCounterCheckBoxMenuItem); }); countersPopupMenu.add(showInfoCounterCheckBoxMenuItem); - counterPanel.setComponentPopupMenu(countersPopupMenu); + basePanel.setComponentPopupMenu(countersPopupMenu); // Progress bar progressBar = new JProgressBar(); @@ -973,7 +1279,7 @@ private void initializeGUI() { c.weighty = 0; basePanel.add(progressBar, c); - // Test overview + // Test overview (table variant) testOverviewTableModel = new TestOverviewTableModel(); testOverviewTable = new JTable(testOverviewTableModel); testOverviewTable.getTableHeader().setReorderingAllowed(false); @@ -989,7 +1295,7 @@ private void initializeGUI() { testOwnerTextField.setText(test.getOwnerName()); testPackageTextField.setText(test.getObjectName()); testProcedureTextField.setText(test.getProcedureName()); - testDescriptionTextArea.setText(test.getDescription() != null ? test.getDescription().trim() : null); + testDescriptionTextArea.setText(StringTools.trim(test.getDescription())); testIdTextArea.setText(test.getId()); testStartTextField.setText(StringTools.formatDateTime(test.getStartTime())); failuresTableModel.setModel(test.getFailedExpectations()); @@ -998,16 +1304,15 @@ private void initializeGUI() { if (test.getFailedExpectations() != null && !test.getFailedExpectations().isEmpty()) { failuresTable.setRowSelectionInterval(0, 0); } - testErrorStackTextPane - .setText(getHtml(test.getErrorStack() != null ? test.getErrorStack().trim() : null)); - testWarningsTextPane.setText(getHtml(test.getWarnings() != null ? test.getWarnings().trim() : null)); - testServerOutputTextPane - .setText(getHtml(test.getServerOutput() != null ? test.getServerOutput().trim() : null)); + testErrorStackTextPane.setText(getHtml(StringTools.trim(test.getErrorStack()))); + testWarningsTextPane.setText(getHtml(StringTools.trim(test.getWarnings()))); + testServerOutputTextPane.setText(getHtml(StringTools.trim(test.getServerOutput()))); syncDetailTab(); testOverviewRunMenuItem.setEnabled(true); testOverviewRunWorksheetMenuItem.setEnabled(true); testOverviewDebugMenuItem.setEnabled(true); testOverviewCodeCoverageMenuItem.setEnabled(true); + } }); testOverviewTable.addMouseListener(new MouseAdapter() { @@ -1029,23 +1334,23 @@ public void mouseClicked(final MouseEvent e) { overviewTableStatus.setPreferredWidth(INDICATOR_WIDTH); overviewTableStatus.setMaxWidth(INDICATOR_WIDTH); overviewTableStatus.setHeaderRenderer(testTableHeaderRenderer); - final TableColumn overviewTableWarning = testOverviewTable.getColumnModel().getColumn(1); + final TableColumn overviewTableId = testOverviewTable.getColumnModel().getColumn(1); + overviewTableId.setHeaderRenderer(testTableHeaderRenderer); + final TableColumn overviewTableWarning = testOverviewTable.getColumnModel().getColumn(2); overviewTableWarning.setMinWidth(INDICATOR_WIDTH); overviewTableWarning.setPreferredWidth(INDICATOR_WIDTH); overviewTableWarning.setMaxWidth(INDICATOR_WIDTH); overviewTableWarning.setHeaderRenderer(testTableHeaderRenderer); - final TableColumn overviewTableInfo = testOverviewTable.getColumnModel().getColumn(2); + final TableColumn overviewTableInfo = testOverviewTable.getColumnModel().getColumn(3); overviewTableInfo.setMinWidth(INDICATOR_WIDTH); overviewTableInfo.setPreferredWidth(INDICATOR_WIDTH); overviewTableInfo.setMaxWidth(INDICATOR_WIDTH); overviewTableInfo.setHeaderRenderer(testTableHeaderRenderer); - final TableColumn overviewTableId = testOverviewTable.getColumnModel().getColumn(3); - overviewTableId.setHeaderRenderer(testTableHeaderRenderer); final TableColumn overviewTableTime = testOverviewTable.getColumnModel().getColumn(4); overviewTableTime.setPreferredWidth(60); overviewTableTime.setMaxWidth(100); overviewTableTime.setHeaderRenderer(testTableHeaderRenderer); - overviewTableTime.setCellRenderer(new DefaultTableCellRenderer() { + final DefaultTableCellRenderer timeColumnRenderer = new DefaultTableCellRenderer() { private static final long serialVersionUID = 7720067427609773267L; { setHorizontalAlignment(JLabel.RIGHT); @@ -1057,8 +1362,113 @@ public Component getTableCellRendererComponent(final JTable table, final Object final SmartTime smartTime = new SmartTime(((Double) value), useSmartTimes); return super.getTableCellRendererComponent(table, smartTime.toString(), isSelected, hasFocus, row, col); } + }; + overviewTableTime.setCellRenderer(timeColumnRenderer); + + // Test overview (tree-table variant) + testOverviewTreeTableModel = new TestOverviewTreeTableModel(); + testOverviewTreeTable = new JFastTreeTable(testOverviewTreeTableModel); + testOverviewTreeTable.setShowGrid(false); // first column is the tree and is not affected in SQLDev, true does not look good + testOverviewTreeTable.getTableHeader().setReorderingAllowed(false); + testOverviewTreeTable.setAutoCreateRowSorter(false); + testOverviewTreeTable.setRowHeight(OVERVIEW_TABLE_ROW_HEIGHT); + testOverviewTreeTable.getTableHeader().setPreferredSize( + new Dimension(testOverviewTreeTable.getTableHeader().getPreferredSize().width, OVERVIEW_TABLE_ROW_HEIGHT)); + testOverviewTreeTable.getTree().setRootVisible(false); + // calling setDoubleBuffered on tree leads to suppressed painting + RepaintManager.currentManager(testOverviewTreeTable).setDoubleBufferingEnabled(true); + testOverviewTreeTable.getTree().getSelectionModel().addTreeSelectionListener(event -> { + final TreePath path = event.getPath(); + if (path != null) { + final ItemNode node = (ItemNode) path.getLastPathComponent(); + final Item item = (Item) node.getUserObject(); + testOwnerTextField.setText(node.getOwnerName()); + testPackageTextField.setText(node.getPackageName()); + testProcedureTextField.setText(node.getProcedureName()); + testDescriptionTextArea.setText(node.getDescription()); + testIdTextArea.setText(node.getId()); + testStartTextField.setText(StringTools.formatDateTime(item.getStartTime())); + if (item instanceof Test) { + Test test = (Test) item; + failuresTableModel.setModel(test.getFailedExpectations()); + failuresTableModel.fireTableDataChanged(); + testFailureMessageTextPane.setText(null); + if (test.getFailedExpectations() != null && !test.getFailedExpectations().isEmpty()) { + failuresTable.setRowSelectionInterval(0, 0); + } + } else { + failuresTableModel.setModel(null); + failuresTableModel.fireTableDataChanged(); + testFailureMessageTextPane.setText(null); + + } + testErrorStackTextPane.setText(getHtml(StringTools.trim(item.getErrorStack()))); + testWarningsTextPane.setText(getHtml(StringTools.trim(item.getWarnings()))); + testServerOutputTextPane.setText(getHtml(StringTools.trim(item.getServerOutput()))); + syncDetailTab(); + testOverviewRunMenuItem.setEnabled(true); + testOverviewRunWorksheetMenuItem.setEnabled(true); + testOverviewDebugMenuItem.setEnabled(true); + testOverviewCodeCoverageMenuItem.setEnabled(true); + } }); - final JScrollPane testOverviewScrollPane = new JScrollPane(testOverviewTable); + + final JTree overviewTreeTableName = testOverviewTreeTable.getTree(); + overviewTreeTableName.setCellRenderer(new DefaultTreeCellRenderer() { + private static final long serialVersionUID = 580783625740405285L; + + @Override + public Component getTreeCellRendererComponent(JTree tree, Object value, boolean selected, boolean expanded, + boolean leaf, int row, boolean hasFocus) { + this.hasFocus = hasFocus; + final ItemNode node = (ItemNode) value; + setText((String) testOverviewTreeTableModel.getValueAt(value, 0)); + + Color foregroundColor; + if (selected) { + foregroundColor = getTextSelectionColor(); + } else { + foregroundColor = getTextNonSelectionColor(); + } + setForeground(foregroundColor); + + Icon icon = node.getStatusIcon(); + if (icon == null) { + if (leaf) { + icon = getLeafIcon(); + } else if (expanded) { + icon = getOpenIcon(); + } else { + icon = getClosedIcon(); + } + } + setIcon(icon); + this.selected = selected; + return this; + } + }); + final TestTreeTableHeaderRenderer testTreeTableHeaderRenderer = new TestTreeTableHeaderRenderer(); + final TableColumn overviewTreeTableSuite = testOverviewTreeTable.getColumnModel().getColumn(0); + overviewTreeTableSuite.setHeaderRenderer(testTreeTableHeaderRenderer); + final TableColumn overviewTreeTableWarning = testOverviewTreeTable.getColumnModel().getColumn(1); + overviewTreeTableWarning.setMinWidth(INDICATOR_WIDTH); + overviewTreeTableWarning.setPreferredWidth(INDICATOR_WIDTH); + overviewTreeTableWarning.setMaxWidth(INDICATOR_WIDTH); + overviewTreeTableWarning.setHeaderRenderer(testTreeTableHeaderRenderer); + final TableColumn overviewTreeTableInfo = testOverviewTreeTable.getColumnModel().getColumn(2); + overviewTreeTableInfo.setMinWidth(INDICATOR_WIDTH); + overviewTreeTableInfo.setPreferredWidth(INDICATOR_WIDTH); + overviewTreeTableInfo.setMaxWidth(INDICATOR_WIDTH); + overviewTreeTableInfo.setHeaderRenderer(testTreeTableHeaderRenderer); + final TableColumn overviewTreeTableTime = testOverviewTreeTable.getColumnModel().getColumn(3); + overviewTreeTableTime.setPreferredWidth(60); + overviewTreeTableTime.setMaxWidth(100); + overviewTreeTableTime.setHeaderRenderer(testTreeTableHeaderRenderer); + overviewTreeTableTime.setCellRenderer(timeColumnRenderer); + + // Scroll pane for test overview containing either the tree-table or table variant, populated in applyPreferences() + testOverviewScrollPane = new JScrollPane(); + RepaintManager.currentManager(testOverviewScrollPane).setDoubleBufferingEnabled(true); // Context menu for test overview final JPopupMenu testOverviewPopupMenu = new JPopupMenu(); @@ -1124,9 +1534,19 @@ public Component getTableCellRendererComponent(final JTable table, final Object syncDetailTab(); fixCheckBoxMenuItem(syncDetailTabCheckBoxMenuItem); }); + testOverviewPopupMenu.add(new JSeparator()); testOverviewPopupMenu.add(syncDetailTabCheckBoxMenuItem); + showSuitesCheckBoxMenuItem = new JCheckBoxMenuItem(UtplsqlResources.getString("PREF_SHOW_SUITES_LABEL").replace("?", ""), true); + showSuitesCheckBoxMenuItem.addActionListener(event -> { + applyShowSuites(); + fixCheckBoxMenuItem(showSuitesCheckBoxMenuItem); + }); + testOverviewPopupMenu.add(showSuitesCheckBoxMenuItem); testOverviewTable.setComponentPopupMenu(testOverviewPopupMenu); testOverviewTable.getTableHeader().setComponentPopupMenu(testOverviewPopupMenu); + testOverviewTreeTable.setComponentPopupMenu(testOverviewPopupMenu); + testOverviewTreeTable.getTableHeader().setComponentPopupMenu(testOverviewPopupMenu); + testOverviewScrollPane.setComponentPopupMenu(testOverviewPopupMenu); // Test tabbed pane (Test Properties) final ScrollablePanel testInfoPanel = new ScrollablePanel(); @@ -1390,7 +1810,12 @@ public void mouseClicked(final MouseEvent e) { testWarningsTextPane.setContentType("text/html"); testWarningsTextPane.setMinimumSize(TEXTPANE_DIM); testWarningsTextPane.setPreferredSize(TEXTPANE_DIM); - testWarningsTextPane.addHyperlinkListener(event -> openLink(event.getDescription())); + testWarningsTextPane.addHyperlinkListener(event -> { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + final String link = event.getDescription(); + openLink(link); + } + }); final JScrollPane testWarningsScrollPane = new JScrollPane(testWarningsTextPane); c.gridx = 0; c.gridy = 0; @@ -1412,7 +1837,12 @@ public void mouseClicked(final MouseEvent e) { testServerOutputTextPane.setContentType("text/html"); testServerOutputTextPane.setMinimumSize(TEXTPANE_DIM); testServerOutputTextPane.setPreferredSize(TEXTPANE_DIM); - testServerOutputTextPane.addHyperlinkListener(event -> openLink(event.getDescription())); + testServerOutputTextPane.addHyperlinkListener(event -> { + if (event.getEventType() == HyperlinkEvent.EventType.ACTIVATED) { + final String link = event.getDescription(); + openLink(link); + } + }); final JScrollPane testServerOutputScrollPane = new JScrollPane(testServerOutputTextPane); c.gridx = 0; c.gridy = 0; diff --git a/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/SmartTime.java b/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/SmartTime.java index 69ea1500..fc4b2812 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/SmartTime.java +++ b/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/SmartTime.java @@ -16,6 +16,7 @@ package org.utplsql.sqldev.ui.runner; import java.text.DecimalFormat; +import java.util.Locale; public class SmartTime { private Double seconds; @@ -45,6 +46,7 @@ public Double getSeconds() { @Override public String toString() { + Locale.setDefault(new Locale("en", "US")); String ret; if (seconds == null) { ret = null; diff --git a/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/TestOverviewTableModel.java b/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/TestOverviewTableModel.java index 25349b20..ea2729e5 100644 --- a/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/TestOverviewTableModel.java +++ b/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/TestOverviewTableModel.java @@ -61,7 +61,7 @@ public void updateModel(final boolean showDescription) { fireTableDataChanged(); } - public CharSequence getTestIdColumnName() { + public String getTestIdColumnName() { StringBuilder sb = new StringBuilder(); calcCommonPrefix(); if (commonPrefix == null || commonPrefix.isEmpty()) { @@ -108,34 +108,34 @@ public int getColumnCount() { public Object getValueAt(final int row, final int col) { final Test test = getTest(row); switch (col) { - case 0: - return test.getStatusIcon(); - case 1: - return test.getWarningIcon(); - case 2: - return test.getInfoIcon(); - case 3: - if (showDescription && test.getDescription() != null) { - return test.getDescription(); - } else { - return test.getId().substring(commonPrefix == null ? 0 : commonPrefix.length()); - } - case 4: - return test.getExecutionTime(); - default: - return null; + case 0: + return test.getStatusIcon(); + case 1: + if (showDescription && test.getDescription() != null) { + return test.getDescription(); + } else { + return test.getId().substring(commonPrefix == null ? 0 : commonPrefix.length()); + } + case 2: + return test.getWarningIcon(); + case 3: + return test.getInfoIcon(); + case 4: + return test.getExecutionTime(); + default: + return null; } } @Override public String getColumnName(final int col) { switch (col) { - case 0: case 1: + return UtplsqlResources.getString(showDescription ? "RUNNER_DESCRIPTION_LABEL" : "RUNNER_TEST_ID_COLUMN"); + case 0: case 2: - return ""; // icons are used instead of descriptions case 3: - return UtplsqlResources.getString(showDescription ? "RUNNER_DESCRIPTION_LABEL" : "RUNNER_TEST_ID_COLUMN"); + return ""; // icons are used instead of descriptions case 4: return getTimeColumnName(); default: @@ -153,8 +153,8 @@ public boolean isCellEditable(final int row, final int column) { public Class getColumnClass(final int col) { switch (col) { case 0: - case 1: case 2: + case 3: return Icon.class; case 4: return Double.class; diff --git a/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/TestOverviewTreeTableModel.java b/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/TestOverviewTreeTableModel.java new file mode 100644 index 00000000..c1562d2c --- /dev/null +++ b/sqldev/src/main/java/org/utplsql/sqldev/ui/runner/TestOverviewTreeTableModel.java @@ -0,0 +1,518 @@ +/* + * Copyright 2021 Philipp Salvisberg + * + * 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 org.utplsql.sqldev.ui.runner; + +import java.util.Enumeration; +import java.util.LinkedHashMap; + +import javax.swing.Icon; +import javax.swing.event.EventListenerList; +import javax.swing.event.TreeModelEvent; +import javax.swing.event.TreeModelListener; +import javax.swing.tree.TreeNode; +import javax.swing.tree.TreePath; + +import org.utplsql.sqldev.model.runner.Counter; +import org.utplsql.sqldev.model.runner.Item; +import org.utplsql.sqldev.model.runner.ItemNode; +import org.utplsql.sqldev.model.runner.Run; +import org.utplsql.sqldev.model.runner.Suite; +import org.utplsql.sqldev.model.runner.Test; +import org.utplsql.sqldev.resources.UtplsqlResources; + +import oracle.javatools.ui.treetable.TreeTableModel; + +public class TestOverviewTreeTableModel implements TreeTableModel { + private boolean showDescription; + private boolean useSmartTimes; + private boolean showSuccessfulTests; + private boolean showDisabledTests; + private String rootId; + private LinkedHashMap sources = new LinkedHashMap<>(); + private final LinkedHashMap nodes = new LinkedHashMap<>(); + protected final EventListenerList listenerList = new EventListenerList(); + + public TestOverviewTreeTableModel() { + super(); + } + + private boolean hasVisibleDisabledTests(ItemNode startNode) { + if (!showDisabledTests || startNode.getUserObject() instanceof Test) { + return false; + } + Enumeration children = sources.get(startNode.getId()).preorderEnumeration(); + while (children.hasMoreElements()) { + ItemNode child = (ItemNode) children.nextElement(); + Item item = (Item) child.getUserObject(); + if (item.getStatusIcon() == UtplsqlResources.getIcon("DISABLED_ICON")) { + return true; + } + } + return false; + } + + /** + * Determines if a node should be shown in the tree. + */ + private boolean isVisible(ItemNode node) { + if (node != null) { + Item item = (Item) node.getUserObject(); + if (item.getStatusIcon() == UtplsqlResources.getIcon("SUCCESS_ICON") && !showSuccessfulTests + && !hasVisibleDisabledTests(node)) { + return false; + } + if (item.getStatusIcon() == UtplsqlResources.getIcon("DISABLED_ICON") && !showDisabledTests) { + return false; + } + return true; + } + return true; + } + + /** + * Copies the original nodes determined by the run to the local nodes. + * Keeps references to items in the run, so changes in the run are automatically applied. + * However, the listeners need to by notified about the changes to display them + * in the underlying tree and table. + */ + private void setClonedItemNodes() { + nodes.clear(); + for (ItemNode source : sources.values()) { + ItemNode node = new ItemNode((Item) source.getUserObject()); + nodes.put(node.getId(), node); + } + for (ItemNode source : sources.values()) { + if (source.getUserObject() instanceof Suite) { + ItemNode parent = nodes.get(source.getId()); + Enumeration sourceChildren = source.children(); + while (sourceChildren.hasMoreElements()) { + ItemNode sourceChild = (ItemNode) sourceChildren.nextElement(); + ItemNode child = nodes.get(sourceChild.getId()); + if (isVisible(child)) { + parent.add(child); + } + } + } + } + reload(); + } + + /** + * Sets the complete model. For example when changing a run. + */ + public void setModel(final Run run, final boolean showDescription, final boolean useSmartTimes, + final boolean showSuccessfulTests, final boolean showDisabledTests) { + this.showDescription = showDescription; + this.useSmartTimes = useSmartTimes; + this.showSuccessfulTests = showSuccessfulTests; + this.showDisabledTests = showDisabledTests; + this.rootId = run.getReporterId(); + this.sources = run.getItemNodes(); + setClonedItemNodes(); + } + + /** + * Updates the description only. + */ + public void updateModel(final boolean showDescription) { + this.showDescription = showDescription; + } + + /** + * Updates filter criteria. If a change is detected the model is re-created from scratch. + */ + public void updateModel(final boolean showSuccessfulTests, final boolean showDisabledTests) { + if (this.showSuccessfulTests != showSuccessfulTests || this.showDisabledTests != showDisabledTests) { + this.showSuccessfulTests = showSuccessfulTests; + this.showDisabledTests = showDisabledTests; + setClonedItemNodes(); + } + } + + /** + * Re-creates the model from scratch. + */ + public void updateModel() { + setClonedItemNodes(); + } + + /** + * Applies the filter criteria for a part of the tree after an update. + * Technically it will remove nodes from the tree. + */ + private void removeInvisibleNodes(ItemNode startNode) { + TreeNode[] path = startNode.getPath(); + for (TreeNode node : path) { + ItemNode parent = (ItemNode) node.getParent(); + if (parent != null) { + if (!isVisible((ItemNode) node)) { + int childIndex = parent.getIndex(node); + if (childIndex >= 0) { + parent.remove(childIndex); + // Delaying the fireTreeNodesRemove call would avoid the following exception: + // Exception in thread "AWT-EventQueue-0" java.lang.ArrayIndexOutOfBoundsException: n >= m + // at java.util.Vector.elementAt(Vector.java:479) + // at javax.swing.tree.DefaultMutableTreeNode.getChildAt(DefaultMutableTreeNode.java:245) + // ... + // This exception is raised in the event dispatching thread (another thread). The line numbers + // may differ based on the JDK you are using. I suspect that events are not processed + // fast enough. + // + // However, it looks like these errors can be ignored. At least there seems to be no negative + // side effect in the runner GUI. In any case the user could always use the refresh action + // to get a clean state, should something look wrong. But I've never experienced this. + // + // These exceptions are thrown only when filtering is enabled, but then it happens quite often. + // This means it happens for 5-10% of the nodes. + // + // Calling fireTreeNodesRemoved in another thread, e.g. via SwingUtilities.invokeLater() + // will reduce the number of exceptions significantly (almost zero). However, in this case + // the subsequent updates might fail and this will cause an exception in this thread + // (in the method {@link #nodeChanged(String id)}. I tried the following: + // - calling all fireTreeNode... methods via SwingUtilities.invokeLater(). + // - catching the exception, trying to re-fire or ignore it + // In the end the user experience was always worse. Even if I've got no exceptions + // anymore the result in the TreeTable was wrong. I've got wrong rows, even empty rows. + // + // Therefore I decided to live with some exceptions in the event dispatching thread. + fireTreeNodesRemoved(this, parent.getPath(), new int[] { childIndex }, new Object[] { node }); + // removing the parent removes also all its children, hence no need for further processing + break; + } + } + } + } + } + + /** + * Updates a node and its parents and then apply the filter criteria. + */ + public void updateModel(final String id) { + nodeChanged(id); + ItemNode startNode = nodes.get(id); + if (startNode != null) { + removeInvisibleNodes(startNode); + } + } + + /** + * Notifies all listeners that the complete tree has changed. + * For that a root node must be available. + * However, a root node does not mean the model {@link #isComplete()}. + */ + public void reload() { + if (getRoot() != null) { + fireTreeStructureChanged(this, getRoot().getPath(), null, null); + } + } + + /** + * Notifies all listeners that a node and its parents have changed. + */ + public void nodeChanged(String id) { + ItemNode startNode = nodes.get(id); + if (startNode != null) { + TreeNode[] path = startNode.getPath(); + for (TreeNode node : path) { + ItemNode parent = (ItemNode) node.getParent(); + if (parent != null) { + int childIndex = parent.getIndex(node); + if (childIndex >= 0) { + fireTreeNodesChanged(this, parent.getPath(), new int[] { childIndex }, new Object[] { node }); + } + } + } + } + } + + /** + * Determines if the model is fully initialized and can be used. + * For that it the pseudo root must contain a child. + */ + public boolean isComplete() { + return nodes.size() > 1; // return sources.size() != nodes.size(); + } + + /** + * Calculates the row of the underlying table when the tree is fully expanded. + */ + public int getRow(final String id) { + // do not count root + int i = -1; + + // The order of orderedNodes can differ to nodes.values() + // when run is based on list of tests. + Enumeration orderedNodes = getRoot().preorderEnumeration(); + while (orderedNodes.hasMoreElements()) { + ItemNode node = (ItemNode) orderedNodes.nextElement(); + if (((Item) node.getUserObject()).getId().equals(id)) { + return i; + } + i++; + } + return -1; + } + + public Test getTestOf(final ItemNode startNode) { + Enumeration orderedNodes = startNode.preorderEnumeration(); + while (orderedNodes.hasMoreElements()) { + ItemNode node = (ItemNode) orderedNodes.nextElement(); + Item item = (Item) node.getUserObject(); + if (item instanceof Test) { + return (Test) item; + } + } + return null; + } + + public ItemNode getItemNode(final String id) { + return nodes.get(id); + } + + private interface CounterChecker { + boolean matchedStatus (Counter counter); + } + + private boolean ItemNodeStatus(final String id, final CounterChecker checker) { + ItemNode startNode = sources.get(id); + if (startNode != null) { + Enumeration orderedNodes = startNode.preorderEnumeration(); + while (orderedNodes.hasMoreElements()) { + ItemNode node = (ItemNode) orderedNodes.nextElement(); + Item item = (Item) node.getUserObject(); + if (checker.matchedStatus(item.getCounter())) { + return true; + } + } + } + return false; + } + + /** + * Returns true if a node or one of its children have errors. + */ + public boolean ItemNodeHasErrors(final String id) { + return ItemNodeStatus(id, counter -> counter.getError() > 0); + } + + /** + * Returns true if a node or one of its children have failed tests. + */ + public boolean ItemNodeHasFailedTests(final String id) { + return ItemNodeStatus(id, counter -> counter.getFailure() > 0); + } + + /** + * Returns true if a node or one of its children have successful tests. + */ + public boolean ItemNodeHasSuccessfulTests(final String id) { + return ItemNodeStatus(id, counter -> counter.getSuccess() > 0); + } + + public String getTreeColumnName() { + return UtplsqlResources.getString(showDescription ? "RUNNER_DESCRIPTION_LABEL" : "RUNNER_TEST_ID_COLUMN"); + } + + public String getTimeColumnName() { + return UtplsqlResources.getString("RUNNER_TEST_EXECUTION_TIME_COLUMN") + (!useSmartTimes ? " [s]" : ""); + } + + @Override + public ItemNode getRoot() { + return nodes.get(rootId); + } + + @Override + public ItemNode getChild(Object parent, int index) { + return (ItemNode) ((ItemNode) parent).getChildAt(index); + } + + @Override + public int getChildCount(Object parent) { + return ((ItemNode) parent).getChildCount(); + } + + @Override + public boolean isLeaf(Object node) { + return !((ItemNode) node).getAllowsChildren(); + } + + @Override + public void valueForPathChanged(TreePath path, Object newValue) { + // ignore, no implementation required + } + + @Override + public int getIndexOfChild(Object parent, Object child) { + return ((ItemNode) parent).getIndex((ItemNode) child); + } + + @Override + public void addTreeModelListener(TreeModelListener l) { + listenerList.add(TreeModelListener.class, l); + } + + @Override + public void removeTreeModelListener(TreeModelListener l) { + listenerList.remove(TreeModelListener.class, l); + } + + /** + * Copied from DefaultTreeModel + */ + protected void fireTreeNodesChanged(Object source, Object[] path, int[] childIndices, Object[] children) { + Object[] listeners = this.listenerList.getListenerList(); + TreeModelEvent e = null; + + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == TreeModelListener.class) { + if (e == null) { + e = new TreeModelEvent(source, path, childIndices, children); + } + // might fail with IndexOutOfBoundsException + ((TreeModelListener) listeners[i + 1]).treeNodesChanged(e); + } + } + } + + /** + * Copied from DefaultTreeModel + */ + protected void fireTreeNodesInserted(Object source, Object[] path, int[] childIndices, Object[] children) { + Object[] listeners = this.listenerList.getListenerList(); + TreeModelEvent e = null; + + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == TreeModelListener.class) { + if (e == null) { + e = new TreeModelEvent(source, path, childIndices, children); + } + + ((TreeModelListener) listeners[i + 1]).treeNodesInserted(e); + } + } + } + + /** + * Copied from DefaultTreeModel + */ + protected void fireTreeNodesRemoved(Object source, Object[] path, int[] childIndices, Object[] children) { + Object[] listeners = this.listenerList.getListenerList(); + TreeModelEvent e = null; + + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == TreeModelListener.class) { + if (e == null) { + e = new TreeModelEvent(source, path, childIndices, children); + } + + ((TreeModelListener) listeners[i + 1]).treeNodesRemoved(e); + } + } + } + + /** + * Copied from DefaultTreeModel + */ + protected void fireTreeStructureChanged(Object source, Object[] path, int[] childIndices, Object[] children) { + Object[] listeners = this.listenerList.getListenerList(); + TreeModelEvent e = null; + + for (int i = listeners.length - 2; i >= 0; i -= 2) { + if (listeners[i] == TreeModelListener.class) { + if (e == null) { + e = new TreeModelEvent(source, path, childIndices, children); + } + + ((TreeModelListener) listeners[i + 1]).treeStructureChanged(e); + } + } + } + + @Override + public int getColumnCount() { + return 4; + } + + @Override + public String getColumnName(int col) { + switch (col) { + case 0: + return getTreeColumnName(); + case 1: + case 2: + return ""; // icons are used instead of descriptions + case 3: + return getTimeColumnName(); + default: + return null; + } + } + + @Override + public Class getColumnClass(int col) { + switch (col) { + case 0: + return TreeTableModel.class; + case 1: + case 2: + return Icon.class; + case 3: + return Double.class; + default: + return String.class; + } + } + + @Override + public Object getValueAt(Object node, int col) { + final ItemNode itemNode = (ItemNode) node; + switch (col) { + case 0: + if (showDescription && itemNode.getDescription() != null) { + if (itemNode.getUserObject() instanceof Suite) { + if (!itemNode.getName().contains("context_#")) { + // description of suites might be bewildering, hence use it for contexts only + return itemNode.getName(); + } + } + return itemNode.getDescription(); + } else { + return itemNode.getName(); + } + case 1: + return itemNode.getWarningIcon(); + case 2: + return itemNode.getInfoIcon(); + case 3: + return itemNode.getExecutionTime(); + default: + return null; + } + } + + @Override + public boolean isCellEditable(Object node, int col) { + // make the tree column editable to forward mouse events for collapse/expand + return getColumnClass(col) == TreeTableModel.class; + } + + @Override + public void setValueAt(Object value, Object node, int col) { + // ignore, no implementation required + } + +} diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/UtplsqlResources.properties b/sqldev/src/main/resources/org/utplsql/sqldev/resources/UtplsqlResources.properties index ce1045cb..680cabc3 100644 --- a/sqldev/src/main/resources/org/utplsql/sqldev/resources/UtplsqlResources.properties +++ b/sqldev/src/main/resources/org/utplsql/sqldev/resources/UtplsqlResources.properties @@ -24,6 +24,11 @@ STOP_ICON=/org/utplsql/sqldev/resources/images/stop.png CLEAR_ICON=/org/utplsql/sqldev/resources/images/clear.png CHECKMARK_ICON=/org/utplsql/sqldev/resources/images/checkmark.png STATUS_ICON=/org/utplsql/sqldev/resources/images/status.png +PROCEDURE_ICON=/org/utplsql/sqldev/resources/images/procedure.png +PROCEDURE_FOLDER_ICON=/org/utplsql/sqldev/resources/images/procedure_folder.png +PACKAGE_ICON=/org/utplsql/sqldev/resources/images/package.png +PACKAGE_FOLDER_ICON=/org/utplsql/sqldev/resources/images/package_folder.png + # progress.gif - the animated version - does not work PROGRESS_ICON=/org/utplsql/sqldev/resources/images/progress.png CODE_COVERAGE_ICON=/org/utplsql/sqldev/resources/images/coverage.png @@ -51,6 +56,7 @@ PREF_SHOW_SUCCESSFUL_TESTS_LABEL=Show successful tests? PREF_SHOW_DISABLED_TESTS_LABEL=Show disabled tests? PREF_SHOW_TEST_DESCRIPTION_LABEL=Show description (if present)? PREF_SYNC_DETAIL_TAB_LABEL=Synchronize detail tab based on test status? +PREF_SHOW_SUITES_LABEL=Show suites (hierarchical view of tests)? PREF_TEST_PACKAGE_PREFIX_LABEL=Test package prefix PREF_TEST_PACKAGE_SUFFIX_LABEL=Test package suffix PREF_TEST_UNIT_PREFIX_LABEL=Test unit prefix @@ -91,6 +97,7 @@ RUNNER_STOP_TOOLTIP=Stops the consumer session of the current test run immediate RUNNER_STOP_TEST_MESSAGE=Test disabled due to abortion of the test run. RUNNER_STOP_RUN_MESSAGE=Test run aborted. RUNNER_MISSING_TEST_RESULT_MESSAGE=Missing test results. +RUNNER_SHOW_SUITES_BUTTON=Switch between hierarchical view (shows suites and tests) and tabular view (shows tests only). RUNNER_CLEAR_BUTTON=Clear run history RUNNER_TESTS_LABEL=Tests RUNNER_FAILURES_LABEL=Failures @@ -112,7 +119,7 @@ RUNNER_PROCEDURE_LABEL=Procedure RUNNER_DESCRIPTION_LABEL=Description RUNNER_START_LABEL=Start RUNNER_ASSERT_DESCRIPTION_COLUMN=Assert description (failed line) -RUNNER_TEST_TAB_LABEL=Test +RUNNER_TEST_TAB_LABEL=Test/Suite RUNNER_FAILURES_TAB_LABEL=Failures RUNNER_ERRORS_TAB_LABEL=Errors RUNNER_WARNINGS_TAB_LABEL=Warnings diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/UtplsqlResources_de.properties b/sqldev/src/main/resources/org/utplsql/sqldev/resources/UtplsqlResources_de.properties index 2a15b1c7..8ba5566e 100644 --- a/sqldev/src/main/resources/org/utplsql/sqldev/resources/UtplsqlResources_de.properties +++ b/sqldev/src/main/resources/org/utplsql/sqldev/resources/UtplsqlResources_de.properties @@ -24,6 +24,7 @@ PREF_SHOW_SUCCESSFUL_TESTS_LABEL=Erfolgreiche Tests anzeigen? PREF_SHOW_DISABLED_TESTS_LABEL=Deaktivierte Tests anzeigen? PREF_SHOW_TEST_DESCRIPTION_LABEL=Beschreibung anzeigen (falls vorhanden)? PREF_SYNC_DETAIL_TAB_LABEL=Detailansicht basierend auf dem Teststatus synchronisieren? +PREF_SHOW_SUITES_LABEL=Suiten anzeigen (hierarchische Ansicht der Tests)? PREF_TEST_PACKAGE_PREFIX_LABEL=Test Package Pr\u00e4fix PREF_TEST_PACKAGE_SUFFIX_LABEL=Test Package Suffix PREF_TEST_UNIT_PREFIX_LABEL=Test Unit Pr\u00e4fix @@ -64,6 +65,7 @@ RUNNER_STOP_TOOLTIP=Stoppt die Verbrauchersitzung des aktuellen Testlaufs, die J RUNNER_STOP_TEST_MESSAGE=Test wurde aufgrund eines Abbruchs des Testlaufs deaktiviert. RUNNER_STOP_RUN_MESSAGE=Testlauf abgebrochen. RUNNER_MISSING_TEST_RESULT_MESSAGE=Testergebnis fehlt. +RUNNER_SHOW_SUITES_BUTTON=Zwischen hierarchischer Ansicht (zeigt Suites und Tests) und tabellarischer Ansicht (zeigt nur Tests) wechseln. RUNNER_CLEAR_BUTTON=Run History l\u00f6schen RUNNER_TESTS_LABEL=Tests RUNNER_FAILURES_LABEL=Fehlschl\u00e4ge @@ -85,7 +87,7 @@ RUNNER_PROCEDURE_LABEL=Prozedur RUNNER_DESCRIPTION_LABEL=Beschreibung RUNNER_START_LABEL=Start RUNNER_ASSERT_DESCRIPTION_COLUMN=Assert Beschreibung (gescheiterte Zeile) -RUNNER_TEST_TAB_LABEL=Test +RUNNER_TEST_TAB_LABEL=Test/Suite RUNNER_FAILURES_TAB_LABEL=Misserfolge RUNNER_ERRORS_TAB_LABEL=Fehler RUNNER_WARNINGS_TAB_LABEL=Warnungen diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/disabled.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/disabled.png index b2afa029..257ffde5 100644 Binary files a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/disabled.png and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/disabled.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/error.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/error.png index aefaed04..efed0b8f 100644 Binary files a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/error.png and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/error.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/failure.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/failure.png index 27bc6d87..40b596c6 100644 Binary files a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/failure.png and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/failure.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/info.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/info.png index eaf2ead8..1c8e3e75 100644 Binary files a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/info.png and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/info.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/info_2240x2240.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/info_2240x2240.png new file mode 100644 index 00000000..80100bc2 Binary files /dev/null and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/info_2240x2240.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/package.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/package.png new file mode 100644 index 00000000..eb839017 Binary files /dev/null and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/package.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/package_folder.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/package_folder.png new file mode 100644 index 00000000..10620057 Binary files /dev/null and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/package_folder.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/procedure.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/procedure.png new file mode 100644 index 00000000..6c4b7cfe Binary files /dev/null and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/procedure.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/procedure_folder.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/procedure_folder.png new file mode 100644 index 00000000..6c0e51f4 Binary files /dev/null and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/procedure_folder.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/status.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/status.png index f6d3dd70..5c3abfd5 100644 Binary files a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/status.png and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/status.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/status_64x64.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/status_64x64.png new file mode 100644 index 00000000..4ce21d74 Binary files /dev/null and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/status_64x64.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/success.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/success.png index 525198af..3c4d4c2f 100644 Binary files a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/success.png and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/success.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/warning.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/warning.png index 63e614db..66a081c8 100644 Binary files a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/warning.png and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/warning.png differ diff --git a/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/warning_672x672.png b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/warning_672x672.png new file mode 100644 index 00000000..b1740b4a Binary files /dev/null and b/sqldev/src/main/resources/org/utplsql/sqldev/resources/images/warning_672x672.png differ diff --git a/sqldev/src/test/java/org/utplsql/sqldev/test/ItemNodeTest.java b/sqldev/src/test/java/org/utplsql/sqldev/test/ItemNodeTest.java new file mode 100644 index 00000000..4bdfdb10 --- /dev/null +++ b/sqldev/src/test/java/org/utplsql/sqldev/test/ItemNodeTest.java @@ -0,0 +1,151 @@ +/* + * Copyright 2021 Philipp Salvisberg + * + * 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 org.utplsql.sqldev.test; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Set; + +import org.junit.Assert; +import org.junit.Before; +import org.junit.Test; +import org.utplsql.sqldev.model.runner.ItemNode; + +public class ItemNodeTest { + private final HashMap model = new HashMap<>(); + private final List selection = new ArrayList<>(); + + private void addItem(String id, String parentId) { + // using suite only + org.utplsql.sqldev.model.runner.Suite suite = new org.utplsql.sqldev.model.runner.Suite(); + suite.setId(id); + ItemNode node = new ItemNode(suite); + model.put(id, node); + ItemNode parent; + if (parentId != null) { + parent = model.get(parentId); + parent.add(node); + } + } + + @Before + public void setup() { + /* + * Setup model for all test cases: + * + * a + * +- a.a + * +- a.a.a + * +- a.a.b + * +- a.a.b.a + * +- a.a.b.b + * +- a.b + * +- a.b.a + * +- a.b.b + * b + * +- b.a + * +- b.b + */ + model.clear(); + addItem("a" , null); + addItem("a.a" , "a"); + addItem("a.a.a" , "a.a"); + addItem("a.a.b" , "a.a"); + addItem("a.a.b.a", "a.a.b"); + addItem("a.a.b.b", "a.a.b"); + addItem("a.b" , "a"); + addItem("a.b.a" , "a.b"); + addItem("a.b.b" , "a.b"); + addItem("b" , null); + addItem("b.a" , "b"); + addItem("b.b" , "b"); + selection.clear(); + } + + + @Test + public void null_input() { + Set actual = ItemNode.createNonOverlappingSet(null); + Assert.assertEquals(0, actual.size()); + } + + @Test + public void empty_input() { + Set actual = ItemNode.createNonOverlappingSet(selection); + Assert.assertEquals(0, actual.size()); + } + + @Test + public void one_top_node() { + selection.add(model.get("a")); + Set actual = ItemNode.createNonOverlappingSet(selection); + Assert.assertEquals(1, actual.size()); + Assert.assertTrue(actual.contains(model.get("a"))); + } + + @Test + public void one_top_node_one_child() { + selection.add(model.get("a")); + selection.add(model.get("a.a.b.a")); + Set actual = ItemNode.createNonOverlappingSet(selection); + Assert.assertEquals(1, actual.size()); + Assert.assertTrue(actual.contains(model.get("a"))); + } + + @Test + public void one_top_node_two_chidren() { + selection.add(model.get("a.b")); + selection.add(model.get("a")); + selection.add(model.get("a.a.b.a")); + Set actual = ItemNode.createNonOverlappingSet(selection); + Assert.assertEquals(1, actual.size()); + Assert.assertTrue(actual.contains(model.get("a"))); + } + + @Test + public void one_top_node_three_chidren() { + selection.add(model.get("a")); + selection.add(model.get("a.a.b.a")); + selection.add(model.get("a.b")); + selection.add(model.get("a.b.a")); + Set actual = ItemNode.createNonOverlappingSet(selection); + Assert.assertEquals(1, actual.size()); + Assert.assertTrue(actual.contains(model.get("a"))); + } + + @Test + public void three_chidren() { + selection.add(model.get("a.a.b.a")); + selection.add(model.get("a.b")); + selection.add(model.get("a.b.a")); + Set actual = ItemNode.createNonOverlappingSet(selection); + Assert.assertEquals(2, actual.size()); + Assert.assertTrue(actual.contains(model.get("a.a.b.a"))); + Assert.assertTrue(actual.contains(model.get("a.b"))); + } + + @Test + public void two_top_nodes() { + selection.add(model.get("a")); + selection.add(model.get("b")); + Set actual = ItemNode.createNonOverlappingSet(selection); + Assert.assertEquals(2, actual.size()); + Assert.assertTrue(actual.contains(model.get("a"))); + Assert.assertTrue(actual.contains(model.get("b"))); + } + +} diff --git a/sqldev/src/test/java/org/utplsql/sqldev/test/JsonToStringStylerTest.java b/sqldev/src/test/java/org/utplsql/sqldev/test/JsonToStringStylerTest.java index 84668056..a5c548ce 100644 --- a/sqldev/src/test/java/org/utplsql/sqldev/test/JsonToStringStylerTest.java +++ b/sqldev/src/test/java/org/utplsql/sqldev/test/JsonToStringStylerTest.java @@ -92,6 +92,31 @@ public void emptyRun() { sb.append(" \"errorStack\": null,\n"); sb.append(" \"serverOutput\": null,\n"); sb.append(" \"tests\": [],\n"); + sb.append(" \"rootNode\": {\n"); + sb.append(" \"className\": \"Suite\",\n"); + sb.append(" \"id\": \"1\",\n"); + sb.append(" \"name\": \"1\",\n"); + sb.append(" \"description\": null,\n"); + sb.append(" \"startTime\": null,\n"); + sb.append(" \"endTime\": null,\n"); + sb.append(" \"executionTime\": null,\n"); + sb.append(" \"counter\": {\n"); + sb.append(" \"className\": \"Counter\",\n"); + sb.append(" \"disabled\": 0,\n"); + sb.append(" \"success\": 0,\n"); + sb.append(" \"failure\": 0,\n"); + sb.append(" \"error\": 0,\n"); + sb.append(" \"warning\": 0\n"); + sb.append(" },\n"); + sb.append(" \"errorStack\": null,\n"); + sb.append(" \"serverOutput\": null,\n"); + sb.append(" \"warnings\": null,\n"); + sb.append(" \"parentId\": null,\n"); + sb.append(" \"statusIcon\": null,\n"); + sb.append(" \"warningIcon\": null,\n"); + sb.append(" \"infoIcon\": null,\n"); + sb.append(" \"items\": []\n"); + sb.append(" },\n"); sb.append(" \"status\": null,\n"); sb.append(" \"start\": null,\n"); sb.append(" \"endTime\": null,\n"); @@ -141,6 +166,8 @@ public void runWithTests() { sb.append(" {\n"); sb.append(" \"className\": \"Test\",\n"); sb.append(" \"id\": \"1\",\n"); + sb.append(" \"name\": \"Test One\",\n"); + sb.append(" \"description\": null,\n"); sb.append(" \"startTime\": null,\n"); sb.append(" \"endTime\": null,\n"); sb.append(" \"executionTime\": null,\n"); @@ -155,22 +182,23 @@ public void runWithTests() { sb.append(" \"errorStack\": null,\n"); sb.append(" \"serverOutput\": null,\n"); sb.append(" \"warnings\": null,\n"); + sb.append(" \"parentId\": null,\n"); + sb.append(" \"statusIcon\": null,\n"); + sb.append(" \"warningIcon\": null,\n"); + sb.append(" \"infoIcon\": null,\n"); sb.append(" \"executableType\": null,\n"); sb.append(" \"ownerName\": null,\n"); sb.append(" \"objectName\": null,\n"); sb.append(" \"procedureName\": null,\n"); sb.append(" \"disabled\": null,\n"); - sb.append(" \"name\": \"Test One\",\n"); - sb.append(" \"description\": null,\n"); sb.append(" \"testNumber\": null,\n"); - sb.append(" \"failedExpectations\": null,\n"); - sb.append(" \"statusIcon\": null,\n"); - sb.append(" \"warningIcon\": null,\n"); - sb.append(" \"infoIcon\": null\n"); + sb.append(" \"failedExpectations\": null\n"); sb.append(" },\n"); sb.append(" {\n"); sb.append(" \"className\": \"Test\",\n"); sb.append(" \"id\": \"2\",\n"); + sb.append(" \"name\": \"Test Two\",\n"); + sb.append(" \"description\": null,\n"); sb.append(" \"startTime\": null,\n"); sb.append(" \"endTime\": null,\n"); sb.append(" \"executionTime\": null,\n"); @@ -185,20 +213,44 @@ public void runWithTests() { sb.append(" \"errorStack\": null,\n"); sb.append(" \"serverOutput\": null,\n"); sb.append(" \"warnings\": null,\n"); + sb.append(" \"parentId\": null,\n"); + sb.append(" \"statusIcon\": null,\n"); + sb.append(" \"warningIcon\": null,\n"); + sb.append(" \"infoIcon\": null,\n"); sb.append(" \"executableType\": null,\n"); sb.append(" \"ownerName\": null,\n"); sb.append(" \"objectName\": null,\n"); sb.append(" \"procedureName\": null,\n"); sb.append(" \"disabled\": null,\n"); - sb.append(" \"name\": \"Test Two\",\n"); - sb.append(" \"description\": null,\n"); sb.append(" \"testNumber\": null,\n"); - sb.append(" \"failedExpectations\": null,\n"); - sb.append(" \"statusIcon\": null,\n"); - sb.append(" \"warningIcon\": null,\n"); - sb.append(" \"infoIcon\": null\n"); + sb.append(" \"failedExpectations\": null\n"); sb.append(" }\n"); sb.append(" ],\n"); + sb.append(" \"rootNode\": {\n"); + sb.append(" \"className\": \"Suite\",\n"); + sb.append(" \"id\": \"1\",\n"); + sb.append(" \"name\": \"1\",\n"); + sb.append(" \"description\": null,\n"); + sb.append(" \"startTime\": null,\n"); + sb.append(" \"endTime\": null,\n"); + sb.append(" \"executionTime\": null,\n"); + sb.append(" \"counter\": {\n"); + sb.append(" \"className\": \"Counter\",\n"); + sb.append(" \"disabled\": 0,\n"); + sb.append(" \"success\": 0,\n"); + sb.append(" \"failure\": 0,\n"); + sb.append(" \"error\": 0,\n"); + sb.append(" \"warning\": 0\n"); + sb.append(" },\n"); + sb.append(" \"errorStack\": null,\n"); + sb.append(" \"serverOutput\": null,\n"); + sb.append(" \"warnings\": null,\n"); + sb.append(" \"parentId\": null,\n"); + sb.append(" \"statusIcon\": null,\n"); + sb.append(" \"warningIcon\": null,\n"); + sb.append(" \"infoIcon\": null,\n"); + sb.append(" \"items\": []\n"); + sb.append(" },\n"); sb.append(" \"status\": null,\n"); sb.append(" \"start\": null,\n"); sb.append(" \"endTime\": null,\n"); diff --git a/sqldev/src/test/java/org/utplsql/sqldev/test/StringToolsTest.java b/sqldev/src/test/java/org/utplsql/sqldev/test/StringToolsTest.java index 8cb642ca..7cc4f286 100644 --- a/sqldev/src/test/java/org/utplsql/sqldev/test/StringToolsTest.java +++ b/sqldev/src/test/java/org/utplsql/sqldev/test/StringToolsTest.java @@ -17,6 +17,7 @@ import java.util.ArrayList; import java.util.List; +import java.util.TimeZone; import org.junit.Assert; import org.junit.Test; @@ -53,5 +54,36 @@ public void two_entries_as_simpleCSV() { list.add("world"); Assert.assertEquals("hello, world", StringTools.getSimpleCSV(list)); } + + @Test + public void earliest_date_from__millis() { + long zoneDiff = TimeZone.getDefault().getRawOffset(); + Assert.assertEquals("1970-01-01T00:00:00.000000", StringTools.millisToDateTimeString(-zoneDiff)); + } + + @Test + public void date_from__millis_with_millis() { + long zoneDiff = TimeZone.getDefault().getRawOffset(); + Assert.assertEquals("1970-01-01T00:00:42.123000", StringTools.millisToDateTimeString(42123 - zoneDiff)); + } + + @Test + public void earliest_date_in_millis() { + long zoneDiff = TimeZone.getDefault().getRawOffset(); + final String dateTime = "1970-01-01T00:00:00.000000"; + Assert.assertEquals(0, StringTools.dateTimeStringToMillis(dateTime) + zoneDiff); + } + + @Test + public void date_with_millis() { + long zoneDiff = TimeZone.getDefault().getRawOffset(); + final String dateTime = "1970-01-01T00:00:42.123456"; + Assert.assertEquals(42123, StringTools.dateTimeStringToMillis(dateTime) + zoneDiff , 0); + } + + @Test + public void elapsed_time_from_start_and_end_time() { + Assert.assertEquals(42.123, StringTools.elapsedTime("2021-01-01T14:00:00.000000", "2021-01-01T14:00:42.123000"), 0); + } } diff --git a/sqldev/src/test/java/org/utplsql/sqldev/test/preference/PreferenceModelTest.java b/sqldev/src/test/java/org/utplsql/sqldev/test/preference/PreferenceModelTest.java index 904681b6..1ed23f7e 100644 --- a/sqldev/src/test/java/org/utplsql/sqldev/test/preference/PreferenceModelTest.java +++ b/sqldev/src/test/java/org/utplsql/sqldev/test/preference/PreferenceModelTest.java @@ -41,6 +41,7 @@ public void defaultValues() { Assert.assertTrue(model.isShowDisabledTests()); Assert.assertFalse(model.isShowTestDescription()); Assert.assertTrue(model.isSyncDetailTab()); + Assert.assertTrue(model.isShowSuites()); Assert.assertEquals("test_", model.getTestPackagePrefix()); Assert.assertEquals("", model.getTestPackageSuffix()); Assert.assertEquals("", model.getTestUnitPrefix()); diff --git a/sqldev/src/test/java/org/utplsql/sqldev/test/runner/UtplsqlRunnerPanelTest.java b/sqldev/src/test/java/org/utplsql/sqldev/test/runner/UtplsqlRunnerPanelTest.java index b4da9385..4100b120 100644 --- a/sqldev/src/test/java/org/utplsql/sqldev/test/runner/UtplsqlRunnerPanelTest.java +++ b/sqldev/src/test/java/org/utplsql/sqldev/test/runner/UtplsqlRunnerPanelTest.java @@ -38,7 +38,7 @@ public class UtplsqlRunnerPanelTest { @Before public void setup() { final String reporterId = UUID.randomUUID().toString().replace("-", ""); - run = new Run(null, reporterId, Collections.emptyList()); + run = new Run(reporterId, null, Collections.emptyList()); run.setStartTime("2019-06-09T13:42:42.123456"); run.getCounter().setDisabled(0); run.getCounter().setSuccess(0); diff --git a/sqldev/src/test/java/org/utplsql/sqldev/test/runner/UtplsqlRunnerTest.java b/sqldev/src/test/java/org/utplsql/sqldev/test/runner/UtplsqlRunnerTest.java index e2a24001..d64d1302 100644 --- a/sqldev/src/test/java/org/utplsql/sqldev/test/runner/UtplsqlRunnerTest.java +++ b/sqldev/src/test/java/org/utplsql/sqldev/test/runner/UtplsqlRunnerTest.java @@ -25,11 +25,35 @@ import org.springframework.jdbc.datasource.SingleConnectionDataSource; import org.utplsql.sqldev.model.DatabaseTools; import org.utplsql.sqldev.model.SystemTools; +import org.utplsql.sqldev.model.preference.PreferenceModel; import org.utplsql.sqldev.runner.UtplsqlRunner; import org.utplsql.sqldev.test.AbstractJdbcTest; import org.utplsql.sqldev.test.coverage.CodeCoverageReporterTest; +import oracle.ide.config.Preferences; + public class UtplsqlRunnerTest extends AbstractJdbcTest { + PreferenceModel preferences; + + @Before + public void setupDefaultPreferences() { + try { + // first call will fail, second call will succeed (using preferences from user.home) + preferences = PreferenceModel.getInstance(Preferences.getPreferences()); + } catch (NoClassDefFoundError e) { + // running outside of SQL Developer, the following log message is shown: + // WARNING: No extension registry present. Loading preferences from user.home + preferences = PreferenceModel.getInstance(null); + // the second call will call will succeed and use preferences from user.home + // this ensures that the test and the runner use the same preferences + preferences = PreferenceModel.getInstance(Preferences.getPreferences()); + } finally { + // set defaults manually, since all tests are using the same preference store + preferences.setShowSuccessfulTests(true); + preferences.setShowWarningIndicator(false); + preferences.setShowInfoIndicator(false); + } + } @Before public void setup() { @@ -158,6 +182,22 @@ public void runTestsWithMaxTime() { runner.dispose(); } + @Test + public void runTestsHidingSuccesfulRuns() { + preferences.setShowSuccessfulTests(false); + preferences.setShowWarningIndicator(true); + preferences.setShowInfoIndicator(true); + UtplsqlRunner runner = new UtplsqlRunner(Collections.singletonList(":a"), getNewConnection(), getNewConnection()); + runner.runTestAsync(); + + SystemTools.waitForThread(runner.getProducerThread(), 200000); + SystemTools.waitForThread(runner.getConsumerThread(), 200000); + SystemTools.sleep(4 * 1000); + Assert.assertNotNull(runner); + runner.dispose(); + } + + @Test public void runTestsWithCodeCoverage() { UtplsqlRunner runner = new UtplsqlRunner(Collections.singletonList(":test_f"), null, null, null, getNewConnection(), getNewConnection());