diff --git a/README.md b/README.md index 04afc8fa..56172bcf 100644 --- a/README.md +++ b/README.md @@ -1,4 +1,4 @@ -# th2 check1 (3.8.0) +# th2 check1 (3.9.0) ## Overview @@ -6,7 +6,7 @@ The component is responsible for verifying decoded messages. Communication with the script takes place via grpc, messages are received via rabbit mq. -The component subscribes to queues specified in the configuration and accumulates messages from them in a FIFO buffer. +The component subscribes to the queues specified in the configuration and accumulates messages from them in a FIFO buffer. The buffer size is configurable, and it is set to 1000 by default. @@ -17,7 +17,53 @@ When the component starts, the grpc server also starts and then the component wa Available requests are described in [this repository](https://gitlab.exactpro.com/vivarium/th2/th2-core-open-source/th2-grpc-check1) - CheckSequenceRuleRequest - prefilters the messages and verify all of them by filter. Order checking configured from request. + Depending on the request and check1 configuration **SilenceCheckRule** can be added after the CheckSequenceRule. + It verifies that there were not any messages matching the pre-filter in the original request. + It awaits for realtime timeout that is equal to clean-up timeout. + Reports about unexpected messages only after the timeout is exceeded. Reports nothing if any task is added to the chain. - CheckRuleRequest - get message filter from request and check it with messages in the cache or await specified time in case of empty cache or message absence. +- NoMessageCheckRequest - prefilters messages and verifies that no other messages have been received. + +## Request parameters + +### Common + +#### Required + +* **parent_event_id** - all events generated by the rule will be attached to that event +* **connectivity_id** (the `session_alias` inside `connectivity_id` must not be empty) + +#### Optional + +* **direction** - the direction of messages to be checked by rule. By default, it is _FIRST_ +* **chain_id** - the id to connect rules (rule starts checking after the previous one in the chain). Considers **connectivity_id** +* **description** - the description that will be added to the root event produced by the rule +* **timeout** - defines the allowed timeout for messages matching by real time. If not set the default value from check1 settings will be taken +* **message_timeout** - defines the allowed timeout for messages matching by the time they were received. +* **checkpoint** (must be set if `message_timeout` is used and no valid `chain_id` has been provided) + +### CheckRuleRequest + +#### Required + +* **root_filter** or **filter** (please note, that the `filter` parameter is deprecated and will be removed in the future releases) + +### CheckSequenceRuleRequest + +#### Required + +* **root_message_filters** or **message_filters** with at least one filter + (please note, that the `message_filters` parameter is deprecated and will be removed in the future releases) + +#### Optional +* **pre_filter** - pre-filtering for messages. Only messages that passed the filter will be checked by the main filters. +* **check_order** - enables order validation in message's collections +* **silence_check** - enables auto-check for messages that match the `pre_filter` after the rule has finished + +### NoMessageCheckRequest + +#### Optional +* **pre_filter** pre-filtering for messages that should not be received. ## Quick start General view of the component will look like this: @@ -35,6 +81,10 @@ spec: cleanup-older-than: '60' cleanup-time-unit: 'SECONDS' max-event-batch-content-size: '1048576' + rule-execution-timeout: '5000' + auto-silence-check-after-sequence-rule: false + time-precision: 'PT0.000000001S' + decimal-precision: '0.00001' type: th2-check1 pins: - name: server @@ -67,7 +117,12 @@ This block describes the configuration for check1. "message-cache-size": 1000, "cleanup-older-than": 60, "cleanup-time-unit": "SECONDS", - "max-event-batch-content-size": "1048576" + "max-event-batch-content-size": "1048576", + "rule-execution-timeout": 5000, + "auto-silence-check-after-sequence-rule": false, + "time-precision": "PT0.000000001S", + "decimal-precision": 0.00001, + "check-null-value-as-empty": false } ``` @@ -86,11 +141,27 @@ The time unit for _cleanup-older-than_ setting. The available values are MILLIS, #### max-event-batch-content-size The max size in bytes of summary events content in a batch defined in _max-event-batch-content-size_ setting. _The default value is set to 1048576_ +#### rule-execution-timeout +The default rule execution timeout is used if no rule timeout is specified. Measured in milliseconds + +#### auto-silence-check-after-sequence-rule +Defines a default behavior for creating CheckSequenceRule if `silence_check` parameter is not specified in the request. The default value is `false` + +#### time-precision +The time precision is used to compare two time values. It is based on the `ISO-8601` duration format `PnDTnHnMn.nS` with days considered to be exactly 24 hours. Additional information can be found [here](https://docs.oracle.com/en/java/javase/11/docs/api/java.base/java/time/Duration.html#parse(java.lang.CharSequence)) + +#### decimal-precision +The decimal precision is used to compare the value of two numbers. Can be specified in number or string format. For example `0.0001`, `0.125`, `125E-3` + +#### check-null-value-as-empty +`check-null-value-as-empty` is used for `EMPTY` and `NOT_EMPTY` operations to check if `NULL_VALUE` value is empty. By default, this parameter is set to `false`. For example, if the `checkNullValueAsEmpty` parameter is: ++ `true`, then `NULL_VALUE` is equal to `EMPTY`, otherwise `NULL_VALUE` is equal to `NOT_EMPTY` + ## Required pins The Check1 component has two types of pin: * gRPC server pin: it allows other components to connect via `com.exactpro.th2.check1.grpc.Check1Service` class. -* MQ pin: it is used for listening to parsed messages. You can link several sources with different directions and session alises to it. +* MQ pin: it is used for listening to parsed messages. You can link several sources with different directions and session aliases to it. ```yaml apiVersion: th2.exactpro.com/v1 @@ -121,6 +192,31 @@ The `th2_check1_active_tasks_number` metric separate rules with label `rule_type ## Release Notes +### 3.9.0 + +#### Added: ++ Implemented NoMessageCheck rule task. Updated CheckRule and CheckSequence rule tasks ++ New configuration parameter `rule-execution-timeout` which is used if the user has not specified a timeout for the rule execution ++ Auto silence check after the CheckSequenceRule. ++ `auto-silence-check-after-sequence-rule` to setup a default behavior for CheckSequenceRule ++ New configuration parameter `time-precision` which is used if the user has not specified a time precision ++ New configuration parameter `decimal-precision` which is used if the user has not specified a number precision ++ New parameter `hint` for verification event which is used to display the reason for the failed field comparison. For example the type mismatch of the compared values ++ New configuration parameter `check-null-value-as-empty` witch us used to configure the `EMPTY` and `NOT_EMPTY` operations + +#### Changed: ++ Migrated `common` version from `3.26.4` to `3.31.3` ++ Migrated `grpc-check1` version from `3.4.2` to `3.5.1` ++ Migrated `sailfish-utils` version from `3.9.1` to `3.12.2` + + Fixed conversion of `null` values + + Add marker for `null` values to determine whether the field was set with `null` value or was not set at all + + Allow checking for exact `null` value in message + + Added new parameter `checkNullValueAsEmpty` in the `FilterSettings` ++ Corrected verification entry when the `null` value and string `"null"` looked the same for the expected value ++ Fixed setting of the `failUnexpected` parameter while converting a message filter ++ Migrated `sailfish-core` version to `3.2.1752` + + Fix incorrect matching in repeating groups with reordered messages + ### 3.8.0 #### Added: diff --git a/build.gradle b/build.gradle index 4de63bf9..8549860f 100644 --- a/build.gradle +++ b/build.gradle @@ -1,6 +1,6 @@ plugins { id 'com.palantir.docker' version '0.25.0' - id 'org.jetbrains.kotlin.jvm' version '1.3.72' + id 'org.jetbrains.kotlin.jvm' version '1.5.30' id "io.github.gradle-nexus.publish-plugin" version "1.0.0" id 'signing' id 'java-library' @@ -10,7 +10,7 @@ plugins { ext { sharedDir = file("${project.rootDir}/shared") - sailfishVersion = '3.2.1050' + sailfishVersion = '3.2.1752' } group = 'com.exactpro.th2' @@ -42,6 +42,7 @@ repositories { name 'Sonatype_releases' url 'https://s01.oss.sonatype.org/content/repositories/releases/' } + mavenLocal() configurations.all { resolutionStrategy.cacheChangingModulesFor 0, 'seconds' @@ -165,21 +166,20 @@ signing { dependencies { api platform('com.exactpro.th2:bom:3.0.0') - implementation 'com.exactpro.th2:grpc-check1:3.4.2' - implementation 'com.exactpro.th2:common:3.26.4' - implementation 'com.exactpro.th2:sailfish-utils:3.9.1' + implementation 'com.exactpro.th2:grpc-check1:3.5.1' + implementation 'com.exactpro.th2:common:3.31.3' + implementation 'com.exactpro.th2:sailfish-utils:3.12.2' implementation "org.slf4j:slf4j-log4j12" implementation "org.slf4j:slf4j-api" implementation "com.exactpro.sf:sailfish-core:${sailfishVersion}" - implementation "com.datastax.cassandra:cassandra-driver-core" //FIXME: Migrate to another library for UUID - implementation "io.reactivex.rxjava2:rxjava:2.2.19" // https://github.com/salesforce/reactive-grpc/issues/202 implementation('io.prometheus:simpleclient') { because('metrics from messages and rules') } + implementation 'org.jetbrains.kotlin:kotlin-reflect' testImplementation 'org.junit.jupiter:junit-jupiter:5.6.2' testImplementation 'org.jetbrains.kotlin:kotlin-test-junit' @@ -206,4 +206,4 @@ dockerPrepare { docker { copySpec.from(tarTree("$buildDir/distributions/${applicationName}.tar")) -} \ No newline at end of file +} diff --git a/gradle.properties b/gradle.properties index a340e47d..8cd48de2 100644 --- a/gradle.properties +++ b/gradle.properties @@ -1,4 +1,4 @@ -release_version = 3.8.0 +release_version = 3.9.0 description = 'th2 check1 box' diff --git a/src/main/java/com/exactpro/th2/check1/Check1Handler.java b/src/main/java/com/exactpro/th2/check1/Check1Handler.java index 3a517c3c..ae2bbd1f 100644 --- a/src/main/java/com/exactpro/th2/check1/Check1Handler.java +++ b/src/main/java/com/exactpro/th2/check1/Check1Handler.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2021 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 @@ -14,19 +14,22 @@ import static com.exactpro.th2.common.grpc.RequestStatus.Status.ERROR; import static com.exactpro.th2.common.grpc.RequestStatus.Status.SUCCESS; -import static com.google.protobuf.TextFormat.shortDebugString; +import com.exactpro.th2.check1.utils.ProtoMessageUtilsKt; +import com.exactpro.th2.common.message.MessageUtils; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import com.exactpro.th2.check1.grpc.ChainID; +import com.exactpro.th2.check1.grpc.CheckpointResponse; import com.exactpro.th2.check1.grpc.Check1Grpc.Check1ImplBase; import com.exactpro.th2.check1.grpc.CheckRuleRequest; import com.exactpro.th2.check1.grpc.CheckRuleResponse; import com.exactpro.th2.check1.grpc.CheckSequenceRuleRequest; import com.exactpro.th2.check1.grpc.CheckSequenceRuleResponse; import com.exactpro.th2.check1.grpc.CheckpointRequest; -import com.exactpro.th2.check1.grpc.CheckpointResponse; +import com.exactpro.th2.check1.grpc.NoMessageCheckResponse; +import com.exactpro.th2.check1.grpc.NoMessageCheckRequest; import com.exactpro.th2.common.grpc.RequestStatus; import io.grpc.stub.StreamObserver; @@ -45,20 +48,20 @@ public void createCheckpoint(CheckpointRequest request, StreamObserver responseObserver) { try { if (logger.isInfoEnabled()) { - logger.info("Submit CheckRule request: " + shortDebugString(request)); + logger.info("Submit CheckRule request: " + MessageUtils.toJson(request)); } CheckRuleResponse.Builder response = CheckRuleResponse.newBuilder(); @@ -77,7 +80,7 @@ public void submitCheckRule(CheckRuleRequest request, StreamObserver responseObserver) { + try { + if (logger.isInfoEnabled()) { + logger.info("Submitting no message check rule for request '{}' started", MessageUtils.toJson(request)); + } + + NoMessageCheckResponse.Builder response = NoMessageCheckResponse.newBuilder(); + try { + ChainID chainID = collectorService.verifyNoMessageCheck(request); + response.setChainId(chainID) + .setStatus(RequestStatus.newBuilder().setStatus(SUCCESS)); + } catch (Exception e) { + if (logger.isErrorEnabled()) { + logger.error("No message check rule task for request '{}' isn't submitted", MessageUtils.toJson(request), e); + } + RequestStatus status = RequestStatus.newBuilder() + .setStatus(ERROR) + .setMessage("No message check rule rejected by internal process: " + e.getMessage()) + .build(); + response.setStatus(status); + } + responseObserver.onNext(response.build()); + } catch (Exception e) { + if (logger.isErrorEnabled()) { + logger.error("No message check rule failed. Request " + MessageUtils.toJson(request), e); + } + responseObserver.onNext(NoMessageCheckResponse.newBuilder() + .setStatus(RequestStatus.newBuilder().setStatus(ERROR).setMessage("No message check rule failed. See the logs.").build()) + .build()); + } finally { responseObserver.onCompleted(); } } diff --git a/src/main/java/com/exactpro/th2/check1/Checkpoint.java b/src/main/java/com/exactpro/th2/check1/Checkpoint.java deleted file mode 100644 index d94e5ef9..00000000 --- a/src/main/java/com/exactpro/th2/check1/Checkpoint.java +++ /dev/null @@ -1,124 +0,0 @@ -/* - * Copyright 2020-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.th2.check1; - -import com.exactpro.th2.common.grpc.Checkpoint.DirectionCheckpoint; -import com.exactpro.th2.common.grpc.Direction; - -import java.util.Collections; -import java.util.HashMap; -import java.util.Map; -import static com.datastax.driver.core.utils.UUIDs.timeBased; - -import org.apache.commons.lang3.builder.EqualsBuilder; -import org.apache.commons.lang3.builder.HashCodeBuilder; -import org.apache.commons.lang3.builder.ToStringBuilder; - -public class Checkpoint { - - private final String id; - private final Map sessionKeyToSequence; - - private Checkpoint(String id, Map sessionKeyToSequence) { - this.id = id; - this.sessionKeyToSequence = Map.copyOf(sessionKeyToSequence); - } - - public Checkpoint(Map sessionKeyToSequence) { - this(timeBased().toString(), sessionKeyToSequence); - } - - public String getId() { - return id; - } - - public boolean contains(SessionKey sessionKey) { - return sessionKeyToSequence.containsKey(sessionKey); - } - - public long getSequence(SessionKey sessionKey) { - return sessionKeyToSequence.get(sessionKey); - } - - public com.exactpro.th2.common.grpc.Checkpoint convert() { - Map intermediateMap = new HashMap<>(); - - for (Map.Entry entry : sessionKeyToSequence.entrySet()) { - SessionKey sessionKey = entry.getKey(); - intermediateMap.computeIfAbsent(sessionKey.getSessionAlias(), alias -> DirectionCheckpoint.newBuilder()) - .putDirectionToSequence(sessionKey.getDirection().getNumber(), entry.getValue()); - } - - var checkpointBuilder = com.exactpro.th2.common.grpc.Checkpoint.newBuilder() - .setId(id); - for (Map.Entry entry : intermediateMap.entrySet()) { - checkpointBuilder.putSessionAliasToDirectionCheckpoint(entry.getKey(), - entry.getValue().build()); - } - return checkpointBuilder.build(); - } - - public Map asMap() { - return Collections.unmodifiableMap(sessionKeyToSequence); - } - - @Override - public String toString() { - return new ToStringBuilder(this) - .append("id", id) - .append("sessionKeyToSequence", sessionKeyToSequence) - .toString(); - } - - @Override - public boolean equals(Object obj) { - if (this == obj) { - return true; - } - - if (obj == null || getClass() != obj.getClass()) { - return false; - } - - Checkpoint other = (Checkpoint)obj; - - return new EqualsBuilder() - .append(id, other.id) - .append(sessionKeyToSequence, other.sessionKeyToSequence) - .isEquals(); - } - - @Override - public int hashCode() { - return new HashCodeBuilder(17, 37) - .append(id) - .append(sessionKeyToSequence) - .toHashCode(); - } - - public static Checkpoint convert(com.exactpro.th2.common.grpc.Checkpoint protoCheckpoint) { - Map sessionKeyToSequence = new HashMap<>(); - for (Map.Entry sessionAliasDirectionCheckpointEntry : protoCheckpoint.getSessionAliasToDirectionCheckpointMap().entrySet()) { - String sessionAlias = sessionAliasDirectionCheckpointEntry.getKey(); - DirectionCheckpoint directionCheckpoint = sessionAliasDirectionCheckpointEntry.getValue(); - for (Map.Entry directionSequenceEntry : directionCheckpoint.getDirectionToSequenceMap().entrySet()) { - SessionKey sessionKey = new SessionKey(sessionAlias, Direction.forNumber(directionSequenceEntry.getKey())); - sessionKeyToSequence.put(sessionKey, directionSequenceEntry.getValue()); - } - } - return new Checkpoint(protoCheckpoint.getId(), sessionKeyToSequence); - } -} diff --git a/src/main/java/com/exactpro/th2/check1/configuration/Check1Configuration.java b/src/main/java/com/exactpro/th2/check1/configuration/Check1Configuration.java index f43653e1..469907b6 100644 --- a/src/main/java/com/exactpro/th2/check1/configuration/Check1Configuration.java +++ b/src/main/java/com/exactpro/th2/check1/configuration/Check1Configuration.java @@ -13,9 +13,13 @@ package com.exactpro.th2.check1.configuration; -import java.time.temporal.ChronoUnit; - import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.annotation.JsonPropertyDescription; +import com.fasterxml.jackson.databind.annotation.JsonDeserialize; +import com.fasterxml.jackson.datatype.jsr310.deser.DurationDeserializer; + +import java.time.Duration; +import java.time.temporal.ChronoUnit; public class Check1Configuration { @@ -31,6 +35,24 @@ public class Check1Configuration { @JsonProperty(value="cleanup-time-unit", defaultValue = "SECONDS") private ChronoUnit cleanupTimeUnit = ChronoUnit.SECONDS; + @JsonProperty(value="rule-execution-timeout", defaultValue = "5000") + private long ruleExecutionTimeout = 5000L; + + @JsonProperty("auto-silence-check-after-sequence-rule") + @JsonPropertyDescription("The default behavior in case the SequenceCheckRule does not have silenceCheck parameter specified") + private boolean autoSilenceCheckAfterSequenceRule; + + @JsonProperty(value="decimal-precision", defaultValue = "0.00001") + private double decimalPrecision = 0.00001; + + @JsonProperty(value="time-precision", defaultValue = "PT0.000000001S") + @JsonDeserialize(using = DurationDeserializer.class) + @JsonPropertyDescription("The default time precision value uses java duration format") + private Duration timePrecision = Duration.parse("PT0.000000001S"); + + @JsonProperty(value = "check-null-value-as-empty") + private boolean checkNullValueAsEmpty = false; + public int getMessageCacheSize() { return messageCacheSize; } @@ -46,4 +68,24 @@ public int getMaxEventBatchContentSize() { public ChronoUnit getCleanupTimeUnit() { return cleanupTimeUnit; } + + public long getRuleExecutionTimeout() { + return ruleExecutionTimeout; + } + + public boolean isAutoSilenceCheckAfterSequenceRule() { + return autoSilenceCheckAfterSequenceRule; + } + + public double getDecimalPrecision() { + return decimalPrecision; + } + + public Duration getTimePrecision() { + return timePrecision; + } + + public boolean isCheckNullValueAsEmpty() { + return checkNullValueAsEmpty; + } } diff --git a/src/main/java/com/exactpro/th2/check1/event/VerificationEntryUtils.java b/src/main/java/com/exactpro/th2/check1/event/VerificationEntryUtils.java index b6edde0b..b7b41c92 100644 --- a/src/main/java/com/exactpro/th2/check1/event/VerificationEntryUtils.java +++ b/src/main/java/com/exactpro/th2/check1/event/VerificationEntryUtils.java @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2021 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. @@ -20,6 +20,8 @@ import java.util.Objects; import java.util.stream.Collectors; +import org.jetbrains.annotations.Nullable; + import com.exactpro.sf.aml.scriptutil.StaticUtil.IFilter; import com.exactpro.sf.comparison.ComparisonResult; import com.exactpro.sf.comparison.Formatter; @@ -28,16 +30,18 @@ import com.exactpro.th2.common.event.bean.VerificationStatus; import com.exactpro.th2.common.grpc.FilterOperation; import com.exactpro.th2.sailfish.utils.filter.IOperationFilter; +import com.exactpro.th2.sailfish.utils.filter.util.FilterUtils; public class VerificationEntryUtils { public static VerificationEntry createVerificationEntry(ComparisonResult result) { VerificationEntry verificationEntry = new VerificationEntry(); - verificationEntry.setActual(Objects.toString(result.getActual(), null)); - verificationEntry.setExpected(Formatter.formatExpected(result)); + verificationEntry.setActual(convertActual(result)); + verificationEntry.setExpected(convertExpectedResult(result)); verificationEntry.setStatus(toVerificationStatus(result.getStatus())); verificationEntry.setKey(result.isKey()); verificationEntry.setOperation(resolveOperation(result)); + verificationEntry.setHint(extractHint(result)); if (result.hasResults()) { verificationEntry.setFields(result.getResults().entrySet().stream() .collect(Collectors.toMap( @@ -53,6 +57,15 @@ public static VerificationEntry createVerificationEntry(ComparisonResult result) return verificationEntry; } + @Nullable + private static String convertActual(ComparisonResult result) { + Object actual = result.getActual(); + if (actual == FilterUtils.NULL_VALUE) { + return null; + } + return Objects.toString(actual, null); + } + private static String resolveOperation(ComparisonResult result) { Object expected = result.getExpected(); if (expected instanceof IFilter) { @@ -89,4 +102,13 @@ private static VerificationStatus toVerificationStatus(StatusType statusType) { throw new IllegalArgumentException("Unsupported status type '" + statusType + '\''); } } + + private static String convertExpectedResult(ComparisonResult result) { + return result.getExpected() == null ? null : Formatter.formatExpected(result); + } + + private static String extractHint(ComparisonResult result) { + Exception exception = result.getException(); + return exception == null ? null : exception.getMessage(); + } } diff --git a/src/main/java/com/exactpro/th2/check1/util/VerificationUtil.java b/src/main/java/com/exactpro/th2/check1/util/VerificationUtil.java index 7213ba6b..a7133e93 100644 --- a/src/main/java/com/exactpro/th2/check1/util/VerificationUtil.java +++ b/src/main/java/com/exactpro/th2/check1/util/VerificationUtil.java @@ -58,11 +58,6 @@ public static MetaContainer toMetaContainer(MetadataFilter metadataFilter) { public static MetaContainer toMetaContainer(MessageFilter messageFilter, boolean listItemAsSeparate) { MetaContainer metaContainer = new MetaContainer(); - Set keyFields = new HashSet<>(); - - messageFilter.getFieldsMap().forEach((name, value) -> { - toMetaContainer(name, value, metaContainer, keyFields, listItemAsSeparate); - }); if (messageFilter.hasComparisonSettings()) { FailUnexpected failUnexpected = messageFilter.getComparisonSettings().getFailUnexpected(); @@ -73,6 +68,13 @@ public static MetaContainer toMetaContainer(MessageFilter messageFilter, boolean metaContainer.setFailUnexpected(AMLLangConst.ALL); } } + + Set keyFields = new HashSet<>(); + + messageFilter.getFieldsMap().forEach((name, value) -> { + toMetaContainer(name, value, metaContainer, keyFields, listItemAsSeparate); + }); + metaContainer.setKeyFields(keyFields); @@ -96,13 +98,11 @@ private static void toMetaContainer(String fieldName, ValueFilter value, MetaCon } private static void convertList(MetaContainer parent, String fieldName, ListValueFilter listFilter) { - List result = new ArrayList<>(); for (ValueFilter valueFilter : listFilter.getValuesList()) { if (valueFilter.hasMessageFilter()) { - result.add(toMetaContainer(valueFilter.getMessageFilter(), false)); + parent.add(fieldName, toMetaContainer(valueFilter.getMessageFilter(), false)); } } - parent.getChildren().put(fieldName, result); } private static void convertListAsSeparateContainers(MetaContainer parent, String fieldName, diff --git a/src/main/kotlin/com/exactpro/th2/check1/CheckpointSubscriber.kt b/src/main/kotlin/com/exactpro/th2/check1/CheckpointSubscriber.kt index 35bb14ff..1136ef6c 100644 --- a/src/main/kotlin/com/exactpro/th2/check1/CheckpointSubscriber.kt +++ b/src/main/kotlin/com/exactpro/th2/check1/CheckpointSubscriber.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2021 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 @@ -13,6 +13,8 @@ package com.exactpro.th2.check1 +import com.exactpro.th2.check1.entities.Checkpoint +import com.exactpro.th2.check1.entities.CheckpointData import java.util.concurrent.ConcurrentHashMap class CheckpointSubscriber : AbstractSessionObserver() { @@ -22,7 +24,13 @@ class CheckpointSubscriber : AbstractSessionObserver() { sessionSet += item } - fun createCheckpoint() : Checkpoint = Checkpoint(sessionSet.associateBy( + fun createCheckpoint(): Checkpoint = Checkpoint( + sessionKeyToCheckpointData = sessionSet.associateBy( { session -> session.sessionKey }, - { session -> session.lastMessage.metadata.id.sequence })) + { session -> + session.lastMessage.metadata.run { + CheckpointData(id.sequence, timestamp) + } + }) + ) } \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/CollectorService.kt b/src/main/kotlin/com/exactpro/th2/check1/CollectorService.kt index fcea0869..52766748 100644 --- a/src/main/kotlin/com/exactpro/th2/check1/CollectorService.kt +++ b/src/main/kotlin/com/exactpro/th2/check1/CollectorService.kt @@ -13,11 +13,14 @@ package com.exactpro.th2.check1 import com.exactpro.th2.check1.configuration.Check1Configuration +import com.exactpro.th2.check1.entities.Checkpoint +import com.exactpro.th2.check1.entities.CheckpointData import com.exactpro.th2.check1.grpc.ChainID import com.exactpro.th2.check1.grpc.CheckRuleRequest import com.exactpro.th2.check1.grpc.CheckSequenceRuleRequest import com.exactpro.th2.check1.grpc.CheckpointRequestOrBuilder import com.exactpro.th2.check1.metrics.BufferMetric +import com.exactpro.th2.check1.grpc.NoMessageCheckRequest import com.exactpro.th2.check1.rule.AbstractCheckTask import com.exactpro.th2.check1.rule.RuleFactory import com.exactpro.th2.common.event.Event @@ -40,6 +43,7 @@ import java.time.Instant import java.time.temporal.ChronoUnit import java.util.concurrent.ConcurrentHashMap import java.util.concurrent.ForkJoinPool +import com.exactpro.th2.common.grpc.Checkpoint as GrpcCheckpoint class CollectorService( private val messageRouter: MessageRouter, private val eventBatchRouter: MessageRouter, configuration: Check1Configuration @@ -58,8 +62,8 @@ class CollectorService( private val olderThanDelta = configuration.cleanupOlderThan private val olderThanTimeUnit = configuration.cleanupTimeUnit - private val maxEventBatchContentSize = configuration.maxEventBatchContentSize - + private val defaultAutoSilenceCheck: Boolean = configuration.isAutoSilenceCheckAfterSequenceRule + private var ruleFactory: RuleFactory init { @@ -80,17 +84,17 @@ class CollectorService( checkpointSubscriber = streamObservable.subscribeWith(CheckpointSubscriber()) - ruleFactory = RuleFactory(maxEventBatchContentSize, streamObservable, eventBatchRouter) + ruleFactory = RuleFactory(configuration, streamObservable, eventBatchRouter) } @Throws(InterruptedException::class) fun verifyCheckRule(request: CheckRuleRequest): ChainID { val chainID = request.getChainIdOrGenerate() - val task = ruleFactory.createCheckRule(request) cleanupTasksOlderThan(olderThanDelta, olderThanTimeUnit) eventIdToLastCheckTask.compute(CheckTaskKey(chainID, request.connectivityId)) { _, value -> + val task = ruleFactory.createCheckRule(request, value != null) task.apply { addToChainOrBegin(value, request.checkpoint) } } return chainID @@ -99,20 +103,44 @@ class CollectorService( @Throws(InterruptedException::class) fun verifyCheckSequenceRule(request: CheckSequenceRuleRequest): ChainID { val chainID = request.getChainIdOrGenerate() - val task = ruleFactory.createSequenceCheckRule(request) cleanupTasksOlderThan(olderThanDelta, olderThanTimeUnit) + val silenceCheck = if (request.hasSilenceCheck()) request.silenceCheck.value else defaultAutoSilenceCheck + + val silenceCheckTask: AbstractCheckTask? = if (silenceCheck) { + ruleFactory.createSilenceCheck(request, olderThanTimeUnit.duration.toMillis() * olderThanDelta) + } else { + null + } eventIdToLastCheckTask.compute(CheckTaskKey(chainID, request.connectivityId)) { _, value -> + val task = ruleFactory.createSequenceCheckRule(request, value != null) task.apply { addToChainOrBegin(value, request.checkpoint) } + .run { silenceCheckTask?.also { subscribeNextTask(it) } ?: this } } return chainID } - private fun AbstractCheckTask.addToChainOrBegin( - value: AbstractCheckTask?, - checkpoint: com.exactpro.th2.common.grpc.Checkpoint - ): Unit = value?.subscribeNextTask(this) ?: begin(checkpoint) + fun verifyNoMessageCheck(request: NoMessageCheckRequest): ChainID { + val chainID = request.getChainIdOrGenerate() + + cleanupTasksOlderThan(olderThanDelta, olderThanTimeUnit) + + eventIdToLastCheckTask.compute(CheckTaskKey(chainID, request.connectivityId)) { _, value -> + val task = ruleFactory.createNoMessageCheckRule(request, value != null) + task.apply { addToChainOrBegin(value, request.checkpoint) } + } + return chainID + } + + private fun AbstractCheckTask.addToChainOrBegin(value: AbstractCheckTask?, checkpoint: GrpcCheckpoint) { + val realCheckpoint = if (checkpoint === GrpcCheckpoint.getDefaultInstance()) { + null + } else { + checkpoint + } + value?.subscribeNextTask(this) ?: begin(realCheckpoint) + } private fun CheckRuleRequest.getChainIdOrGenerate(): ChainID { return if (hasChainId()) { @@ -130,6 +158,14 @@ class CollectorService( } } + private fun NoMessageCheckRequest.getChainIdOrGenerate(): ChainID { + return if (hasChainId()) { + chainId + } else { + generateChainID() + } + } + private fun generateChainID() = ChainID.newBuilder().setId(EventUtils.generateUUID()).build() private fun cleanupTasksOlderThan(delta: Long, unit: ChronoUnit = ChronoUnit.SECONDS) { @@ -183,11 +219,11 @@ class CollectorService( val checkpoint = checkpointSubscriber.createCheckpoint() rootEvent.endTimestamp() .bodyData(EventUtils.createMessageBean("Checkpoint id '${checkpoint.id}'")) - checkpoint.asMap().forEach { (sessionKey: SessionKey, sequence: Long) -> - val messageID = sessionKey.toMessageID(sequence) + checkpoint.sessionKeyToCheckpointData.forEach { (sessionKey: SessionKey, checkpointData: CheckpointData) -> + val messageID = sessionKey.toMessageID(checkpointData.sequence) rootEvent.messageID(messageID) .addSubEventWithSamePeriod() - .name("Checkpoint for session alias '${sessionKey.sessionAlias}' direction '${sessionKey.direction}' sequence '$sequence'") + .name("Checkpoint for session alias '${sessionKey.sessionAlias}' direction '${sessionKey.direction}' sequence '${checkpointData.sequence}'") .type("Checkpoint for session") .messageID(messageID) } diff --git a/src/main/kotlin/com/exactpro/th2/check1/entities/Checkpoint.kt b/src/main/kotlin/com/exactpro/th2/check1/entities/Checkpoint.kt new file mode 100644 index 00000000..30124200 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/check1/entities/Checkpoint.kt @@ -0,0 +1,23 @@ +/* + * Copyright 2021-2021 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.th2.check1.entities + +import com.exactpro.th2.check1.SessionKey +import com.exactpro.th2.common.event.EventUtils + + +data class Checkpoint @JvmOverloads constructor( + val id: String = EventUtils.generateUUID(), + val sessionKeyToCheckpointData: Map +) \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/entities/CheckpointData.kt b/src/main/kotlin/com/exactpro/th2/check1/entities/CheckpointData.kt new file mode 100644 index 00000000..8e624daf --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/check1/entities/CheckpointData.kt @@ -0,0 +1,18 @@ +/* + * Copyright 2021-2021 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.th2.check1.entities + +import com.google.protobuf.Timestamp + +data class CheckpointData(val sequence: Long, val timestamp: Timestamp? = null) \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/entities/RequestAdaptor.kt b/src/main/kotlin/com/exactpro/th2/check1/entities/RequestAdaptor.kt new file mode 100644 index 00000000..750b3fa2 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/check1/entities/RequestAdaptor.kt @@ -0,0 +1,52 @@ +/* + * Copyright 2021 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.th2.check1.entities + +import com.exactpro.th2.check1.grpc.ChainID +import com.exactpro.th2.check1.grpc.CheckRuleRequest +import com.exactpro.th2.check1.grpc.CheckSequenceRuleRequest +import com.exactpro.th2.check1.grpc.NoMessageCheckRequest +import com.exactpro.th2.common.grpc.Checkpoint + +class RequestAdaptor(val chainId: ChainID?, val checkpoint: Checkpoint?) { + + companion object { + fun from(request: CheckRuleRequest): RequestAdaptor { + return request.run { + RequestAdaptor( + if (hasChainId()) chainId else null, + if (hasCheckpoint()) checkpoint else null + ) + } + } + + fun from(request: CheckSequenceRuleRequest): RequestAdaptor { + return request.run { + RequestAdaptor( + if (hasChainId()) chainId else null, + if (hasCheckpoint()) checkpoint else null + ) + } + } + + fun from(request: NoMessageCheckRequest): RequestAdaptor { + return request.run { + RequestAdaptor( + if (hasChainId()) chainId else null, + if (hasCheckpoint()) checkpoint else null + ) + } + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/entities/RuleConfiguration.kt b/src/main/kotlin/com/exactpro/th2/check1/entities/RuleConfiguration.kt new file mode 100644 index 00000000..26d7a125 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/check1/entities/RuleConfiguration.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2021-2021 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.th2.check1.entities + +import java.time.Duration + +data class RuleConfiguration( + val taskTimeout: TaskTimeout, + val description: String?, + val timePrecision: Duration, + val decimalPrecision: Double, + val maxEventBatchContentSize: Int, + val isCheckNullValueAsEmpty: Boolean +) { + init { + require(!timePrecision.isNegative) { "Time precision cannot be negative" } + require(decimalPrecision >= 0) { "Decimal precision cannot be negative" } + require(maxEventBatchContentSize > 0) { + "'maxEventBatchContentSize' should be greater than zero, actual: $maxEventBatchContentSize" + } + with(taskTimeout) { + require(timeout > 0) { + "'timeout' should be set or be greater than zero, actual: $timeout" + } + } + } +} diff --git a/src/main/kotlin/com/exactpro/th2/check1/entities/TaskTimeout.kt b/src/main/kotlin/com/exactpro/th2/check1/entities/TaskTimeout.kt new file mode 100644 index 00000000..04c94cfd --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/check1/entities/TaskTimeout.kt @@ -0,0 +1,16 @@ +/* + * Copyright 2021-2021 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.th2.check1.entities + +data class TaskTimeout(val timeout: Long, val messageTimeout: Long = 0) \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/metrics/BufferMetric.kt b/src/main/kotlin/com/exactpro/th2/check1/metrics/BufferMetric.kt index a415547d..6a1daf2f 100644 --- a/src/main/kotlin/com/exactpro/th2/check1/metrics/BufferMetric.kt +++ b/src/main/kotlin/com/exactpro/th2/check1/metrics/BufferMetric.kt @@ -15,8 +15,8 @@ package com.exactpro.th2.check1.metrics import com.exactpro.th2.check1.SessionKey import com.exactpro.th2.check1.configuration.Check1Configuration -import com.exactpro.th2.common.metrics.DEFAULT_DIRECTION_LABEL_NAME -import com.exactpro.th2.common.metrics.DEFAULT_SESSION_ALIAS_LABEL_NAME +import com.exactpro.th2.common.metrics.DIRECTION_LABEL +import com.exactpro.th2.common.metrics.SESSION_ALIAS_LABEL import io.prometheus.client.Counter import java.util.concurrent.ConcurrentHashMap import kotlin.math.min @@ -25,7 +25,7 @@ object BufferMetric { private val actualBufferCountMetric: Counter = Counter .build("th2_check1_actual_cache_number", "The actual number of messages in caches") - .labelNames(DEFAULT_SESSION_ALIAS_LABEL_NAME, DEFAULT_DIRECTION_LABEL_NAME) + .labelNames(SESSION_ALIAS_LABEL, DIRECTION_LABEL) .register() private val bufferMessagesSizeBySessionKey: MutableMap = ConcurrentHashMap() diff --git a/src/main/kotlin/com/exactpro/th2/check1/rule/AbstractCheckTask.kt b/src/main/kotlin/com/exactpro/th2/check1/rule/AbstractCheckTask.kt index 3a1683d5..ebdd121d 100644 --- a/src/main/kotlin/com/exactpro/th2/check1/rule/AbstractCheckTask.kt +++ b/src/main/kotlin/com/exactpro/th2/check1/rule/AbstractCheckTask.kt @@ -21,10 +21,16 @@ import com.exactpro.sf.scriptrunner.StatusType import com.exactpro.th2.check1.AbstractSessionObserver import com.exactpro.th2.check1.SessionKey import com.exactpro.th2.check1.StreamContainer +import com.exactpro.th2.check1.entities.CheckpointData +import com.exactpro.th2.check1.entities.RuleConfiguration +import com.exactpro.th2.check1.entities.TaskTimeout import com.exactpro.th2.check1.event.bean.builder.VerificationBuilder import com.exactpro.th2.check1.exception.RuleInternalException import com.exactpro.th2.check1.metrics.RuleMetric import com.exactpro.th2.check1.util.VerificationUtil +import com.exactpro.th2.check1.utils.convert +import com.exactpro.th2.check1.utils.getStatusType +import com.exactpro.th2.check1.utils.isAfter import com.exactpro.th2.common.event.Event import com.exactpro.th2.common.event.Event.Status.FAILED import com.exactpro.th2.common.event.Event.Status.PASSED @@ -37,11 +43,17 @@ import com.exactpro.th2.common.grpc.MessageFilter import com.exactpro.th2.common.grpc.MessageMetadata import com.exactpro.th2.common.grpc.MetadataFilter import com.exactpro.th2.common.grpc.RootMessageFilter +import com.exactpro.th2.common.message.toJavaDuration import com.exactpro.th2.common.message.toJson import com.exactpro.th2.common.message.toReadableBodyCollection import com.exactpro.th2.common.schema.message.MessageRouter +import com.exactpro.th2.sailfish.utils.FilterSettings import com.exactpro.th2.sailfish.utils.ProtoToIMessageConverter +import com.exactpro.th2.sailfish.utils.ProtoToIMessageConverter.createParameters import com.google.protobuf.TextFormat.shortDebugString +import com.google.protobuf.Timestamp +import com.google.protobuf.util.Durations +import com.google.protobuf.util.Timestamps import io.reactivex.Observable import io.reactivex.Single import io.reactivex.disposables.Disposable @@ -61,9 +73,7 @@ import java.util.concurrent.atomic.AtomicReference * **Class in not thread-safe** */ abstract class AbstractCheckTask( - val description: String?, - private val timeout: Long, - private val maxEventBatchContentSize: Int, + private val ruleConfiguration: RuleConfiguration, submitTime: Instant, protected val sessionKey: SessionKey, private val parentEventID: EventID, @@ -71,23 +81,21 @@ abstract class AbstractCheckTask( private val eventBatchRouter: MessageRouter ) : AbstractSessionObserver() { - init { - require(maxEventBatchContentSize > 0) { - "'maxEventBatchContentSize' should be greater than zero, actual: $maxEventBatchContentSize" - } - require(timeout > 0) { - "'timeout' should be set or be greater than zero, actual: $timeout" - } - } + val description: String? = ruleConfiguration.description + private val taskTimeout: TaskTimeout = ruleConfiguration.taskTimeout protected var handledMessageCounter: Long = 0 - - protected val converter = ProtoToIMessageConverter(VerificationUtil.FACTORY_PROXY, null, null) protected val rootEvent: Event = Event.from(submitTime).description(description) private val sequenceSubject = SingleSubject.create() private val hasNextTask = AtomicBoolean(false) private val taskState = AtomicReference(State.CREATED) + @Volatile + private var streamCompletedState = State.STREAM_COMPLETED + @Volatile + private var completed = false + protected var isParentCompleted: Boolean? = null + private set /** * Used for observe messages in one thread. @@ -100,18 +108,27 @@ abstract class AbstractCheckTask( val endTime: Instant? get() = _endTime - protected enum class State { - CREATED, - BEGIN, - TIMEOUT, - COMPLETED, - PUBLISHED + protected enum class State(val callOnTimeoutCallback: Boolean) { + CREATED(false), + BEGIN(false), + TIMEOUT(true), + MESSAGE_TIMEOUT(true), + TASK_COMPLETED(false), + STREAM_COMPLETED(true), + PUBLISHED(false), + ERROR(false) } @Volatile private lateinit var endFuture: Disposable private var lastSequence = DEFAULT_SEQUENCE + private var checkpointTimeout: Timestamp? = null + private var lastMessageTimestamp: Timestamp? = null + private var untrusted: Boolean = false + private var hasMessagesInTimeoutInterval: Boolean = false + private var bufferContainsStartMessage: Boolean = false + private var isDefaultSequence: Boolean = false override fun onStart() { super.onStart() @@ -125,7 +142,7 @@ abstract class AbstractCheckTask( rootEvent.status(FAILED) .bodyData(EventUtils.createMessageBean(e.message)) - end("Error ${e.message} received in message stream") + end(State.ERROR, "Error ${e.message} received in message stream") } /** @@ -151,15 +168,18 @@ abstract class AbstractCheckTask( */ fun subscribeNextTask(checkTask: AbstractCheckTask) { if (hasNextTask.compareAndSet(false, true)) { + onChainedTaskSubscription() sequenceSubject.subscribe { legacy -> val executor = if (legacy.executorService.isShutdown) { - LOGGER.warn("Executor has been shutdown before next task has been subscribed. Create a new one") - createExecutorService() - } else { - legacy.executorService - } - checkTask.begin(legacy.lastSequence, executor) - } + LOGGER.warn("Executor has been shutdown before next task has been subscribed. Create a new one") + createExecutorService() + } else { + legacy.executorService + } + legacy.sequenceData.apply { + checkTask.begin(lastSequence, lastMessageTimestamp, executor, PreviousExecutionData(untrusted, completed)) + } + } LOGGER.info("Task {} ({}) subscribed to task {} ({})", checkTask.description, checkTask.hashCode(), description, hashCode()) } else { throw IllegalStateException("Subscription to last sequence for task $description (${hashCode()}) is already executed, subscriber ${checkTask.description} (${checkTask.hashCode()})") @@ -170,33 +190,50 @@ abstract class AbstractCheckTask( * Observe a message sequence from the checkpoint. * Task subscribe to messages stream with its sequence after call. * This method should be called only once otherwise it throws IllegalStateException. - * @param checkpoint message sequence from previous task. + * @param checkpoint message sequence and checkpoint timestamp from previous task. * @throws IllegalStateException when method is called more than once. */ fun begin(checkpoint: Checkpoint? = null) { - begin(checkpoint?.getSequence(sessionKey) ?: DEFAULT_SEQUENCE) + val checkpointData = checkpoint?.getCheckpointData(sessionKey) + begin(checkpointData?.sequence ?: DEFAULT_SEQUENCE, checkpointData?.timestamp) } + /** + * Callback when another task is subscribed to the result of the current task + */ + protected open fun onChainedTaskSubscription() {} + /** * It is called when the timeout is over and the task is not complete yet */ protected open fun onTimeout() {} /** - * Marks the task as successfully completed. If the task timeout had been exited and then the task was marked as successfully completed - * the task will be considered as successfully completed because it had actually found that it should + * Marks the task as successfully completed. If the task timeout, message timeout or stream had been exited and then + * the task was marked as successfully completed the task will be considered as successfully completed because + * it had actually found that it should */ protected fun checkComplete() { LOGGER.info("Check completed for session alias '{}' with sequence '{}'", sessionKey, lastSequence) - val prevValue = taskState.getAndSet(State.COMPLETED) + val prevValue = taskState.getAndSet(State.TASK_COMPLETED) dispose() endFuture.dispose() + completed = true - if (prevValue == State.TIMEOUT) { - LOGGER.info("Task '{}' for session alias '{}' is completed right after timeout exited. Consider it as completed", description, sessionKey) - } else { - LOGGER.debug("Task '{}' for session alias '{}' is completed normally", description, sessionKey) + when (prevValue) { + State.TIMEOUT -> { + LOGGER.info("Task '{}' for session alias '{}' is completed right after timeout exited. Consider it as completed", description, sessionKey) + } + State.MESSAGE_TIMEOUT -> { + LOGGER.info("Task '{}' for session alias '{}' is completed right after message timeout exited. Consider it as completed", description, sessionKey) + } + State.STREAM_COMPLETED -> { + LOGGER.info("Task '{}' for session alias '{}' is completed right after the end of streaming messages. Consider it as completed", description, sessionKey) + } + else -> { + LOGGER.debug("Task '{}' for session alias '{}' is completed normally", description, sessionKey) + } } } @@ -205,6 +242,12 @@ abstract class AbstractCheckTask( */ protected open fun Observable.taskPipeline() : Observable = this + /** + * @return `true` if another task has been subscribed to the result of the current task. + * Otherwise, returns `false` + */ + protected fun hasNextTask(): Boolean = hasNextTask.get() + protected abstract fun name(): String protected abstract fun type(): String protected abstract fun setup(rootEvent: Event) @@ -214,22 +257,34 @@ abstract class AbstractCheckTask( * Task subscribe to messages stream with sequence after call. * This method should be called only once otherwise it throws IllegalStateException. * @param sequence message sequence from the previous task. + * @param checkpointTimestamp checkpoint timestamp from the previous task * @param executorService executor to schedule pipeline execution. + * @param untrusted flag is guarantee that the previous sequence data is correct + * @param parentTaskCompleted indicates whether the parent task was completed normally. `null` if no parent task exists. * @throws IllegalStateException when method is called more than once. */ - private fun begin(sequence: Long = DEFAULT_SEQUENCE, executorService: ExecutorService = createExecutorService()) { + private fun begin( + sequence: Long = DEFAULT_SEQUENCE, + checkpointTimestamp: Timestamp? = null, + executorService: ExecutorService = createExecutorService(), + previousExecutionData: PreviousExecutionData = PreviousExecutionData.DEFAULT + ) { configureRootEvent() + isParentCompleted = previousExecutionData.completed if (!taskState.compareAndSet(State.CREATED, State.BEGIN)) { throw IllegalStateException("Task $description already has been started") } - LOGGER.info("Check begin for session alias '{}' with sequence '{}' timeout '{}'", sessionKey, sequence, timeout) + LOGGER.info("Check begin for session alias '{}' with sequence '{}' and task timeout '{}'", sessionKey, sequence, taskTimeout) RuleMetric.incrementActiveRule(type()) this.lastSequence = sequence this.executorService = executorService + this.untrusted = previousExecutionData.untrusted + this.checkpointTimeout = calculateCheckpointTimeout(checkpointTimestamp, taskTimeout.messageTimeout) + this.isDefaultSequence = sequence == DEFAULT_SEQUENCE val scheduler = Schedulers.from(executorService) - endFuture = Single.timer(timeout, MILLISECONDS, Schedulers.computation()) - .subscribe { _ -> end("Timeout is exited") } + endFuture = Single.timer(taskTimeout.timeout, MILLISECONDS, Schedulers.computation()) + .subscribe { _ -> end(State.TIMEOUT, "Timeout is exited") } try { messageStream.observeOn(scheduler) // Defined scheduler to execution in one thread to avoid race-condition. @@ -252,6 +307,7 @@ abstract class AbstractCheckTask( rootEvent.messageID(this) } } + .takeWhileMessagesInTimeout() .mapToMessageContainer() .taskPipeline() .subscribe(this) @@ -270,8 +326,8 @@ abstract class AbstractCheckTask( private fun taskFinished() { try { val currentState = taskState.get() - LOGGER.info("Finishes task '$description' in state $currentState") - if (currentState == State.TIMEOUT) { + LOGGER.info("Finishes task '$description' in state ${currentState.name}") + if (currentState.callOnTimeoutCallback) { callOnTimeoutCallback() } publishEvent() @@ -291,7 +347,7 @@ abstract class AbstractCheckTask( .build()) } finally { RuleMetric.decrementActiveRule(type()) - sequenceSubject.onSuccess(Legacy(executorService, lastSequence)) + sequenceSubject.onSuccess(Legacy(executorService, SequenceData(lastSequence, lastMessageTimestamp, !hasMessagesInTimeoutInterval))) } } @@ -309,10 +365,11 @@ abstract class AbstractCheckTask( * Disposes the task when the timeout is over or the message stream is completed normally or with an exception. * Task unsubscribe from the message stream. * + * @param state of the stopped task * @param reason the cause why a task must be stopped */ - private fun end(reason: String) { - if (taskState.compareAndSet(State.BEGIN, State.TIMEOUT)) { + private fun end(state: State, reason: String) { + if (taskState.compareAndSet(State.BEGIN, state)) { LOGGER.info("Stop task for session alias '{}' with sequence '{}' because: {}", sessionKey, lastSequence, reason) dispose() endFuture.dispose() @@ -323,14 +380,18 @@ abstract class AbstractCheckTask( override fun onComplete() { super.onComplete() - end("Message stream is completed") + end(streamCompletedState, "Message stream is completed") } /** * Prepare the root event or children events for publication. * This method is invoked in [State.PUBLISHED] state. */ - protected open fun completeEvent(canceled: Boolean) {} + protected open fun completeEvent(taskState: State) {} + + protected open val skipPublication: Boolean = false + + protected fun isCheckpointLastReceivedMessage(): Boolean = bufferContainsStartMessage && !hasMessagesInTimeoutInterval /** * Publishes the event to [eventBatchRouter]. @@ -338,10 +399,14 @@ abstract class AbstractCheckTask( private fun publishEvent() { val prevState = taskState.getAndSet(State.PUBLISHED) if (prevState != State.PUBLISHED) { - completeEventOrReportError(prevState) + val hasError = completeEventOrReportError(prevState) _endTime = Instant.now() - val batches = rootEvent.disperseToBatches(maxEventBatchContentSize, parentEventID) + if (skipPublication && !hasError) { + LOGGER.info("Skip event publication for task ${type()} '$description' (${hashCode()})") + return + } + val batches = rootEvent.disperseToBatches(ruleConfiguration.maxEventBatchContentSize, parentEventID) RESPONSE_EXECUTOR.execute { batches.forEach { batch -> @@ -361,9 +426,11 @@ abstract class AbstractCheckTask( } } - private fun completeEventOrReportError(prevState: State) { - try { - completeEvent(prevState == State.TIMEOUT) + private fun completeEventOrReportError(prevState: State): Boolean { + return try { + completeEvent(prevState) + doAfterCompleteEvent() + false } catch (e: Exception) { LOGGER.error("Result event cannot be completed", e) rootEvent.addSubEventWithSamePeriod() @@ -372,6 +439,7 @@ abstract class AbstractCheckTask( .bodyData(EventUtils.createMessageBean("An unexpected exception has been thrown during result check build")) .bodyData(EventUtils.createMessageBean(e.message)) .status(FAILED) + true } } @@ -380,7 +448,46 @@ abstract class AbstractCheckTask( setup(rootEvent) } - protected fun matchFilter( + private fun doAfterCompleteEvent() { + if (untrusted) { + fillUntrustedExecutionEvent() + } else if (!isDefaultSequence && !bufferContainsStartMessage) { + if (hasMessagesInTimeoutInterval) { + fillEmptyStartMessageEvent() + } else { + fillMissedStartMessageAndMessagesInIntervalEvent() + } + } + } + + private fun fillUntrustedExecutionEvent() { + rootEvent.addSubEvent( + Event.start() + .name("The current check is untrusted because the start point of the check interval has been selected approximately") + .status(FAILED) + .type("untrustedExecution") + ) + } + + private fun fillMissedStartMessageAndMessagesInIntervalEvent() { + rootEvent.addSubEvent( + Event.start() + .name("Check cannot be executed because buffer for session alias '${sessionKey.sessionAlias}' and direction '${sessionKey.direction}' contains neither message in the requested check interval with sequence '$lastSequence' and checkpoint timestamp '${checkpointTimeout?.toJson()}'") + .status(FAILED) + .type("missedMessagesInInterval") + ) + } + + private fun fillEmptyStartMessageEvent() { + rootEvent.addSubEvent( + Event.start() + .name("Buffer for session alias '${sessionKey.sessionAlias}' and direction '${sessionKey.direction}' doesn't contain starting message, but contains several messages in the requested check interval") + .status(FAILED) + .type("missedStartMessage") + ) + } + + internal fun matchFilter( messageContainer: MessageContainer, messageFilter: SailfishFilter, metadataFilter: SailfishFilter?, @@ -416,7 +523,10 @@ abstract class AbstractCheckTask( return if (comparisonResult != null || metadataComparisonResult != null) { if (significant) { - lastSequence = messageContainer.protoMessage.metadata.id.sequence + messageContainer.protoMessage.metadata.apply { + lastSequence = id.sequence + lastMessageTimestamp = timestamp + } } AggregatedFilterResult(comparisonResult, metadataComparisonResult) } else { @@ -427,6 +537,8 @@ abstract class AbstractCheckTask( companion object { const val DEFAULT_SEQUENCE = Long.MIN_VALUE private val RESPONSE_EXECUTOR = ForkJoinPool.commonPool() + @JvmField + val CONVERTER = ProtoToIMessageConverter(VerificationUtil.FACTORY_PROXY, null, null, createParameters().setUseMarkerForNullsInMessage(true)) } protected fun RootMessageFilter.metadataFilterOrNull(): MetadataFilter? = @@ -518,11 +630,27 @@ abstract class AbstractCheckTask( protected fun ProtoToIMessageConverter.fromProtoPreFilter(protoPreMessageFilter: RootMessageFilter, messageName: String = protoPreMessageFilter.messageType): IMessage { - return fromProtoFilter(protoPreMessageFilter.messageFilter, messageName) + val filterSettings = protoPreMessageFilter.comparisonSettings.run { + FilterSettings().apply { + decimalPrecision = if (this@run.decimalPrecision.isBlank()) { + ruleConfiguration.decimalPrecision + } else { + this@run.decimalPrecision.toDouble() + } + timePrecision = if (this@run.hasTimePrecision()) { + this@run.timePrecision.toJavaDuration() + } else { + ruleConfiguration.timePrecision + } + isCheckNullValueAsEmpty = ruleConfiguration.isCheckNullValueAsEmpty + } + } + + return fromProtoFilter(protoPreMessageFilter.messageFilter, filterSettings, messageName) } private fun Observable.mapToMessageContainer(): Observable = - map { message -> MessageContainer(message, converter.fromProtoMessage(message, false)) } + map { message -> MessageContainer(message, CONVERTER.fromProtoMessage(message, false)) } /** * Filters incoming {@link StreamContainer} via session alias and then @@ -531,22 +659,78 @@ abstract class AbstractCheckTask( private fun Observable.continueObserve(sessionKey: SessionKey, sequence: Long): Observable = filter { streamContainer -> streamContainer.sessionKey == sessionKey } .flatMap(StreamContainer::bufferedMessages) - .filter { message -> message.metadata.id.sequence > sequence } + .filter { message -> + if (message.metadata.id.sequence == sequence) { + bufferContainsStartMessage = true + } + message.metadata.id.sequence > sequence + } - private fun Checkpoint.getSequence(sessionKey: SessionKey): Long { - val sequence = sessionAliasToDirectionCheckpointMap[sessionKey.sessionAlias] - ?.directionToSequenceMap?.get(sessionKey.direction.number) + private fun Checkpoint.getCheckpointData(sessionKey: SessionKey): CheckpointData { + val checkpointData = sessionAliasToDirectionCheckpointMap[sessionKey.sessionAlias] + ?.directionToCheckpointDataMap?.get(sessionKey.direction.number) - if (sequence == null) { + if (checkpointData == null) { if (LOGGER.isWarnEnabled) { - LOGGER.warn("Checkpoint '{}' doesn't contain sequence for session '{}'", shortDebugString(this), sessionKey) + LOGGER.warn("Checkpoint '{}' doesn't contain checkpoint data for session '{}'", this.toJson(), sessionKey) } - } else { + val sequence = sessionAliasToDirectionCheckpointMap[sessionKey.sessionAlias] + ?.directionToSequenceMap?.get(sessionKey.direction.number) + if (sequence == null) { + if (LOGGER.isWarnEnabled) { + LOGGER.warn("Checkpoint '{}' doesn't contain sequence for session '{}'", this.toJson(), sessionKey) + } + return CheckpointData(DEFAULT_SEQUENCE) + } + return CheckpointData(sequence) + } + + return checkpointData.convert().apply { LOGGER.info("Use sequence '{}' from checkpoint for session '{}'", sequence, sessionKey) } + } - return sequence ?: DEFAULT_SEQUENCE + private fun checkOnMessageTimeout(timestamp: Timestamp): Boolean { + return checkpointTimeout == null || checkpointTimeout!!.isAfter(timestamp) || checkpointTimeout == timestamp } - private data class Legacy(val executorService: ExecutorService, val lastSequence: Long) + /** + * Provides the ability to stop observing if a message timeout is set. + */ + private fun Observable.takeWhileMessagesInTimeout() : Observable = + takeWhile { + checkOnMessageTimeout(it.metadata.timestamp).also { continueObservation -> + hasMessagesInTimeoutInterval = hasMessagesInTimeoutInterval or continueObservation + if (!continueObservation) { + streamCompletedState = State.MESSAGE_TIMEOUT + } + } + } + + private fun calculateCheckpointTimeout(timestamp: Timestamp?, messageTimeout: Long): Timestamp? = + if (timestamp != null && messageTimeout > 0) { + Timestamps.add(timestamp, Durations.fromMillis(messageTimeout)) + } else { + null + } + + + private data class Legacy(val executorService: ExecutorService, val sequenceData: SequenceData) + private data class SequenceData(val lastSequence: Long, val lastMessageTimestamp: Timestamp?, val untrusted: Boolean) + private data class PreviousExecutionData( + /** + * `true` if the previous rule in the chain marked as untrusted + */ + val untrusted: Boolean = false, + /** + * `true` if previous rule has been completed normally. Otherwise, `false` + * + * `null` if there is no previous rule in chain + */ + val completed: Boolean? = null + ) { + companion object { + val DEFAULT = PreviousExecutionData() + } + } } diff --git a/src/main/kotlin/com/exactpro/th2/check1/rule/ComparisonContainer.kt b/src/main/kotlin/com/exactpro/th2/check1/rule/ComparisonContainer.kt index 9ac69387..0f5b2101 100644 --- a/src/main/kotlin/com/exactpro/th2/check1/rule/ComparisonContainer.kt +++ b/src/main/kotlin/com/exactpro/th2/check1/rule/ComparisonContainer.kt @@ -14,8 +14,9 @@ package com.exactpro.th2.check1.rule import com.exactpro.sf.common.messages.IMessage import com.exactpro.sf.comparison.ComparisonResult -import com.exactpro.sf.comparison.ComparisonUtil import com.exactpro.sf.scriptrunner.StatusType +import com.exactpro.th2.check1.utils.FilterUtils +import com.exactpro.th2.check1.utils.FilterUtils.fullMatch import com.exactpro.th2.common.grpc.Message import com.exactpro.th2.common.grpc.RootMessageFilter @@ -37,7 +38,7 @@ class ComparisonContainer( * Otherwise, returns `true` only if [AggregatedFilterResult.messageResult] is not `null` */ val matchesByKeys: Boolean - get() = result.allMatches { it != null } + get() = FilterUtils.allMatches(result, protoFilter) { it != null } /** * If [RootMessageFilter.hasMetadataFilter] for [protoFilter] is `true` @@ -47,16 +48,7 @@ class ComparisonContainer( * and its aggregated status is not [StatusType.FAILED] */ val fullyMatches: Boolean - get() = result.allMatches { it.fullMatch } - - private fun AggregatedFilterResult.allMatches(test: (ComparisonResult?) -> Boolean): Boolean { - return test(messageResult) && (!protoFilter.hasMetadataFilter() || test(metadataResult)) - } - - companion object { - private val ComparisonResult?.fullMatch: Boolean - get() = this != null && getStatusType() != StatusType.FAILED - } + get() = FilterUtils.allMatches(result, protoFilter) { it.fullMatch } } class AggregatedFilterResult( @@ -69,6 +61,4 @@ class AggregatedFilterResult( @JvmField val EMPTY = AggregatedFilterResult(null, null) } -} - -fun ComparisonResult.getStatusType(): StatusType = ComparisonUtil.getStatusType(this) \ No newline at end of file +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/rule/MessageContainer.kt b/src/main/kotlin/com/exactpro/th2/check1/rule/MessageContainer.kt index 2a8fe5e1..d5865160 100644 --- a/src/main/kotlin/com/exactpro/th2/check1/rule/MessageContainer.kt +++ b/src/main/kotlin/com/exactpro/th2/check1/rule/MessageContainer.kt @@ -12,6 +12,7 @@ */ package com.exactpro.th2.check1.rule +import com.exactpro.sf.common.impl.messages.DefaultMessageFactory import com.exactpro.sf.common.messages.IMessage import com.exactpro.sf.comparison.ComparatorSettings import com.exactpro.sf.comparison.ComparisonResult @@ -25,9 +26,17 @@ class MessageContainer( val metadataMessage: IMessage by lazy { VerificationUtil.toMessage(protoMessage.metadata) } + + companion object { + private val EMPTY_MESSAGE = DefaultMessageFactory.getFactory().createMessage("empty", "empty") + @JvmField + val FAKE = MessageContainer(Message.getDefaultInstance(), EMPTY_MESSAGE) + } } class SailfishFilter( val message: IMessage, val comparatorSettings: ComparatorSettings -) \ No newline at end of file +) + +fun MessageContainer.isNotFake(): Boolean = this !== MessageContainer.FAKE \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/rule/RuleFactory.kt b/src/main/kotlin/com/exactpro/th2/check1/rule/RuleFactory.kt index aa3479cf..30018ec0 100644 --- a/src/main/kotlin/com/exactpro/th2/check1/rule/RuleFactory.kt +++ b/src/main/kotlin/com/exactpro/th2/check1/rule/RuleFactory.kt @@ -15,12 +15,19 @@ package com.exactpro.th2.check1.rule import com.exactpro.th2.check1.SessionKey import com.exactpro.th2.check1.StreamContainer +import com.exactpro.th2.check1.configuration.Check1Configuration +import com.exactpro.th2.check1.entities.RequestAdaptor +import com.exactpro.th2.check1.entities.RuleConfiguration +import com.exactpro.th2.check1.entities.TaskTimeout import com.exactpro.th2.check1.exception.RuleCreationException import com.exactpro.th2.check1.exception.RuleInternalException import com.exactpro.th2.check1.grpc.CheckRuleRequest import com.exactpro.th2.check1.grpc.CheckSequenceRuleRequest +import com.exactpro.th2.check1.grpc.NoMessageCheckRequest import com.exactpro.th2.check1.rule.check.CheckRuleTask +import com.exactpro.th2.check1.rule.nomessage.NoMessageCheckTask import com.exactpro.th2.check1.rule.sequence.SequenceCheckRuleTask +import com.exactpro.th2.check1.rule.sequence.SilenceCheckTask import com.exactpro.th2.common.event.Event import com.exactpro.th2.common.grpc.ComparisonSettings import com.exactpro.th2.common.grpc.Direction @@ -31,7 +38,6 @@ import com.exactpro.th2.common.grpc.RootComparisonSettings import com.exactpro.th2.common.grpc.RootMessageFilter import com.exactpro.th2.common.message.toJson import com.exactpro.th2.common.schema.message.MessageRouter -import com.google.protobuf.GeneratedMessageV3 import io.reactivex.Observable import mu.KotlinLogging import org.slf4j.Logger @@ -39,17 +45,24 @@ import java.time.Instant import java.util.concurrent.ForkJoinPool class RuleFactory( - private val maxEventBatchContentSize: Int, + configuration: Check1Configuration, private val streamObservable: Observable, private val eventBatchRouter: MessageRouter ) { + private val maxEventBatchContentSize = configuration.maxEventBatchContentSize + private val defaultRuleExecutionTimeout = configuration.ruleExecutionTimeout + private val timePrecision = configuration.timePrecision + private val decimalPrecision = configuration.decimalPrecision + private val isCheckNullValueAsEmpty = configuration.isCheckNullValueAsEmpty - fun createCheckRule(request: CheckRuleRequest): CheckRuleTask = - ruleCreation(request, request.parentEventId) { - checkAndCreateRule { request -> + fun createCheckRule(request: CheckRuleRequest, isChainIdExist: Boolean): CheckRuleTask = + ruleCreation(request.parentEventId) { + checkAndCreateRule { check(request.hasParentEventId()) { "Parent event id can't be null" } - check(request.connectivityId.sessionAlias.isNotEmpty()) { "Session alias cannot be empty" } val sessionAlias: String = request.connectivityId.sessionAlias + check(sessionAlias.isNotEmpty()) { "Session alias cannot be empty" } + val sessionKey = SessionKey(sessionAlias, directionOrDefault(request.direction)) + checkMessageTimeout(request.messageTimeout) { checkCheckpoint(RequestAdaptor.from(request), sessionKey, isChainIdExist) } check(request.kindCase != CheckRuleRequest.KindCase.KIND_NOT_SET) { "Either old filter or root filter must be set" @@ -59,14 +72,20 @@ class RuleFactory( } else { request.filter.toRootMessageFilter() }.also { it.validateRootMessageFilter() } - val direction = directionOrDefault(request.direction) - CheckRuleTask( + val ruleConfiguration = RuleConfiguration( + createTaskTimeout(request.timeout, request.messageTimeout), request.description, - Instant.now(), - SessionKey(sessionAlias, direction), - request.timeout, + timePrecision, + decimalPrecision, maxEventBatchContentSize, + isCheckNullValueAsEmpty + ) + + CheckRuleTask( + ruleConfiguration, + Instant.now(), + sessionKey, filter, request.parentEventId, streamObservable, @@ -80,13 +99,14 @@ class RuleFactory( } } - fun createSequenceCheckRule(request: CheckSequenceRuleRequest): SequenceCheckRuleTask = - ruleCreation(request, request.parentEventId) { - checkAndCreateRule { request -> + fun createSequenceCheckRule(request: CheckSequenceRuleRequest, isChainIdExist: Boolean): SequenceCheckRuleTask = + ruleCreation(request.parentEventId) { + checkAndCreateRule { check(request.hasParentEventId()) { "Parent event id can't be null" } - check(request.connectivityId.sessionAlias.isNotEmpty()) { "Session alias cannot be empty" } val sessionAlias: String = request.connectivityId.sessionAlias - val direction = directionOrDefault(request.direction) + check(sessionAlias.isNotEmpty()) { "Session alias cannot be empty" } + val sessionKey = SessionKey(sessionAlias, directionOrDefault(request.direction)) + checkMessageTimeout(request.messageTimeout) { checkCheckpoint(RequestAdaptor.from(request), sessionKey, isChainIdExist) } check((request.messageFiltersList.isEmpty() && request.rootMessageFiltersList.isNotEmpty()) || (request.messageFiltersList.isNotEmpty() && request.rootMessageFiltersList.isEmpty())) { @@ -97,12 +117,19 @@ class RuleFactory( request.messageFiltersList.map { it.toRootMessageFilter() } }.onEach { it.validateRootMessageFilter() } - SequenceCheckRuleTask( + val ruleConfiguration = RuleConfiguration( + createTaskTimeout(request.timeout, request.messageTimeout), request.description, - Instant.now(), - SessionKey(sessionAlias, direction), - request.timeout, + timePrecision, + decimalPrecision, maxEventBatchContentSize, + isCheckNullValueAsEmpty + ) + + SequenceCheckRuleTask( + ruleConfiguration, + Instant.now(), + sessionKey, request.preFilter, protoMessageFilters, request.checkOrder, @@ -118,11 +145,83 @@ class RuleFactory( } } + fun createNoMessageCheckRule(request: NoMessageCheckRequest, isChainIdExist: Boolean): NoMessageCheckTask = + ruleCreation(request.parentEventId) { + checkAndCreateRule { + check(request.hasParentEventId()) { "Parent event id can't be null" } + val parentEventID: EventID = request.parentEventId + val sessionAlias: String = request.connectivityId.sessionAlias + check(sessionAlias.isNotEmpty()) { "Session alias cannot be empty" } + val sessionKey = SessionKey(sessionAlias, directionOrDefault(request.direction)) + checkMessageTimeout(request.messageTimeout) { checkCheckpoint(RequestAdaptor.from(request), sessionKey, isChainIdExist) } + + val ruleConfiguration = RuleConfiguration( + createTaskTimeout(request.timeout, request.messageTimeout), + request.description, + timePrecision, + decimalPrecision, + maxEventBatchContentSize, + isCheckNullValueAsEmpty + ) - private inline fun ruleCreation(request: T, parentEventId: EventID, block: RuleCreationContext.() -> Unit): R { - val ruleCreationContext = RuleCreationContext().apply(block) + NoMessageCheckTask( + ruleConfiguration, + Instant.now(), + sessionKey, + request.preFilter, + parentEventID, + streamObservable, + eventBatchRouter + ) + } + onErrorEvent { + Event.start() + .name("Check rule cannot be created") + .type("checkRuleCreation") + } + } + + fun createSilenceCheck( + request: CheckSequenceRuleRequest, + timeout: Long + ): SilenceCheckTask { + return ruleCreation(request.parentEventId) { + checkAndCreateRule { + check(timeout > 0) { "timeout must be greater that zero" } + val sessionAlias: String = request.connectivityId.sessionAlias + val sessionKey = SessionKey(sessionAlias, directionOrDefault(request.direction)) + + val ruleConfiguration = RuleConfiguration( + createTaskTimeout(timeout), + request.description.takeIf(String::isNotEmpty), + timePrecision, + decimalPrecision, + maxEventBatchContentSize, + isCheckNullValueAsEmpty + ) + + SilenceCheckTask( + ruleConfiguration, + request.preFilter, + Instant.now(), + sessionKey, + request.parentEventId, + streamObservable, + eventBatchRouter + ) + } + onErrorEvent { + Event.start() + .name("Auto silence check rule cannot be created") + .type("checkRuleCreation") + } + } + } + + private inline fun ruleCreation(parentEventId: EventID, block: RuleCreationContext.() -> Unit): R { + val ruleCreationContext = RuleCreationContext().apply(block) try { - return ruleCreationContext.action(request) + return ruleCreationContext.action() } catch (e: RuleInternalException) { throw e } catch (e: Exception) { @@ -178,11 +277,50 @@ class RuleFactory( } } - private class RuleCreationContext { - lateinit var action: (T) -> R + private fun checkCheckpoint(requestAdaptor: RequestAdaptor, sessionKey: SessionKey, isChainIdExist: Boolean) { + if (requestAdaptor.chainId != null) { + check(isChainIdExist) { + "The request has an invalid chain ID or connectivity ID. Please use checkpoint instead of chain ID" + } + return // We should validate checkpoint only if the request doesn't contain a chain id + } + checkNotNull(requestAdaptor.checkpoint) { + "Request must contain a checkpoint, because the 'messageTimeout' is used and no chain ID is specified" + } + with(sessionKey) { + val directionCheckpoint = requestAdaptor.checkpoint.sessionAliasToDirectionCheckpointMap[sessionAlias] + checkNotNull(directionCheckpoint) { "The checkpoint doesn't contain a direction checkpoint with session alias '$sessionAlias'" } + val checkpointData = directionCheckpoint.directionToCheckpointDataMap[direction.number] + checkNotNull(checkpointData) { "The direction checkpoint doesn't contain a checkpoint data with direction '$direction'" } + with(checkpointData) { + check(sequence > 0L) { "The checkpoint data has incorrect sequence number '$sequence'" } + check(this.hasTimestamp()) { "The checkpoint data doesn't contain timestamp" } + } + } + } + + private fun checkMessageTimeout(messageTimeout: Long, checkpointCheckAction: () -> Unit) { + when { + messageTimeout > 0 -> checkpointCheckAction() + messageTimeout < 0 -> error("Message timeout cannot be negative") + } + } + + private fun createTaskTimeout(timeout: Long, messageTimeout: Long = 0): TaskTimeout { + val newRuleTimeout = if (timeout <= 0) { + LOGGER.info("Rule execution timeout is less than or equal to zero, used default rule execution timeout '$defaultRuleExecutionTimeout'") + defaultRuleExecutionTimeout + } else { + timeout + } + return TaskTimeout(newRuleTimeout, messageTimeout) + } + + private class RuleCreationContext { + lateinit var action: () -> R lateinit var event: () -> Event - fun checkAndCreateRule(block: (T) -> R) { + fun checkAndCreateRule(block: () -> R) { action = block } diff --git a/src/main/kotlin/com/exactpro/th2/check1/rule/StreamUtils.kt b/src/main/kotlin/com/exactpro/th2/check1/rule/StreamUtils.kt new file mode 100644 index 00000000..5507219f --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/check1/rule/StreamUtils.kt @@ -0,0 +1,38 @@ +/* + * Copyright 2021 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.th2.check1.rule + +import com.exactpro.th2.common.grpc.RootMessageFilter +import com.exactpro.th2.common.message.toJson +import io.reactivex.Observable +import org.slf4j.Logger + +fun AbstractCheckTask.preFilterBy( + stream: Observable, + protoPreMessageFilter: RootMessageFilter, + messagePreFilter: SailfishFilter, + metadataPreFilter: SailfishFilter?, + logger: Logger, + onMatch: (ComparisonContainer) -> Unit +): Observable = + stream.map { messageContainer -> // Compare the message with pre-filter + if (logger.isDebugEnabled) { + logger.debug("Pre-filtering message with id: {}", messageContainer.protoMessage.metadata.id.toJson()) + } + val result = matchFilter(messageContainer, messagePreFilter, metadataPreFilter, matchNames = false, significant = false) + ComparisonContainer(messageContainer, protoPreMessageFilter, result) + .takeIf(ComparisonContainer::fullyMatches) + ?.also(onMatch) + ?.messageContainer ?: MessageContainer.FAKE + }.filter(MessageContainer::isNotFake) \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/rule/check/CheckRuleTask.kt b/src/main/kotlin/com/exactpro/th2/check1/rule/check/CheckRuleTask.kt index ab90e41a..b94dd543 100644 --- a/src/main/kotlin/com/exactpro/th2/check1/rule/check/CheckRuleTask.kt +++ b/src/main/kotlin/com/exactpro/th2/check1/rule/check/CheckRuleTask.kt @@ -15,6 +15,7 @@ package com.exactpro.th2.check1.rule.check import com.exactpro.th2.check1.SessionKey import com.exactpro.th2.check1.StreamContainer +import com.exactpro.th2.check1.entities.RuleConfiguration import com.exactpro.th2.check1.rule.AbstractCheckTask import com.exactpro.th2.check1.rule.ComparisonContainer import com.exactpro.th2.check1.rule.MessageContainer @@ -35,24 +36,22 @@ import java.time.Instant * This rule checks for the presence of a single message in the messages stream. */ class CheckRuleTask( - description: String?, + ruleConfiguration: RuleConfiguration, startTime: Instant, sessionKey: SessionKey, - timeout: Long, - maxEventBatchContentSize: Int, private val protoMessageFilter: RootMessageFilter, parentEventID: EventID, messageStream: Observable, eventBatchRouter: MessageRouter -) : AbstractCheckTask(description, timeout, maxEventBatchContentSize, startTime, sessionKey, parentEventID, messageStream, eventBatchRouter) { +) : AbstractCheckTask(ruleConfiguration, startTime, sessionKey, parentEventID, messageStream, eventBatchRouter) { private val messageFilter: SailfishFilter = SailfishFilter( - converter.fromProtoPreFilter(protoMessageFilter), + CONVERTER.fromProtoPreFilter(protoMessageFilter), protoMessageFilter.toCompareSettings() ) private val metadataFilter: SailfishFilter? = protoMessageFilter.metadataFilterOrNull()?.let { SailfishFilter( - converter.fromMetadataFilter(it, METADATA_MESSAGE_NAME), + CONVERTER.fromMetadataFilter(it, METADATA_MESSAGE_NAME), it.toComparisonSettings() ) } diff --git a/src/main/kotlin/com/exactpro/th2/check1/rule/nomessage/NoMessageCheckTask.kt b/src/main/kotlin/com/exactpro/th2/check1/rule/nomessage/NoMessageCheckTask.kt new file mode 100644 index 00000000..c580df8a --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/check1/rule/nomessage/NoMessageCheckTask.kt @@ -0,0 +1,119 @@ +/* + * Copyright 2021-2021 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.th2.check1.rule.nomessage + +import com.exactpro.th2.check1.SessionKey +import com.exactpro.th2.check1.StreamContainer +import com.exactpro.th2.check1.entities.RuleConfiguration +import com.exactpro.th2.check1.grpc.PreFilter +import com.exactpro.th2.check1.rule.AbstractCheckTask +import com.exactpro.th2.check1.rule.MessageContainer +import com.exactpro.th2.check1.rule.SailfishFilter +import com.exactpro.th2.check1.rule.preFilterBy +import com.exactpro.th2.check1.util.VerificationUtil +import com.exactpro.th2.check1.utils.toRootMessageFilter +import com.exactpro.th2.common.event.Event +import com.exactpro.th2.common.event.EventUtils +import com.exactpro.th2.common.grpc.EventBatch +import com.exactpro.th2.common.grpc.EventID +import com.exactpro.th2.common.grpc.RootMessageFilter +import com.exactpro.th2.common.message.toTreeTable +import com.exactpro.th2.common.schema.message.MessageRouter +import io.reactivex.Observable +import java.time.Instant + +class NoMessageCheckTask( + ruleConfiguration: RuleConfiguration, + startTime: Instant, + sessionKey: SessionKey, + protoPreFilter: PreFilter, + parentEventID: EventID, + messageStream: Observable, + eventBatchRouter: MessageRouter +) : AbstractCheckTask(ruleConfiguration, startTime, sessionKey, parentEventID, messageStream, eventBatchRouter) { + + private val protoPreMessageFilter: RootMessageFilter = protoPreFilter.toRootMessageFilter() + private val messagePreFilter = SailfishFilter( + CONVERTER.fromProtoPreFilter(protoPreMessageFilter), + protoPreMessageFilter.toCompareSettings() + ) + + private val metadataPreFilter: SailfishFilter? = protoPreMessageFilter.metadataFilterOrNull()?.let { + SailfishFilter( + CONVERTER.fromMetadataFilter(it, VerificationUtil.METADATA_MESSAGE_NAME), + it.toComparisonSettings() + ) + } + + private lateinit var preFilterEvent: Event + private lateinit var resultEvent: Event + + private var extraMessagesCounter: Int = 0 + + + override fun onStart() { + super.onStart() + preFilterEvent = Event.start() + .type("preFiltering") + .bodyData(protoPreMessageFilter.toTreeTable()) + rootEvent.addSubEvent(preFilterEvent) + resultEvent = Event.start() + .type("noMessagesCheckResult") + rootEvent.addSubEvent(resultEvent) + } + + override fun Observable.taskPipeline(): Observable = + preFilterBy(this, protoPreMessageFilter, messagePreFilter, metadataPreFilter, LOGGER) { preFilterContainer -> // Update pre-filter state + with(preFilterContainer) { + preFilterEvent.appendEventsWithVerification(preFilterContainer) + preFilterEvent.messageID(protoActual.metadata.id) + } + } + + override fun name(): String = "No message check" + + override fun type(): String = "noMessageCheck" + + override fun setup(rootEvent: Event) { + rootEvent.bodyData(EventUtils.createMessageBean("No message check rule for messages from ${sessionKey.run { "$sessionAlias ($direction direction)" }}")) + } + + override fun onNext(messageContainer: MessageContainer) { + messageContainer.protoMessage.metadata.apply { + extraMessagesCounter++ + resultEvent.messageID(id) + } + } + + override fun completeEvent(taskState: State) { + preFilterEvent.name("Prefilter: $extraMessagesCounter messages were filtered.") + + if (extraMessagesCounter == 0) { + resultEvent.status(Event.Status.PASSED).name("Check passed") + } else { + resultEvent.status(Event.Status.FAILED) + .name("Check failed: $extraMessagesCounter extra messages were found.") + } + + if (taskState == State.TIMEOUT || taskState == State.STREAM_COMPLETED) { + val executionStopEvent = Event.start() + .name("Task has been completed because: ${taskState.name}") + .type("noMessageCheckExecutionStop") + if (taskState != State.TIMEOUT || !isCheckpointLastReceivedMessage()) { + executionStopEvent.status(Event.Status.FAILED) + } + resultEvent.addSubEvent(executionStopEvent) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/rule/sequence/SequenceCheckRuleTask.kt b/src/main/kotlin/com/exactpro/th2/check1/rule/sequence/SequenceCheckRuleTask.kt index f7495a08..5b1810fc 100644 --- a/src/main/kotlin/com/exactpro/th2/check1/rule/sequence/SequenceCheckRuleTask.kt +++ b/src/main/kotlin/com/exactpro/th2/check1/rule/sequence/SequenceCheckRuleTask.kt @@ -17,6 +17,7 @@ package com.exactpro.th2.check1.rule.sequence import com.exactpro.th2.check1.SessionKey import com.exactpro.th2.check1.StreamContainer +import com.exactpro.th2.check1.entities.RuleConfiguration import com.exactpro.th2.check1.event.CheckSequenceUtils import com.exactpro.th2.check1.event.bean.CheckSequenceRow import com.exactpro.th2.check1.grpc.PreFilter @@ -25,6 +26,7 @@ import com.exactpro.th2.check1.rule.AggregatedFilterResult import com.exactpro.th2.check1.rule.ComparisonContainer import com.exactpro.th2.check1.rule.MessageContainer import com.exactpro.th2.check1.rule.SailfishFilter +import com.exactpro.th2.check1.rule.preFilterBy import com.exactpro.th2.check1.util.VerificationUtil import com.exactpro.th2.common.event.Event import com.exactpro.th2.common.event.Event.Status.FAILED @@ -56,27 +58,25 @@ import kotlin.collections.set * If this parameter is set to `false`, the order won't be checked. */ class SequenceCheckRuleTask( - description: String?, + ruleConfiguration: RuleConfiguration, startTime: Instant, sessionKey: SessionKey, - timeout: Long, - maxEventBatchContentSize: Int, protoPreFilter: PreFilter, private val protoMessageFilters: List, private val checkOrder: Boolean, parentEventID: EventID, messageStream: Observable, eventBatchRouter: MessageRouter -) : AbstractCheckTask(description, timeout, maxEventBatchContentSize, startTime, sessionKey, parentEventID, messageStream, eventBatchRouter) { +) : AbstractCheckTask(ruleConfiguration, startTime, sessionKey, parentEventID, messageStream, eventBatchRouter) { private val protoPreMessageFilter: RootMessageFilter = protoPreFilter.toRootMessageFilter() private val messagePreFilter = SailfishFilter( - converter.fromProtoPreFilter(protoPreMessageFilter), + CONVERTER.fromProtoPreFilter(protoPreMessageFilter), protoPreMessageFilter.toCompareSettings() ) private val metadataPreFilter: SailfishFilter? = protoPreMessageFilter.metadataFilterOrNull()?.let { SailfishFilter( - converter.fromMetadataFilter(it, VerificationUtil.METADATA_MESSAGE_NAME), + CONVERTER.fromMetadataFilter(it, VerificationUtil.METADATA_MESSAGE_NAME), it.toComparisonSettings() ) } @@ -104,9 +104,9 @@ class SequenceCheckRuleTask( messageFilters = protoMessageFilters.map { MessageFilterContainer( it, - SailfishFilter(converter.fromProtoPreFilter(it), it.toCompareSettings()), + SailfishFilter(CONVERTER.fromProtoPreFilter(it), it.toCompareSettings()), it.metadataFilterOrNull()?.let { metadataFilter -> - SailfishFilter(converter.fromMetadataFilter(metadataFilter, VerificationUtil.METADATA_MESSAGE_NAME), + SailfishFilter(CONVERTER.fromMetadataFilter(metadataFilter, VerificationUtil.METADATA_MESSAGE_NAME), metadataFilter.toComparisonSettings()) } ) @@ -122,22 +122,14 @@ class SequenceCheckRuleTask( } override fun Observable.taskPipeline(): Observable = - map { messageContainer -> // Compare the message with pre-filter - if (LOGGER.isDebugEnabled) { - LOGGER.debug("Pre-filtering message with id: {}", shortDebugString(messageContainer.protoMessage.metadata.id)) - } - val result = matchFilter(messageContainer, messagePreFilter, metadataPreFilter, matchNames = false, significant = false) - ComparisonContainer(messageContainer, protoPreMessageFilter, result) - }.filter { preFilterContainer -> // Filter check result of pre-filter - preFilterContainer.fullyMatches - }.doOnNext { preFilterContainer -> // Update pre-filter state + preFilterBy(this, protoPreMessageFilter, messagePreFilter, metadataPreFilter, LOGGER) { preFilterContainer -> // Update pre-filter state with(preFilterContainer) { preFilterEvent.appendEventsWithVerification(preFilterContainer) preFilterEvent.messageID(protoActual.metadata.id) preFilteringResults[protoActual.metadata.id] = preFilterContainer } - }.map(ComparisonContainer::messageContainer) + } override fun onNext(messageContainer: MessageContainer) { for (index in messageFilters.indices) { @@ -175,7 +167,7 @@ class SequenceCheckRuleTask( } } - override fun completeEvent(canceled: Boolean) { + override fun completeEvent(taskState: State) { preFilterEvent.name("Pre-filtering (filtered ${preFilteringResults.size} / processed $handledMessageCounter) messages") fillSequenceEvent() diff --git a/src/main/kotlin/com/exactpro/th2/check1/rule/sequence/SilenceCheckTask.kt b/src/main/kotlin/com/exactpro/th2/check1/rule/sequence/SilenceCheckTask.kt new file mode 100644 index 00000000..0fdfb386 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/check1/rule/sequence/SilenceCheckTask.kt @@ -0,0 +1,146 @@ +/* + * Copyright 2021 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.th2.check1.rule.sequence + +import com.exactpro.th2.check1.SessionKey +import com.exactpro.th2.check1.StreamContainer +import com.exactpro.th2.check1.entities.RuleConfiguration +import com.exactpro.th2.check1.grpc.PreFilter +import com.exactpro.th2.check1.rule.AbstractCheckTask +import com.exactpro.th2.check1.rule.MessageContainer +import com.exactpro.th2.check1.rule.SailfishFilter +import com.exactpro.th2.check1.rule.preFilterBy +import com.exactpro.th2.check1.util.VerificationUtil +import com.exactpro.th2.check1.utils.toRootMessageFilter +import com.exactpro.th2.common.event.Event +import com.exactpro.th2.common.event.EventUtils.createMessageBean +import com.exactpro.th2.common.grpc.EventBatch +import com.exactpro.th2.common.grpc.EventID +import com.exactpro.th2.common.grpc.RootMessageFilter +import com.exactpro.th2.common.message.toReadableBodyCollection +import com.exactpro.th2.common.schema.message.MessageRouter +import io.reactivex.Observable +import java.time.Instant +import java.util.concurrent.atomic.AtomicBoolean + +class SilenceCheckTask( + ruleConfiguration: RuleConfiguration, + protoPreFilter: PreFilter, + submitTime: Instant, + sessionKey: SessionKey, + parentEventID: EventID, + messageStream: Observable, + eventBatchRouter: MessageRouter +) : AbstractCheckTask(ruleConfiguration, submitTime, sessionKey, parentEventID, messageStream, eventBatchRouter) { + private val protoPreMessageFilter: RootMessageFilter = protoPreFilter.toRootMessageFilter() + private val messagePreFilter = SailfishFilter( + CONVERTER.fromProtoPreFilter(protoPreMessageFilter), + protoPreMessageFilter.toCompareSettings() + ) + private val metadataPreFilter: SailfishFilter? = protoPreMessageFilter.metadataFilterOrNull()?.let { + SailfishFilter( + CONVERTER.fromMetadataFilter(it, VerificationUtil.METADATA_MESSAGE_NAME), + it.toComparisonSettings() + ) + } + private lateinit var preFilterEvent: Event + private lateinit var resultEvent: Event + private var extraMessagesCounter: Int = 0 + + @Volatile + private var started = false + private val isCanceled = AtomicBoolean() + + override fun onStart() { + super.onStart() + started = true + val hasNextTask = hasNextTask() + if (isParentCompleted == false || hasNextTask) { + if (hasNextTask) { + LOGGER.info("Has subscribed task. Skip checking extra messages") + } else { + LOGGER.info("Parent task was not finished normally. Skip checking extra messages") + } + cancel() + return + } + preFilterEvent = Event.start() + .type("preFiltering") + .bodyData(protoPreMessageFilter.toReadableBodyCollection()) + + rootEvent.addSubEvent(preFilterEvent) + + resultEvent = Event.start() + .type("noMessagesCheckResult") + rootEvent.addSubEvent(resultEvent) + } + + override fun onChainedTaskSubscription() { + if (started) { // because we cannot cancel task before it is actually started + cancel() + } else { + if (LOGGER.isInfoEnabled) { + LOGGER.info("The ${type()} task '$description' will be automatically canceled when it begins") + } + } + } + + override fun name(): String = "AutoSilenceCheck" + + override fun type(): String = "AutoSilenceCheck" + + override fun setup(rootEvent: Event) { + rootEvent.bodyData(createMessageBean("AutoSilenceCheck for session ${sessionKey.run { "$sessionAlias ($direction)" }}")) + } + + override fun Observable.taskPipeline(): Observable = + preFilterBy(this, protoPreMessageFilter, messagePreFilter, metadataPreFilter, LOGGER) { preFilterContainer -> // Update pre-filter state + with(preFilterContainer) { + preFilterEvent.appendEventsWithVerification(preFilterContainer) + preFilterEvent.messageID(protoActual.metadata.id) + } + } + + override fun onNext(container: MessageContainer) { + container.protoMessage.metadata.apply { + extraMessagesCounter++ + resultEvent.messageID(id) + } + } + + override fun completeEvent(taskState: State) { + if (skipPublication) { + return + } + preFilterEvent.name("Prefilter: $extraMessagesCounter messages were filtered.") + + if (extraMessagesCounter == 0) { + resultEvent.status(Event.Status.PASSED).name("Check passed") + } else { + resultEvent.status(Event.Status.FAILED) + .name("Check failed: $extraMessagesCounter extra messages were found.") + } + } + + override val skipPublication: Boolean + get() = isCanceled.get() + + private fun cancel() { + if (isCanceled.compareAndSet(false, true)) { + checkComplete() + } else { + LOGGER.debug("Task {} '{}' already canceled", type(), description) + } + } +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/utils/FilterUtils.kt b/src/main/kotlin/com/exactpro/th2/check1/utils/FilterUtils.kt new file mode 100644 index 00000000..f0cbe93e --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/check1/utils/FilterUtils.kt @@ -0,0 +1,37 @@ +/* + * Copyright 2021-2021 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.th2.check1.utils + +import com.exactpro.sf.comparison.ComparisonResult +import com.exactpro.sf.comparison.ComparisonUtil +import com.exactpro.sf.scriptrunner.StatusType +import com.exactpro.th2.check1.rule.AggregatedFilterResult +import com.exactpro.th2.common.grpc.RootMessageFilter + +object FilterUtils { + @JvmStatic + val ComparisonResult?.fullMatch: Boolean + get() = this != null && getStatusType() != StatusType.FAILED + + @JvmStatic + fun allMatches( + aggregatedFilterResult: AggregatedFilterResult, + protoFilter: RootMessageFilter, + condition: (ComparisonResult?) -> Boolean + ): Boolean = condition(aggregatedFilterResult.messageResult) && (!protoFilter.hasMetadataFilter() || condition( + aggregatedFilterResult.metadataResult + )) +} + +fun ComparisonResult.getStatusType(): StatusType = ComparisonUtil.getStatusType(this) \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/utils/ProtoMessageUtils.kt b/src/main/kotlin/com/exactpro/th2/check1/utils/ProtoMessageUtils.kt new file mode 100644 index 00000000..5eb51751 --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/check1/utils/ProtoMessageUtils.kt @@ -0,0 +1,100 @@ +/* + * Copyright 2021-2021 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.th2.check1.utils + +import com.exactpro.sf.common.messages.IMessage +import com.exactpro.th2.check1.SessionKey +import com.exactpro.th2.check1.grpc.PreFilter +import com.exactpro.th2.check1.rule.sequence.SequenceCheckRuleTask +import com.exactpro.th2.common.grpc.Checkpoint +import com.exactpro.th2.common.grpc.Checkpoint.CheckpointData +import com.exactpro.th2.common.grpc.Checkpoint.DirectionCheckpoint +import com.exactpro.th2.common.grpc.Direction +import com.exactpro.th2.common.grpc.MessageFilter +import com.exactpro.th2.common.grpc.RootMessageFilter +import com.exactpro.th2.sailfish.utils.ProtoToIMessageConverter +import mu.KotlinLogging +import com.exactpro.th2.check1.entities.Checkpoint as InternalCheckpoint +import com.exactpro.th2.check1.entities.CheckpointData as InternalCheckpointData + +val LOGGER = KotlinLogging.logger {} + +fun ProtoToIMessageConverter.fromProtoPreFilter(protoPreMessageFilter: RootMessageFilter): IMessage = + fromProtoFilter(protoPreMessageFilter.messageFilter, SequenceCheckRuleTask.PRE_FILTER_MESSAGE_NAME) + +fun PreFilter.toRootMessageFilter(): RootMessageFilter = RootMessageFilter.newBuilder() + .setMessageType(SequenceCheckRuleTask.PRE_FILTER_MESSAGE_NAME) + .setMessageFilter(toMessageFilter()) + .also { + if (hasMetadataFilter()) { + it.metadataFilter = metadataFilter + } + } + .build() + +fun PreFilter.toMessageFilter(): MessageFilter = MessageFilter.newBuilder() + .putAllFields(fieldsMap) + .build() + +fun CheckpointData.convert(): InternalCheckpointData = InternalCheckpointData(sequence, timestamp) + +fun InternalCheckpointData.convert(): CheckpointData { + val builder = CheckpointData.newBuilder().setSequence(sequence) + if (timestamp != null) + builder.timestamp = timestamp + return builder.build() +} + +fun InternalCheckpoint.convert(): Checkpoint { + val intermediateMap: MutableMap = HashMap() + sessionKeyToCheckpointData.forEach { (sessionKey, checkpointData) -> + intermediateMap.computeIfAbsent(sessionKey.sessionAlias) { + DirectionCheckpoint.newBuilder() + }.apply { + sessionKey.direction.number.run { + putDirectionToCheckpointData(this, checkpointData.convert()) + putDirectionToSequence(this, checkpointData.sequence) + } + } + } + + val checkpointBuilder = Checkpoint.newBuilder().setId(id) + intermediateMap.forEach { (sessionAlias, directionCheckpoint) -> + checkpointBuilder.putSessionAliasToDirectionCheckpoint(sessionAlias, directionCheckpoint.build()) + } + + return checkpointBuilder.build() +} + +fun Checkpoint.convert(): InternalCheckpoint { + val sessionKeyToSequence: MutableMap = HashMap() + sessionAliasToDirectionCheckpointMap.forEach { (sessionAlias, directionCheckpoint) -> + if (directionCheckpoint.run { directionToCheckpointDataCount != 0 && directionToSequenceCount != 0 }) { + LOGGER.warn("Session alias '{}' contains both of these fields: 'direction to checkpoint data' and 'direction to sequence'. Please use 'direction to checkpoint data' instead", sessionAlias) + } + if (directionCheckpoint.directionToCheckpointDataCount == 0) { + directionCheckpoint.directionToSequenceMap.forEach { (directionNumber, sequence) -> + val sessionKey = SessionKey(sessionAlias, Direction.forNumber(directionNumber)) + sessionKeyToSequence[sessionKey] = InternalCheckpointData(sequence, null) + } + } else { + directionCheckpoint.directionToCheckpointDataMap.forEach { (directionNumber, checkpointData) -> + val sessionKey = SessionKey(sessionAlias, Direction.forNumber(directionNumber)) + sessionKeyToSequence[sessionKey] = checkpointData.convert() + + } + } + } + return InternalCheckpoint(id, sessionKeyToSequence) +} \ No newline at end of file diff --git a/src/main/kotlin/com/exactpro/th2/check1/utils/TimeUtils.kt b/src/main/kotlin/com/exactpro/th2/check1/utils/TimeUtils.kt new file mode 100644 index 00000000..20c4ca5a --- /dev/null +++ b/src/main/kotlin/com/exactpro/th2/check1/utils/TimeUtils.kt @@ -0,0 +1,22 @@ +/* + * Copyright 2021-2021 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.th2.check1.utils + +import com.google.protobuf.Timestamp + +private val timestampComparator = Comparator.comparingLong(Timestamp::getSeconds).thenComparingInt(Timestamp::getNanos) +operator fun Timestamp.compareTo(other: Timestamp): Int = timestampComparator.compare(this, other) + +fun Timestamp.isBefore(other: Timestamp): Boolean = this < other +fun Timestamp.isAfter(other: Timestamp): Boolean = this > other \ No newline at end of file diff --git a/src/test/java/com/exactpro/th2/check1/CheckpointTest.java b/src/test/java/com/exactpro/th2/check1/CheckpointTest.java index 038a129b..8b1d84de 100644 --- a/src/test/java/com/exactpro/th2/check1/CheckpointTest.java +++ b/src/test/java/com/exactpro/th2/check1/CheckpointTest.java @@ -12,37 +12,48 @@ */ package com.exactpro.th2.check1; -import java.util.Map; - +import com.exactpro.th2.check1.entities.Checkpoint; +import com.exactpro.th2.check1.entities.CheckpointData; +import com.exactpro.th2.check1.utils.ProtoMessageUtilsKt; +import com.exactpro.th2.common.grpc.Direction; +import com.google.protobuf.Timestamp; import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Test; -import com.exactpro.th2.common.grpc.Direction; +import java.util.Map; public class CheckpointTest { @Test public void testConvertation() { var origCheckpoint = new Checkpoint(Map.of( - new SessionKey("A", Direction.FIRST), 1L, - new SessionKey("A", Direction.SECOND), 2L, - new SessionKey("B", Direction.FIRST), 3L, - new SessionKey("B", Direction.SECOND), 4L + new SessionKey("A", Direction.FIRST), generateCheckpointData(1L), + new SessionKey("A", Direction.SECOND), generateCheckpointData(2L), + new SessionKey("B", Direction.FIRST), generateCheckpointData(3L), + new SessionKey("B", Direction.SECOND), generateCheckpointData(4L) )); - var protoCheckpoint = origCheckpoint.convert(); + var protoCheckpoint = ProtoMessageUtilsKt.convert(origCheckpoint); - var parsedCheckpoint = Checkpoint.convert(protoCheckpoint); + var parsedCheckpoint = ProtoMessageUtilsKt.convert(protoCheckpoint); Assertions.assertEquals(origCheckpoint, parsedCheckpoint); } private Checkpoint generateCheckpoint() { return new Checkpoint(Map.of( - new SessionKey("A", Direction.FIRST), 1L, - new SessionKey("A", Direction.FIRST), 2L, - new SessionKey("B", Direction.SECOND), 3L, - new SessionKey("B", Direction.SECOND), 4L + new SessionKey("A", Direction.FIRST), generateCheckpointData(1L), + new SessionKey("A", Direction.FIRST), generateCheckpointData(2L), + new SessionKey("B", Direction.SECOND), generateCheckpointData(3L), + new SessionKey("B", Direction.SECOND), generateCheckpointData(4L) )); } + + private CheckpointData generateCheckpointData(Long sequence, Timestamp timestamp) { + return new CheckpointData(sequence, timestamp); + } + + private CheckpointData generateCheckpointData(Long sequence) { + return generateCheckpointData(sequence, Timestamp.getDefaultInstance()); + } } \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/check1/entities/RuleConfigurationTest.kt b/src/test/kotlin/com/exactpro/th2/check1/entities/RuleConfigurationTest.kt new file mode 100644 index 00000000..355350e2 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/check1/entities/RuleConfigurationTest.kt @@ -0,0 +1,82 @@ +/* + * Copyright 2020-2021 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.th2.check1.entities + +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertThrows +import java.time.Duration +import kotlin.test.assertEquals + +class RuleConfigurationTest { + + @Test + fun `check that time precision is negative`() { + val exception = assertThrows { + RuleConfiguration( + TaskTimeout(0, 0), null, Duration.ofSeconds(-1), + 0.005, + 1, + true + ) + } + assertEquals(exception.message, "Time precision cannot be negative") + } + + @Test + fun `check that decimal precision is negative`() { + val exception = assertThrows { + RuleConfiguration( + TaskTimeout(0, 0), null, Duration.ofSeconds(1), + -0.005, + 1, + true + ) + } + assertEquals(exception.message, "Decimal precision cannot be negative") + } + + @Test + fun `check that max event batch content size is negative`() { + val maxEventBatchContentSize = -1 + + val exception = assertThrows { + RuleConfiguration( + TaskTimeout(0, 0), + null, + Duration.ofSeconds(1), + 0.005, + maxEventBatchContentSize, + true + ) + } + assertEquals(exception.message, "'maxEventBatchContentSize' should be greater than zero, actual: $maxEventBatchContentSize") + } + + @Test + fun `check that task timeout is negative`() { + val timeout = -1L + + val exception = assertThrows { + RuleConfiguration( + TaskTimeout(timeout, 0), + null, + Duration.ofSeconds(1), + 0.005, + 1, + true + ) + } + assertEquals(exception.message, "'timeout' should be set or be greater than zero, actual: $timeout") + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/check1/event/TestVerificationEntryUtils.kt b/src/test/kotlin/com/exactpro/th2/check1/event/TestVerificationEntryUtils.kt index a9e8157d..61d13183 100644 --- a/src/test/kotlin/com/exactpro/th2/check1/event/TestVerificationEntryUtils.kt +++ b/src/test/kotlin/com/exactpro/th2/check1/event/TestVerificationEntryUtils.kt @@ -1,5 +1,5 @@ /* - * Copyright 2020-2020 Exactpro (Exactpro Systems Limited) + * Copyright 2020-2021 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 @@ -15,22 +15,67 @@ package com.exactpro.th2.check1.event import com.exactpro.sf.comparison.ComparatorSettings import com.exactpro.sf.comparison.MessageComparator +import com.exactpro.th2.check1.rule.AbstractCheckTask import com.exactpro.th2.check1.util.VerificationUtil import com.exactpro.th2.common.event.bean.VerificationEntry +import com.exactpro.th2.common.grpc.FilterOperation import com.exactpro.th2.common.grpc.ListValueFilter import com.exactpro.th2.common.grpc.MessageFilter import com.exactpro.th2.common.grpc.RootMessageFilter +import com.exactpro.th2.common.grpc.Value import com.exactpro.th2.common.grpc.ValueFilter import com.exactpro.th2.common.message.message +import com.exactpro.th2.common.message.messageFilter +import com.exactpro.th2.common.value.nullValue import com.exactpro.th2.common.value.toValue +import com.exactpro.th2.common.value.toValueFilter import com.exactpro.th2.sailfish.utils.ProtoToIMessageConverter +import com.exactpro.th2.sailfish.utils.ProtoToIMessageConverter.createParameters import com.fasterxml.jackson.databind.ObjectMapper import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream class TestVerificationEntryUtils { - private val converter = ProtoToIMessageConverter(VerificationUtil.FACTORY_PROXY, null, null) + private val converter = AbstractCheckTask.CONVERTER + + @Test + fun `null value in message`() { + val filter = RootMessageFilter.newBuilder() + .setMessageType("test") + .setMessageFilter( + MessageFilter.newBuilder() + .putFields("A", ValueFilter.newBuilder().setOperation(FilterOperation.EMPTY).build()) + ).build() + + val message = message("test").putFields("A", nullValue()).putFields("B", nullValue()).build() + + val expected = converter.fromProtoFilter(filter.messageFilter, "test") + val actual = converter.fromProtoMessage(message, false) + val settings = ComparatorSettings().apply { + isKeepResultGroupOrder = true + } + + val result = MessageComparator.compare(actual, expected, settings).assertNotNull { + "Result must not be null" + } + val entry = VerificationEntryUtils.createVerificationEntry(result) + entry.fields["A"].assertNotNull { "Field A must be set in entry: ${entry.toDebugString()}" } + .also { + Assertions.assertEquals("#", it.expected) { "Expected value is different in entry: ${it.toDebugString()}" } + Assertions.assertNull(it.actual) { "Actual value must be null in entry: ${it.toDebugString()}" } + } + entry.fields["B"].assertNotNull { "Field B must be set in entry: ${entry.toDebugString()}" } + .also { + Assertions.assertNull(it.expected) { "Expected value must be null in entry: ${it.toDebugString()}" } + Assertions.assertNull(it.actual) { "Actual value must be null in entry: ${it.toDebugString()}" } + } + } @Test fun `key field in reordered collection`() { @@ -188,11 +233,161 @@ class TestVerificationEntryUtils { Assertions.assertTrue(msgEntry.isKey) { "msg is not a key field in ${msgEntry.toDebugString()}" } } + @Test + fun `expected value converted as null value`() { + val filter: RootMessageFilter = RootMessageFilter.newBuilder() + .setMessageType("Test") + .setMessageFilter(MessageFilter.newBuilder() + .putFields("A", "1".toValueFilter()) + .build()) + .build() + + val actual = message("Test").apply { + putFields("A", "1".toValue()) + putFields("B", "2".toValue()) + }.build() + + val container = VerificationUtil.toMetaContainer(filter.messageFilter, false) + val settings = ComparatorSettings().apply { + metaContainer = container + } + + val actualIMessage = converter.fromProtoMessage(actual, false) + val filterIMessage = converter.fromProtoFilter(filter.messageFilter, filter.messageType) + val result = MessageComparator.compare( + actualIMessage, + filterIMessage, + settings + ) + + val entry = VerificationEntryUtils.createVerificationEntry(result) + val keyEntry = entry.fields["B"].assertNotNull { "The key 'B' is missing in ${entry.toDebugString()}" } + Assertions.assertNull(keyEntry.expected) { "Expected value should be null" } + } + + @Test + fun `actual value converted as null value`() { + val filter: RootMessageFilter = RootMessageFilter.newBuilder() + .setMessageType("Test") + .setMessageFilter(MessageFilter.newBuilder() + .putFields("A", "1".toValueFilter()) + .putFields("B", "2".toValueFilter()) + .build()) + .build() + + val actual = message("Test").apply { + putFields("A", "1".toValue()) + }.build() + + val container = VerificationUtil.toMetaContainer(filter.messageFilter, false) + val settings = ComparatorSettings().apply { + metaContainer = container + } + + val actualIMessage = converter.fromProtoMessage(actual, false) + val filterIMessage = converter.fromProtoFilter(filter.messageFilter, filter.messageType) + val result = MessageComparator.compare( + actualIMessage, + filterIMessage, + settings + ) + + val entry = VerificationEntryUtils.createVerificationEntry(result) + val keyEntry = entry.fields["B"].assertNotNull { "The key 'B' is missing in ${entry.toDebugString()}" } + Assertions.assertNull(keyEntry.actual) { "Actual value should be null" } + } + + @ParameterizedTest + @MethodSource("unexpectedTypeMismatch") + fun `verify messages with different value type`(actualValue: Value, expectedValueFilter: ValueFilter, expectedHint: String?) { + val filter: RootMessageFilter = RootMessageFilter.newBuilder() + .setMessageType("Test") + .setMessageFilter( + messageFilter().apply { + putFields("A", "1".toValueFilter()) + putFields("B", expectedValueFilter) + }.build()) + .build() + + val actual = message("Test").apply { + putFields("A", "1".toValue()) + putFields("B", actualValue) + }.build() + + val actualIMessage = converter.fromProtoMessage(actual, false) + val filterIMessage = converter.fromProtoFilter(filter.messageFilter, filter.messageType) + val result = MessageComparator.compare( + actualIMessage, + filterIMessage, + ComparatorSettings() + ) + + val entry = VerificationEntryUtils.createVerificationEntry(result) + val keyEntry = entry.fields["B"].assertNotNull { "The key 'B' is missing in ${entry.toDebugString()}" } + Assertions.assertEquals(expectedHint, keyEntry.hint, "Hint must be equal") + } + + companion object { private fun VerificationEntry.toDebugString(): String = ObjectMapper().writeValueAsString(this) private fun T?.assertNotNull(msg: () -> String): T { Assertions.assertNotNull(this, msg) return this!! } + + @JvmStatic + fun unexpectedTypeMismatch(): Stream = Stream.of( + arguments("2".toValue(), "2".toValueFilter(), null), + arguments( + message().putFields("A", "1".toValue()).toValue(), + messageFilter().putFields("A", "1".toValueFilter()).toValueFilter(), + null + ), + arguments( + listOf("2".toValue()).toValue(), + listOf("2".toValueFilter()).toValueFilter(), + null + ), + arguments( + "2".toValue(), + messageFilter().putFields("A", "1".toValueFilter()).toValueFilter(), + "Value type mismatch - actual: String, expected: Message" + ), + arguments( + "2".toValue(), + listOf(messageFilter().putFields("A", "1".toValueFilter())).toValueFilter(), + "Value type mismatch - actual: String, expected: Collection of Messages" + ), + arguments( + message().putFields("A", "1".toValue()).toValue(), + listOf(messageFilter().putFields("A", "1".toValueFilter())).toValueFilter(), + "Value type mismatch - actual: Message, expected: Collection of Messages" + ), + arguments( + "2".toValue(), + listOf("2".toValueFilter()).toValueFilter(), + "Value type mismatch - actual: String, expected: Collection" + ), + arguments( + message().putFields("A", "1".toValue()).toValue(), + "2".toValueFilter(), + "Value type mismatch - actual: Message, expected: String" + ), + arguments( + listOf(message().putFields("A", "1".toValue()).build()).toValue(), + "2".toValueFilter(), + "Value type mismatch - actual: Collection of Messages, expected: String" + ), + arguments( + listOf(message().putFields("A", "1".toValue()).build()).toValue(), + messageFilter().putFields("A", "1".toValueFilter()).toValueFilter(), + "Value type mismatch - actual: Collection of Messages, expected: Message" + ), + arguments( + message().putFields("A", "1".toValue()).toValue(), + listOf("2".toValueFilter()).toValueFilter(), + "Value type mismatch - actual: Message, expected: Collection" + ) + ) } } \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/check1/rule/AbstractCheckTaskTest.kt b/src/test/kotlin/com/exactpro/th2/check1/rule/AbstractCheckTaskTest.kt index e1d9cda0..695a89f3 100644 --- a/src/test/kotlin/com/exactpro/th2/check1/rule/AbstractCheckTaskTest.kt +++ b/src/test/kotlin/com/exactpro/th2/check1/rule/AbstractCheckTaskTest.kt @@ -14,29 +14,42 @@ package com.exactpro.th2.check1.rule import com.exactpro.th2.check1.SessionKey import com.exactpro.th2.check1.StreamContainer +import com.exactpro.th2.check1.configuration.Check1Configuration +import com.exactpro.th2.check1.entities.RuleConfiguration +import com.exactpro.th2.check1.entities.TaskTimeout +import com.exactpro.th2.check1.grpc.PreFilter +import com.exactpro.th2.common.event.EventUtils import com.exactpro.th2.common.event.IBodyData import com.exactpro.th2.common.event.bean.Verification import com.exactpro.th2.common.event.bean.VerificationEntry +import com.exactpro.th2.common.grpc.Checkpoint import com.exactpro.th2.common.grpc.Direction import com.exactpro.th2.common.grpc.Direction.FIRST import com.exactpro.th2.common.grpc.Event import com.exactpro.th2.common.grpc.EventBatch +import com.exactpro.th2.common.grpc.EventID +import com.exactpro.th2.common.grpc.FilterOperation import com.exactpro.th2.common.grpc.Message +import com.exactpro.th2.common.grpc.ValueFilter +import com.exactpro.th2.common.message.toTimestamp import com.exactpro.th2.common.schema.message.MessageRouter import com.fasterxml.jackson.annotation.JsonSubTypes import com.fasterxml.jackson.annotation.JsonTypeInfo import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper import com.fasterxml.jackson.module.kotlin.readValue +import com.google.protobuf.Timestamp import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.spy import com.nhaarman.mockitokotlin2.timeout import com.nhaarman.mockitokotlin2.verify import io.reactivex.Observable +import java.time.Instant import kotlin.test.assertEquals import kotlin.test.assertNotNull abstract class AbstractCheckTaskTest { protected val clientStub: MessageRouter = spy { } + protected val configuration = Check1Configuration() fun awaitEventBatchRequest(timeoutValue: Long = 1000L, times: Int): List { val argumentCaptor = argumentCaptor() @@ -50,9 +63,16 @@ abstract class AbstractCheckTaskTest { ) } - fun constructMessage(sequence: Long = 0, alias: String = SESSION_ALIAS, type: String = MESSAGE_TYPE, direction: Direction = FIRST): Message.Builder = Message.newBuilder().apply { + fun constructMessage( + sequence: Long = 0, + alias: String = SESSION_ALIAS, + type: String = MESSAGE_TYPE, + direction: Direction = FIRST, + timestamp: Timestamp = Timestamp.getDefaultInstance() + ): Message.Builder = Message.newBuilder().apply { metadataBuilder.apply { - messageType = type + this.messageType = type + this.timestamp = timestamp idBuilder.apply { this.sequence = sequence this.direction = direction @@ -61,6 +81,39 @@ abstract class AbstractCheckTaskTest { } } + protected fun createEvent(id: String): EventID { + return requireNotNull(EventUtils.toEventID(id)) + } + + protected fun getMessageTimestamp(start: Instant, delta: Long): Timestamp = + start.plusMillis(delta).toTimestamp() + + protected fun createCheckpoint(timestamp: Instant? = null, sequence: Long = -1) : Checkpoint = + Checkpoint.newBuilder().apply { + putSessionAliasToDirectionCheckpoint( + SESSION_ALIAS, + Checkpoint.DirectionCheckpoint.newBuilder().apply { + putDirectionToCheckpointData( + FIRST.number, + Checkpoint.CheckpointData.newBuilder().apply { + this.sequence = sequence + if (timestamp != null) { + this.timestamp = timestamp.toTimestamp() + } + }.build() + ) + }.build() + ) + }.build() + + protected fun createPreFilter(fieldName: String, value: String, operation: FilterOperation): PreFilter = + PreFilter.newBuilder() + .putFields(fieldName, ValueFilter.newBuilder().setSimpleFilter(value).setKey(true).setOperation(operation).build()) + .build() + + protected fun List.findEventByType(eventType: String): Event? = + this.find { it.type == eventType } + protected fun extractEventBody(verificationEvent: Event): List { return jacksonObjectMapper() .addMixIn(IBodyData::class.java, IBodyDataMixIn::class.java) @@ -103,6 +156,17 @@ abstract class AbstractCheckTaskTest { } } + protected fun createRuleConfiguration(taskTimeout: TaskTimeout, description: String = "Test", maxEventBatchContentSize: Int = 1024 * 1024): RuleConfiguration { + return RuleConfiguration( + taskTimeout, + description, + configuration.timePrecision, + configuration.decimalPrecision, + maxEventBatchContentSize, + true + ) + } + @JsonSubTypes(value = [ JsonSubTypes.Type(value = Verification::class, name = Verification.TYPE), diff --git a/src/test/kotlin/com/exactpro/th2/check1/rule/RuleFactoryTest.kt b/src/test/kotlin/com/exactpro/th2/check1/rule/RuleFactoryTest.kt index 692c1026..fc29377d 100644 --- a/src/test/kotlin/com/exactpro/th2/check1/rule/RuleFactoryTest.kt +++ b/src/test/kotlin/com/exactpro/th2/check1/rule/RuleFactoryTest.kt @@ -15,16 +15,22 @@ package com.exactpro.th2.check1.rule import com.exactpro.th2.check1.SessionKey import com.exactpro.th2.check1.StreamContainer +import com.exactpro.th2.check1.configuration.Check1Configuration import com.exactpro.th2.check1.exception.RuleCreationException +import com.exactpro.th2.check1.grpc.ChainID import com.exactpro.th2.check1.grpc.CheckRuleRequest +import com.exactpro.th2.check1.util.assertThrowsWithMessages import com.exactpro.th2.common.event.EventUtils import com.exactpro.th2.common.grpc.Checkpoint +import com.exactpro.th2.common.grpc.ConnectionID import com.exactpro.th2.common.grpc.Direction import com.exactpro.th2.common.grpc.EventBatch import com.exactpro.th2.common.grpc.EventID import com.exactpro.th2.common.grpc.Message import com.exactpro.th2.common.grpc.MessageMetadata +import com.exactpro.th2.common.grpc.RootMessageFilter import com.exactpro.th2.common.message.message +import com.exactpro.th2.common.message.toTimestamp import com.exactpro.th2.common.schema.message.MessageRouter import com.nhaarman.mockitokotlin2.argumentCaptor import com.nhaarman.mockitokotlin2.spy @@ -33,14 +39,16 @@ import com.nhaarman.mockitokotlin2.verify import io.reactivex.Observable import org.junit.jupiter.api.Test import org.junit.jupiter.api.assertAll -import org.junit.jupiter.api.assertThrows +import org.junit.jupiter.api.assertDoesNotThrow +import java.time.Instant import kotlin.test.assertEquals +import kotlin.test.assertNotNull class RuleFactoryTest { private val clientStub: MessageRouter = spy { } @Test - fun `failed rule creation because one of required fields is empty`() { + fun `failed rule creation because session alias is empty`() { val streams = createStreams(AbstractCheckTaskTest.SESSION_ALIAS, Direction.FIRST, listOf( message(AbstractCheckTaskTest.MESSAGE_TYPE, Direction.FIRST, AbstractCheckTaskTest.SESSION_ALIAS) .mergeMetadata(MessageMetadata.newBuilder() @@ -50,16 +58,362 @@ class RuleFactoryTest { .build() )) - val ruleFactory = RuleFactory(1024 * 1024, streams, clientStub) + val ruleFactory = RuleFactory(Check1Configuration(), streams, clientStub) val request = CheckRuleRequest.newBuilder() .setParentEventId(EventID.newBuilder().setId("root").build()) .setCheckpoint(Checkpoint.newBuilder().setId(EventUtils.generateUUID()).build()).build() - assertThrows { - ruleFactory.createCheckRule(request) + assertThrowsWithMessages( + "An error occurred while creating rule", + "Session alias cannot be empty" + ) { ruleFactory.createCheckRule(request, true) } + + assertEvents() + } + + @Test + fun `success rule creation with missed checkpoint`() { + val streams = createStreams(AbstractCheckTaskTest.SESSION_ALIAS, Direction.FIRST, listOf( + message(AbstractCheckTaskTest.MESSAGE_TYPE, Direction.FIRST, AbstractCheckTaskTest.SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .build()) + .build() + )) + + val ruleFactory = RuleFactory(Check1Configuration(), streams, clientStub) + + val request = CheckRuleRequest.newBuilder() + .setParentEventId(EventID.newBuilder().setId("root").build()) + .setConnectivityId(ConnectionID.newBuilder() + .setSessionAlias("test_alias") + ) + .setRootFilter(RootMessageFilter.newBuilder() + .setMessageType("TestMsgType") + ) + .setMessageTimeout(5) + .setChainId(ChainID.newBuilder().setId("test_chain_id")) + .build() + + val createCheckRule = assertDoesNotThrow { + ruleFactory.createCheckRule(request, true) } + assertNotNull(createCheckRule) { "Rule cannot be null" } + } + + @Test + fun `failed rule creation with missed checkpoint and invalid chain id`() { + val streams = createStreams(AbstractCheckTaskTest.SESSION_ALIAS, Direction.FIRST, listOf( + message(AbstractCheckTaskTest.MESSAGE_TYPE, Direction.FIRST, AbstractCheckTaskTest.SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .build()) + .build() + )) + + val ruleFactory = RuleFactory(Check1Configuration(), streams, clientStub) + + val request = CheckRuleRequest.newBuilder() + .setParentEventId(EventID.newBuilder().setId("root").build()) + .setConnectivityId(ConnectionID.newBuilder() + .setSessionAlias("test_alias") + ) + .setRootFilter(RootMessageFilter.newBuilder() + .setMessageType("TestMsgType") + ) + .setMessageTimeout(5) + .setChainId(ChainID.newBuilder().setId("test_chain_id")) + .build() + + assertThrowsWithMessages( + "An error occurred while creating rule", + "The request has an invalid chain ID or connectivity ID. Please use checkpoint instead of chain ID" + ) { ruleFactory.createCheckRule(request, false) } + + assertEvents() + } + + @Test + fun `success rule creation with missed chain id`() { + val streams = createStreams(AbstractCheckTaskTest.SESSION_ALIAS, Direction.FIRST, listOf( + message(AbstractCheckTaskTest.MESSAGE_TYPE, Direction.FIRST, AbstractCheckTaskTest.SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .build()) + .build() + )) + + val ruleFactory = RuleFactory(Check1Configuration(), streams, clientStub) + + val request = CheckRuleRequest.newBuilder() + .setParentEventId(EventID.newBuilder().setId("root").build()) + .setConnectivityId(ConnectionID.newBuilder() + .setSessionAlias("test_alias") + ) + .setRootFilter(RootMessageFilter.newBuilder() + .setMessageType("TestMsgType") + ) + .setMessageTimeout(5) + .setCheckpoint( + Checkpoint.newBuilder() + .setId(EventUtils.generateUUID()) + .putSessionAliasToDirectionCheckpoint( + "test_alias", + Checkpoint.DirectionCheckpoint.newBuilder() + .putDirectionToCheckpointData( + Direction.FIRST.number, + Checkpoint.CheckpointData.newBuilder() + .setSequence(1) + .setTimestamp(Instant.now().toTimestamp()) + .build()) + .build()) + .build() + ) + .setDirection(Direction.FIRST) + .build() + + + val createCheckRule = assertDoesNotThrow { + ruleFactory.createCheckRule(request, false) + } + assertNotNull(createCheckRule) { "Rule cannot be null" } + } + + @Test + fun `failed rule creation because direction checkpoint is missed`() { + val sessionAlias = "diff_test_alias" + val streams = createStreams(AbstractCheckTaskTest.SESSION_ALIAS, Direction.FIRST, listOf( + message(AbstractCheckTaskTest.MESSAGE_TYPE, Direction.FIRST, AbstractCheckTaskTest.SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .build()) + .build() + )) + + val ruleFactory = RuleFactory(Check1Configuration(), streams, clientStub) + + val request = CheckRuleRequest.newBuilder() + .setParentEventId(EventID.newBuilder().setId("root").build()) + .setConnectivityId(ConnectionID.newBuilder() + .setSessionAlias(sessionAlias) + ) + .setRootFilter(RootMessageFilter.newBuilder() + .setMessageType("TestMsgType") + ) + .setMessageTimeout(5) + .setCheckpoint( + Checkpoint.newBuilder() + .setId(EventUtils.generateUUID()) + .putSessionAliasToDirectionCheckpoint( + "test_alias", + Checkpoint.DirectionCheckpoint.newBuilder() + .putDirectionToCheckpointData( + Direction.FIRST.number, + Checkpoint.CheckpointData.newBuilder() + .setSequence(1) + .setTimestamp(Instant.now().toTimestamp()) + .build()) + .build()) + .build() + ) + .setDirection(Direction.FIRST) + .build() + + assertThrowsWithMessages( + "An error occurred while creating rule", + "The checkpoint doesn't contain a direction checkpoint with session alias '$sessionAlias'" + ) { ruleFactory.createCheckRule(request, true) } + + assertEvents() + } + + @Test + fun `failed rule creation because checkpoint is missed`() { + val streams = createStreams(AbstractCheckTaskTest.SESSION_ALIAS, Direction.FIRST, listOf( + message(AbstractCheckTaskTest.MESSAGE_TYPE, Direction.FIRST, AbstractCheckTaskTest.SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .build()) + .build() + )) + + val ruleFactory = RuleFactory(Check1Configuration(), streams, clientStub) + + val request = CheckRuleRequest.newBuilder() + .setParentEventId(EventID.newBuilder().setId("root").build()) + .setConnectivityId(ConnectionID.newBuilder() + .setSessionAlias("test_alias") + ) + .setRootFilter(RootMessageFilter.newBuilder() + .setMessageType("TestMsgType") + ) + .setMessageTimeout(5) + .setDirection(Direction.FIRST) + .build() + + assertThrowsWithMessages( + "An error occurred while creating rule", + "Request must contain a checkpoint, because the 'messageTimeout' is used and no chain ID is specified" + ) { ruleFactory.createCheckRule(request, true) } + + assertEvents() + } + + @Test + fun `failed rule creation because checkpoint data is missed`() { + val sessionAlias = "test_alias" + val direction = Direction.SECOND + val streams = createStreams(AbstractCheckTaskTest.SESSION_ALIAS, Direction.FIRST, listOf( + message(AbstractCheckTaskTest.MESSAGE_TYPE, Direction.FIRST, AbstractCheckTaskTest.SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .build()) + .build() + )) + + val ruleFactory = RuleFactory(Check1Configuration(), streams, clientStub) + + val request = CheckRuleRequest.newBuilder() + .setParentEventId(EventID.newBuilder().setId("root").build()) + .setConnectivityId(ConnectionID.newBuilder() + .setSessionAlias(sessionAlias) + ) + .setRootFilter(RootMessageFilter.newBuilder() + .setMessageType("TestMsgType") + ) + .setMessageTimeout(5) + .setCheckpoint( + Checkpoint.newBuilder() + .setId(EventUtils.generateUUID()) + .putSessionAliasToDirectionCheckpoint( + sessionAlias, + Checkpoint.DirectionCheckpoint.newBuilder() + .putDirectionToCheckpointData( + Direction.FIRST.number, + Checkpoint.CheckpointData.newBuilder() + .setSequence(1) + .setTimestamp(Instant.now().toTimestamp()) + .build()) + .build()) + .build() + ) + .setDirection(direction) + .build() + + assertThrowsWithMessages( + "An error occurred while creating rule", + "The direction checkpoint doesn't contain a checkpoint data with direction '$direction'" + ) { ruleFactory.createCheckRule(request, true) } + + assertEvents() + } + + @Test + fun `failed rule creation because checkpoint data has incorrect sequence number`() { + val sessionAlias = "test_alias" + val sequence: Long = -1 + val streams = createStreams(AbstractCheckTaskTest.SESSION_ALIAS, Direction.FIRST, listOf( + message(AbstractCheckTaskTest.MESSAGE_TYPE, Direction.FIRST, AbstractCheckTaskTest.SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .build()) + .build() + )) + + val ruleFactory = RuleFactory(Check1Configuration(), streams, clientStub) + + val request = CheckRuleRequest.newBuilder() + .setParentEventId(EventID.newBuilder().setId("root").build()) + .setConnectivityId(ConnectionID.newBuilder() + .setSessionAlias(sessionAlias) + ) + .setRootFilter(RootMessageFilter.newBuilder() + .setMessageType("TestMsgType") + ) + .setMessageTimeout(5) + .setCheckpoint( + Checkpoint.newBuilder() + .setId(EventUtils.generateUUID()) + .putSessionAliasToDirectionCheckpoint( + sessionAlias, + Checkpoint.DirectionCheckpoint.newBuilder() + .putDirectionToCheckpointData( + Direction.FIRST.number, + Checkpoint.CheckpointData.newBuilder() + .setSequence(sequence) + .setTimestamp(Instant.now().toTimestamp()) + .build()) + .build()) + .build() + ) + .setDirection(Direction.FIRST) + .build() + + assertThrowsWithMessages( + "An error occurred while creating rule", + "The checkpoint data has incorrect sequence number '$sequence'" + ) { ruleFactory.createCheckRule(request, true) } + + assertEvents() + } + + @Test + fun `failed rule creation because checkpoint data missed timestamp`() { + val sessionAlias = "test_alias" + val streams = createStreams(AbstractCheckTaskTest.SESSION_ALIAS, Direction.FIRST, listOf( + message(AbstractCheckTaskTest.MESSAGE_TYPE, Direction.FIRST, AbstractCheckTaskTest.SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .build()) + .build() + )) + + val ruleFactory = RuleFactory(Check1Configuration(), streams, clientStub) + + val request = CheckRuleRequest.newBuilder() + .setParentEventId(EventID.newBuilder().setId("root").build()) + .setConnectivityId(ConnectionID.newBuilder() + .setSessionAlias(sessionAlias) + ) + .setRootFilter(RootMessageFilter.newBuilder() + .setMessageType("TestMsgType") + ) + .setMessageTimeout(5) + .setCheckpoint( + Checkpoint.newBuilder() + .setId(EventUtils.generateUUID()) + .putSessionAliasToDirectionCheckpoint( + sessionAlias, + Checkpoint.DirectionCheckpoint.newBuilder() + .putDirectionToCheckpointData( + Direction.FIRST.number, + Checkpoint.CheckpointData.newBuilder() + .setSequence(1) + .build()) + .build()) + .build() + ) + .setDirection(Direction.FIRST) + .build() + + assertThrowsWithMessages( + "An error occurred while creating rule", + "The checkpoint data doesn't contain timestamp" + ) { ruleFactory.createCheckRule(request, true) } + + assertEvents() + } + private fun assertEvents() { val eventBatches = awaitEventBatchRequest(1000L, 1) val eventList = eventBatches.flatMap(EventBatch::getEventsList) assertAll({ diff --git a/src/test/kotlin/com/exactpro/th2/check1/rule/TestChain.kt b/src/test/kotlin/com/exactpro/th2/check1/rule/TestChain.kt index 1bfe47b3..a8d83f7f 100644 --- a/src/test/kotlin/com/exactpro/th2/check1/rule/TestChain.kt +++ b/src/test/kotlin/com/exactpro/th2/check1/rule/TestChain.kt @@ -14,8 +14,10 @@ package com.exactpro.th2.check1.rule import com.exactpro.th2.check1.SessionKey import com.exactpro.th2.check1.StreamContainer +import com.exactpro.th2.check1.entities.TaskTimeout import com.exactpro.th2.check1.grpc.PreFilter import com.exactpro.th2.check1.rule.check.CheckRuleTask +import com.exactpro.th2.check1.rule.nomessage.NoMessageCheckTask import com.exactpro.th2.check1.rule.sequence.SequenceCheckRuleTask import com.exactpro.th2.check1.rule.sequence.SequenceCheckRuleTask.Companion.CHECK_MESSAGES_TYPE import com.exactpro.th2.common.grpc.Direction.FIRST @@ -32,8 +34,10 @@ import com.exactpro.th2.common.grpc.ValueFilter import com.exactpro.th2.common.value.toValue import io.reactivex.Observable import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll import java.time.Instant import kotlin.test.assertEquals +import kotlin.test.assertTrue class TestChain: AbstractCheckTaskTest() { @@ -131,6 +135,113 @@ class TestChain: AbstractCheckTaskTest() { assertEquals(listOf(1L, 2L, 3L, 4L), eventList.filter { it.type == VERIFICATION_TYPE }.flatMap(Event::getAttachedMessageIdsList).map(MessageID::getSequence)) } + @Test + fun `sequence rules - untrusted execution`() { + val checkpointTimestamp = Instant.now() + val streams = createStreams(messages = (1..5L).map { + constructMessage(it, timestamp = getMessageTimestamp(checkpointTimestamp, it * 1000)) + .putAllFields( + mapOf( + KEY_FIELD to "$KEY_FIELD$it".toValue(), + NOT_KEY_FIELD to "$NOT_KEY_FIELD$it".toValue() + ) + ).build() + }) + + val task = sequenceCheckRuleTask( + listOf(1, 2), + eventID, + streams, + taskTimeout = TaskTimeout(2000L, 500) + ).also { it.begin(createCheckpoint(checkpointTimestamp, 0)) } + var eventsList = awaitEventBatchAndGetEvents(4, 4) + assertAll({ + val rootEvent = eventsList.first() + assertEquals(FAILED, rootEvent.status, "Event status should be failed") + assertTrue(rootEvent.attachedMessageIdsCount == 1) + }) + + sequenceCheckRuleTask( + listOf(3, 4), + eventID, + streams, + taskTimeout = TaskTimeout(2000L, 1500L) + ).also { task.subscribeNextTask(it) } + eventsList = awaitEventBatchAndGetEvents(10, 6) + assertEquals(UNTRUSTED_EXECUTION_EVENT_NAME, eventsList.last().name) + } + + @Test + fun `no messages sequence rules - untrusted execution`() { + val checkpointTimestamp = Instant.now() + val streams = createStreams(messages = (1..5L).map { + constructMessage(it, timestamp = getMessageTimestamp(checkpointTimestamp, it * 1000)) + .putAllFields( + mapOf( + KEY_FIELD to "$KEY_FIELD$it".toValue(), + NOT_KEY_FIELD to "$NOT_KEY_FIELD$it".toValue() + ) + ).build() + }) + + val task = noMessageCheckTask( + eventID, + streams, + taskTimeout = TaskTimeout(2000L, 500), + preFilterParam = createPreFilter("E", "5", FilterOperation.EQUAL) + ).also { it.begin(createCheckpoint(checkpointTimestamp, 0)) } + var eventsList = awaitEventBatchAndGetEvents(2, 2) + assertAll({ + val rootEvent = eventsList.first() + assertEquals(FAILED, rootEvent.status, "Event status should be failed") + assertTrue(rootEvent.attachedMessageIdsCount == 1) + }) + + noMessageCheckTask( + eventID, + streams, + taskTimeout = TaskTimeout(2000L, 1500L), + preFilterParam = createPreFilter("E", "5", FilterOperation.EQUAL) + ).also { task.subscribeNextTask(it) } + eventsList = awaitEventBatchAndGetEvents(6, 4) + assertEquals(UNTRUSTED_EXECUTION_EVENT_NAME, eventsList.last().name) + } + + @Test + fun `simple rules - ignored untrusted execution`() { + val checkpointTimestamp = Instant.now() + val streams = createStreams(messages = (1..5L).map { + constructMessage(it, timestamp = getMessageTimestamp(checkpointTimestamp, it * 1000)) + .putAllFields( + mapOf( + KEY_FIELD to "$KEY_FIELD$it".toValue(), + NOT_KEY_FIELD to "$NOT_KEY_FIELD$it".toValue() + ) + ).build() + }) + + val task = checkRuleTask( + 1, eventID, streams, taskTimeout = TaskTimeout(2000L, 500) + ).also { it.begin(createCheckpoint(checkpointTimestamp, 0)) } + var eventsList = awaitEventBatchAndGetEvents(2, 2) + assertAll({ + val rootEvent = eventsList.first() + assertEquals(FAILED, rootEvent.status, "Event status should be failed") + assertTrue(rootEvent.attachedMessageIdsCount == 1) + }) + + checkRuleTask(3, eventID, streams).also { task.subscribeNextTask(it) } + eventsList = awaitEventBatchAndGetEvents(4, 2) + assertAll({ + val rootEvent = eventsList.first() + assertEquals(FAILED, rootEvent.status) + assertEquals(3, rootEvent.attachedMessageIdsCount) + assertEquals(1, eventsList[2].attachedMessageIdsCount) + assertEquals(FAILED, eventsList.last().status) + }) + } + + private fun awaitEventBatchAndGetEvents(times: Int, last: Int): List = awaitEventBatchRequest(1000L, times).drop(times - last).flatMap(EventBatch::getEventsList) @@ -162,14 +273,12 @@ class TestChain: AbstractCheckTaskTest() { messageStream: Observable, checkOrder: Boolean = true, preFilterParam: PreFilter = preFilter, - maxEventBatchContentSize: Int = 1024 * 1024 + taskTimeout: TaskTimeout = TaskTimeout(1000L) ): SequenceCheckRuleTask { return SequenceCheckRuleTask( - description = "Test", + ruleConfiguration = createRuleConfiguration(taskTimeout), startTime = Instant.now(), sessionKey = SessionKey(SESSION_ALIAS, FIRST), - timeout = 1000L, - maxEventBatchContentSize = maxEventBatchContentSize, protoPreFilter = preFilterParam, protoMessageFilters = sequence.map(::createMessageFilter).toList(), checkOrder = checkOrder, @@ -183,19 +292,34 @@ class TestChain: AbstractCheckTaskTest() { sequence: Int, parentEventID: EventID, messageStream: Observable, - maxEventBatchContentSize: Int = 1024 * 1024 + taskTimeout: TaskTimeout = TaskTimeout(1000L) ) = CheckRuleTask( - SESSION_ALIAS, + createRuleConfiguration(taskTimeout, SESSION_ALIAS), Instant.now(), SessionKey(SESSION_ALIAS, FIRST), - 1000, - maxEventBatchContentSize, createMessageFilter(sequence), parentEventID, messageStream, clientStub ) + private fun noMessageCheckTask( + parentEventID: EventID, + messageStream: Observable, + preFilterParam: PreFilter, + taskTimeout: TaskTimeout = TaskTimeout(5000L, 3500L) + ): NoMessageCheckTask { + return NoMessageCheckTask( + ruleConfiguration = createRuleConfiguration(taskTimeout), + startTime = Instant.now(), + sessionKey = SessionKey(SESSION_ALIAS, FIRST), + protoPreFilter = preFilterParam, + parentEventID = parentEventID, + messageStream = messageStream, + eventBatchRouter = clientStub + ) + } + private fun createMessage(sequence: Int) = constructMessage(sequence.toLong()) .putAllFields(mapOf( KEY_FIELD to "$KEY_FIELD$sequence".toValue(), @@ -215,5 +339,6 @@ class TestChain: AbstractCheckTaskTest() { companion object { private const val KEY_FIELD = "key" private const val NOT_KEY_FIELD = "not_key" + private const val UNTRUSTED_EXECUTION_EVENT_NAME: String = "The current check is untrusted because the start point of the check interval has been selected approximately" } } diff --git a/src/test/kotlin/com/exactpro/th2/check1/rule/check/TestCheckRuleTask.kt b/src/test/kotlin/com/exactpro/th2/check1/rule/check/TestCheckRuleTask.kt index 7d9abb79..de2b76f8 100644 --- a/src/test/kotlin/com/exactpro/th2/check1/rule/check/TestCheckRuleTask.kt +++ b/src/test/kotlin/com/exactpro/th2/check1/rule/check/TestCheckRuleTask.kt @@ -15,6 +15,7 @@ package com.exactpro.th2.check1.rule.check import com.exactpro.th2.check1.SessionKey import com.exactpro.th2.check1.StreamContainer +import com.exactpro.th2.check1.entities.TaskTimeout import com.exactpro.th2.check1.rule.AbstractCheckTaskTest import com.exactpro.th2.check1.util.createVerificationEntry import com.exactpro.th2.check1.util.toSimpleFilter @@ -23,9 +24,11 @@ import com.exactpro.th2.common.grpc.Direction import com.exactpro.th2.common.grpc.EventBatch import com.exactpro.th2.common.grpc.EventID import com.exactpro.th2.common.grpc.EventStatus +import com.exactpro.th2.common.grpc.EventStatus.FAILED import com.exactpro.th2.common.grpc.EventStatus.SUCCESS import com.exactpro.th2.common.grpc.FilterOperation import com.exactpro.th2.common.grpc.ListValueFilter +import com.exactpro.th2.common.grpc.MessageID import com.exactpro.th2.common.grpc.MessageMetadata import com.exactpro.th2.common.grpc.MetadataFilter import com.exactpro.th2.common.grpc.RootComparisonSettings @@ -57,13 +60,11 @@ internal class TestCheckRuleTask : AbstractCheckTaskTest() { parentEventID: EventID, messageStream: Observable, maxEventBatchContentSize: Int = 1024 * 1024, - timeout: Long = 1000 + taskTimeout: TaskTimeout = TaskTimeout(1000L) ) = CheckRuleTask( - SESSION_ALIAS, + createRuleConfiguration(taskTimeout, SESSION_ALIAS, maxEventBatchContentSize), Instant.now(), SessionKey(SESSION_ALIAS, Direction.FIRST), - timeout, - maxEventBatchContentSize, messageFilter, parentEventID, messageStream, @@ -81,7 +82,7 @@ internal class TestCheckRuleTask : AbstractCheckTaskTest() { .build() )) - val eventID = EventID.newBuilder().setId("root").build() + val eventID = createEvent("root") val filter = RootMessageFilter.newBuilder() .setMessageType(MESSAGE_TYPE) .setMetadataFilter(MetadataFilter.newBuilder() @@ -107,7 +108,7 @@ internal class TestCheckRuleTask : AbstractCheckTaskTest() { .build() )) - val eventID = EventID.newBuilder().setId("root").build() + val eventID = createEvent("root") val filter = RootMessageFilter.newBuilder() .setMessageType(MESSAGE_TYPE) .setMetadataFilter(MetadataFilter.newBuilder() @@ -133,7 +134,7 @@ internal class TestCheckRuleTask : AbstractCheckTaskTest() { .build() )) - val eventID = EventID.newBuilder().setId("root").build() + val eventID = createEvent("root") val filter = RootMessageFilter.newBuilder() .setMessageType(MESSAGE_TYPE) .setMetadataFilter(MetadataFilter.newBuilder() @@ -159,7 +160,7 @@ internal class TestCheckRuleTask : AbstractCheckTaskTest() { .build() )) - val eventID = EventID.newBuilder().setId("root").build() + val eventID = createEvent("root") val filter = RootMessageFilter.newBuilder() .setMessageType(MESSAGE_TYPE) .setMetadataFilter(MetadataFilter.newBuilder() @@ -189,7 +190,7 @@ internal class TestCheckRuleTask : AbstractCheckTaskTest() { .build() )) - val eventID = EventID.newBuilder().setId("root").build() + val eventID = createEvent("root") val filter = RootMessageFilter.newBuilder() .setMessageType(MESSAGE_TYPE) .setMetadataFilter(MetadataFilter.newBuilder() @@ -220,7 +221,7 @@ internal class TestCheckRuleTask : AbstractCheckTaskTest() { .build() )) - val eventID = EventID.newBuilder().setId("root").build() + val eventID = createEvent("root") val filter = RootMessageFilter.newBuilder() .setMessageType(MESSAGE_TYPE) .setMetadataFilter(MetadataFilter.newBuilder() @@ -228,7 +229,7 @@ internal class TestCheckRuleTask : AbstractCheckTaskTest() { .build() val exception = assertThrows("Task cannot be created due to invalid timeout") { - checkTask(filter, eventID, streams, timeout = timeout) + checkTask(filter, eventID, streams, taskTimeout = TaskTimeout(timeout)) } assertEquals("'timeout' should be set or be greater than zero, actual: $timeout", exception.message) } @@ -276,7 +277,7 @@ internal class TestCheckRuleTask : AbstractCheckTaskTest() { }.build() }.build() - val eventID = EventID.newBuilder().setId("root").build() + val eventID = createEvent("root") checkTask(messageFilterForCheckOrder, eventID, streams).begin() @@ -336,6 +337,110 @@ internal class TestCheckRuleTask : AbstractCheckTaskTest() { }) } + @Test + fun `success verification with message timeout`() { + val checkpointTimestamp = Instant.now() + val streams = createStreams(SESSION_ALIAS, Direction.FIRST, listOf( + message(MESSAGE_TYPE, Direction.FIRST, SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .setTimestamp(getMessageTimestamp(checkpointTimestamp, 500)) + .setId(MessageID.newBuilder().setSequence(0L)) + .build()) + .build(), + message(MESSAGE_TYPE, Direction.FIRST, SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .setTimestamp(getMessageTimestamp(checkpointTimestamp, 500)) + .setId(MessageID.newBuilder().setSequence(1L)) + .build()) + .build() + )) + + val eventID = createEvent("root") + val filter = RootMessageFilter.newBuilder() + .setMessageType(MESSAGE_TYPE) + .setMetadataFilter(MetadataFilter.newBuilder() + .putPropertyFilters("keyProp", "42".toSimpleFilter(FilterOperation.EQUAL, true))) + .build() + val task = checkTask(filter, eventID, streams, taskTimeout = TaskTimeout(1000, 500)) + task.begin(createCheckpoint(checkpointTimestamp, 0)) + + val eventBatches = awaitEventBatchRequest(1000L, 2) + val eventList = eventBatches.flatMap(EventBatch::getEventsList) + assertEquals(4, eventList.size) + assertEquals(4, eventList.filter { it.status == SUCCESS }.size) + } + + @Test + fun `success verification, but failed event because missed start message`() { + val checkpointTimestamp = Instant.now() + val streams = createStreams(SESSION_ALIAS, Direction.FIRST, listOf( + message(MESSAGE_TYPE, Direction.FIRST, SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .setTimestamp(getMessageTimestamp(checkpointTimestamp, 500)) + .setId(MessageID.newBuilder().setSequence(1L)) + .build()) + .build() + )) + + val eventID = createEvent("root") + val filter = RootMessageFilter.newBuilder() + .setMessageType(MESSAGE_TYPE) + .setMetadataFilter(MetadataFilter.newBuilder() + .putPropertyFilters("keyProp", "42".toSimpleFilter(FilterOperation.EQUAL, true))) + .build() + val task = checkTask(filter, eventID, streams) + task.begin(createCheckpoint(sequence = 0)) + + val eventBatches = awaitEventBatchRequest(1000L, 2) + val eventList = eventBatches.flatMap(EventBatch::getEventsList) + assertEquals(5, eventList.size) + assertEquals(2, eventList.filter { it.status == SUCCESS && it.type == "Verification" }.size) + assertEquals(FAILED, eventList.last().status) + } + + @Test + fun `failed verification with expired message timeout`() { + val checkpointTimestamp = Instant.now() + val streams = createStreams(SESSION_ALIAS, Direction.FIRST, listOf( + message(MESSAGE_TYPE, Direction.FIRST, SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .setTimestamp(getMessageTimestamp(checkpointTimestamp, 600)) + .setId(MessageID.newBuilder().setSequence(0L)) + .build()) + .build(), + message(MESSAGE_TYPE, Direction.FIRST, SESSION_ALIAS) + .mergeMetadata(MessageMetadata.newBuilder() + .putProperties("keyProp", "42") + .putProperties("notKeyProp", "2") + .setTimestamp(getMessageTimestamp(checkpointTimestamp, 600)) + .setId(MessageID.newBuilder().setSequence(1L)) + .build()) + .build() + )) + + val eventID = createEvent("root") + val filter = RootMessageFilter.newBuilder() + .setMessageType(MESSAGE_TYPE) + .setMetadataFilter(MetadataFilter.newBuilder() + .putPropertyFilters("keyProp", "42".toSimpleFilter(FilterOperation.EQUAL, true))) + .build() + val task = checkTask(filter, eventID, streams, taskTimeout = TaskTimeout(1000, 500)) + task.begin(createCheckpoint(checkpointTimestamp, 0)) + + val eventBatches = awaitEventBatchRequest(1000L, 2) + val eventList = eventBatches.flatMap(EventBatch::getEventsList) + assertEquals(3, eventList.size) + assertEquals(2, eventList.filter { it.status == FAILED }.size) + } + @Test fun `verify repeating groups according to defined filters`() { val streams = createStreams(SESSION_ALIAS, Direction.FIRST, listOf( @@ -421,4 +526,4 @@ internal class TestCheckRuleTask : AbstractCheckTaskTest() { companion object { private const val VERIFICATION_DESCRIPTION = "Test verification with description" } -} \ No newline at end of file +} diff --git a/src/test/kotlin/com/exactpro/th2/check1/rule/nomessage/TestNoMessageCheckTask.kt b/src/test/kotlin/com/exactpro/th2/check1/rule/nomessage/TestNoMessageCheckTask.kt new file mode 100644 index 00000000..6daaa953 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/check1/rule/nomessage/TestNoMessageCheckTask.kt @@ -0,0 +1,213 @@ +/* + * Copyright 2021-2021 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.th2.check1.rule.nomessage + +import com.exactpro.th2.check1.SessionKey +import com.exactpro.th2.check1.StreamContainer +import com.exactpro.th2.check1.entities.TaskTimeout +import com.exactpro.th2.check1.grpc.PreFilter +import com.exactpro.th2.check1.rule.AbstractCheckTaskTest +import com.exactpro.th2.common.grpc.Direction +import com.exactpro.th2.common.grpc.EventBatch +import com.exactpro.th2.common.grpc.EventID +import com.exactpro.th2.common.grpc.EventStatus +import com.exactpro.th2.common.grpc.FilterOperation +import com.exactpro.th2.common.grpc.Message +import com.exactpro.th2.common.grpc.Value +import com.exactpro.th2.common.value.toValue +import com.google.protobuf.Timestamp +import io.reactivex.Observable +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import java.time.Instant +import kotlin.test.assertEquals +import kotlin.test.assertNotNull +import kotlin.test.assertTrue + +class TestNoMessageCheckTask : AbstractCheckTaskTest() { + @Test + fun `no messages outside the prefilter`() { + val checkpointTimestamp = Instant.now() + val streams = createStreams( + messages = createMessages( + MessageData("A", "1".toValue(), getMessageTimestamp(checkpointTimestamp, 100)), + MessageData("A", "1".toValue(), getMessageTimestamp(checkpointTimestamp, 500)), + MessageData("B", "2".toValue(), getMessageTimestamp(checkpointTimestamp, 1000)), + MessageData("C", "3".toValue(), getMessageTimestamp(checkpointTimestamp, 1300)), + MessageData("D", "4".toValue(), getMessageTimestamp(checkpointTimestamp, 1500)), + MessageData("E", "5".toValue(), getMessageTimestamp(checkpointTimestamp, 1600)), + // should be skipped because of message timeout + MessageData("F", "6".toValue(), getMessageTimestamp(checkpointTimestamp, 1600)) + ) + ) + + val eventID = createEvent("root") + val task = noMessageCheckTask( + eventID, + streams, + createPreFilter("E", "5", FilterOperation.EQUAL), + TaskTimeout(5000L, 1500L) + ) + task.begin(createCheckpoint(checkpointTimestamp, 1)) + + val eventBatch = awaitEventBatchRequest(1000L, 2) + val eventsList = eventBatch.flatMap(EventBatch::getEventsList) + + assertAll({ + assertTrue(eventsList.all { it.status == EventStatus.SUCCESS }, "Has messages outside the prefilter") + }, { + val rootEvent = eventsList.first() + assertTrue(rootEvent.attachedMessageIdsCount == 5) + assertEquals((2..6L).toList(), rootEvent.attachedMessageIdsList.map { it.sequence }) + }, { + val prefilteredEvent = eventsList.findEventByType("preFiltering") + assertNotNull(prefilteredEvent, "Missed pre filtering event") + assertTrue(prefilteredEvent.attachedMessageIdsCount == 0) + assertEquals(emptyList(), prefilteredEvent.attachedMessageIdsList.map { it.sequence }) + }, { + val unexpectedMessagesEvent = eventsList.findEventByType("noMessagesCheckResult") + assertNotNull(unexpectedMessagesEvent, "Missed resulting event") + assertTrue(unexpectedMessagesEvent.attachedMessageIdsCount == 0) + assertEquals(emptyList(), unexpectedMessagesEvent.attachedMessageIdsList.map { it.sequence }) + }) + } + + @Test + fun `with messages outside the prefilter`() { + val checkpointTimestamp = Instant.now() + val messageTimeout = 1500L + val streams = createStreams( + messages = createMessages( + MessageData("A", "1".toValue(), getMessageTimestamp(checkpointTimestamp, 50)), + MessageData("A", "1".toValue(), getMessageTimestamp(checkpointTimestamp, 100)), + MessageData("B", "2".toValue(), getMessageTimestamp(checkpointTimestamp, 500)), + MessageData("C", "3".toValue(), getMessageTimestamp(checkpointTimestamp, 700)), + MessageData("D", "4".toValue(), getMessageTimestamp(checkpointTimestamp, 1600)), + // should be skipped because of message timeout + MessageData("E", "5".toValue(), getMessageTimestamp(checkpointTimestamp, 1700)) + ) + ) + + val eventID = createEvent("root") + val task = noMessageCheckTask( + eventID, + streams, + createPreFilter("A", "1", FilterOperation.EQUAL), + TaskTimeout(5000, messageTimeout) + ) + task.begin(createCheckpoint(checkpointTimestamp, 1)) + + val eventBatch = awaitEventBatchRequest(1000L, 4) + val eventsList = eventBatch.flatMap(EventBatch::getEventsList) + + assertAll({ + val rootEvent = eventsList.first() + assertEquals(rootEvent.status, EventStatus.FAILED, "Event status should be failed") + assertTrue(rootEvent.attachedMessageIdsCount == 4) + assertEquals((2..5L).toList(), rootEvent.attachedMessageIdsList.map { it.sequence }) + }, { + val prefilteredEvent = eventsList.findEventByType("preFiltering") + assertNotNull(prefilteredEvent, "Missed pre filtering event") + assertTrue(prefilteredEvent.attachedMessageIdsCount == 1) + val verificationEvents = eventsList.filter { it.type == "Verification" } + assertEquals(1, verificationEvents.size) + assertTrue(verificationEvents.all { it.parentId == prefilteredEvent.id }) + assertEquals(listOf(2L), prefilteredEvent.attachedMessageIdsList.map { it.sequence }) + }, { + val unexpectedMessagesEvent = eventsList.findEventByType("noMessagesCheckResult") + assertNotNull(unexpectedMessagesEvent, "Missed resulting event") + assertTrue(unexpectedMessagesEvent.attachedMessageIdsCount == 1) + assertEquals(listOf(2L), unexpectedMessagesEvent.attachedMessageIdsList.map { it.sequence }) + }) + } + + @Test + fun `check messages without message timeout`() { + val checkpointTimestamp = Instant.now() + val streams = createStreams( + messages = createMessages( + MessageData("A", "1".toValue(), getMessageTimestamp(checkpointTimestamp, 100)), + MessageData("A", "1".toValue(), getMessageTimestamp(checkpointTimestamp, 500)), + MessageData("A", "1".toValue(), getMessageTimestamp(checkpointTimestamp, 700)), + MessageData("A", "1".toValue(), getMessageTimestamp(checkpointTimestamp, 1000)), + MessageData("A", "1".toValue(), getMessageTimestamp(checkpointTimestamp, 1300)) + ) + ) + + val eventID = createEvent("root") + val task = noMessageCheckTask( + eventID, + streams, + createPreFilter("B", "2", FilterOperation.EQUAL), + TaskTimeout(2000) + ) + task.begin(createCheckpoint(checkpointTimestamp)) + + val eventBatch = awaitEventBatchRequest(1000L, 4) + val eventsList = eventBatch.flatMap(EventBatch::getEventsList) + + assertAll({ + val rootEvent = eventsList.first() + assertEquals(rootEvent.status, EventStatus.FAILED, "Root event should be failed due to timeout") + assertTrue(rootEvent.attachedMessageIdsCount == 5) + assertEquals((1..5L).toList(), rootEvent.attachedMessageIdsList.map { it.sequence }) + }, { + val prefilteredEvent = eventsList.findEventByType("preFiltering") + assertNotNull(prefilteredEvent, "Missed pre filtering event") + assertTrue(prefilteredEvent.attachedMessageIdsCount == 0) + assertEquals(emptyList(), prefilteredEvent.attachedMessageIdsList.map { it.sequence }) + }, { + val unexpectedMessagesEvent = eventsList.findEventByType("noMessagesCheckResult") + assertNotNull(unexpectedMessagesEvent, "Missed resulting event") + assertTrue(unexpectedMessagesEvent.attachedMessageIdsCount == 0) + assertEquals(emptyList(), unexpectedMessagesEvent.attachedMessageIdsList.map { it.sequence }) + assertTrue(unexpectedMessagesEvent.name == "Check passed", "All messages should be ignored due to prefilter") + }) + } + + + private fun createMessages( + vararg messageData: MessageData, + sessionAlias: String = SESSION_ALIAS, + messageType: String = MESSAGE_TYPE, + direction: Direction = Direction.FIRST + ): List { + var sequence = 1L; + return messageData.map { data -> + constructMessage(sequence++, sessionAlias, messageType, direction, data.timestamp) + .putFields(data.fieldName, data.value) + .build() + } + } + + private fun noMessageCheckTask( + parentEventID: EventID, + messageStream: Observable, + preFilterParam: PreFilter, + taskTimeout: TaskTimeout = TaskTimeout(5000L, 3500L) + ): NoMessageCheckTask { + return NoMessageCheckTask( + ruleConfiguration = createRuleConfiguration(taskTimeout), + startTime = Instant.now(), + sessionKey = SessionKey(SESSION_ALIAS, Direction.FIRST), + protoPreFilter = preFilterParam, + parentEventID = parentEventID, + messageStream = messageStream, + eventBatchRouter = clientStub + ) + } + + + data class MessageData(val fieldName: String, val value: Value, val timestamp: Timestamp) +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/check1/rule/sequence/TestSequenceCheckTask.kt b/src/test/kotlin/com/exactpro/th2/check1/rule/sequence/TestSequenceCheckTask.kt index 5c9af9c0..6af402bc 100644 --- a/src/test/kotlin/com/exactpro/th2/check1/rule/sequence/TestSequenceCheckTask.kt +++ b/src/test/kotlin/com/exactpro/th2/check1/rule/sequence/TestSequenceCheckTask.kt @@ -15,6 +15,7 @@ package com.exactpro.th2.check1.rule.sequence import com.exactpro.th2.check1.SessionKey import com.exactpro.th2.check1.StreamContainer import com.exactpro.th2.check1.exception.RuleInternalException +import com.exactpro.th2.check1.entities.TaskTimeout import com.exactpro.th2.check1.grpc.PreFilter import com.exactpro.th2.check1.rule.AbstractCheckTaskTest import com.exactpro.th2.check1.rule.sequence.SequenceCheckRuleTask.Companion.CHECK_MESSAGES_TYPE @@ -114,7 +115,7 @@ class TestSequenceCheckTask : AbstractCheckTaskTest() { val messages = Observable.fromIterable(messagesInCorrectOrder) val messageStream: Observable = Observable.just(StreamContainer(SessionKey(SESSION_ALIAS, Direction.FIRST), 10, messages)) - val parentEventID = EventID.newBuilder().setId(EventUtils.generateUUID()).build() + val parentEventID = createEvent(EventUtils.generateUUID()) sequenceCheckRuleTask(parentEventID, messageStream, checkOrder).begin() @@ -172,10 +173,9 @@ class TestSequenceCheckTask : AbstractCheckTaskTest() { set(indexesToSwitch.first, get(indexesToSwitch.second)) set(indexesToSwitch.second, tmp) } - val messages = Observable.fromIterable(messagesUnordered) - val messageStream: Observable = Observable.just(StreamContainer(SessionKey(SESSION_ALIAS, Direction.FIRST), 10, messages)) - val parentEventID = EventID.newBuilder().setId(EventUtils.generateUUID()).build() + val messageStream = createStreams(SESSION_ALIAS, Direction.FIRST, messagesUnordered) + val parentEventID = createEvent(EventUtils.generateUUID()) sequenceCheckRuleTask(parentEventID, messageStream, true).begin() @@ -235,7 +235,7 @@ class TestSequenceCheckTask : AbstractCheckTaskTest() { val messages = Observable.fromIterable(messagesWithKeyFields) val messageStream: Observable = Observable.just(StreamContainer(SessionKey(SESSION_ALIAS, Direction.FIRST), 10, messages)) - val parentEventID = EventID.newBuilder().setId(EventUtils.generateUUID()).build() + val parentEventID = createEvent(EventUtils.generateUUID()) sequenceCheckRuleTask(parentEventID, messageStream, checkOrder, filtersParam = messageFilters).begin() @@ -301,7 +301,7 @@ class TestSequenceCheckTask : AbstractCheckTaskTest() { val messages = Observable.fromIterable(messagesWithKeyFields) val messageStream: Observable = Observable.just(StreamContainer(SessionKey(SESSION_ALIAS, Direction.FIRST), 10, messages)) - val parentEventID = EventID.newBuilder().setId(EventUtils.generateUUID()).build() + val parentEventID = createEvent(EventUtils.generateUUID()) sequenceCheckRuleTask(parentEventID, messageStream, true, filtersParam = messageFilters).begin() @@ -323,6 +323,286 @@ class TestSequenceCheckTask : AbstractCheckTaskTest() { }) } + @Test + fun `check sequence of messages with the same value of key field and message timeout`() { + val checkpointTimestamp = Instant.now() + val messagesWithKeyFields: List = listOf( + constructMessage(0, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 100)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build(), + constructMessage(1, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 100)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build(), + constructMessage(2, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 200)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build(), + constructMessage(3, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 300)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build() + ) + + val messageFilter = RootMessageFilter.newBuilder() + .setMessageType(MESSAGE_TYPE) + .setMessageFilter( + MessageFilter.newBuilder() + .putAllFields( + mapOf( + "A" to ValueFilter.newBuilder().setKey(true).setSimpleFilter("42").build(), + "B" to ValueFilter.newBuilder().setSimpleFilter("AAA").build() + ) + ) + ).build() + val messageFilters: List = listOf( + RootMessageFilter.newBuilder(messageFilter).build(), + RootMessageFilter.newBuilder(messageFilter).build(), + RootMessageFilter.newBuilder(messageFilter).build() + ) + + val messages = Observable.fromIterable(messagesWithKeyFields) + + val messageStream: Observable = Observable.just(StreamContainer(SessionKey(SESSION_ALIAS, Direction.FIRST), 10, messages)) + val parentEventID = createEvent(EventUtils.generateUUID()) + + sequenceCheckRuleTask( + parentEventID, + messageStream, + true, + filtersParam = messageFilters, + taskTimeout = TaskTimeout(5000L, 500L) + ).begin(createCheckpoint(checkpointTimestamp, 0)) + + val batchRequest = awaitEventBatchRequest(1000L, 6) + val eventsList: List = batchRequest.flatMap(EventBatch::getEventsList) + + assertAll({ + val rootEvent = assertNotNull(eventsList.find { it.parentId == parentEventID }) + assertEquals(3, rootEvent.attachedMessageIdsCount) + assertEquals(listOf(1L, 2L, 3L), rootEvent.attachedMessageIdsList.map { it.sequence }) + }, { + val checkedMessages = assertNotNull(eventsList.find { it.type == CHECK_MESSAGES_TYPE }, "Cannot find checkMessages event") + val verifications = eventsList.filter { it.parentId == checkedMessages.id } + assertEquals(3, verifications.size, "Unexpected verifications count: $verifications") + assertTrue("Some verifications are not success: $verifications") { verifications.all { it.status == EventStatus.SUCCESS } } + assertEquals(listOf(1L, 2L, 3L), verifications.flatMap { verification -> verification.attachedMessageIdsList.map { it.sequence } }) + }, { + assertCheckSequenceStatus(EventStatus.SUCCESS, eventsList) // because all key fields are in a correct order + }) + } + + @Test + fun `check sequence of messages with the same value of key field and expired message timeout`() { + val checkpointTimestamp = Instant.now() + val messagesWithKeyFields: List = listOf( + constructMessage(0, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 100)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build(), + constructMessage(1, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 500)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build(), + constructMessage(2, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 600)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build(), + constructMessage(3, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 700)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build() + ) + + val messageFilter = RootMessageFilter.newBuilder() + .setMessageType(MESSAGE_TYPE) + .setMessageFilter( + MessageFilter.newBuilder() + .putAllFields( + mapOf( + "A" to ValueFilter.newBuilder().setKey(true).setSimpleFilter("42").build(), + "B" to ValueFilter.newBuilder().setSimpleFilter("AAA").build() + ) + ) + ).build() + val messageFilters: List = listOf( + RootMessageFilter.newBuilder(messageFilter).build() + ) + + val messageStream = createStreams(SESSION_ALIAS, Direction.FIRST, messagesWithKeyFields) + val parentEventID = createEvent(EventUtils.generateUUID()) + + sequenceCheckRuleTask( + parentEventID, + messageStream, + true, + filtersParam = messageFilters, + taskTimeout = TaskTimeout(5000L, 500L) + ).begin(createCheckpoint(checkpointTimestamp, 0)) + + val batchRequest = awaitEventBatchRequest(1000L, 6) + val eventsList: List = batchRequest.flatMap(EventBatch::getEventsList) + + assertAll({ + val rootEvent = assertNotNull(eventsList.find { it.parentId == parentEventID }) + assertEquals(1, rootEvent.attachedMessageIdsCount) + assertEquals(listOf(1L), rootEvent.attachedMessageIdsList.map { it.sequence }) + }, { + val checkedMessages = assertNotNull(eventsList.find { it.type == CHECK_MESSAGES_TYPE }, "Cannot find checkMessages event") + val verifications = eventsList.filter { it.parentId == checkedMessages.id } + assertEquals(1, verifications.size, "Unexpected verifications count: $verifications") + assertTrue("Some verifications are not success: $verifications") { verifications.all { it.status == EventStatus.SUCCESS } } + assertEquals(listOf(1L), verifications.flatMap { verification -> verification.attachedMessageIdsList.map { it.sequence } }) + }, { + assertCheckSequenceStatus(EventStatus.SUCCESS, eventsList) // because all key fields are in a correct order + }) + } + + @Test + fun `check sequence of messages with the same value of key field and one missed message due to message timeout`() { + val checkpointTimestamp = Instant.now() + val messagesWithKeyFields: List = listOf( + constructMessage(1, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 500)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build(), + constructMessage(2, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 600)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build() + ) + + val messageFilter = RootMessageFilter.newBuilder() + .setMessageType(MESSAGE_TYPE) + .setMessageFilter( + MessageFilter.newBuilder() + .putAllFields( + mapOf( + "A" to ValueFilter.newBuilder().setKey(true).setSimpleFilter("42").build(), + "B" to ValueFilter.newBuilder().setSimpleFilter("AAA").build() + ) + ) + ).build() + val messageFilters: List = listOf( + RootMessageFilter.newBuilder(messageFilter).build(), + RootMessageFilter.newBuilder(messageFilter).build() + ) + + val messageStream = createStreams(SESSION_ALIAS, Direction.FIRST, messagesWithKeyFields) + val parentEventID = createEvent(EventUtils.generateUUID()) + + sequenceCheckRuleTask( + parentEventID, + messageStream, + true, + filtersParam = messageFilters, + taskTimeout = TaskTimeout(5000L, 500L) + ).begin(createCheckpoint(checkpointTimestamp, Long.MIN_VALUE)) + + val batchRequest = awaitEventBatchRequest(1000L, 6) + val eventsList: List = batchRequest.flatMap(EventBatch::getEventsList) + + assertAll({ + val rootEvent = assertNotNull(eventsList.find { it.parentId == parentEventID }) + assertEquals(2, rootEvent.attachedMessageIdsCount) + assertEquals(listOf(1L, 2L), rootEvent.attachedMessageIdsList.map { it.sequence }) + }, { + val checkedMessages = assertNotNull(eventsList.find { it.type == CHECK_MESSAGES_TYPE }, "Cannot find checkMessages event") + val verifications = eventsList.filter { it.parentId == checkedMessages.id } + assertEquals(2, verifications.size, "Unexpected verifications count: $verifications") + assertTrue("The first verification should be success") { verifications.first().status == EventStatus.SUCCESS } + assertTrue("The second verification should be failed due to message timeout") { verifications.last().status == EventStatus.FAILED } + assertEquals(listOf(1L), verifications.flatMap { verification -> verification.attachedMessageIdsList.map { it.sequence } }) + }, { + assertCheckSequenceStatus(EventStatus.FAILED, eventsList) // because the second message was skipped due to message timeout + }) + } + + @Test + fun `check sequence of messages with message timeout and missed sequence`() { + val checkpointTimestamp = Instant.now() + val messagesWithKeyFields: List = listOf( + constructMessage(1, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 500)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build(), + constructMessage(2, SESSION_ALIAS, MESSAGE_TYPE, timestamp = getMessageTimestamp(checkpointTimestamp, 600)) + .putAllFields(mapOf( + "A" to Value.newBuilder().setSimpleValue("42").build(), + "B" to Value.newBuilder().setSimpleValue("AAA").build() + )) + .build() + ) + + val messageFilter = RootMessageFilter.newBuilder() + .setMessageType(MESSAGE_TYPE) + .setMessageFilter( + MessageFilter.newBuilder() + .putAllFields( + mapOf( + "A" to ValueFilter.newBuilder().setKey(true).setSimpleFilter("42").build(), + "B" to ValueFilter.newBuilder().setSimpleFilter("AAA").build() + ) + ) + ).build() + val messageFilters: List = listOf( + RootMessageFilter.newBuilder(messageFilter).build() + ) + + val messageStream = createStreams(SESSION_ALIAS, Direction.FIRST, messagesWithKeyFields) + val parentEventID = createEvent(EventUtils.generateUUID()) + + sequenceCheckRuleTask( + parentEventID, + messageStream, + true, + filtersParam = messageFilters, + taskTimeout = TaskTimeout(5000L, 500L) + ).begin(createCheckpoint(checkpointTimestamp, 0)) + + val batchRequest = awaitEventBatchRequest(1000L, 6) + val eventsList: List = batchRequest.flatMap(EventBatch::getEventsList) + + assertAll({ + val rootEvent = assertNotNull(eventsList.find { it.parentId == parentEventID }) + assertEquals(1, rootEvent.attachedMessageIdsCount) + assertEquals(listOf(1L), rootEvent.attachedMessageIdsList.map { it.sequence }) + }, { + val checkedMessages = assertNotNull(eventsList.find { it.type == CHECK_MESSAGES_TYPE }, "Cannot find checkMessages event") + val verifications = eventsList.filter { it.parentId == checkedMessages.id } + assertEquals(1, verifications.size, "Unexpected verifications count: $verifications") + assertTrue("Some verifications are not success: $verifications") { verifications.all { it.status == EventStatus.SUCCESS } } + assertEquals(listOf(1L), verifications.flatMap { verification -> verification.attachedMessageIdsList.map { it.sequence } }) + }, { + assertCheckSequenceStatus(EventStatus.SUCCESS, eventsList) // because all key fields are in a correct order + }, { + assertEquals(EventStatus.FAILED, eventsList.last().status) // check event with missed start sequence + }) + } + @Test fun `check ordering is not failed in case key fields are matches the order but the rest are not`() { val messagesWithKeyFields: List = listOf( @@ -349,7 +629,7 @@ class TestSequenceCheckTask : AbstractCheckTaskTest() { val messages = Observable.fromIterable(messagesWithKeyFields) val messageStream: Observable = Observable.just(StreamContainer(SessionKey(SESSION_ALIAS, Direction.FIRST), 10, messages)) - val parentEventID = EventID.newBuilder().setId(EventUtils.generateUUID()).build() + val parentEventID = createEvent(EventUtils.generateUUID()) sequenceCheckRuleTask(parentEventID, messageStream, true).begin() @@ -379,10 +659,9 @@ class TestSequenceCheckTask : AbstractCheckTaskTest() { set(indexesToSwitch.first, get(indexesToSwitch.second)) set(indexesToSwitch.second, tmp) } - val messages = Observable.fromIterable(messagesUnordered) - val messageStream: Observable = Observable.just(StreamContainer(SessionKey(SESSION_ALIAS, Direction.FIRST), 10, messages)) - val parentEventID = EventID.newBuilder().setId(EventUtils.generateUUID()).build() + val messageStream = createStreams(SESSION_ALIAS, Direction.FIRST, messagesUnordered) + val parentEventID = createEvent(EventUtils.generateUUID()) sequenceCheckRuleTask(parentEventID, messageStream, false).begin() @@ -436,7 +715,7 @@ class TestSequenceCheckTask : AbstractCheckTaskTest() { val messages = Observable.fromIterable(messagesWithKeyFields) val messageStream: Observable = Observable.just(StreamContainer(SessionKey(SESSION_ALIAS, Direction.FIRST), 10, messages)) - val parentEventID = EventID.newBuilder().setId(EventUtils.generateUUID()).build() + val parentEventID = createEvent(EventUtils.generateUUID()) sequenceCheckRuleTask(parentEventID, messageStream, false).begin() @@ -500,14 +779,12 @@ class TestSequenceCheckTask : AbstractCheckTaskTest() { checkOrder: Boolean, preFilterParam: PreFilter = preFilter, filtersParam: List = protoMessageFilters, - maxEventBatchContentSize: Int = 1024 * 1024 + taskTimeout: TaskTimeout = TaskTimeout(5000L) ): SequenceCheckRuleTask { return SequenceCheckRuleTask( - description = "Test", + ruleConfiguration = createRuleConfiguration(taskTimeout), startTime = Instant.now(), sessionKey = SessionKey(SESSION_ALIAS, Direction.FIRST), - timeout = 5000L, - maxEventBatchContentSize = maxEventBatchContentSize, protoPreFilter = preFilterParam, protoMessageFilters = filtersParam, checkOrder = checkOrder, diff --git a/src/test/kotlin/com/exactpro/th2/check1/rule/sequence/TestSequenceCheckTaskWithSilenceCheck.kt b/src/test/kotlin/com/exactpro/th2/check1/rule/sequence/TestSequenceCheckTaskWithSilenceCheck.kt new file mode 100644 index 00000000..dfe1efa0 --- /dev/null +++ b/src/test/kotlin/com/exactpro/th2/check1/rule/sequence/TestSequenceCheckTaskWithSilenceCheck.kt @@ -0,0 +1,234 @@ +/* + * Copyright 2021 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.th2.check1.rule.sequence + +import com.exactpro.th2.check1.configuration.Check1Configuration +import com.exactpro.th2.check1.grpc.CheckSequenceRuleRequest +import com.exactpro.th2.check1.rule.AbstractCheckTaskTest +import com.exactpro.th2.check1.rule.RuleFactory +import com.exactpro.th2.common.grpc.ConnectionID +import com.exactpro.th2.common.grpc.Direction +import com.exactpro.th2.common.grpc.EventBatch +import com.exactpro.th2.common.grpc.EventStatus +import com.exactpro.th2.common.grpc.FilterOperation +import com.exactpro.th2.common.grpc.ValueFilter +import com.exactpro.th2.common.message.messageFilter +import com.exactpro.th2.common.message.rootMessageFilter +import com.exactpro.th2.common.message.toJson +import com.exactpro.th2.common.value.toValue +import org.junit.jupiter.api.Assertions.assertEquals +import org.junit.jupiter.api.Test +import org.junit.jupiter.api.assertAll +import kotlin.test.assertNull + +class TestSequenceCheckTaskWithSilenceCheck : AbstractCheckTaskTest() { + + @Test + fun `reports about extra messages when timeout exceeded`() { + val streams = createStreams(messages = (0..2L).map { + constructMessage(sequence = it) + .putFields("A", 42.toValue()) + .putFields("B", it.toValue()) + .build() + }) + val factory = RuleFactory(Check1Configuration(), streams, clientStub) + + val filters = (0..1).map { + rootMessageFilter(MESSAGE_TYPE) + .setMessageFilter(messageFilter() + .putFields("A", ValueFilter.newBuilder().setOperation(FilterOperation.EQUAL).setSimpleFilter("42").setKey(true).build()) + .putFields("B", ValueFilter.newBuilder().setOperation(FilterOperation.EQUAL).setSimpleFilter(it.toString()).build()) + ).build() + } + val preFilter = createPreFilter("A", "42", FilterOperation.EQUAL) + val parentId = createEvent("root") + + val request = CheckSequenceRuleRequest.newBuilder() + .setConnectivityId(ConnectionID.newBuilder().setSessionAlias(SESSION_ALIAS)) + .setDirection(Direction.FIRST) + .setTimeout(1000) + .addAllRootMessageFilters(filters) + .setPreFilter(preFilter) + .setParentEventId(parentId) + .build() + val sequenceRule = factory.createSequenceCheckRule(request, true) + val silenceCheck = factory.createSilenceCheck(request, 1000) + sequenceRule.subscribeNextTask(silenceCheck) + sequenceRule.begin() + + val events = awaitEventBatchRequest(2000, 10).flatMap(EventBatch::getEventsList) + val silenceCheckRoot = events.first { it.type == "AutoSilenceCheck" }.id + val result = events.first { it.type == "noMessagesCheckResult" && it.parentId == silenceCheckRoot } + assertAll( + { assertEquals(EventStatus.FAILED, result.status) { "Unexpected status for event: ${result.toJson()}" } }, + { assertEquals("Check failed: 1 extra messages were found.", result.name) { "Unexpected name for event: ${result.toJson()}" } }, + { + assertEquals(listOf(2L), result.attachedMessageIdsList.map { it.sequence }) { + "Unexpected messages attached: ${result.attachedMessageIdsList.map { it.toJson() }}" + } + } + ) + } + + @Test + fun `reports no extra messages found when timeout exceeded`() { + val streams = createStreams(messages = (0..1L).map { + constructMessage(sequence = it) + .putFields("A", 42.toValue()) + .putFields("B", it.toValue()) + .build() + }) + val factory = RuleFactory(Check1Configuration(), streams, clientStub) + + val filters = (0..1).map { + rootMessageFilter(MESSAGE_TYPE) + .setMessageFilter(messageFilter() + .putFields("A", ValueFilter.newBuilder().setOperation(FilterOperation.EQUAL).setSimpleFilter("42").setKey(true).build()) + .putFields("B", ValueFilter.newBuilder().setOperation(FilterOperation.EQUAL).setSimpleFilter(it.toString()).build()) + ).build() + } + val preFilter = createPreFilter("A", "42", FilterOperation.EQUAL) + val parentId = createEvent("root") + + val request = CheckSequenceRuleRequest.newBuilder() + .setConnectivityId(ConnectionID.newBuilder().setSessionAlias(SESSION_ALIAS)) + .setDirection(Direction.FIRST) + .setTimeout(1000) + .addAllRootMessageFilters(filters) + .setPreFilter(preFilter) + .setParentEventId(parentId) + .build() + val sequenceRule = factory.createSequenceCheckRule(request, true) + val silenceCheck = factory.createSilenceCheck(request, 1000) + sequenceRule.subscribeNextTask(silenceCheck) + sequenceRule.begin() + + val events = awaitEventBatchRequest(2000, 8).flatMap(EventBatch::getEventsList) + val silenceCheckRoot = events.first { it.type == "AutoSilenceCheck" }.id + val result = events.first { it.type == "noMessagesCheckResult" && it.parentId == silenceCheckRoot } + assertAll( + { assertEquals(EventStatus.SUCCESS, result.status) { "Unexpected status for event: ${result.toJson()}" } }, + { assertEquals("Check passed", result.name) { "Unexpected name for event: ${result.toJson()}" } } + ) + } + + @Test + fun `does not report if next rule is subscribed in chain before beginning`() { + val streams = createStreams(messages = (0..2L).map { + constructMessage(sequence = it) + .putFields("A", 42.toValue()) + .putFields("B", it.toValue()) + .build() + }) + val factory = RuleFactory(Check1Configuration(), streams, clientStub) + + val filters = (0..1).map { + rootMessageFilter(MESSAGE_TYPE) + .setMessageFilter(messageFilter() + .putFields("A", ValueFilter.newBuilder().setOperation(FilterOperation.EQUAL).setSimpleFilter("42").setKey(true).build()) + .putFields("B", ValueFilter.newBuilder().setOperation(FilterOperation.EQUAL).setSimpleFilter(it.toString()).build()) + ).build() + } + val preFilter = createPreFilter("A", "42", FilterOperation.EQUAL) + val parentId = createEvent("root") + + val request = CheckSequenceRuleRequest.newBuilder() + .setConnectivityId(ConnectionID.newBuilder().setSessionAlias(SESSION_ALIAS)) + .setDescription("1") + .setDirection(Direction.FIRST) + .setTimeout(1000) + .addAllRootMessageFilters(filters) + .setPreFilter(preFilter) + .setParentEventId(parentId) + .build() + val anotherRequest = request.toBuilder() + .clearRootMessageFilters() + .setDescription("2") + .addRootMessageFilters(rootMessageFilter(MESSAGE_TYPE) + .setMessageFilter(messageFilter() + .putFields("A", ValueFilter.newBuilder().setOperation(FilterOperation.EQUAL).setSimpleFilter("42").setKey(true).build()) + .putFields("B", ValueFilter.newBuilder().setOperation(FilterOperation.EQUAL).setSimpleFilter("2").build()) + ).build()) + .build() + val sequenceRule = factory.createSequenceCheckRule(request, true) + val silenceCheck = factory.createSilenceCheck(request, 1000) + val anotherRule = factory.createSequenceCheckRule(anotherRequest, true) + sequenceRule.subscribeNextTask(silenceCheck) + sequenceRule.begin() + silenceCheck.subscribeNextTask(anotherRule) + + val events = awaitEventBatchRequest(2000, 12).flatMap(EventBatch::getEventsList) + assertAll( + { assertNull(events.find { it.type == "AutoSilenceCheck" }, "Unexpected events: $events") }, + { + events.filter { it.name == "Check sequence rule - 1" } + .also { + assertEquals(1, it.size) { "Unexpected count of events for the first rule: $it" } + } + }, + { + events.filter { it.name == "Check sequence rule - 2" } + .also { + assertEquals(1, it.size) { "Unexpected count of events for the second rule: $it" } + } + } + ) + } + + @Test + fun `does not report when next rule added to the chain before timeout exceeds`() { + val streams = createStreams(messages = (0..1L).map { + constructMessage(sequence = it) + .putFields("A", 42.toValue()) + .putFields("B", it.toValue()) + .build() + }) + val factory = RuleFactory(Check1Configuration(), streams, clientStub) + + val filters = (0..1).map { + rootMessageFilter(MESSAGE_TYPE) + .setMessageFilter(messageFilter() + .putFields("A", ValueFilter.newBuilder().setOperation(FilterOperation.EQUAL).setSimpleFilter("42").setKey(true).build()) + .putFields("B", ValueFilter.newBuilder().setOperation(FilterOperation.EQUAL).setSimpleFilter(it.toString()).build()) + ).build() + } + val preFilter = createPreFilter("A", "42", FilterOperation.EQUAL) + val parentId = createEvent("root") + + val request = CheckSequenceRuleRequest.newBuilder() + .setConnectivityId(ConnectionID.newBuilder().setSessionAlias(SESSION_ALIAS)) + .setDescription("1") + .setDirection(Direction.FIRST) + .setTimeout(1000) + .addAllRootMessageFilters(filters) + .setPreFilter(preFilter) + .setParentEventId(parentId) + .build() + val silenceCheck = factory.createSilenceCheck(request, 1000) + val sequenceRule = factory.createSequenceCheckRule(request.toBuilder().setDescription("2").build(), true) + silenceCheck.begin() + silenceCheck.subscribeNextTask(sequenceRule) + + val events = awaitEventBatchRequest(2000, 6).flatMap(EventBatch::getEventsList) + assertAll( + { assertNull(events.find { it.type == "AutoSilenceCheck" }, "Unexpected events: $events") }, + { + events.filter { it.name == "Check sequence rule - 2" } + .also { + assertEquals(1, it.size) { "Unexpected count of events for the rule: $it" } + } + } + ) + } +} \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/check1/util/TestVerificationUtil.kt b/src/test/kotlin/com/exactpro/th2/check1/util/TestVerificationUtil.kt index acb065de..9d71702a 100644 --- a/src/test/kotlin/com/exactpro/th2/check1/util/TestVerificationUtil.kt +++ b/src/test/kotlin/com/exactpro/th2/check1/util/TestVerificationUtil.kt @@ -13,10 +13,32 @@ package com.exactpro.th2.check1.util +import com.exactpro.sf.comparison.ComparatorSettings +import com.exactpro.sf.comparison.ComparisonUtil +import com.exactpro.sf.comparison.MessageComparator +import com.exactpro.sf.scriptrunner.StatusType +import com.exactpro.th2.check1.rule.AbstractCheckTask +import com.exactpro.th2.common.grpc.ComparisonSettings +import com.exactpro.th2.common.grpc.FailUnexpected import com.exactpro.th2.common.grpc.FilterOperation +import com.exactpro.th2.common.grpc.MessageFilter import com.exactpro.th2.common.grpc.MetadataFilter +import com.exactpro.th2.common.grpc.RootMessageFilter +import com.exactpro.th2.common.grpc.Value +import com.exactpro.th2.common.grpc.ValueFilter +import com.exactpro.th2.common.message.message +import com.exactpro.th2.common.message.messageFilter +import com.exactpro.th2.common.value.nullValue +import com.exactpro.th2.common.value.toValue +import com.exactpro.th2.common.value.toValueFilter +import com.exactpro.th2.sailfish.utils.ProtoToIMessageConverter import org.junit.jupiter.api.Assertions import org.junit.jupiter.api.Test +import org.junit.jupiter.params.ParameterizedTest +import org.junit.jupiter.params.provider.Arguments +import org.junit.jupiter.params.provider.Arguments.arguments +import org.junit.jupiter.params.provider.MethodSource +import java.util.stream.Stream internal class TestVerificationUtil { @Test @@ -33,4 +55,93 @@ internal class TestVerificationUtil { "keyProp" to false /*not transitive*/ ), metaContainer.keyFields) } + + @ParameterizedTest + @MethodSource("failUnexpectedByFieldsAndMessages") + fun `fail unexpected test`(valueFilter: ValueFilter, value: Value) { + val filter: RootMessageFilter = RootMessageFilter.newBuilder() + .setMessageType("Test") + .setMessageFilter(MessageFilter.newBuilder() + .putFields("A", valueFilter) + .setComparisonSettings(ComparisonSettings.newBuilder() + .setFailUnexpected(FailUnexpected.FIELDS_AND_MESSAGES) + .build() + ) + .build()) + .build() + + val actual = message("Test").apply { + putFields("A", value) + }.build() + + val settings = ComparatorSettings().apply { + metaContainer = VerificationUtil.toMetaContainer(filter.messageFilter, false) + } + + val actualIMessage = converter.fromProtoMessage(actual, false) + val filterIMessage = converter.fromProtoFilter(filter.messageFilter, filter.messageType) + val result = MessageComparator.compare( + actualIMessage, + filterIMessage, + settings + ) + + Assertions.assertNotNull(result) { "Result cannot be null" } + Assertions.assertEquals(StatusType.FAILED, ComparisonUtil.getStatusType(result)) + } + + companion object { + private val converter = AbstractCheckTask.CONVERTER + + @JvmStatic + fun failUnexpectedByFieldsAndMessages(): Stream = Stream.of( + arguments( + listOf( + messageFilter().putFields("A1", "1".toValueFilter()) + ).toValueFilter(), + listOf( + message().putFields("A1", "1".toValue()).putFields("B1", "2".toValue()).build() + ).toValue() + ), + arguments( + messageFilter().putFields("A1", "1".toValueFilter()).toValueFilter(), + message().putFields("A1", "1".toValue()).putFields("B1", "2".toValue()).build().toValue() + ), + arguments( + messageFilter().putFields( + "A1", listOf("1", "2").toValueFilter() + ).toValueFilter(), + message().putFields( + "A1", + listOf("1", "2", "3").toValue() + ).build().toValue() + ), + arguments( + messageFilter().putFields( + "A1", + messageFilter().putFields("A2", "1".toValueFilter()).toValueFilter() + ).toValueFilter(), + message().putFields( + "A1", + message() + .putFields("A2", "1".toValue()) + .putFields("B2", "2".toValue()) + .build().toValue() + ).build().toValue() + ), + arguments( + listOf( + "1".toValueFilter() + ).toValueFilter(), + listOf( + "1".toValue(), + "2".toValue() + ).toValue() + ), + arguments( + messageFilter().putFields("A", 42.toValueFilter()).toValueFilter(), + message().putFields("A", 42.toValue()).putFields("B", nullValue()).toValue() + ) + ) + } } \ No newline at end of file diff --git a/src/test/kotlin/com/exactpro/th2/check1/util/Utils.kt b/src/test/kotlin/com/exactpro/th2/check1/util/Utils.kt index 02bec5cc..6c6082db 100644 --- a/src/test/kotlin/com/exactpro/th2/check1/util/Utils.kt +++ b/src/test/kotlin/com/exactpro/th2/check1/util/Utils.kt @@ -17,6 +17,8 @@ import com.exactpro.th2.common.event.bean.VerificationEntry import com.exactpro.th2.common.event.bean.VerificationStatus import com.exactpro.th2.common.grpc.FilterOperation import com.exactpro.th2.common.grpc.MetadataFilter +import org.junit.jupiter.api.assertThrows +import kotlin.test.assertEquals fun String.toSimpleFilter(op: FilterOperation, key: Boolean = false): MetadataFilter.SimpleFilter = MetadataFilter.SimpleFilter.newBuilder() .setOperation(op) @@ -31,4 +33,15 @@ fun createVerificationEntry(status: VerificationStatus): VerificationEntry = Ver fun createVerificationEntry(vararg verificationEntries: Pair): VerificationEntry = VerificationEntry().apply { fields = linkedMapOf(*verificationEntries) +} + +inline fun assertThrowsWithMessages(vararg exceptionMessages: String?, crossinline action: () -> Unit) { + val exception = assertThrows { + action() + } + var currentException: Throwable? = exception + for (exceptionMessage in exceptionMessages) { + assertEquals(exceptionMessage, currentException?.message) + currentException = currentException?.cause + } } \ No newline at end of file