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

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

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