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
The newest version!
/*
* 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.ArrayList;
import java.util.Arrays;
import java.util.Collection;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
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.collections.CollectionUtils;
import org.conqat.lib.commons.string.StringUtils;
import org.conqat.lib.commons.test.IndexValueClass;
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.
*/
@IndexValueClass(containedInBackup = true)
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 #testLocations}. */
private static final String TEST_LOCATIONS_PROPERTY = "testLocations";
/** 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 name of the JSON property name for {@link #hash}. */
protected static final String HASH_PROPERTY = "hash";
/** The name of the JSON property for {@link #externalLink}. */
private static final String EXTERNAL_LINK_PROPERTY = "externalLink";
/** The name of the JSON property name for {@link #executionUnit}. */
private static final String EXECUTION_UNIT_PROPERTY = "executionUnit";
/** The name of the JSON property name for {@link #associatedSpecItems}. */
private static final String ASSOCIATED_SPEC_ITEMS_PROPERTY = "associatedSpecItems";
/**
* 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 execution (-test-execution-) that was executed. This is an absolute
* (i.e. hierarchical) reference which identifies the test uniquely in the scope of the Teamscale
* project in combination with the partition. 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;
/**
* A list of ANT-patterns limiting the search space for a corresponding test implementation.
*/
@JsonProperty(TEST_LOCATIONS_PROPERTY)
@Nullable
private List testLocations;
/**
* 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;
/**
* Link to an external build or test management tool that offers information about the run of the
* executable unit. Present in reports of version 2+.
*/
@JsonProperty(EXTERNAL_LINK_PROPERTY)
@Nullable
private final String externalLink;
/**
* 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;
/**
* Some kind of hash like value that can be specified in a Testwise Coverage report that allows to
* tell whether the test specification has changed. Can be for example a revision number or hash
* over the specification or similar.
*/
@JsonProperty(HASH_PROPERTY)
@Nullable
protected final String hash;
@JsonProperty(EXECUTION_UNIT_PROPERTY)
@Nullable
private final String executionUnit;
/** Still unparsed spec-item references (Strings from the report). */
@JsonProperty(ASSOCIATED_SPEC_ITEMS_PROPERTY)
@Nullable
private final List associatedSpecItems;
private TestExecution(UniformPath uniformPath, @Nullable List testLocations, double durationSeconds,
ETestExecutionResult result, @Nullable String message, @Nullable String hash, @Nullable String externalLink,
@Nullable String executionUnit, @Nullable List associatedSpecItems) {
this(uniformPath.toString(), testLocations, durationSeconds, result, message, hash, externalLink, executionUnit,
associatedSpecItems);
}
@JsonCreator
public TestExecution(@JsonProperty(UNIFORM_PATH_PROPERTY) String uniformPath,
@JsonProperty(TEST_LOCATIONS_PROPERTY) @Nullable List testLocations,
@JsonProperty(DURATION_SECONDS_PROPERTY) double durationSeconds,
@JsonProperty(RESULT_PROPERTY) ETestExecutionResult result,
@JsonProperty(MESSAGE_PROPERTY) @Nullable String message,
@JsonProperty(HASH_PROPERTY) @Nullable String hash,
@JsonProperty(EXTERNAL_LINK_PROPERTY) @Nullable String externalLink,
@JsonProperty(EXECUTION_UNIT_PROPERTY) @Nullable String executionUnit,
@JsonProperty(ASSOCIATED_SPEC_ITEMS_PROPERTY) @Nullable List associatedSpecItems) {
TestUniformPathUtils.assertIsTestExecutionPath(uniformPath);
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.testLocations = testLocations;
this.durationSeconds = durationSeconds;
this.result = result;
this.message = message;
this.hash = hash;
this.externalLink = externalLink;
this.executionUnit = executionUnit;
this.associatedSpecItems = associatedSpecItems;
}
/**
* @see #uniformPath
*/
public String getUniformPath() {
return uniformPath;
}
/**
* Compute the test execution path.
*/
public UniformPath toUniformPath() {
return UniformPathCompatibilityUtil.convert(uniformPath);
}
/**
* @see #testLocations
*/
@Nullable
public List getTestLocations() {
return testLocations;
}
/**
* @see #durationSeconds
*/
public double getDurationMillis() {
return durationSeconds * 1000.0;
}
/**
* @see #durationSeconds
*/
public double getDurationSeconds() {
return durationSeconds;
}
/**
* @see #result
*/
public ETestExecutionResult getResult() {
return result;
}
/**
* @see #externalLink
*/
public @Nullable String getExternalLink() {
return externalLink;
}
/**
* @see #message
*/
public Optional getMessage() {
return Optional.ofNullable(message);
}
public @Nullable String getExecutionUnit() {
return executionUnit;
}
public @Nullable List getAssociatedSpecItems() {
return associatedSpecItems;
}
/** @see #hash */
@Nullable
public String getHash() {
return hash;
}
@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);
case INCONCLUSIVE:
return new Assessment(ETrafficLightColor.YELLOW);
default:
return new Assessment(ETrafficLightColor.UNKNOWN);
}
}
/**
* 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();
HashSet associatedSpecItems = new HashSet<>();
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;
}
if (nextExecution.associatedSpecItems != null) {
associatedSpecItems.addAll(nextExecution.associatedSpecItems);
}
}
double maxDurationSeconds = testExecutions.stream().mapToDouble(TestExecution::getDurationSeconds).max()
.getAsDouble();
return new TestExecution(worstExecution.uniformPath, worstExecution.getTestLocations(), maxDurationSeconds,
worstExecution.result, worstExecution.message, worstExecution.hash, worstExecution.externalLink,
worstExecution.getExecutionUnit(), new ArrayList<>(associatedSpecItems));
}
/**
* Builder for creating {@link TestExecution} instances.
*/
public static class Builder {
private static final Pattern UNESCAPED_SLASH = Pattern.compile("(? testLocations;
private double durationInSeconds = 0;
private ETestExecutionResult result;
private String failureMessage;
@Nullable
private String hash;
private String externalLink;
@Nullable
private String executionUnit;
@Nullable
private List associatedSpecItems;
private Builder(UniformPath uniformPath) {
Preconditions.checkArgument(uniformPath.isTestExecutionPath(),
"Uniform path for test execution must start with -test-execution- but was " + uniformPath);
this.uniformPath = uniformPath;
}
private Builder(RelativeUniformPath uniformPath) {
Preconditions.checkArgument(
!uniformPath.toString().startsWith(UniformPath.EType.TEST_EXECUTION.getPrefix()),
"Uniform path for test execution must not start with -test-execution- but was " + uniformPath);
this.uniformPath = UniformPath.testExecutionRoot().resolve(uniformPath);
}
/**
* Creates a builder for a fully qualified test class name and test method name. This also supports
* (unescaped) slashes as separator.
*/
public static Builder fromClassAndName(String fullyQualifiedClassName, String testName) {
// replace all slashes by a dot, so both dots and slashes build hierarchy
fullyQualifiedClassName = UNESCAPED_SLASH.matcher(fullyQualifiedClassName).replaceAll(".");
return fromPathAndName(Arrays.asList(fullyQualifiedClassName.split("\\.")), testName);
}
/**
* Creates a builder for a uniform path.
*/
public static Builder fromUniformPath(String path) {
return new Builder(UniformPathCompatibilityUtil.convert(truncateIfNeeded(path)));
}
/**
* Creates a builder for a path.
*/
public static Builder fromRelativePath(String path) {
return new Builder(UniformPathCompatibilityUtil.convertRelative(truncateIfNeeded(path)));
}
/**
* Creates a builder for a path and test name.
*/
private static Builder fromPathAndName(List pathSegments, String testName) {
return new Builder(RelativeUniformPath.of(CollectionUtils.map(pathSegments, UniformPath::escapeSegment))
.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(UniformPath uniformPath) {
Builder builder = new Builder(uniformPath);
builder.setDurationInSeconds(durationInSeconds);
builder.setResult(result);
builder.setHash(hash);
builder.setFailureMessage(failureMessage);
builder.setExternalLink(externalLink);
builder.setExecutionUnit(executionUnit);
builder.setAssociatedSpecItems(associatedSpecItems);
builder.setTestLocations(testLocations);
return builder;
}
/**
* 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;
}
/**
* Sets the execution result.
*/
public Builder setHash(@Nullable String hash) {
this.hash = hash;
return this;
}
/**
* Sets the external Link.
*/
public Builder setExternalLink(String externalLink) {
this.externalLink = externalLink;
return this;
}
/**
* Sets the execution unit.
*/
public Builder setExecutionUnit(String executionUnit) {
this.executionUnit = executionUnit;
return this;
}
/**
* Sets the associatedSpecItems.
*/
public Builder setAssociatedSpecItems(List associatedSpecItems) {
this.associatedSpecItems = associatedSpecItems;
return this;
}
/**
* Creates a new {@link TestExecution} instance from this builder.
*/
public TestExecution build() {
return new TestExecution(uniformPath, testLocations, durationInSeconds, result, failureMessage, hash,
externalLink, executionUnit, associatedSpecItems);
}
/**
* Returns the absolute uniform path of this test execution including test namespace/path and test
* name.
*/
public UniformPath getUniformPath() {
return uniformPath;
}
/**
* Sets the test locations.
*/
public Builder setTestLocations(@Nullable List testLocations) {
this.testLocations = testLocations;
return this;
}
}
}
© 2015 - 2025 Weber Informatics LLC | Privacy Policy