message: [RM] Feature [SF][BB] Parallel upload matrices into slave.

diff:
@@ -27,6 +27,7 @@ import com.exactpro.sf.bigbutton.library.Globals;
import com.exactpro.sf.bigbutton.library.IBBActionExecutor;
import com.exactpro.sf.bigbutton.library.Library;
import com.exactpro.sf.bigbutton.library.Script;
+import com.exactpro.sf.bigbutton.library.Script.ScriptRemoteInformation;
import com.exactpro.sf.bigbutton.library.ScriptList;
import com.exactpro.sf.bigbutton.library.ScriptList.ScriptListStatus;
import com.exactpro.sf.bigbutton.library.Service;
@@ -369,6 +370,9 @@ public class ExecutorClient {
} catch (RuntimeException e) {
toErrorState(e);
}
+
+ stopRunningTestScripts();
+
}
public void registerTags(List<Tag> tags) {
@@ -536,8 +540,92 @@ public class ExecutorClient {
}
- private void executeScript(Script script) throws InterruptedException {
+
+ private XmlTestScriptShortReport waitForScriptExecutionFinish(Script script, int id) throws InterruptedException {
+
+ logger.debug("Got id {}. Monitoring status...", id);
+
+ ScriptExecutionStatistics statistics = script.getStatistics();
+
+ while(true) {
+
+ try {
+
+ if(!running) {
+ logger.info("Invoking stop");
+ apiClient.stopTestScriptRun(id);
+ return null;
+ }
+
+ Thread.sleep(1000);
+
+ XmlTestScriptShortReport response = apiClient.getTestScriptRunShortReport(id);
+
+ if(response != null) {
+ logger.debug("Status: {}", response);
+ }
+
+ statistics.setNumPassed(response.getPassed());
+ statistics.setNumConditionallyPassed(response.getConditionallyPassed());
+ statistics.setNumFailed(response.getFailed());
+
+ statistics.setStatus(response.getStatus());
+
+ if (!response.isLocked()) {
+
+ script.setFinished(true);
+
+ script.setCause(TestScriptDescription.getCauseMessage(response.getCause()));
+
+ statistics.setTotal(response.getTotal());
+
+ return response;
+
+ }
+
+ } catch (InterruptedException e) {
+ throw e;
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ throw new RuntimeException("Could not perform status check call", e); // TODO: replace with checked exception
+
+ }
+
+ }
+
+ }
+
+ private void executeOnFinish(StatusType statusType, Set<BigButtonAction> actionsToExecute, String subSubject, IBBActionExecutor... parameters) {
+ logger.info("executeOnFinish was triggered");
+ for (BigButtonAction action : actionsToExecute) {
+ try {
+ actions.get(action).accept(statusType, subSubject, parameters);
+ } catch (Exception e) {
+ logger.error("Exception during BBAction execute", e.getMessage());
+ }
+ }
+ }
+
+ private void executeOnFinishScript(Script script){
+ logger.info("executeOnFinishScript was triggered");
+ StatusType statusType = script.getStatusType();
+ executeOnFinish(statusType, script.getActions(), StringUtils.join("Script ", statusType.name().toLowerCase()), currentList, script);
+ }
+
+
+ private void executeOnFinishScriptList(ScriptList scriptList){
+ logger.info("executeOnFinishScriptList was triggered");
+ StatusType statusType = scriptList.getStatusType();
+ executeOnFinish(statusType, scriptList.getActions(), StringUtils.join("Script list ", statusType.name().toLowerCase()), scriptList);
+ }
+
+ /**
+ * Upload and start compile script on slave
+ * @param script
+ * @throws InterruptedException
+ */
+ private void compileScript(Script script) throws InterruptedException {
//SfApiOptions scriptOptions = script.getApiOptions();
SfApiOptions scriptOptions = library.getDefaultApiOptions();
@@ -587,7 +675,7 @@ public class ExecutorClient {
0, // aml version is deprecated
scriptOptions.getContinueIfFailed(),
scriptOptions.getAutoStart(),
- true,
+ false,
scriptOptions.getIgnoreAskForContinue(),
scriptOptions.getRunNetDumper(),
scriptOptions.getSkipOptional(),
@@ -596,8 +684,54 @@ public class ExecutorClient {
targetReportFolder,
scriptOptions.getLanguage());
- long scriptRunId = response.getId();
+ apiClient.compileTestScriptRun((int) response.getId());
+ script.setRemoteInformation(new ScriptRemoteInformation(response.getId(), matrix.getId()));
+ }
+ }
+ catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ throw new RuntimeException("Could not perform api call", e); // TODO: replace with checked exception
+
+ }
+ }
+
+ /**
+ * Wait while script will be compiled
+ * @param testScriptId
+ * @throws InterruptedException
+ */
+ private void waitForCompileTestScript(int testScriptId) throws InterruptedException{
+ try {
+ while (!"READY".equals(apiClient.getTestScriptRunInfo(testScriptId).getScriptState())) {
+ Thread.sleep(1000);
+ }
+ } catch (APICallException | APIResponseException e) {
+ throw new EPSCommonException("Could not get scripts state for test script with id + {" + testScriptId + '}', e);
+ }
+ }
+ /**
+ * Send request to slave to start script
+ * @param testScriptId
+ * @throws InterruptedException
+ */
+ private void startScript(int testScriptId) throws InterruptedException {
+ try {
+ apiClient.runCompiledTestScript(testScriptId);
+ } catch (APICallException | APIResponseException e) {
+ throw new EPSCommonException("Can`t start test script with id [" + testScriptId + ']', e);
+ }
+ }
+
+ /**
+ * Wait while script will be executed, then update statistic and download report
+ * @param script
+ * @param scriptRunId
+ * @param matrixId
+ * @throws InterruptedException
+ */
+ private void waitForExecutingTestScriptAndCollectStatisticAndReport(Script script, long scriptRunId, long matrixId) throws InterruptedException {
+ try {
XmlTestScriptShortReport executionResult = waitForScriptExecutionFinish(script, (int)scriptRunId);
ListExecutionStatistics statistics = currentList.getExecutionStatistics();
@@ -612,7 +746,6 @@ public class ExecutorClient {
statistics.incNumConditionallyPassed();
-
} else {
statistics.incNumPassed();
@@ -629,100 +762,37 @@ public class ExecutorClient {
reportDownloadNeeded = true;
}
-
executeOnFinishScript(script);
monitor.scriptExecuted(script, (int)scriptRunId, executor, relativeListReportsFolder, reportDownloadNeeded);
- apiClient.deleteMatrix((int)matrix.getId());
- }
-
- }catch (InterruptedException e){
- throw e;
- }
- catch (Exception e) {
+ apiClient.deleteMatrix((int)matrixId);
+ } catch (Exception e) {
logger.error(e.getMessage(), e);
throw new RuntimeException("Could not perform api call", e); // TODO: replace with checked exception
-
}
-
}
- private XmlTestScriptShortReport waitForScriptExecutionFinish(Script script, int id) throws InterruptedException {
-
- logger.debug("Got id {}. Monitoring status...", id);
-
- ScriptExecutionStatistics statistics = script.getStatistics();
-
- while(true) {
-
- try {
-
+ /**
+ * Check BB is not running, or wait if BB is on pause
+ * @return True - if BB is not running, False - if BB is running
+ * @throws InterruptedException
+ */
+ private boolean checkIsNotRun() throws InterruptedException {
if (!running) {
- logger.info("Invoking stop");
- apiClient.stopTestScriptRun(id);
- return null;
- }
-
- Thread.sleep(1000);
-
- XmlTestScriptShortReport response = apiClient.getTestScriptRunShortReport(id);
-
- if(response != null) {
- logger.debug("Status: {}", response);
- }
-
- statistics.setNumPassed(response.getPassed());
- statistics.setNumConditionallyPassed(response.getConditionallyPassed());
- statistics.setNumFailed(response.getFailed());
-
- statistics.setStatus(response.getStatus());
-
- if (!response.isLocked()) {
-
- script.setFinished(true);
-
- script.setCause(TestScriptDescription.getCauseMessage(response.getCause()));
-
- statistics.setTotal(response.getTotal());
-
- return response;
-
- }
-
- } catch (InterruptedException e) {
- throw e;
- } catch (Exception e) {
- logger.error(e.getMessage(), e);
- throw new RuntimeException("Could not perform status check call", e); // TODO: replace with checked exception
-
- }
-
+ return true;
}
- }
+ if (runner.isPause()) {
+ state = ExecutorState.Paused;
- private void executeOnFinish(StatusType statusType, Set<BigButtonAction> actionsToExecute, String subSubject, IBBActionExecutor... parameters) {
- logger.info("executeOnFinish was triggered");
- for (BigButtonAction action : actionsToExecute) {
- try {
- actions.get(action).accept(statusType, subSubject, parameters);
- } catch (Exception e) {
- logger.error("Exception during BBAction execute", e.getMessage());
- }
- }
+ while (runner.isPause()) {
+ Thread.sleep(1000);
}
- private void executeOnFinishScript(Script script){
- logger.info("executeOnFinishScript was triggered");
- StatusType statusType = script.getStatusType();
- executeOnFinish(statusType, script.getActions(), StringUtils.join("Script ", statusType.name().toLowerCase()), currentList, script);
+ state = ExecutorState.Executing;
}
-
- private void executeOnFinishScriptList(ScriptList scriptList){
- logger.info("executeOnFinishScriptList was triggered");
- StatusType statusType = scriptList.getStatusType();
- executeOnFinish(statusType, scriptList.getActions(), StringUtils.join("Script list ", statusType.name().toLowerCase()), scriptList);
+ return false;
}
private void runList() throws InterruptedException {
@@ -734,29 +804,32 @@ public class ExecutorClient {
this.relativeListReportsFolder = createRelativeListRepotsFolder();
- for(int i =0; i< currentList.getScripts().size(); i++) {//Script script : currentList.getScripts()) {
+ for (int i = 0; i < currentList.getScripts().size(); i++) {
- if(!running) {
+ if (checkIsNotRun()){
return;
}
- if(runner.isPause()){
- state = ExecutorState.Paused;
-
- while (runner.isPause()) {
- Thread.sleep(1000);
+ compileScript(currentList.getScripts().get(i));
}
- state = ExecutorState.Executing;
- }
+ for(int i =0; i < currentList.getScripts().size(); i++) {//Script script : currentList.getScripts()) {
+ if (checkIsNotRun()){
+ return;
+ }
Script script = currentList.getScripts().get(i);
currentList.setCurrentScript(script);
- if (!currentList.getStatus().isSkipped()) {
- executeScript(script);
+ if (!currentList.getStatus().isSkipped() && script.getRemoteInformation() != null) {
+
+ waitForCompileTestScript((int) script.getRemoteInformation().getTestScriptId());
+
+ startScript((int) script.getRemoteInformation().getTestScriptId());
+
+ waitForExecutingTestScriptAndCollectStatisticAndReport(script, script.getRemoteInformation().getTestScriptId(), script.getRemoteInformation().getMatrixId());
} else {
script.getStatistics().setStatus("SKIPPED");
script.setFinished(true);
@@ -770,7 +843,6 @@ public class ExecutorClient {
statistics.setExecutionPercent(
RegressionRunnerUtils.calcPercent(i + 1, currentList.getScripts().size()));
-
}
} catch (InterruptedException e){
@@ -1075,4 +1147,29 @@ public class ExecutorClient {
}
}
+ private void stopRunningTestScripts() {
+ try {
+ // currentList may be null when BB end to running all test scripts.
+ // When BB execute action Interrupt currentList is not null
+ if (currentList != null) {
+ currentList
+ .getScripts()
+ .stream()
+ .filter(script -> !script.isFinished())
+ .forEach(script -> {
+ try {
+ apiClient.stopTestScriptRun((int)script.getRemoteInformation().getTestScriptId());
+ apiClient.deleteTestScriptRun((int) script.getRemoteInformation().getTestScriptId());
+ apiClient.deleteMatrix((int)script.getRemoteInformation().getMatrixId());
+ } catch (APICallException | APIResponseException e) {
+ throw new EPSCommonException("Could not perform api call", e);
+ }
+ });
+ }
+ } catch (RuntimeException e){
+ toErrorState(e);
+ }
+ }
+
+
}

diff:
@@ -53,6 +53,33 @@ public class Script implements Serializable, IBBActionExecutor {
private String cause;
+ private ScriptRemoteInformation remoteInformation = null;
+
+ public static class ScriptRemoteInformation {
+ private long testScriptId;
+ private long matrixId;
+
+ public ScriptRemoteInformation(long testScriptId, long matrixId) {
+ this.testScriptId = testScriptId;
+ this.matrixId = matrixId;
+ }
+
+ public long getTestScriptId() {
+ return testScriptId;
+ }
+
+ public void setTestScriptId(long testScriptId) {
+ this.testScriptId = testScriptId;
+ }
+
+ public long getMatrixId() {
+ return matrixId;
+ }
+
+ public void setMatrixId(long matrixId) {
+ this.matrixId = matrixId;
+ }
+ }
public Script(long lineNumber){
this.lineNumber = lineNumber;
@@ -153,6 +180,14 @@ public class Script implements Serializable, IBBActionExecutor {
this.cause = cause;
}
+ public ScriptRemoteInformation getRemoteInformation() {
+ return remoteInformation;
+ }
+
+ public void setRemoteInformation(ScriptRemoteInformation remoteInformation) {
+ this.remoteInformation = remoteInformation;
+ }
+
@Override
@SuppressWarnings("incomplete-switch")
public Set<BigButtonAction> getActions() {

diff:
<NotificationIfSomeServicesNotStarted>false</NotificationIfSomeServicesNotStarted>
<FailUnexpected>false</FailUnexpected>
<!-- Priority of the matrices compilation thread, values [1, 10] -->
- <MatrixCompilerPriority>5</MatrixCompilerPriority>
+ <MatrixCompilerPriority>1</MatrixCompilerPriority>
<!-- Format of report creation: zip - remove files after archiving; files - disable auto-archive; zip_files - do not remove files, enable auto-archive -->
<ReportOutputFormat>zip_files</ReportOutputFormat>
<!-- Set of excluded messages from information block about all messages in report. Property may have several values separated by comma character -->

original_message:
[RM] Feature #59799. [SF][BB] Parallel upload matrices into slave.

Change-Id: If866a6ba5fc9a9897e0b1a3615451370b5269770


message: [RM] Feature [YAML] Please add possibility to copy message structure from Help in yaml format.

change_type: MODIFY

diff:
@@ -677,7 +677,7 @@ public class HelpBuilder {
HelpJsonContainer fieldJsonNode = new FieldJsonContainer(fieldName,
ACTIONS + File.separator + actionName + File.separator + FIELDS + File.separator + fieldName + HTML,
- HelpJsonContainer.FIELD_ICON, HelpEntityType.FIELD, field.getType());
+ HelpJsonContainer.FIELD_ICON, HelpEntityType.FIELD, field.getType(), false);
jsonParent.addChild(fieldJsonNode);
@@ -716,7 +716,7 @@ public class HelpBuilder {
HelpJsonContainer messageJsonNode = new MessageJsonContainer(abstrFM.getName(),
ACTIONS + File.separator + actionName + File.separator + MESSAGES + File.separator + messageName + HTML,
- HelpJsonContainer.MESSAGE_ICON, HelpEntityType.MESSAGE, new ArrayList<HelpJsonContainer>());
+ HelpJsonContainer.MESSAGE_ICON, HelpEntityType.MESSAGE, false, new ArrayList<HelpJsonContainer>());
jsonParent.addChild(messageJsonNode);
@@ -850,7 +850,7 @@ public class HelpBuilder {
HelpJsonContainer messageStructureJsonNode = new MessageJsonContainer(messageName,
DICTIONARIES + File.separator + dictionaryResourceName + File.separator + MESSAGES + File.separator + messageName
- + HTML, HelpJsonContainer.MESSAGE_STRUCTURE_ICON, HelpEntityType.MESSAGE, new ArrayList<HelpJsonContainer>());
+ + HTML, HelpJsonContainer.MESSAGE_STRUCTURE_ICON, HelpEntityType.MESSAGE, false, new ArrayList<HelpJsonContainer>());
jsonParent.addChild(messageStructureJsonNode);
@@ -940,13 +940,13 @@ public class HelpBuilder {
}
}
if (field.isComplex()) {
- fieldJsonNode = new MessageJsonContainer(fieldName, fieldsPathMap.get(equalField), HelpJsonContainer.MESSAGE_ICON, fieldNodeType,
+ fieldJsonNode = new MessageJsonContainer(fieldName, fieldsPathMap.get(equalField), HelpJsonContainer.MESSAGE_ICON, fieldNodeType, field.isCollection(),
new ArrayList<HelpJsonContainer>());
buildMessageFields(field, fieldJsonNode, fieldsPathMap, dictionaryName, fieldsMap);
} else {
fieldJsonNode = new FieldJsonContainer(fieldName, fieldsPathMap.get(equalField), HelpJsonContainer.FIELD_STRUCTURE_ICON, fieldNodeType,
- getJavaTypeLabel(field.getJavaType()));
+ getJavaTypeLabel(field.getJavaType()), field.isCollection());
}
jsonParent.addChild(fieldJsonNode);

diff:
@@ -25,17 +25,20 @@ import java.util.List;
@JsonTypeInfo(use= Id.CLASS, include= As.PROPERTY, property="@class")
public class FieldJsonContainer extends HelpJsonContainer{
private String javaType;
+ private boolean collection;
- public FieldJsonContainer(String name, String htmlPath, String icon, HelpEntityType type, String javaType) {
+ public FieldJsonContainer(String name, String htmlPath, String icon, HelpEntityType type, String javaType, boolean isCollection) {
super(name, htmlPath, icon, type);
this.javaType = javaType;
+ this.collection = isCollection;
}
- public FieldJsonContainer(String name, String htmlPath, String icon, HelpEntityType type, String javaType, List<HelpJsonContainer> childNodes) {
+ public FieldJsonContainer(String name, String htmlPath, String icon, HelpEntityType type, String javaType, boolean isCollection, List<HelpJsonContainer> childNodes) {
super(name, htmlPath, icon, type, childNodes);
this.javaType = javaType;
+ this.collection = isCollection;
}
public FieldJsonContainer() {
@@ -45,5 +48,8 @@ public class FieldJsonContainer extends HelpJsonContainer{
return javaType;
}
+ public boolean isCollection() {
+ return collection;
+ }
}

diff:
@@ -61,32 +61,15 @@ public class HelpJsonContainer {
}
public HelpJsonContainer(HelpEntityName name, String filePath, String icon, HelpEntityType type, List<HelpJsonContainer> childNodes) {
- this.filePath = filePath;
- this.childNodes = childNodes;
- this.name = name.getValue();
- this.icon = icon;
- this.type = type;
-
+ this(name.getValue(), filePath, icon, type, childNodes);
}
public HelpJsonContainer(String name, String filePath, String icon, HelpEntityType type) {
- this.filePath = filePath;
- this.name = name;
- this.icon = icon;
- this.type = type;
-
- this.childNodes = null;
+ this(name, filePath, icon, type, null);
}
-
-
public HelpJsonContainer(String htmlPath, List<HelpJsonContainer> childNodes) {
- this.filePath = htmlPath;
- this.childNodes = childNodes;
-
- this.name = null;
- this.icon = null;
- this.type = null;
+ this((String) null, htmlPath, null, null, childNodes);
}
public HelpJsonContainer() {

diff:
@@ -21,8 +21,8 @@ import java.util.List;
public class MessageJsonContainer extends FieldJsonContainer {
- public MessageJsonContainer(String name, String htmlPath, String icon, HelpEntityType type, List<HelpJsonContainer> childNodes) {
- super(name, htmlPath, icon, type, null, childNodes);
+ public MessageJsonContainer(String name, String htmlPath, String icon, HelpEntityType type, boolean isCollection, List<HelpJsonContainer> childNodes) {
+ super(name, htmlPath, icon, type, null, isCollection, childNodes);
}
public MessageJsonContainer(){

diff:
+<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ ~ Copyright 2009-2019 Exactpro (Exactpro Systems Limited)
+ ~
+ ~ 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.
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
<!DOCTYPE HTML PUBLIC "-//W3C//DTD HTML 4.01//EN"
"http://www.w3.org/TR/html4/strict.dtd">
<p:menuitem id="copyAllStructure" value="All structure" oncomplete="PF('copyDialog').hide()"/>
+ <p:menuitem id="copyAllYamlStructure" value="All structure in YAML (Collapsed)" oncomplete="PF('copyDialog').hide()"/>
+
</p:submenu>
</p:menubar>
<p:tooltip value="Copy an extended header" for="copyAllHeader" />
<p:tooltip value="Copy the structure without a header" for="copyIncompleteStructure" />
<p:tooltip value="Copy the common structure with an extended header" for="copyAllStructure" />
+ <p:tooltip value="Copy the common structure with an extended header in YAML collapsed format" for="copyAllYamlStructure" />
</p:outputPanel>

diff:
@@ -55,11 +55,12 @@ function createHiddenTarget(targetId, toCopy) {
$(exTargetId).val(toCopy);
}
-function copyToClipboard(toCopy1, toCopy2, toCopy3, toCopy4) {
+function copyToClipboard(toCopy1, toCopy2, toCopy3, toCopy4, toCopy5) {
createHiddenTarget("_hiddenCopyText1", toCopy1);
createHiddenTarget("_hiddenCopyText2", toCopy2);
createHiddenTarget("_hiddenCopyText3", toCopy3);
createHiddenTarget("_hiddenCopyText4", toCopy4);
+ createHiddenTarget("_hiddenCopyText5", toCopy5);
}
function copyProcess(targetId) {
@@ -90,6 +91,10 @@ $(document).ready(function() {
copyProcess("#_hiddenCopyText4");
});
+ $('body').on("click", "#copyAllYamlStructure", function (e) {
+ copyProcess("#_hiddenCopyText5");
+ });
+
$('body').on("click", ".eps-guide-panel a:not(.eps-redmine-attachment-link)", function(e){
var link = e.target.getAttribute('href');

diff:
@@ -26,6 +26,8 @@ import java.util.Set;
import org.apache.commons.lang3.StringUtils;
import org.primefaces.model.TreeNode;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
import com.exactpro.sf.aml.generator.matrix.Column;
import com.exactpro.sf.aml.iomatrix.CSVDelimiter;
@@ -34,14 +36,26 @@ import com.exactpro.sf.common.messages.structures.IFieldStructure;
import com.exactpro.sf.help.helpmarshaller.HelpEntityType;
import com.exactpro.sf.help.helpmarshaller.jsoncontainers.FieldJsonContainer;
import com.exactpro.sf.testwebgui.BeanUtil;
+import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
+import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.ObjectWriter;
+import com.fasterxml.jackson.databind.node.ArrayNode;
+import com.fasterxml.jackson.databind.node.JsonNodeFactory;
+import com.fasterxml.jackson.databind.node.ObjectNode;
+import com.fasterxml.jackson.dataformat.yaml.YAMLFactory;
public class CopyingFormat {
+ private static final Logger logger = LoggerFactory.getLogger(HelpBean.class);
private static final CSVDelimiter DEFAULT_SEPARATOR = CSVDelimiter.TAB;
private static final String LINE_SEPARATOR = "\\n";
private static final String REF_POSTFIX = "_ref";
+ private static final ObjectWriter YAML_WRITER = new ObjectMapper(new YAMLFactory())
+ .writerFor(ObjectNode.class).withDefaultPrettyPrinter();
+
private final Set<String> header = new LinkedHashSet<>();
private final Set<String> newColumns = new LinkedHashSet<>();
private final Map<String, Map<String, String>> groups = new HashMap<>();
@@ -159,6 +173,27 @@ public class CopyingFormat {
return builder.toString();
}
+ public static String copyAllYamlStructure(TreeNode selectedNode) {
+ ObjectNode rootNode = new ObjectNode(JsonNodeFactory.instance);
+
+ createYamlStructure(selectedNode, rootNode);
+ try {
+
+ return YAML_WRITER
+ .writeValueAsString(rootNode)
+ .replace(" ", " ")
+ .replace("\n", "\\n ")
+ .replace("\t", " ")
+ .replace("'", "\\'")
+ .replace("\"", "\\\"")
+ .substring(5);
+ } catch (JsonProcessingException e) {
+ logger.warn("Can`t serialize to YAML", e);
+ }
+
+ return "";
+ }
+
private String createAllHeader() {
StringBuilder builder = new StringBuilder();
@@ -249,4 +284,40 @@ public class CopyingFormat {
public int getNewColumnsCount() {
return newColumns.size();
}
+
+ private static JsonNode getOrCreateNode(JsonNode node, String key, boolean getObjectNode){
+ if (node instanceof ObjectNode) {
+ ObjectNode obj = (ObjectNode) node;
+ return obj.has(key) ? obj.get(key) : getObjectNode ? obj.putObject(key) : obj.putArray(key);
+ } else if (node instanceof ArrayNode) {
+ ArrayNode arrayNode = (ArrayNode) node;
+ return getObjectNode ? arrayNode.addObject() : arrayNode.addArray();
+ }
+
+ return null;
+ }
+
+ private static void createYamlStructure(TreeNode selectedNode, JsonNode parent) {
+ if ("MESSAGE".equals(selectedNode.getType())) {
+ FieldJsonContainer container = (FieldJsonContainer) selectedNode.getData();
+
+ JsonNode message;
+
+ if (container.isCollection()) {
+ message = getOrCreateNode(parent, container.getName(), false);
+ message = getOrCreateNode(message, null, true);
+ } else {
+ message = getOrCreateNode(parent, container.getName(), true);
+ }
+
+ for (TreeNode child : selectedNode.getChildren()) {
+ createYamlStructure(child, message);
+ }
+ } else if ("FIELD".equals(selectedNode.getType())) {
+ FieldJsonContainer container = (FieldJsonContainer) selectedNode.getData();
+ if (parent instanceof ObjectNode) {
+ ((ObjectNode) parent).put(container.getName(), container.getJavaType());
+ }
+ }
+ }
}
\ No newline at end of file

diff:
@@ -343,12 +343,12 @@ public class HelpBean implements Serializable {
copyingFormat.format(selectedNode);
copyToClipboardJs(copyingFormat.copyNewColumns(), copyingFormat.copyJustHeader(), copyingFormat.copyToClipboard(),
- copyingFormat.copyAllStructure());
+ copyingFormat.copyAllStructure(), CopyingFormat.copyAllYamlStructure(selectedNode));
RequestContext.getCurrentInstance().update("copyDialog");
RequestContext.getCurrentInstance().execute("PF('copyDialog').show()");
}
- private void copyToClipboardJs(String toCopy1, String toCopy2, String toCopy3, String toCopy4) {
+ private void copyToClipboardJs(String toCopy1, String toCopy2, String toCopy3, String toCopy4, String toCopy5) {
StringBuilder builder = new StringBuilder();
@@ -360,6 +360,8 @@ public class HelpBean implements Serializable {
.append(toCopy3)
.append("', '")
.append(toCopy4)
+ .append("', '")
+ .append(toCopy5)
.append("')");
RequestContext.getCurrentInstance().execute(builder.toString());

license: Apache License 2.0

repo: exactpro/sailfish-core

original_message:
[RM] Feature #60892. [YAML] Please add possibility to copy message structure from Help in yaml format.

Change-Id: I8c97f0921b2f323bcd2811c391a8ca281116df3a


message: [RM] Support Exclude bin folder from zip report.

diff:
@@ -22,8 +22,10 @@ import java.io.FileOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Collection;
+import java.util.Collections;
import java.util.List;
import java.util.Set;
+import java.util.stream.Collectors;
import java.util.zip.Deflater;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;
@@ -52,17 +54,22 @@ public class ZipReport implements IScriptReport {
}
};
+ //TODO: In future it can configure from file or db
+ private static final Set<String> staticExcludeFiles = Collections.singleton("bin");
+
private long id;
private final String workFolder;
private final IWorkspaceDispatcher dispatcher;
private final TestScriptDescription testscriptDescription;
private final ReportOutputFormat reportOutputFormat;
+ private final Set<String> excludeFiles;
public ZipReport(String reportFolder, IWorkspaceDispatcher dispatcher, TestScriptDescription descr, ReportOutputFormat reportOutputFormat) {
this.dispatcher = dispatcher;
this.testscriptDescription = descr;
this.workFolder = reportFolder;
this.reportOutputFormat = reportOutputFormat;
+ this.excludeFiles = staticExcludeFiles.stream().map(str -> buildPath(workFolder, str)).collect(Collectors.toSet());
}
@Override
@@ -185,6 +192,11 @@ public class ZipReport implements IScriptReport {
String relativeFile = null;
for (String fileName : dispatcher.listFiles(FILE_FILTER, FolderType.REPORT, relativeFolder)) {
relativeFile = buildPath(relativeFolder, fileName);
+
+ if (excludeFiles.contains(relativeFile)) {
+ continue;
+ }
+
if (dispatcher.getFile(FolderType.REPORT, relativeFile).isDirectory()) {
zipFolder(dispatcher, zip, relativeFile, zipFolder);
} else {

original_message:
[RM] Support #61770. Exclude bin folder from zip report.

Change-Id: Ibffb6cc3d7c33dcd4aedd520da68560cab834d30


message: [RM] Feature Create abstract classes for mina server.

change_type: ADD

diff:
+/*******************************************************************************
+ * Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+ *
+ * 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 com.exactpro.sf.services.codecs;
+
+import com.exactpro.sf.common.util.ICommonSettings;
+import com.exactpro.sf.configuration.suri.SailfishURI;
+
+/**
+ * Interface for base codec settings
+ */
+public interface ICodecSettings extends ICommonSettings {
+
+ SailfishURI getDictionaryURI();
+ String getFilterValues();
+
+}

diff:
+/*******************************************************************************
+ * Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+ *
+ * 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 com.exactpro.sf.services.mina;
+
+import java.io.IOException;
+import java.net.InetSocketAddress;
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicReference;
+
+import org.apache.mina.core.service.IoHandler;
+import org.apache.mina.core.service.IoHandlerAdapter;
+import org.apache.mina.core.session.IoSession;
+import org.apache.mina.filter.codec.ProtocolCodecFilter;
+
+import com.exactpro.sf.common.codecs.AbstractCodec;
+import com.exactpro.sf.common.codecs.CodecFactory;
+import com.exactpro.sf.common.messages.IMessage;
+import com.exactpro.sf.services.AbstractInitiatorService;
+import com.exactpro.sf.services.IAcceptorService;
+import com.exactpro.sf.services.ISession;
+import com.exactpro.sf.services.ServiceException;
+import com.exactpro.sf.services.ServiceHandlerException;
+import com.exactpro.sf.services.WrapperNioSocketAcceptor;
+
+public abstract class AbstractMINATCPServer extends AbstractInitiatorService implements IoHandler, IAcceptorService {
+
+ protected final AtomicReference<MINAServerSession> serverSession;
+ protected final AtomicReference<WrapperNioSocketAcceptor> acceptor;
+ protected final Map<IoSession, MINASession> sessions;
+
+ public AbstractMINATCPServer() {
+ sessions = new ConcurrentHashMap<>();
+ serverSession = new AtomicReference<>();
+ acceptor = new AtomicReference<>();
+ }
+
+ @Override
+ public List<ISession> getSessions() {
+ return new ArrayList<>(sessions.values());
+ }
+
+ @Override
+ protected void internalStart() throws IOException {
+ WrapperNioSocketAcceptor tmpAcceptor = new WrapperNioSocketAcceptor(taskExecutor);
+
+ try {
+ configureAcceptor(tmpAcceptor);
+ } catch (Exception e) {
+ throw new ServiceException("Can`t configure server`s acceptor", e);
+ }
+
+ CodecFactory codecFactory = new CodecFactory(serviceContext, messageFactory, dictionary, getCodecClass(), getSettings());
+ tmpAcceptor.setHandler(this);
+ tmpAcceptor.getFilterChain().addLast("codec", new ProtocolCodecFilter(codecFactory));
+ tmpAcceptor.bind(new InetSocketAddress(getSettings().getHost(), getSettings().getPort()));
+
+ acceptor.set(tmpAcceptor);
+ serverSession.set(createServerSession());
+ }
+
+ @Override
+ public AbstractMINATCPServerSettings getSettings() {
+ return (AbstractMINATCPServerSettings) settings;
+ }
+
+ @Override
+ public MINAServerSession getSession() {
+ return serverSession.get();
+ }
+
+ @Override
+ public void connect(){
+ //TODO: Must throws exception and use 'accept' or other method for server.
+ //throw new UnsupportedOperationException("Server can't connect to anyone but it can accept incoming connection");
+ }
+
+ @Override
+ protected void disposeResources() {
+
+ MINAServerSession session = serverSession.getAndSet(null);
+
+ try {
+ if (session != null && session.isConnected()) {
+ session.close();
+ }
+ } catch (RuntimeException e) {
+ logger.warn("Server session didn`t close", e);
+ }
+
+ WrapperNioSocketAcceptor localAcceptor = acceptor.getAndSet(null);
+ try {
+ if (localAcceptor != null) {
+ localAcceptor.unbind();
+ localAcceptor.dispose();
+ }
+ } catch (RuntimeException e) {
+ logger.warn("Acceptor didn`t unbind or/and dispose", e);
+ }
+
+ super.disposeResources();
+ }
+
+ @Override
+ protected String getEndpointName() {
+ return String.format("Server for %s", serviceName);
+ }
+
+ @Override
+ public void sessionCreated(IoSession session) {
+ logger.debug("Session created. Session: " + session);
+ }
+
+ @Override
+ public void sessionOpened(IoSession session) throws ServiceHandlerException {
+ MINASession minaSession = createMINASession(session);
+ onSessionOpen(minaSession);
+ sessions.put(session, minaSession);
+ handler.sessionOpened(serverSession.get());
+
+ logger.debug("sessionOpened - {} session is opened", session);
+ }
+
+ @Override
+ public void sessionClosed(IoSession session) throws Exception {
+ onSessionClosed(sessions.remove(session));
+ handler.sessionClosed(serverSession.get());
+ }
+
+ @Override
+ public void exceptionCaught(IoSession session, Throwable cause) {
+ logger.error("Have error: {}", session, cause);
+ }
+
+ @Override
+ public void messageReceived(IoSession session, Object message) throws Exception {
+ if (message instanceof IMessage) {
+ IMessage iMessage = (IMessage) message;
+ onMessageReceived(iMessage);
+ processMessage(serverSession.get(), (IMessage) message);
+ }
+ }
+
+ @Override
+ public void messageSent(IoSession session, Object message) throws Exception {
+ if (message instanceof IMessage && session != null) {
+ IMessage iMessage = (IMessage)message;
+ onMessageSent(iMessage);
+ }
+ }
+
+ /**
+ * Method body is copied from {@link IoHandlerAdapter#inputClosed(IoSession)}
+ */
+ @Override
+ public void inputClosed(IoSession session) {
+ logger.debug("Session input closed: {}", session);
+ session.close(true);
+ }
+
+ protected MINAServerSession createServerSession() {
+ return new MINAServerSession(this);
+ }
+
+ protected MINASession createMINASession(IoSession session) {
+ return new MINASession(serviceName, session, loggingConfigurator);
+ }
+
+ protected abstract Class<? extends AbstractCodec> getCodecClass();
+ protected abstract void configureAcceptor(WrapperNioSocketAcceptor acceptor) throws Exception;
+ protected abstract void onSessionOpen(MINASession session);
+ protected abstract void processMessage(MINAServerSession session, IMessage message) throws Exception;
+ protected abstract void onSessionClosed(MINASession session) throws Exception;
+}

diff:
+/*******************************************************************************
+ * Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+ *
+ * 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 com.exactpro.sf.services.mina;
+
+import com.exactpro.sf.aml.Description;
+import com.exactpro.sf.configuration.suri.SailfishURI;
+import com.exactpro.sf.services.RequiredParam;
+import com.exactpro.sf.services.codecs.ICodecSettings;
+import com.exactpro.sf.services.util.ServiceUtil;
+
+public class AbstractMINATCPServerSettings extends AbstractMINASettings implements ICodecSettings {
+
+ @Description("Service`s hostname. Example: localhost")
+ @RequiredParam
+ private String host = "localhost";
+
+ @Description("Service`s port. Example: 8888")
+ @RequiredParam
+ private int port = 8888;
+
+ @Description("Fields value for which allow receive messages.\n"
+ + "Example: '1, A' or '"+ ServiceUtil.ALIAS_PREFIX + "dataA'")
+ private String filterValues;
+
+ public String getHost() {
+ return host;
+ }
+
+ public void setHost(String host) {
+ this.host = host;
+ }
+
+ public int getPort() {
+ return port;
+ }
+
+ public void setPort(int port) {
+ this.port = port;
+ }
+
+ @Override
+ public SailfishURI getDictionaryURI() {
+ return dictionaryName;
+ }
+
+ @Override
+ public String getFilterValues() {
+ return filterValues;
+ }
+
+ public void setFilterValues(String filterValues) {
+ this.filterValues = filterValues;
+ }
+}

diff:
+/*******************************************************************************
+ * Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+ *
+ * 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 com.exactpro.sf.services.mina;
+
+import java.util.ArrayList;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+
+import org.apache.mina.core.future.WriteFuture;
+import org.apache.mina.core.session.IoSession;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import com.exactpro.sf.common.messages.IMessage;
+import com.exactpro.sf.common.util.SendMessageFailedException;
+import com.exactpro.sf.services.ISession;
+import com.exactpro.sf.services.util.CloseSessionException;
+
+public class MINAServerSession implements ISession {
+
+ private static final int DEFAULT_TIMEOUT = 1000;
+ protected final Logger logger = LoggerFactory.getLogger(getClass().getName() + '@' + Integer.toHexString(hashCode()));
+
+ protected AbstractMINATCPServer server;
+
+ public MINAServerSession(AbstractMINATCPServer server) {
+ this.server = server;
+ logger.info("Server session created: {}", this);
+ }
+
+ @Override
+ public String getName() {
+ return server.getServiceName().toString();
+ }
+
+ protected Object prepareMessage(Object message) {
+ return message;
+ }
+
+ @Override
+ public IMessage send(Object message) throws InterruptedException {
+ return send(prepareMessage(message), DEFAULT_TIMEOUT);
+ }
+
+ public IMessage send(Object message, long timeout) throws InterruptedException {
+ List<WriteFuture> futures = new ArrayList<>();
+ Set<String> errorSending = new HashSet<>();
+
+ for (IoSession session : server.sessions.keySet()) {
+ futures.add(session.write(message));
+ }
+
+ long waitUntil = System.currentTimeMillis() + timeout;
+ for (WriteFuture future : futures) {
+ future.await(waitUntil - System.currentTimeMillis());
+ if (!future.isDone() || !future.isWritten()) {
+ errorSending.add(future.getSession().toString());
+ }
+ }
+
+ if (!errorSending.isEmpty()) {
+ throw new SendMessageFailedException(String.format("Message wasn't send during %d milliseconds. %s", timeout, String.join(System.lineSeparator(), errorSending)));
+ }
+
+ return message instanceof IMessage ? (IMessage) message : null;
+ }
+
+ @Override
+ public IMessage sendDirty(Object message) {
+ throw new UnsupportedOperationException("Dirty send is not supported");
+ }
+
+ @Override
+ public void close() {
+ Set<String> errorClosing = new HashSet<>();
+
+ server.sessions.forEach((ioSession, minaSession) -> {
+ try {
+ minaSession.close();
+ } catch (Exception e) {
+ errorClosing.add(ioSession.toString());
+ }
+ });
+
+ if (!errorClosing.isEmpty()) {
+ throw new CloseSessionException("Sessions send error on close." + String.join(System.lineSeparator(), errorClosing));
+ }
+ }
+
+ public boolean isConnected() {
+ return !isClosed();
+ }
+
+ @Override
+ public boolean isClosed() {
+ return server.sessions.isEmpty() || server.sessions.values().stream().allMatch(MINASession::isClosed);
+ }
+
+ @Override
+ public boolean isLoggedOn() {
+ return server.sessions.values().stream().anyMatch(MINASession::isLoggedOn);
+ }
+}

diff:
+/*******************************************************************************
+ * Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+ *
+ * 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 com.exactpro.sf.services.util;
+
+import com.exactpro.sf.services.ServiceException;
+
+public class CloseSessionException extends ServiceException {
+ private static final long serialVersionUID = -1336216893108240799L;
+
+ public CloseSessionException() {}
+
+ public CloseSessionException(String message) {
+ super(message);
+ }
+
+ public CloseSessionException(String message, Throwable cause) {
+ super(message, cause);
+ }
+
+ public CloseSessionException(Throwable cause) {
+ super(cause);
+ }
+}

diff:
******************************************************************************/
package com.exactpro.sf.services.itch;
-import com.exactpro.sf.common.util.ICommonSettings;
-import com.exactpro.sf.configuration.suri.SailfishURI;
import org.apache.commons.configuration.HierarchicalConfiguration;
-public class ITCHCodecSettings implements ICommonSettings {
+import com.exactpro.sf.configuration.suri.SailfishURI;
+import com.exactpro.sf.services.codecs.ICodecSettings;
+
+public class ITCHCodecSettings implements ICodecSettings {
private byte[] chunkDelimiter;
@@ -59,6 +60,7 @@ public class ITCHCodecSettings implements ICommonSettings {
this.chunkDelimiter = chunkDelimiter;
}
+ @Override
public String getFilterValues() {
return filterValues;
}
@@ -67,6 +69,7 @@ public class ITCHCodecSettings implements ICommonSettings {
this.filterValues = filterValues;
}
+ @Override
public SailfishURI getDictionaryURI() {
return dictionaryURI;
}

original_message:
[RM] Feature #61754. Create abstract classes for mina server.

Change-Id: Ib67cee9640f0715988e22845e48e32212a9b8e08


message: Bug Add polyfilling for chrome 65.

diff:
"dev": true
},
"core-js": {
- "version": "3.5.0",
- "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.5.0.tgz",
- "integrity": "sha512-Ifh3kj78gzQ7NAoJXeTu+XwzDld0QRIwjBLRqAMhuLhP3d2Av5wmgE9ycfnvK6NAEjTkQ1sDPeoEZAWO3Hx1Uw==",
- "dev": true
+ "version": "3.6.4",
+ "resolved": "https://registry.npmjs.org/core-js/-/core-js-3.6.4.tgz",
+ "integrity": "sha512-4paDGScNgZP2IXXilaffL9X7968RuvwlkK3xWtZRVqgd8SYNiVKRJvkFd1aqqEuPfN7E68ZHEp9hDj6lHj4Hyw=="
},
"core-util-is": {
"version": "1.0.2",
}
}
},
- "serialize-javascript": {
- "version": "2.1.2",
- "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-2.1.2.tgz",
- "integrity": "sha512-rs9OggEUF0V4jUSecXazOYsLfu7OGK2qIn3c7IPBiffz32XniEp/TX9Xmc9LQfK2nQ2QKHvZ2oygKUGU0lG4jQ==",
- "dev": true
- },
"serve-index": {
"version": "1.9.1",
"resolved": "https://registry.npmjs.org/serve-index/-/serve-index-1.9.1.tgz",

diff:
@@ -20,6 +20,8 @@ import App from "./components/App";
import ErrorBoundary from './components/util/ErrorBoundary';
import { Provider } from 'react-redux';
import { createAppStore } from './store/store';
+import 'core-js/features/array/flat-map';
+import 'core-js/features/array/flat';
ReactDOM.render(
<Provider store={createAppStore()}>

original_message:
Bug #62353. Add polyfilling for chrome 65.

Change-Id: Ic34e6d2b693d83e423b7f94bc9a7d043eadd389c


message: [RM] Feature Support for loading secondary message storages from extensions and plugins.

diff:
******************************************************************************/
package com.exactpro.sf.center.impl;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileNotFoundException;
+import java.io.IOException;
+import java.io.InputStream;
+import java.net.MalformedURLException;
+import java.net.URL;
+import java.net.URLClassLoader;
+import java.nio.file.Paths;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.ServiceConfigurationError;
+import java.util.ServiceLoader;
+import java.util.Set;
+
+import org.apache.log4j.LogManager;
+import org.apache.log4j.PropertyConfigurator;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
import com.exactpro.sf.center.IVersion;
import com.exactpro.sf.center.SFException;
import com.exactpro.sf.common.logging.CommonLoggers;
@@ -34,30 +58,6 @@ import com.exactpro.sf.scriptrunner.PreprocessorLoader;
import com.exactpro.sf.scriptrunner.ValidatorLoader;
import com.exactpro.sf.scriptrunner.services.PluginServiceLoader;
import com.exactpro.sf.util.DirectoryFilter;
-import org.apache.log4j.LogManager;
-import org.apache.log4j.PropertyConfigurator;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
-
-import java.io.File;
-import java.io.FileInputStream;
-import java.io.FileNotFoundException;
-import java.io.IOException;
-import java.io.InputStream;
-import java.net.MalformedURLException;
-import java.net.URL;
-import java.net.URLClassLoader;
-import java.nio.file.Paths;
-import java.util.ArrayList;
-import java.util.Collections;
-import java.util.HashMap;
-import java.util.HashSet;
-import java.util.Iterator;
-import java.util.List;
-import java.util.Map;
-import java.util.ServiceConfigurationError;
-import java.util.ServiceLoader;
-import java.util.Set;
public class PluginLoader {
@@ -105,6 +105,8 @@ public class PluginLoader {
private final IVersion coreVersion;
+ private final ILoadableManager messageStorageLoader;
+
private final List<IVersion> pluginVersions;
public PluginLoader(
@@ -121,7 +123,8 @@ public class PluginLoader {
ILoadableManager matrixConverterManager,
ILoadableManager statisticsReportsLoader,
PluginServiceLoader pluginServiceLoader,
- IVersion coreVersion) {
+ IVersion coreVersion,
+ ILoadableManager messageStorageLoader) {
if (wd == null) {
throw new NullPointerException("IWorkspaceDispatcher can't be null");
}
@@ -140,6 +143,7 @@ public class PluginLoader {
this.statisticsReportsLoader = statisticsReportsLoader;
this.pluginServiceLoader = pluginServiceLoader;
this.coreVersion = coreVersion;
+ this.messageStorageLoader = messageStorageLoader;
this.pluginVersions = new ArrayList<>();
}
@@ -500,6 +504,19 @@ public class PluginLoader {
logger.info("Ignore statistic reports [No StatisticsReportsLoader]. Plugin: {}", pluginPath);
}
+ //
+ // Load AbstractMessageStorage
+ //
+ if (messageStorageLoader != null) {
+ try {
+ messageStorageLoader.load(loadableContext);
+ } catch (Exception e) {
+ throw new EPSCommonException(e);
+ }
+ } else {
+ logger.info("Ignore message storages [No MessageStorageLoader]. Plugin: {}", pluginPath);
+ }
+
// Collect service description files
if(pluginServiceLoader != null) {
try {

diff:
@@ -22,7 +22,6 @@ import java.util.List;
import java.util.Map;
import java.util.Queue;
-import com.exactpro.sf.configuration.CleanupConfiguration;
import org.hibernate.Session;
import org.hibernate.SessionFactory;
import org.hibernate.cfg.Configuration;
@@ -42,6 +41,7 @@ import com.exactpro.sf.center.SFContextSettings;
import com.exactpro.sf.center.SFException;
import com.exactpro.sf.common.adapting.IAdapterManager;
import com.exactpro.sf.common.util.EPSCommonException;
+import com.exactpro.sf.configuration.CleanupConfiguration;
import com.exactpro.sf.configuration.DataManager;
import com.exactpro.sf.configuration.DefaultAdapterManager;
import com.exactpro.sf.configuration.DictionaryManager;
@@ -95,6 +95,9 @@ import com.exactpro.sf.storage.IServiceStorage;
import com.exactpro.sf.storage.IStorage;
import com.exactpro.sf.storage.ITestScriptStorage;
import com.exactpro.sf.storage.IVariableSetStorage;
+import com.exactpro.sf.storage.MessageStorageLoader;
+import com.exactpro.sf.storage.impl.AbstractMessageStorage;
+import com.exactpro.sf.storage.impl.BroadcastMessageStorage;
import com.exactpro.sf.storage.impl.DatabaseAuthStorage;
import com.exactpro.sf.storage.impl.DatabaseEnvironmentStorage;
import com.exactpro.sf.storage.impl.DatabaseMatrixStorage;
@@ -306,6 +309,7 @@ public class SFLocalContext implements ISFContext {
embeddedServices.add(updateService);
PluginServiceLoader pluginServiceLoader = new PluginServiceLoader();
+ MessageStorageLoader messageStorageLoader = new MessageStorageLoader();
// 5) Load core & plugins
PluginLoader pluginLoader = new PluginLoader(
@@ -322,13 +326,14 @@ public class SFLocalContext implements ISFContext {
matrixConverterLoader,
statisticsService,
pluginServiceLoader,
- version);
+ version,
+ messageStorageLoader);
LoadInfo loadInfo = pluginLoader.load();
loadInfo.appendClassPath(compilerClassPath);
compilerClassPath = loadInfo.getClassPath();
- messageStorage = createMessageStorage(envSettings, sessionFactory, dictionaryManager);
+ messageStorage = createMessageStorage(envSettings, sessionFactory, dictionaryManager, messageStorageLoader.getSecondaryMessageStorages(workspaceDispatcher, envSettings, dictionaryManager));
disposables.add(messageStorage);
serviceStorage = createServiceStorage(envSettings, sessionFactory, workspaceDispatcher, staticServiceManager, dictionaryManager, messageStorage);
@@ -398,15 +403,20 @@ public class SFLocalContext implements ISFContext {
}
}
- private IMessageStorage createMessageStorage(EnvironmentSettings envSettings, SessionFactory sessionFactory, DictionaryManager dictionaryManager) throws WorkspaceStructureException, FileNotFoundException {
+ private IMessageStorage createMessageStorage(EnvironmentSettings envSettings, SessionFactory sessionFactory, IDictionaryManager dictionaryManager, List<AbstractMessageStorage> secondary) throws WorkspaceStructureException, FileNotFoundException {
+ AbstractMessageStorage primaryMessageStorage;
switch (envSettings.getStorageType()) {
case DB:
- return new DatabaseMessageStorage(new DBStorageSettings(workspaceDispatcher, sessionFactory, dictionaryManager, envSettings));
+ primaryMessageStorage = new DatabaseMessageStorage(new DBStorageSettings(workspaceDispatcher, sessionFactory, dictionaryManager, envSettings));
+ break;
case FILE:
- return new FileMessageStorage(new BaseStorageSettings(workspaceDispatcher, dictionaryManager, envSettings));
+ primaryMessageStorage = new FileMessageStorage(new BaseStorageSettings(workspaceDispatcher, dictionaryManager, envSettings));
+ break;
default:
throw new EPSCommonException("Unsupported message storage type. Check your descriptor.xml file.");
}
+
+ return new BroadcastMessageStorage(primaryMessageStorage, secondary);
}
private IMatrixStorage createMatrixStorage(EnvironmentSettings envSettings, SessionFactory sessionFactory) {

diff:
@@ -193,7 +193,7 @@ public class ServiceFactory implements IServiceFactory {
IVersion coreVersion = new CoreVersion();
PluginLoader pluginLoader = new PluginLoader(wd, staticServiceManager, null,
dictionaryManager, null, null, null, dataManager, null, null, null, null,
- null, coreVersion);
+ null, coreVersion, null);
pluginLoader.load();
this.marshalManager = new ServiceMarshalManager(staticServiceManager, dictionaryManager);

diff:
@@ -306,7 +306,7 @@ public class HelpBuilder {
}
PluginLoader pluginLoader = new PluginLoader(wd, staticServiceManager, actionManager, dictionaryManager, preprocessorLoader, validatorLoader,
- adapterManager, dataManager, languageManager, matrixProviderHolder, matrixConverterManager, statisticsReportLoader, null, version);
+ adapterManager, dataManager, languageManager, matrixProviderHolder, matrixConverterManager, statisticsReportLoader, null, version, null);
pluginLoader.load();

diff:
******************************************************************************/
package com.exactpro.sf.storage;
-import com.exactpro.sf.configuration.DictionaryManager;
+import com.exactpro.sf.configuration.IDictionaryManager;
import com.exactpro.sf.configuration.workspace.IWorkspaceDispatcher;
import com.exactpro.sf.scriptrunner.EnvironmentSettings;
-import org.apache.commons.configuration.EnvironmentConfiguration;
public class BaseStorageSettings {
protected final IWorkspaceDispatcher workspaceDispatcher;
- protected final DictionaryManager dictionaryManager;
+ protected final IDictionaryManager dictionaryManager;
private final EnvironmentSettings environmentSettings;
- public BaseStorageSettings(IWorkspaceDispatcher workspaceDispatcher, DictionaryManager dictionaryManager, EnvironmentSettings environmentSettings) {
+ public BaseStorageSettings(IWorkspaceDispatcher workspaceDispatcher, IDictionaryManager dictionaryManager, EnvironmentSettings environmentSettings) {
this.workspaceDispatcher = workspaceDispatcher;
this.dictionaryManager = dictionaryManager;
this.environmentSettings = environmentSettings;
@@ -35,7 +34,7 @@ public class BaseStorageSettings {
return workspaceDispatcher;
}
- public DictionaryManager getDictionaryManager() {
+ public IDictionaryManager getDictionaryManager() {
return dictionaryManager;
}

diff:
******************************************************************************/
package com.exactpro.sf.storage;
-import com.exactpro.sf.configuration.DictionaryManager;
+import org.hibernate.SessionFactory;
+
+import com.exactpro.sf.configuration.IDictionaryManager;
import com.exactpro.sf.configuration.workspace.IWorkspaceDispatcher;
import com.exactpro.sf.scriptrunner.EnvironmentSettings;
-import org.apache.commons.configuration.EnvironmentConfiguration;
-import org.hibernate.SessionFactory;
public class DBStorageSettings extends BaseStorageSettings {
private final SessionFactory sessionFactory;
- public DBStorageSettings(IWorkspaceDispatcher workspaceDispatcher, SessionFactory sessionFactory, DictionaryManager dictionaryManager, EnvironmentSettings environmentSettings
+ public DBStorageSettings(IWorkspaceDispatcher workspaceDispatcher, SessionFactory sessionFactory, IDictionaryManager dictionaryManager, EnvironmentSettings environmentSettings
) {
super(workspaceDispatcher, dictionaryManager, environmentSettings);
this.sessionFactory = sessionFactory;

diff:
+/*******************************************************************************
+ * Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+ *
+ * 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 com.exactpro.sf.storage;
+
+import com.exactpro.sf.configuration.IDictionaryManager;
+import com.exactpro.sf.configuration.workspace.IWorkspaceDispatcher;
+import com.exactpro.sf.scriptrunner.EnvironmentSettings;
+import com.exactpro.sf.storage.impl.AbstractMessageStorage;
+
+public interface IAbstractMessageStorageFactory {
+
+ /**
+ * Create secondary message storage
+ * @param environmentSettings Global settings of Sailfish
+ */
+ AbstractMessageStorage createMessageStorage(IWorkspaceDispatcher dispatcher, EnvironmentSettings environmentSettings, IDictionaryManager dictionaryManager);
+
+}

diff:
+/*******************************************************************************
+ * Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+ *
+ * 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 com.exactpro.sf.storage;
+
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+import java.util.ServiceLoader;
+import java.util.stream.Collectors;
+
+import com.exactpro.sf.configuration.IDictionaryManager;
+import com.exactpro.sf.configuration.ILoadableManager;
+import com.exactpro.sf.configuration.ILoadableManagerContext;
+import com.exactpro.sf.configuration.workspace.IWorkspaceDispatcher;
+import com.exactpro.sf.scriptrunner.EnvironmentSettings;
+import com.exactpro.sf.storage.impl.AbstractMessageStorage;
+
+public class MessageStorageLoader implements ILoadableManager {
+
+ private final List<IAbstractMessageStorageFactory> secondaryMessageStorageFactory;
+
+ public MessageStorageLoader() {
+ this.secondaryMessageStorageFactory = new LinkedList<>();
+ }
+
+ @Override
+ public void load(ILoadableManagerContext context) throws Exception {
+ for (IAbstractMessageStorageFactory factory : ServiceLoader.load(IAbstractMessageStorageFactory.class, context.getClassLoaders()[0])) {
+ secondaryMessageStorageFactory.add(factory);
+ }
+ }
+
+ public List<AbstractMessageStorage> getSecondaryMessageStorages(IWorkspaceDispatcher dispatcher, EnvironmentSettings environmentSettings, IDictionaryManager dictionaryManager){
+ return secondaryMessageStorageFactory
+ .stream()
+ .map(factory -> factory.createMessageStorage(dispatcher, environmentSettings, dictionaryManager))
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+ //Unused
+ @Override
+ public void finalize(ILoadableManagerContext context) throws Exception {}
+}

diff:
@@ -30,7 +30,7 @@ import com.exactpro.sf.common.messages.IMessageFactory;
import com.exactpro.sf.common.messages.MessageUtil;
import com.exactpro.sf.common.messages.structures.IDictionaryStructure;
import com.exactpro.sf.common.messages.structures.IMessageStructure;
-import com.exactpro.sf.configuration.DictionaryManager;
+import com.exactpro.sf.configuration.IDictionaryManager;
import com.exactpro.sf.configuration.suri.SailfishURI;
import com.exactpro.sf.storage.IMessageStorage;
import com.exactpro.sf.storage.ScriptRun;
@@ -39,9 +39,9 @@ import com.exactpro.sf.storage.util.JsonMessageConverter;
public abstract class AbstractMessageStorage implements IMessageStorage {
protected final Logger logger = LoggerFactory.getLogger(getClass().getName() + "@" + Integer.toHexString(hashCode()));
- protected final DictionaryManager dictionaryManager;
+ protected final IDictionaryManager dictionaryManager;
- public AbstractMessageStorage(DictionaryManager dictionaryManager) {
+ public AbstractMessageStorage(IDictionaryManager dictionaryManager) {
this.dictionaryManager = Objects.requireNonNull(dictionaryManager, "Dictionary manager cannot be null");
}

diff:
+/*******************************************************************************
+ * Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+ *
+ * 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 com.exactpro.sf.storage.impl;
+
+import java.util.Arrays;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.Objects;
+
+import com.exactpro.sf.common.messages.IHumanMessage;
+import com.exactpro.sf.common.messages.IMessage;
+import com.exactpro.sf.common.util.EPSCommonException;
+import com.exactpro.sf.storage.MessageFilter;
+import com.exactpro.sf.storage.MessageRow;
+import com.exactpro.sf.storage.ScriptRun;
+
+public class BroadcastMessageStorage extends AbstractMessageStorage {
+
+ private static final String ERROR_MESSAGE_ON_STORE = "Can`t store message to all storage";
+ private static final String ERROR_MESSAGE_ON_CLEAR = "Can`t clear message from all storage";
+ private static final String ERROR_MESSAGE_ON_DISPOSE = "Can`t dispose all storages";
+
+ private final AbstractMessageStorage primaryStorage;
+ private final List<AbstractMessageStorage> writableStorages;
+
+ public BroadcastMessageStorage(AbstractMessageStorage primaryStorage, List<AbstractMessageStorage> secondaryStorages) {
+ super(primaryStorage.dictionaryManager);
+ this.primaryStorage = Objects.requireNonNull(primaryStorage, "Primary storage can`t be null");
+ writableStorages = secondaryStorages == null ? new LinkedList<>() : secondaryStorages;
+ writableStorages.add(this.primaryStorage);
+ }
+
+ public BroadcastMessageStorage(AbstractMessageStorage primaryStorage, AbstractMessageStorage... secondaryStorages) {
+ this(primaryStorage, secondaryStorages.length > 0 ? Arrays.asList(secondaryStorages) : null);
+ }
+
+ @Override
+ public ScriptRun openScriptRun(String name, String description) {
+ return primaryStorage.openScriptRun(name, description);
+ }
+
+ @Override
+ public void closeScriptRun(ScriptRun scriptRun) {
+ primaryStorage.closeScriptRun(scriptRun);
+ }
+
+ @Override
+ protected void storeMessage(IMessage message, IHumanMessage humanMessage, String jsonMessage) {
+ EPSCommonException exception = null;
+ for (AbstractMessageStorage storage : writableStorages) {
+ try {
+ storage.storeMessage(message, humanMessage, jsonMessage);
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ if (exception == null) {
+ exception = new EPSCommonException(ERROR_MESSAGE_ON_STORE);
+ }
+ exception.addSuppressed(e);
+ }
+ }
+
+ if (exception != null && exception.getSuppressed().length > 0) {
+ throw exception;
+ }
+ }
+
+ @Override
+ public Iterable<MessageRow> getMessages(int count, MessageFilter filter) {
+ return primaryStorage.getMessages(count, filter);
+ }
+
+ @Override
+ public Iterable<MessageRow> getMessages(int offset, int count, String where) {
+ return primaryStorage.getMessages(offset, count, where);
+ }
+
+ @Override
+ public void clear() {
+ EPSCommonException exception = null;
+ for (AbstractMessageStorage storage : writableStorages) {
+ try {
+ storage.clear();
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ if (exception == null) {
+ exception = new EPSCommonException(ERROR_MESSAGE_ON_CLEAR);
+ }
+ exception.addSuppressed(e);
+ }
+ }
+
+ if (exception != null && exception.getSuppressed().length > 0) {
+ throw exception;
+ }
+ }
+
+ @Override
+ public void dispose() {
+
+ EPSCommonException exception = null;
+
+ for (AbstractMessageStorage storage : writableStorages) {
+ try {
+ storage.dispose();
+ } catch (Exception e) {
+ logger.error(e.getMessage(), e);
+ if (exception == null) {
+ exception = new EPSCommonException(ERROR_MESSAGE_ON_DISPOSE);
+ }
+ exception.addSuppressed(e);
+ }
+ }
+
+ if (exception != null && exception.getSuppressed().length > 0) {
+ throw exception;
+ }
+ }
+}

diff:
+/*******************************************************************************
+ * Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+ *
+ * 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 com.exactpro.sf.storage.impl;
+
+import java.time.Instant;
+import java.util.Collections;
+
+import com.exactpro.sf.common.messages.IHumanMessage;
+import com.exactpro.sf.common.messages.IMessage;
+import com.exactpro.sf.configuration.DictionaryManager;
+import com.exactpro.sf.storage.MessageFilter;
+import com.exactpro.sf.storage.MessageRow;
+
+public class FakeAbstractMessageStorage extends AbstractMessageStorage {
+
+ public FakeAbstractMessageStorage(DictionaryManager dictionaryManager) {
+ super(dictionaryManager);
+ }
+
+ @Override
+ protected void storeMessage(IMessage message, IHumanMessage humanMessage, String jsonMessage) {
+ logger.trace("Message {} skipped by service settings 'persistMessageToStorage'.", message.getName());
+ }
+
+ @Override
+ public Iterable<MessageRow> getMessages(int count, MessageFilter filter) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public Iterable<MessageRow> getMessages(int offset, int count, String where) {
+ return Collections.emptyList();
+ }
+
+ @Override
+ public void clear() {
+
+ }
+
+ @Override
+ public void dispose() {
+
+ }
+
+ @Override
+ public void removeMessages(Instant olderThan) {
+
+ }
+
+ @Override
+ public void removeMessages(String serviceID) {
+
+ }
+
+
+}

original_message:
[RM] Feature #62422. Support for loading secondary message storages from extensions and plugins.

Change-Id: I6a481308cf9c3b4d6e22b0acd08e113beec67185


message: Feature Use test case 'order' property instead of 'id' in the query parameters

diff:
@@ -36,6 +36,7 @@ import StateActionType from '../actions/stateActions';
import { loadTestCase } from '../thunks/loadTestCase';
import SplashScreen from './SplashScreen';
import Report from '../models/Report';
+import { TestCaseMetadata } from '../models/TestcaseMetadata';
const REPORT_FILE_PATH = 'index.html';
@@ -53,7 +54,7 @@ interface AppDispatchProps {
selectAction: (actionId: number) => any;
selectMessage: (messageId: number) => any;
loadReport: () => any;
- loadTestCase: (testCasePath: string) => any;
+ loadTestCase: (metadata: TestCaseMetadata) => any;
}
interface AppProps extends AppStateProps, AppDispatchProps {}
@@ -86,12 +87,12 @@ class AppBase extends React.Component<AppProps, {}> {
}
handleSharedUrl() {
- const testCaseId = this.searchParams.get(TEST_CASE_PARAM_KEY),
+ const testCaseOrder = this.searchParams.get(TEST_CASE_PARAM_KEY),
actionId = this.searchParams.get(ACTION_PARAM_KEY),
msgId = this.searchParams.get(MESSAGE_PARAM_KEY);
- if (testCaseId != null) {
- this.selectTestCaseById(testCaseId)
+ if (testCaseOrder != null) {
+ this.selectTestCaseByOrder(Number.parseInt(testCaseOrder))
}
if (msgId !== null && !isNaN(Number(msgId))) {
@@ -115,6 +116,21 @@ class AppBase extends React.Component<AppProps, {}> {
}
}
+ selectTestCaseByOrder(testCaseOrder: number) {
+ if (!this.props.report) {
+ console.error("Trying to handle shared url before report load.");
+ return;
+ }
+
+ const testCaseMetadata = this.props.report.metadata.find(metadata => metadata.order === testCaseOrder);
+
+ if (testCaseMetadata) {
+ this.props.loadTestCase(testCaseMetadata);
+ } else {
+ console.warn("Can't handle shared url: Test Case with this id not found");
+ }
+ }
+
selectTestCaseById(testCaseId: string) {
if (!this.props.report) {
console.error("Trying to handle shared url before report load.");
@@ -124,7 +140,7 @@ class AppBase extends React.Component<AppProps, {}> {
const testCaseMetadata = this.props.report.metadata.find(metadata => metadata.id === testCaseId);
if (testCaseMetadata) {
- this.props.loadTestCase(testCaseMetadata.jsonpFileName);
+ this.props.loadTestCase(testCaseMetadata);
} else {
console.warn("Can't handle shared url: Test Case with this id not found");
}
@@ -174,7 +190,7 @@ const App = connect(
fetchToken: () => dispatch(fetchToken()),
setSubmittedMlData: (data: SubmittedData[]) => dispatch(setSubmittedMlData(data)),
loadReport: () => dispatch(initReport()),
- loadTestCase: (testCasePath: string) => dispatch(loadTestCase(testCasePath))
+ loadTestCase: (metadata: TestCaseMetadata) => dispatch(loadTestCase(metadata.jsonpFileName))
})
)(AppBase);

diff:
@@ -68,7 +68,7 @@ function hadnleStateUpdate(prevState: AppState, nextState: AppState, action: Sta
function getNextSearchParams(searchParams: URLSearchParams, prevState: AppState, nextState: AppState): URLSearchParams {
if (prevState.selected.testCase != nextState.selected.testCase) {
if (nextState.selected.testCase) {
- searchParams.set(TEST_CASE_PARAM_KEY, nextState.selected.testCase.id);
+ searchParams.set(TEST_CASE_PARAM_KEY, nextState.selected.testCase.order.toString());
} else {
searchParams.delete(TEST_CASE_PARAM_KEY);
}

original_message:
Feature #63122. Use test case 'order' property instead of 'id' in the query parameters

Change-Id: Ida13071a06637f1ae2b1ecd5b0ef3a28ab4293ba


message: Feature Add an ability to completely hide a panel with the panel divider

diff:
import * as React from 'react';
import '../styles/splitter.scss';
import { createStyleSelector } from '../helpers/styleCreators';
+import { isEqual } from '../helpers/object';
+import ResizeObserver from "resize-observer-polyfill";
/**
* Props for splitter component
*/
export interface SplitViewProps {
- /**
- * Min width for both panels
- */
- minPanelWidth: number;
/**
* (optional) Resize event handler, recieves left and right widths in px.
@@ -38,38 +36,117 @@ export interface SplitViewProps {
children: React.ReactNodeArray;
}
+enum PreviewAreaStep {
+ p0 = 'w0', p25 = 'w25', p50 = 'w50', p75 = 'w75', p100 = 'w100'
+}
+
+class PreviewArea {
+ private _leftArea = PreviewAreaStep.p50
+ private _rightArea = PreviewAreaStep.p50
+
+ get leftArea() {
+ return this._leftArea
+ }
+
+ get rightArea() {
+ return this._rightArea
+ }
+
+ set leftArea(step: PreviewAreaStep) {
+ this._leftArea = step
+ switch (step) {
+ case PreviewAreaStep.p0:
+ this._rightArea = PreviewAreaStep.p100;
+ break;
+ case PreviewAreaStep.p25:
+ this._rightArea = PreviewAreaStep.p75;
+ break;
+ case PreviewAreaStep.p50:
+ this._rightArea = PreviewAreaStep.p50;
+ break;
+ case PreviewAreaStep.p75:
+ this._rightArea = PreviewAreaStep.p25;
+ break;
+ case PreviewAreaStep.p100:
+ this._rightArea = PreviewAreaStep.p0;
+ break;
+ }
+ }
+}
+
+enum ClosedPanel {
+ Left = 1, Right = 2
+}
+
interface SplitState {
- leftPanelWidth: number;
+ splitterPosition: number;
isDragging: boolean;
+ closedPanel: ClosedPanel | null;
+ preview: PreviewArea;
+ steps: number[];
+ closestStep?: number;
}
export class SplitView extends React.Component<SplitViewProps, SplitState> {
private rightPanel = React.createRef<HTMLDivElement>();
private leftPanel = React.createRef<HTMLDivElement>();
+ private leftPreviewArea = React.createRef<HTMLDivElement>();
+ private rightPreviewArea = React.createRef<HTMLDivElement>();
private root = React.createRef<HTMLDivElement>();
private splitter = React.createRef<HTMLDivElement>();
private lastPosition : number = 0;
+ private resizeObserver: ResizeObserver;
+ private rootWidth: number = 0;
+
constructor(props) {
super(props);
this.state = {
- leftPanelWidth: 0,
- isDragging: false
+ splitterPosition: 0,
+ closedPanel: null,
+ isDragging: false,
+ preview: new PreviewArea(),
+ steps: []
};
}
componentDidMount() {
if (this.root.current) {
+ this.resizeObserver = new ResizeObserver(elements => {
+ const nextRootWidth = elements[0]?.contentRect.width;
+
+ if (nextRootWidth != this.rootWidth) {
this.setState({
...this.state,
- leftPanelWidth: this.root.current.offsetWidth / 2
+ ...this.calculateSteps(),
+ splitterPosition: this.root.current.offsetWidth / 2,
});
}
+
+ this.rootWidth = nextRootWidth
+ })
+ this.resizeObserver.observe(this.root.current)
+ }
+ }
+
+ componentWillUnmount() {
+ this.resizeObserver.unobserve(this.root.current)
+ }
+
+ private calculateSteps() {
+ const w25 = this.root.current.offsetWidth / 4,
+ w50 = w25 * 2,
+ w75 = w25 * 3,
+ w100 = this.root.current.offsetWidth;
+ return {
+ steps: [0, w25, w50, w75, w100],
+ closesetStep: w50
+ }
}
componentDidUpdate(prevProps: SplitViewProps, prevState: SplitState) {
- if (this.state.leftPanelWidth != prevState.leftPanelWidth && this.props.resizeHandler) {
+ if (this.state.splitterPosition != prevState.splitterPosition && this.props.resizeHandler) {
this.props.resizeHandler(this.leftPanel.current.offsetWidth, this.rightPanel.current.offsetWidth);
}
}
@@ -79,7 +156,12 @@ export class SplitView extends React.Component<SplitViewProps, SplitState> {
this.root.current.addEventListener("mouseleave", this.onMouseUpOrLeave)
this.root.current.addEventListener("mouseup", this.onMouseUpOrLeave)
this.lastPosition = e.clientX;
+ if (this.state.closedPanel === ClosedPanel.Left) {
+ this.splitter.current.style.left = '0px';
+ }
+ else {
this.splitter.current.style.left = this.leftPanel.current.scrollWidth.toString() + 'px';
+ }
this.setState({
...this.state,
@@ -108,49 +190,133 @@ export class SplitView extends React.Component<SplitViewProps, SplitState> {
const diff = newPosition - this.lastPosition;
this.splitter.current.style.left = (this.getElementStylePropAsNumber(this.splitter.current, 'left') + diff).toString() + 'px';
this.lastPosition = newPosition;
+ const [w0, w25, w50, w75, w100] = this.state.steps,
+ preview = new PreviewArea();
+ const closestStep = this.state.steps.map(step => (
+ {
+ step,
+ diff: Math.abs(step - newPosition)
+ })
+ ).sort((a,b) => a.diff - b.diff)[0].step
+ switch (closestStep) {
+ case w0:
+ preview.leftArea = PreviewAreaStep.p0;
+ break;
+ case w25:
+ preview.leftArea = PreviewAreaStep.p25;
+ break;
+ case w50:
+ preview.leftArea = PreviewAreaStep.p50;
+ break;
+ case w75:
+ preview.leftArea = PreviewAreaStep.p75;
+ break;
+ case w100:
+ preview.leftArea = PreviewAreaStep.p100;
+ break;
+ }
+
+ if (!isEqual(this.state.preview, preview)) {
+ this.setState({
+ preview: preview,
+ closestStep: closestStep
+ })
+ }
}
stopDragging() {
+ const { closedPanel, closestStep, steps } = this.state
+ let closePanelUpdate = {} as {splitterPosition: number, closedPanel: ClosedPanel}
+ const leftMinVisibleWidth = steps[1],
+ rightMinVisibleWidth = steps[steps.length - 2],
+ leftHideStep = steps[0],
+ rightHideStep = steps[steps.length - 1];
+
+ if (closedPanel) {
+ let targetStep : number
+ if (this.state.closedPanel === ClosedPanel.Left) {
+ targetStep = Math.max(closestStep, leftMinVisibleWidth)
+ closePanelUpdate = {
+ splitterPosition: targetStep,
+ closedPanel: null
+ }
+ }
+ if (this.state.closedPanel === ClosedPanel.Right) {
+ targetStep = Math.min(closestStep, rightMinVisibleWidth)
+ closePanelUpdate = {
+ splitterPosition: targetStep,
+ closedPanel: null
+ }
+ }
+ }
+ else {
+ if (closestStep == rightHideStep) {
+ this.state.closedPanel !== ClosedPanel.Right && (closePanelUpdate = {
+ splitterPosition: rightHideStep,
+ closedPanel: ClosedPanel.Right
+ })
+ }
+ else if (closestStep == leftHideStep) {
+ this.state.closedPanel !== ClosedPanel.Left && (closePanelUpdate = {
+ splitterPosition: leftHideStep,
+ closedPanel: ClosedPanel.Left
+ })
+ }
+ }
+
this.setState({
...this.state,
isDragging: false,
- leftPanelWidth: this.getElementStylePropAsNumber(this.splitter.current, 'left')
+ splitterPosition: this.state.closestStep,
+ ...closePanelUpdate
});
this.splitter.current.style.left = null;
}
render() {
- const { children, minPanelWidth } = this.props,
- { leftPanelWidth, isDragging } = this.state;
+ const { children } = this.props,
+ { splitterPosition, isDragging, closedPanel } = this.state;
let rootStyle: React.CSSProperties = null;
// during first render we can't calculate panel's widths
if (this.root.current) {
let splitterWidth = this.splitter.current.offsetWidth,
- rightWidth = (this.root.current.offsetWidth - leftPanelWidth) - splitterWidth / 2,
- leftWidth = leftPanelWidth - splitterWidth / 2;
+ rightWidth = (this.root.current.offsetWidth - splitterPosition) - splitterWidth / 2,
+ leftWidth = splitterPosition - splitterWidth / 2;
+
+ if (closedPanel === ClosedPanel.Left) {
+ rightWidth += leftWidth;
+ leftWidth = 0;
+ }
- if (rightWidth < minPanelWidth) {
- rightWidth = minPanelWidth;
- leftWidth = this.root.current.offsetWidth - rightWidth - splitterWidth;
- } else if (leftWidth < minPanelWidth) {
- leftWidth = minPanelWidth;
- rightWidth = this.root.current.offsetWidth - leftWidth - splitterWidth;
+ if (closedPanel === ClosedPanel.Right) {
+ leftWidth += rightWidth;
+ rightWidth = 0;
}
rootStyle = { gridTemplateColumns: `${leftWidth}px ${splitterWidth}px ${rightWidth}px` };
}
- const leftClassName = createStyleSelector("splitter-pane-left", isDragging ? "dragging" : null),
- rightClassName = createStyleSelector("splitter-pane-right", isDragging ? "dragging" : null),
+ const leftClassName = createStyleSelector("splitter-pane-left", isDragging ? "dragging" : null,
+ closedPanel === ClosedPanel.Left? "hidden": null),
+ rightClassName = createStyleSelector("splitter-pane-right", isDragging ? "dragging" : null,
+ closedPanel === ClosedPanel.Right? "hidden": null),
splitterClassName = createStyleSelector("splitter-bar", isDragging ? "dragging" : null),
- rootClassName = createStyleSelector("splitter", isDragging ? "dragging" : null);
+ rootClassName = createStyleSelector("splitter", isDragging ? "dragging" : null),
+ leftPreviewAreaClassName = createStyleSelector("splitter-preview-area-left",
+ isDragging? null: "hidden",
+ this.state.preview.leftArea),
+ rightPreviewAreaClassName = createStyleSelector("splitter-preview-area-right",
+ isDragging? null: "hidden",
+ this.state.preview.rightArea);
return (

+

+

{children[0]}

diff:
@@ -29,8 +29,6 @@ import { stopWatchingTestCase } from '../thunks/loadTestCase';
import { ThunkDispatch } from 'redux-thunk';
import StateActionType from '../actions/stateActions';
-const MIN_PANEL_WIDTH = 600;
-
interface TestCaseLayoutProps {
testCaseLoadingProgress: number;
isConnectionError: boolean;
@@ -51,7 +49,7 @@ const TestCaseLayout = ({ testCaseLoadingProgress, isConnectionError, stopWatchi
<Header/>
</div>
<div className="layout__body">
- <SplitView minPanelWidth={MIN_PANEL_WIDTH}>
+ <SplitView>
<LeftPanel/>
<RightPanel/>
</SplitView>

diff:
grid-template-columns: calc(50% - 13px) 25px calc(50% - 13px);
grid-template-areas: "left-pane splitter-bar right-pane";
position: relative;
+ overflow: hidden;
&.dragging {
@include unselectable;
&.dragging {
grid-area: initial;
position: absolute;
+ z-index: 2;
}
&:hover:after {
}
}
+ &-preview-area-left {
+ left: 0%;
+ }
+
+ &-preview-area-right {
+ right: 0%;
+ }
+
+ &-preview-area-left, &-preview-area-right {
+ margin: 12px;
+ height: 100%;
+ position: absolute;
+ z-index: 1;
+ background-color: #000000;
+ border-radius: 5px;
+ opacity: 0.1;
+
+ &.hidden {
+ visibility: hidden;
+ }
+
+ &.w0 {
+ width: 0;
+ }
+
+ &.w25 {
+ width: calc(25% - 17px);
+ }
+
+ &.w50 {
+ width: calc(50% - 17px);
+ }
+
+ &.w75 {
+ width: calc(75% - 17px);
+ }
+
+ &.w100 {
+ width: calc(100% - 40px);
+ }
+ }
+
&-pane {
&-left {
grid-area: left-pane;
&.dragging {
filter: opacity(0.3);
}
+
+ &.hidden {
+ visibility: hidden;
+ }
}
}
}
\ No newline at end of file

original_message:
Feature #62909. Add an ability to completely hide a panel with the panel divider

Change-Id: I9b5c069d38397f2fea0218f0af15c4d23ebfa23f


message: Feature Ignore empty parameters table

diff:
@@ -29,6 +29,8 @@ import {ActionMlUploadButton} from "../ActionMlUploadButton";
import { StatusType } from '../../models/Status';
import { stopPropagationHandler } from '../../helpers/react';
import { isVerification } from "../../models/Verification";
+import Message from '../../models/Message';
+import { isMessage } from '../../models/Message';
interface CardProps {
action: Action;
@@ -77,6 +79,11 @@ export function ActionCard({ action, children, isSelected, onSelect, isRoot, isT
const elapsedTime = getSecondsPeriod(startTime, finishTime);
+ const isEmptyParameters = parameters === null,
+ isSingleEmptyMessage = !isEmptyParameters && parameters.length == 1 && parameters[0].name === "Message" &&
+ parameters[0].value === "" && parameters[0].subParameters.length == 0,
+ showParams = !isEmptyParameters && !isSingleEmptyMessage
+
return (
<div className={rootClassName}
onClick={stopPropagationHandler(onSelect, action)}
@@ -160,7 +167,7 @@ export function ActionCard({ action, children, isSelected, onSelect, isRoot, isT
}

-

+ {showParams &&

<SearchExpandablePanel
searchKeyPrefix={keyForAction(id, 'parameters')}
stateKey={keyForAction(id, 'parameters')}>
@@ -171,7 +178,7 @@ export function ActionCard({ action, children, isSelected, onSelect, isRoot, isT
params={parameters}
name={name} />
</SearchExpandablePanel>
-

+
}
{action.isTruncated ? (

original_message:
Feature #63302. Ignore empty parameters table

Change-Id: I31a6d9f5a48e55bc664e20496e923a51693190f4


message: Feature make sub-action names clickable

diff:
@@ -171,7 +171,11 @@ export function ActionCard({ action, children, isSelected, onSelect, isRoot, isT
<SearchExpandablePanel
searchKeyPrefix={keyForAction(id, 'parameters')}
stateKey={keyForAction(id, 'parameters')}>
-
Input parameters

+ {toggleExpand => (
+

+ Input parameters

+ )}
<ParamsTable
actionId={action.id}
stateKey={action.id + '-input-params-nodes'}
@@ -196,7 +200,11 @@ export function ActionCard({ action, children, isSelected, onSelect, isRoot, isT

<RecoverableExpandablePanel
stateKey={keyForAction(id, 'status')}>
-
Status

+ {toggleExpand => (
+

+ Status

+ )}
<RecoverableExceptionChain
exception={action.status.cause}
stateKey={`${keyForAction(id, 'status')}-exception`} />

diff:
@@ -20,6 +20,7 @@ import Action from '../../models/Action';
import { RecoverableExpandablePanel } from '../ExpandablePanel';
import { CustomTable } from './CustomTable';
import { keyForUserTable } from '../../helpers/keys';
+import { stopPropagationHandler } from '../../helpers/react';
interface UserTableCardProps {
table: UserTable;
@@ -31,7 +32,10 @@ function UserTableCard({ parent, table }: UserTableCardProps) {

<RecoverableExpandablePanel
stateKey={keyForUserTable(table, parent)}>
-
{table.name || "Custom table"}

+ {toggleExpand => (
+
{table.name || "Custom table"}

+ )}
<CustomTable
content={table.content} />
</RecoverableExpandablePanel>

diff:
&__item-title {
font-weight: bold;
font-size: 18px;
+
+ &:hover {
+ @include unselectable;
+ cursor: pointer;
+ }
}
&__input-params {
color: var(--status-primary-color);
font-weight: bold;
font-size: 17px;
+
+ &:hover {
+ cursor: pointer;
+ }
}
&__verification-table {

original_message:
Feature #63201. make sub-action names clickable

Change-Id: I58d6d0e6a456809f10198df1d6a3ed244307bc69


message: Feature  Change report summary layout

diff:
@@ -31,14 +31,21 @@ export const AlertCard = ({
const rootClassName = createBemBlock(
"alert",
type,
- ), typeClassName = createBemElement(
+ ), iconClassName = createBemElement(
'alert',
- 'type',
+ 'icon',
type
);
return (

+

+

+ {message}
+

+

+

+

column:
{column}
@@ -47,11 +54,6 @@ export const AlertCard = ({
lines:
{lines}

-

- {type}
-

-

- {message}

)

diff:
import * as React from 'react';
import { connect } from 'react-redux';
import AppState from '../state/models/AppState';
-import { getSecondsPeriod, formatTime } from '../helpers/date';
-import { TestCaseMetadata, isTestCaseMetadata } from '../models/TestcaseMetadata';
+import { getSecondsPeriod } from '../helpers/date';
+import { isTestCaseMetadata, TestCaseMetadata } from '../models/TestcaseMetadata';
import "../styles/report.scss";
-import { StatusType, statusValues } from '../models/Status';
+import { StatusType } from '../models/Status';
import HeatmapScrollbar from './heatmap/HeatmapScrollbar';
import { testCasesHeatmap } from '../helpers/heatmapCreator';
import { createStyleSelector, createBemElement } from '../helpers/styleCreators';
@@ -28,7 +28,9 @@ import { ExceptionChain } from './ExceptionChain';
import TestCaseCard from './TestCaseCard';
import { FailedTestCaseCarousel } from './FailedTestCaseCarousel';
import ReportState from '../state/models/ReportState';
-import AlertCard from './AlertCard';
+import { ToggleButton } from './ToggleButton';
+import RunInformation from './RunInformation';
+import ReportSummary from './ReportSummary';
import Tag from './Tag';
const OLD_REPORT_PATH = 'report.html';
@@ -40,8 +42,11 @@ interface StateProps {
interface Props extends StateProps { }
+enum Panel {ReportSummary, RunInfo}
+
interface State {
showKnownBugs: boolean;
+ panel: Panel;
}
export class ReportLayoutBase extends React.Component<Props, State> {
@@ -50,7 +55,8 @@ export class ReportLayoutBase extends React.Component<Props, State> {
super(props);
this.state = {
- showKnownBugs: true
+ showKnownBugs: true,
+ panel: Panel.ReportSummary
};
}
@@ -60,7 +66,25 @@ export class ReportLayoutBase extends React.Component<Props, State> {
})
}
+ getPanel(): React.ReactNode {
+ return this.state.panel === Panel.RunInfo &&
+ <RunInformation report={this.props.report} /> ||
+ this.state.panel === Panel.ReportSummary &&
+ <ReportSummary report={this.props.report} />;
+ }
+
+ changePanel(panel: Panel) {
+ this.setState({
+ panel: panel
+ })
+ }
+
render() {
+ const changePanel= (panel: Panel) => {
+ return () => {
+ this.changePanel(panel)
+ }
+ }
const { report, selectedTestCaseId } = this.props,
{ showKnownBugs } = this.state;
@@ -70,9 +94,7 @@ export class ReportLayoutBase extends React.Component<Props, State> {
knownBugsClass = showKnownBugs ? "active" : "enabled",
failedTestCasesEnabled = filteredMetadata.some(({status}) => status.status === StatusType.FAILED),
failedTcTitleClass = createBemElement('report', 'title', failedTestCasesEnabled ? 'failed': 'disabled'),
- isLive = report.finishTime == null,
- alerts = report.alerts || [],
- tags = report.tags || [];
+ isLive = report.finishTime == null;
const knownBugsButton = (
knownBugsPresent ?
@@ -89,9 +111,6 @@ export class ReportLayoutBase extends React.Component<Props, State> {
)
);
- const executionTime = getSecondsPeriod(report.startTime, report.finishTime),
- plugins = report.plugins ? Object.entries(report.plugins) : [];
-
return (

@@ -120,80 +139,29 @@ export class ReportLayoutBase extends React.Component<Props, State> {

-

+

+

Version

{report.version}

-

-

-
Host

-
{report.hostName}

-

-

-
User

-
{report.userName}

-

-

-

-
ScriptRun ID

-
{report.scriptRunId}

-

-

-
Report Date

-
{formatTime(report.startTime)}

-

-

-
Execution time

-
{executionTime}

-

-

-

-
Test Cases

-
{report.metadata.length}

-

- {
- statusValues.map(statusValue => this.renderStatusInfo(statusValue, filteredMetadata))
- }
-

- {
- plugins.length ?
- (
-

-
Plugins

-

- {plugins.map(([name, version], index) =>
{name}: {version}
)}
-

- ) : (
-

-
No plugins

+

+ <ToggleButton
+ textClass="report-summary__button_text"
+ isToggled={this.state.panel == Panel.ReportSummary}
+ onClick={changePanel(Panel.ReportSummary)}
+ text="Report Summary"/>
+ <ToggleButton
+ textClass="report-summary__button_text"
+ isToggled={this.state.panel == Panel.RunInfo}
+ onClick={changePanel(Panel.RunInfo)}
+ text="Run Information"/>

- )
- }
-

{
- tags.length ?
- (
-

-

-
Tags

-
{tags.length}

-

-

- {tags.map((tag, i) => <Tag tag={tag} key={i}/>)}
-

-

- ) : (
-

-
No tags

-

- )
+ this.getPanel()
}

-

- {alerts.map((alert, i) => <AlertCard {...alert} key={i}/>)}
-

@@ -222,26 +190,6 @@ export class ReportLayoutBase extends React.Component<Props, State> {

)
}
-
- private renderStatusInfo(status: StatusType, metadata: TestCaseMetadata[]): React.ReactNode {
- const testCasesCount = metadata.filter(metadata => metadata.status.status == status).length,
- valueClassName = createStyleSelector(
- "report-summary__element-value",
- "bold",
- status.toLowerCase()
- );
-
- if (!testCasesCount) {
- return null;
- }
-
- return (
-

-
{status.toUpperCase()}

-
{testCasesCount}

-

- )
- }
}
const ReportLayout = connect(

diff:
+/******************************************************************************
+* Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+*
+* 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.
+******************************************************************************/
+import React from 'react';
+import ReportState from '../state/models/ReportState';
+import AlertCard from './AlertCard';
+import Tag from './Tag';
+import { statusValues, StatusType } from '../models/Status';
+import { TestCaseMetadata, isTestCaseMetadata } from '../models/TestcaseMetadata';
+import { createStyleSelector } from '../helpers/styleCreators';
+
+export interface ReportSummaryProps {
+ report: ReportState;
+}
+
+function renderStatusInfo(status: StatusType, metadata: TestCaseMetadata[]): React.ReactNode {
+ const testCasesCount = metadata.filter(metadata => metadata.status.status == status).length,
+ valueClassName = createStyleSelector(
+ "report-summary__element-value",
+ "bold",
+ status
+ );
+
+ if (!testCasesCount) {
+ return null;
+ }
+
+ return (
+

+
{status.toUpperCase()}

+
{testCasesCount}

+

+ )
+}
+
+const ReportSummary = ({ report }: ReportSummaryProps) => {
+ const filteredMetadata = report.metadata.filter(isTestCaseMetadata);
+ const tags = report.tags || [];
+ const alerts = report.alerts || [];
+ return (
+ <React.Fragment>
+

+

+
Test Cases

+
{report.metadata.length}

+

+ {
+ statusValues.map(statusValue => renderStatusInfo(statusValue, filteredMetadata))
+ }
+

+

+ {
+ tags.length ?
+ (
+

+

+
Tags

+
{tags.length}

+

+

+ {tags.map(tag => <Tag tag={tag}/>)}
+

+

+ ) : (
+

+
No tags

+

+ )
+ }
+

+

+
Errors and Warnings

+
{alerts.length}

+

+

+ {alerts.map((alert, i) => <AlertCard {...alert} key={i}/>)}
+

+ </React.Fragment>)
+}
+
+export default ReportSummary;
\ No newline at end of file

diff:
+/******************************************************************************
+* Copyright 2009-2020 Exactpro (Exactpro Systems Limited)
+*
+* 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.
+******************************************************************************/
+import React from 'react';
+import ReportState from '../state/models/ReportState';
+import { formatTime, getSecondsPeriod } from '../helpers/date';
+
+export interface RunInformationProps {
+ report: ReportState;
+}
+
+const RunInformation = ({ report }: RunInformationProps) => {
+ const executionTime = getSecondsPeriod(report.startTime, report.finishTime);
+ const plugins = report.plugins ? Object.entries(report.plugins) : [];
+ return (<React.Fragment>
+

+

+
Host

+
{report.hostName}

+

+

+
User

+
{report.userName}

+

+

+

+

+

+
ScriptRun ID

+
{report.scriptRunId}

+

+

+
Report Date

+
{formatTime(report.startTime)}

+

+

+
Execution time

+
{executionTime}

+

+

+

+ {
+ plugins.length ?
+ (
+

+
Plugins

+

+ {plugins.map(([name, version], index) =>
{name}: {version}
)}
+

+

+ ) : (
+

+
No plugins

+

+ )
+ }
+ </React.Fragment>)
+}
+
+export default RunInformation;
\ No newline at end of file

diff:
@@ -25,9 +25,10 @@ interface Props {
text: string;
title?: string;
theme?: string;
+ textClass?: string;
}
-export function ToggleButton({ onClick = () => {}, isToggled = false, text, theme = 'default', isDisabled = false, title = '' }: Props) {
+export function ToggleButton({ onClick = () => {}, isToggled = false, text, textClass, theme = 'default', isDisabled = false, title = '' }: Props) {
const className = createStyleSelector(
"toggle-button",
theme,
@@ -38,7 +39,7 @@ export function ToggleButton({ onClick = () => {}, isToggled = false, text, them
return (

 !isDisabled && onClick(text)} title={title}>

-
{text}

+
{text}

)

diff:
******************************************************************************/
@import "./common/vars";
+@import "./common/mixins";
.alert {
- border: 2px solid;
- padding: 7px 15px;
+ border-top: 2px solid;
font-size: 13px;
margin-bottom: 6px;
- border-radius: 5px;
- display: grid;
- grid-template-columns: 1fr 100px;
- grid-template-rows: 1fr 1fr;
- grid-template-areas: "column type"
- "lines type"
- "message message";
+ min-height: 65px;
+ display: flex;
+ flex-direction: column;
+ padding: 0;
+
+ &__row {
+ margin-top: 10px;
+ justify-content: space-between;
+ display: flex;
+ }
&__column {
- grid-area: column;
+ width: 60%;
&-title {
color: $secondaryTextColor;
}
}
&__lines {
- grid-area: lines;
+ text-align: end;
+ width: 40%;
&-title {
color: $secondaryTextColor;
}
}
&__type {
- grid-area: type;
text-align: right;
font-weight: bold;
font-size: 15px;
}
&__message {
- grid-area: message;
+ width: 70%;
+ margin-right: 0px;
}
&.warning {
border-color: $failedBorderColorSecondary;
color: $failedSecondaryTextColor;
}
+
+ &__icon {
+ &.error {
+ @include icon(url(../../resources/icons/report-summary-error.svg), 20px, 20px);
+ }
+ &.warning {
+ @include icon(url(../../resources/icons/report-summary-warning.svg), 20px, 20px);
+ }
+ }
}
\ No newline at end of file

diff:
border-radius: 5px;
padding: 15px;
margin-bottom: 12px;
+ height: 100%;
}
&__logo {
justify-content: space-between;
}
+ &__vertical_element {
+ width: 100%;
+ padding: 15px 0;
+ display: flex;
+ align-items: flex-end;
+ flex-direction: column;
+ }
+
+ &__button_text {
+ font-family: Open Sans;
+ font-style: normal;
+ font-weight: 600;
+ font-size: 12px;
+ line-height: 22px;
+ white-space: nowrap;
+
+ display: flex;
+ align-items: center;
+ text-align: center
+ }
+
&__element-title {
font-size: 13px;
color: $secondaryTextColor;
&__tags-header {
margin-bottom: 5px;
}
+
+ &__block {
+ margin-top: 30px;
+ }
+
+ &__alerts {
+ margin-top: 10px;
+ }
+
+
+}
+
+.run-information {
+
+ &__element {
+ width: 100%;
+ padding: 5px 0;
+ display: flex;
+ align-items: flex-start;
+ justify-content: space-between;
+ }
+
+ &__element-title {
+ font-size: 13px;
+ color: $secondaryTextColor;
+ }
+
+ &__element-value {
+ @include status-text;
+ font-size: 15px;
+ color: $primaryTextColor;
+
+ &.bold {
+ font-weight: bold;
+ }
+ }
}
.tc-card {

original_message:
Feature #62433.  Change report summary layout

Change-Id: I6ae2dd440ea2d8390fc0a0699c0f54ca66023c2f


message: Feature Show status tab by default when no actions are present

diff:
@@ -56,11 +56,6 @@ class LeftPanelBase extends React.Component<Props> {
render() {
const { panel, isCheckpointsEnabled, statusEnabled, currentCheckpointHandler } = this.props;
- const cpRootClass = createStyleSelector(
- "layout-control",
- isCheckpointsEnabled ? null : "disabled"
- );
-
return (

@@ -133,12 +128,16 @@ class LeftPanelBase extends React.Component<Props> {
}
export const LeftPanel = connect(
- (state: AppState) => ({
- panel: state.view.leftPanel,
+ (state: AppState) => {
+ const isActionsEmpty = !(state.selected.testCase.actions && state.selected.testCase.actions.length > 0)
+ return ({
selectedCheckpointId: state.selected.checkpointActionId,
checkpointActions: getCheckpointActions(state),
- statusEnabled: !!state.selected.testCase.status.cause
- }),
+ statusEnabled: !!state.selected.testCase.status.cause,
+ isActionsEmpty: isActionsEmpty,
+ panel: state.view.leftPanel == Panel.Actions && isActionsEmpty? Panel.Status: state.view.leftPanel
+ })
+ },
dispatch => ({
panelSelectHandler: (panel: Panel) => dispatch(setLeftPane(panel)),
setSelectedCheckpoint: (checkpointAciton: Action) => dispatch(selectCheckpointAction(checkpointAciton))

original_message:
Feature #63304. Show status tab by default when no actions are present

Change-Id: Iad4981fbf94547562ea4202bef8acd25e6a4fda8