All Downloads are FREE. Search and download functionalities are using the official Maven repository.

org.conqat.engine.index.shared.tests.TestExecution Maven / Gradle / Ivy

There is a newer version: 2025.1.0
Show 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.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