org.conqat.engine.index.shared.tests.TestExecution Maven / Gradle / Ivy
Go to download
Show more of this group Show more artifacts with this name
Show all versions of teamscale-commons Show documentation
Show all versions of teamscale-commons Show documentation
Provides common DTOs for Teamscale
/*
* Copyright (c) CQSE GmbH
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.conqat.engine.index.shared.tests;
import java.io.Serializable;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.Arrays;
import java.util.Collection;
import java.util.Iterator;
import java.util.List;
import java.util.Objects;
import java.util.Optional;
import java.util.regex.Pattern;
import org.apache.commons.lang3.builder.ReflectionToStringBuilder;
import org.checkerframework.checker.nullness.qual.NonNull;
import org.checkerframework.checker.nullness.qual.Nullable;
import org.conqat.engine.sourcecode.coverage.TestUniformPathUtils;
import org.conqat.lib.commons.assessment.Assessment;
import org.conqat.lib.commons.assessment.ETrafficLightColor;
import org.conqat.lib.commons.js_export.ExportToTypeScript;
import org.conqat.lib.commons.string.StringUtils;
import org.conqat.lib.commons.uniformpath.RelativeUniformPath;
import org.conqat.lib.commons.uniformpath.UniformPath;
import org.conqat.lib.commons.uniformpath.UniformPathCompatibilityUtil;
import com.fasterxml.jackson.annotation.JsonCreator;
import com.fasterxml.jackson.annotation.JsonProperty;
import com.google.common.base.Preconditions;
import com.google.common.collect.Iterables;
import com.google.common.hash.Hashing;
import com.teamscale.commons.utils.StringPool;
/**
* Representation of a single test (method) execution.
*/
@ExportToTypeScript
public class TestExecution implements Serializable {
private static final long serialVersionUID = 1L;
/** The name of the JSON property name for {@link #uniformPath}. */
private static final String UNIFORM_PATH_PROPERTY = "uniformPath";
/** The name of the JSON property name for {@link #testDetailPath}. */
private static final String TEST_DETAIL_PATH_PROPERTY = "testDetailPath";
/** The name of the JSON property name for {@link #durationSeconds}. */
private static final String DURATION_SECONDS_PROPERTY = "durationSeconds";
/** The name of the JSON property name for {@link #result}. */
private static final String RESULT_PROPERTY = "result";
/** The name of the JSON property name for {@link #message}. */
private static final String MESSAGE_PROPERTY = "message";
/**
* The maximum number of characters that are accepted as test name before
* truncating the name. Such cases can occur if the test supplied by the user is
* a parameterized test and the full input for the test is encoded within the
* test name, because no @DisplayName or @ParameterizedTest name override was
* provided.
* https://junit.org/junit5/docs/current/user-guide/#writing-tests-parameterized-tests-display-names
*/
public static final int MAX_TEST_NAME_LENGTH = 400;
/**
* {@link #MAX_TEST_NAME_LENGTH} without the length of the hash (32) and the
* characters added in {@link Builder#buildTruncatedTestName(String, String)} so
* that the truncated version of the test name stays smaller than
* {@link #MAX_TEST_NAME_LENGTH}.
*/
public static final int MAX_TEST_NAME_LENGTH_WITHOUT_HASH = MAX_TEST_NAME_LENGTH - 32 - 21;
/**
* The uniform path of the test (method) that was executed. This is an absolute
* (i.e. hierarchical) reference which identifies the test uniquely in the scope
* of the Teamscale project. It may (but is not required to) correspond to the
* path of some automated test case source code known to Teamscale. If the test
* was parameterized, this path is expected to reflect the parameter in some
* manner. The test name (the last segment of the path) is expected to have all
* forward slashes escaped.
*/
@JsonProperty(UNIFORM_PATH_PROPERTY)
private final String uniformPath;
/**
* The uniform path of the test that refers to the actual code of an automated
* test case source known to Teamscale. This path might not be unique (in case
* of parameterized test cases or subclasses). If the test was parameterized, it
* is not reflected in this path. Might be the same as {@link #uniformPath}. The
* test name (the last segment of the path) is expected to have all forward
* slashes escaped.
*/
@JsonProperty(TEST_DETAIL_PATH_PROPERTY)
@Nullable
private final String testDetailPath;
/**
* Duration of the execution in seconds.
*/
@JsonProperty(DURATION_SECONDS_PROPERTY)
private final double durationSeconds;
/**
* The actual execution result state.
*/
@JsonProperty(RESULT_PROPERTY)
private final ETestExecutionResult result;
/**
* Optional message given for test failures (normally contains a stack trace).
* For skipped and ignored tests the message will hold the message of skipped
* and ignored tests. e.g. "Flickers see TS-12345". May be {@code null}.
*/
@JsonProperty(MESSAGE_PROPERTY)
@Nullable
private final String message;
private TestExecution(RelativeUniformPath uniformPath, RelativeUniformPath testDetailPath, double durationSeconds,
ETestExecutionResult result, String message) {
this(uniformPath.toString(), testDetailPath.toString(), durationSeconds, result, message);
}
@JsonCreator
public TestExecution(@JsonProperty(UNIFORM_PATH_PROPERTY) String uniformPath,
@JsonProperty(TEST_DETAIL_PATH_PROPERTY) String testDetailPath,
@JsonProperty(DURATION_SECONDS_PROPERTY) double durationSeconds,
@JsonProperty(RESULT_PROPERTY) ETestExecutionResult result,
@JsonProperty(MESSAGE_PROPERTY) @Nullable String message) {
Preconditions.checkArgument(uniformPath != null, "Uniform path can't be null for test execution");
Preconditions.checkArgument(!uniformPath.startsWith(UniformPath.EType.TEST.getPrefix()),
"Uniform path for test execution can't start with -test-");
Preconditions.checkArgument(result != null, "Result can't be null for test execution");
Preconditions.checkArgument(durationSeconds >= 0, "Test duration can't be negative");
this.uniformPath = StringPool.intern(uniformPath);
this.testDetailPath = StringPool.intern(testDetailPath);
this.durationSeconds = durationSeconds;
this.result = result;
this.message = message;
}
/**
* @see #uniformPath
*/
public String getUniformPath() {
return uniformPath;
}
/**
* @see #testDetailPath
*/
public String getTestDetailPath() {
return testDetailPath;
}
/**
* @see #durationSeconds
*/
public double getDurationMillis() {
return durationSeconds * 1000.0;
}
/**
* @see #durationSeconds
*/
public double getDurationSeconds() {
return durationSeconds;
}
/**
* @see #result
*/
public ETestExecutionResult getResult() {
return result;
}
/**
* @see #message
*/
public Optional getMessage() {
return Optional.ofNullable(message);
}
/**
* Returns true if the test detail path does not match the test execution path.
*/
public boolean isParameterizedTest() {
return testDetailPath != null && !uniformPath.equals(testDetailPath);
}
@Override
public boolean equals(Object o) {
if (this == o) {
return true;
}
if (o == null || getClass() != o.getClass()) {
return false;
}
TestExecution that = (TestExecution) o;
return Double.compare(that.durationSeconds, durationSeconds) == 0
&& Objects.equals(uniformPath, that.uniformPath) && Objects.equals(testDetailPath, that.testDetailPath)
&& result == that.result && Objects.equals(message, that.message);
}
@Override
public int hashCode() {
return Objects.hash(uniformPath, testDetailPath, durationSeconds, result, message);
}
@Override
public String toString() {
return ReflectionToStringBuilder.toString(this);
}
/**
* Returns whether something went wrong during execution of this test (either
* direct test failure or error during execution).
*/
public boolean isFailure() {
return result == ETestExecutionResult.ERROR || result == ETestExecutionResult.FAILURE;
}
/**
* Create an assessment for this test result.
*/
public Assessment createAssessment() {
switch (result) {
case PASSED:
return new Assessment(ETrafficLightColor.GREEN);
case IGNORED:
return new Assessment(ETrafficLightColor.YELLOW);
case SKIPPED:
return new Assessment(ETrafficLightColor.YELLOW);
case ERROR:
return new Assessment(ETrafficLightColor.RED);
case FAILURE:
return new Assessment(ETrafficLightColor.RED);
default:
return new Assessment(ETrafficLightColor.UNKNOWN);
}
}
/**
* Compute the test execution path.
*/
public UniformPath toUniformPath() {
return TestUniformPathUtils.convertToTestUniformPath(uniformPath);
}
/**
* Merges the {@link TestExecution}s. The result is the worst
* {@link ETestExecutionResult} with the maximum encountered
* {@link TestExecution#durationSeconds}.
*/
public static TestExecution merge(Collection testExecutions) {
Preconditions.checkArgument(!testExecutions.isEmpty(), "Can't merge empty collection of test executions.");
if (testExecutions.size() == 1) {
return Iterables.getOnlyElement(testExecutions);
}
Iterator it = testExecutions.iterator();
TestExecution worstExecution = it.next();
while (it.hasNext()) {
TestExecution nextExecution = it.next();
Preconditions.checkArgument(nextExecution.uniformPath.equals(worstExecution.uniformPath),
"Can't merge test executions from separate uniform paths.");
if (ETestExecutionResult.worst(nextExecution.result, worstExecution.result) != worstExecution.result) {
worstExecution = nextExecution;
}
}
double maxDurationSeconds = testExecutions.stream().mapToDouble(TestExecution::getDurationSeconds).max()
.getAsDouble();
return new TestExecution(worstExecution.uniformPath, worstExecution.testDetailPath, maxDurationSeconds,
worstExecution.result, worstExecution.message);
}
/**
* Builder for creating {@link TestExecution} instances.
*/
public static class Builder {
private static final Pattern UNESCAPED_SLASH = Pattern.compile("(? pathSegments, String testName) {
return new Builder(RelativeUniformPath.of(pathSegments)
.addSuffix(UniformPath.escapeSegment(truncateIfNeeded(testName))));
}
/**
* Truncates too long test names either after the first line break or after
* {@link #MAX_TEST_NAME_LENGTH_WITHOUT_HASH} characters.
*/
public static String truncateIfNeeded(String testName) {
String testNameFirstLine = StringUtils.getFirstLine(testName);
if (testName.length() <= MAX_TEST_NAME_LENGTH && testNameFirstLine.length() == testName.length()) {
return testName;
}
int visibleTestCharacters = Math.min(testNameFirstLine.length(), MAX_TEST_NAME_LENGTH_WITHOUT_HASH);
return buildTruncatedTestName(testName.substring(0, visibleTestCharacters),
testName.substring(visibleTestCharacters));
}
/**
* Builds a truncated test name by appending a hashed version of the truncated
* part to the prefix.
*/
@NonNull
public static String buildTruncatedTestName(String prefix, String truncatedPart) {
return prefix + "[hashed parameters: " + hashToString(truncatedPart) + "]";
}
@NonNull
private static String hashToString(String testParameter) {
return Hashing.murmur3_128().hashString(testParameter, StandardCharsets.UTF_8).toString();
}
/** Creates a new builder with the given uniform path and the existing info. */
public Builder fromBuilder(RelativeUniformPath uniformPath) {
Builder builder = new Builder(uniformPath);
builder.setTestDetailPath(testDetailPath);
builder.setDurationInSeconds(durationInSeconds);
builder.setResult(result);
builder.setFailureMessage(failureMessage);
return builder;
}
/**
* Sets the code path.
*/
public Builder setTestDetailPath(RelativeUniformPath testDetailPath) {
this.testDetailPath = testDetailPath;
return this;
}
/**
* Sets the failure message. Trims leading whitespace on each line.
*/
public Builder setFailureMessage(String failureMessage) {
if (failureMessage != null) {
failureMessage = StringUtils.removeWhitespaceAtBeginningOfLine(failureMessage.trim());
if (!failureMessage.isEmpty()) {
this.failureMessage = failureMessage;
}
}
return this;
}
/**
* Sets the test duration.
*/
public Builder setDuration(Duration duration) {
return setDurationInSeconds(duration.toMillis() / 1000d);
}
/**
* Sets the duration.
*/
public Builder setDurationInSeconds(double durationInSeconds) {
this.durationInSeconds = durationInSeconds;
return this;
}
/**
* Sets the execution result.
*/
public Builder setResult(ETestExecutionResult result) {
this.result = result;
return this;
}
/**
* Creates a new {@link TestExecution} instance from this builder.
*/
public TestExecution build() {
return new TestExecution(uniformPath, testDetailPath, durationInSeconds, result, failureMessage);
}
/**
* Returns the absolute uniform path of this test execution including test
* namespace/path and test name.
*/
public RelativeUniformPath getAbsolutePath() {
return uniformPath;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy