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

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

/*
 * 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.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.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 #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 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;

	/**
	 * 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;

	private TestExecution(UniformPath uniformPath, double durationSeconds, ETestExecutionResult result,
			@Nullable String message, @Nullable String hash, @Nullable String externalLink,
			@Nullable String executionUnit) {
		this(uniformPath.toString(), durationSeconds, result, message, hash, externalLink, executionUnit);
	}

	@JsonCreator
	public TestExecution(@JsonProperty(UNIFORM_PATH_PROPERTY) String uniformPath,
			@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) {
		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.durationSeconds = durationSeconds;
		this.result = result;
		this.message = message;
		this.hash = hash;
		this.externalLink = externalLink;
		this.executionUnit = executionUnit;
	}

	/**
	 * @see #uniformPath
	 */
	public String getUniformPath() {
		return uniformPath;
	}

	/**
	 * Compute the test execution path.
	 */
	public UniformPath toUniformPath() {
		return UniformPathCompatibilityUtil.convert(uniformPath);
	}

	/**
	 * @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 String getExternalLink() {
		return externalLink;
	}

	/**
	 * @see #message
	 */
	public Optional getMessage() {
		return Optional.ofNullable(message);
	}

	public @Nullable String getExecutionUnit() {
		return executionUnit;
	}

	/** @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();
		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, maxDurationSeconds, worstExecution.result,
				worstExecution.message, worstExecution.hash, worstExecution.externalLink,
				worstExecution.getExecutionUnit());
	}

	/**
	 * 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(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);
			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;
		}

		/**
		 * Creates a new {@link TestExecution} instance from this builder.
		 */
		public TestExecution build() {
			return new TestExecution(uniformPath, durationInSeconds, result, failureMessage, hash, externalLink,
					executionUnit);
		}

		/**
		 * Returns the absolute uniform path of this test execution including test
		 * namespace/path and test name.
		 */
		public UniformPath getUniformPath() {
			return uniformPath;
		}

	}
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy