001/*-------------------------------------------------------------------------+ 002| | 003| Copyright (c) 2005-2018 The ConQAT Project | 004| | 005| Licensed under the Apache License, Version 2.0 (the "License"); | 006| you may not use this file except in compliance with the License. | 007| You may obtain a copy of the License at | 008| | 009| http://www.apache.org/licenses/LICENSE-2.0 | 010| | 011| Unless required by applicable law or agreed to in writing, software | 012| distributed under the License is distributed on an "AS IS" BASIS, | 013| WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. | 014| See the License for the specific language governing permissions and | 015| limitations under the License. | 016| | 017+-------------------------------------------------------------------------*/ 018package org.conqat.engine.index.shared.tests; 019 020import java.io.Serializable; 021import java.time.Duration; 022import java.util.Arrays; 023import java.util.Collection; 024import java.util.Iterator; 025import java.util.List; 026import java.util.Objects; 027import java.util.Optional; 028import java.util.regex.Pattern; 029 030import javax.annotation.Nullable; 031 032import org.apache.commons.lang3.builder.ReflectionToStringBuilder; 033import org.conqat.engine.sourcecode.coverage.TestUniformPathUtils; 034import org.conqat.lib.commons.assessment.Assessment; 035import org.conqat.lib.commons.assessment.ETrafficLightColor; 036import org.conqat.lib.commons.js_export.ExportToJavaScript; 037import org.conqat.lib.commons.string.StringUtils; 038import org.conqat.lib.commons.uniformpath.RelativeUniformPath; 039import org.conqat.lib.commons.uniformpath.UniformPath; 040 041import com.fasterxml.jackson.annotation.JsonCreator; 042import com.fasterxml.jackson.annotation.JsonProperty; 043import com.google.common.base.Preconditions; 044import com.google.common.collect.Iterables; 045 046/** 047 * Representation of a single test (method) execution. 048 */ 049@ExportToJavaScript 050public class TestExecution implements Serializable { 051 052 private static final long serialVersionUID = 1L; 053 054 /** The name of the JSON property name for {@link #uniformPath}. */ 055 private static final String UNIFORM_PATH_PROPERTY = "uniformPath"; 056 057 /** The name of the JSON property name for {@link #durationSeconds}. */ 058 private static final String DURATION_SECONDS_PROPERTY = "durationSeconds"; 059 060 /** The name of the JSON property name for {@link #result}. */ 061 private static final String RESULT_PROPERTY = "result"; 062 063 /** The name of the JSON property name for {@link #message}. */ 064 private static final String MESSAGE_PROPERTY = "message"; 065 066 /** 067 * The uniform path of the test (method) that was executed. This is an absolute 068 * (i.e. hierarchical) reference which identifies the test uniquely in the scope 069 * of the Teamscale project. It may (but is not required to) correspond to the 070 * path of some automated test case source code known to Teamscale. If the test 071 * was parameterized, this path is expected to reflect the parameter in some 072 * manner. 073 */ 074 @JsonProperty(UNIFORM_PATH_PROPERTY) 075 private final String uniformPath; 076 077 /** 078 * Duration of the execution in seconds. 079 */ 080 @JsonProperty(DURATION_SECONDS_PROPERTY) 081 private final double durationSeconds; 082 083 /** 084 * The actual execution result state. 085 */ 086 @JsonProperty(RESULT_PROPERTY) 087 private final ETestExecutionResult result; 088 089 /** 090 * Optional message given for test failures (normally contains a stack trace). 091 * For skipped and ignored tests the message will hold the message of skipped 092 * and ignored tests. e.g. "Flickers see TS-12345". May be {@code null}. 093 */ 094 @JsonProperty(MESSAGE_PROPERTY) 095 @Nullable 096 private final String message; 097 098 private TestExecution(RelativeUniformPath uniformPath, double durationSeconds, ETestExecutionResult result, 099 String message) { 100 this(uniformPath.toString(), durationSeconds, result, message); 101 } 102 103 @JsonCreator 104 protected TestExecution(@JsonProperty(UNIFORM_PATH_PROPERTY) String uniformPath, 105 @JsonProperty(DURATION_SECONDS_PROPERTY) double durationSeconds, 106 @JsonProperty(RESULT_PROPERTY) ETestExecutionResult result, 107 @JsonProperty(MESSAGE_PROPERTY) String message) { 108 Preconditions.checkArgument(uniformPath != null, "Uniform path can't be null for test execution"); 109 Preconditions.checkArgument(result != null, "Result can't be null for test execution"); 110 Preconditions.checkArgument(durationSeconds >= 0, "Test duration can't be negative"); 111 this.uniformPath = uniformPath; 112 this.durationSeconds = durationSeconds; 113 this.result = result; 114 this.message = message; 115 } 116 117 /** 118 * @see #uniformPath 119 */ 120 public String getUniformPath() { 121 return uniformPath; 122 } 123 124 /** 125 * @see #durationSeconds 126 */ 127 public double getDurationMillis() { 128 return durationSeconds * 1000.0; 129 } 130 131 /** 132 * @see #durationSeconds 133 */ 134 public double getDurationSeconds() { 135 return durationSeconds; 136 } 137 138 /** 139 * @see #result 140 */ 141 public ETestExecutionResult getResult() { 142 return result; 143 } 144 145 /** 146 * @see #message 147 */ 148 public Optional<String> getMessage() { 149 return Optional.ofNullable(message); 150 } 151 152 @Override 153 public boolean equals(Object o) { 154 if (this == o) { 155 return true; 156 } 157 if (o == null || getClass() != o.getClass()) { 158 return false; 159 } 160 TestExecution that = (TestExecution) o; 161 return Double.compare(that.durationSeconds, durationSeconds) == 0 162 && Objects.equals(uniformPath, that.uniformPath) && result == that.result 163 && Objects.equals(message, that.message); 164 } 165 166 @Override 167 public int hashCode() { 168 return Objects.hash(uniformPath, durationSeconds, result, message); 169 } 170 171 @Override 172 public String toString() { 173 return ReflectionToStringBuilder.toString(this); 174 } 175 176 /** 177 * Returns whether something went wrong during execution of this test (either 178 * direct test failure or error during execution). 179 */ 180 public boolean isFailure() { 181 return result == ETestExecutionResult.ERROR || result == ETestExecutionResult.FAILURE; 182 } 183 184 /** 185 * Create an assessment for this test result. 186 */ 187 public Assessment createAssessment() { 188 switch (result) { 189 case PASSED: 190 return new Assessment(ETrafficLightColor.GREEN); 191 case IGNORED: 192 return new Assessment(ETrafficLightColor.YELLOW); 193 case SKIPPED: 194 return new Assessment(ETrafficLightColor.YELLOW); 195 case ERROR: 196 return new Assessment(ETrafficLightColor.RED); 197 case FAILURE: 198 return new Assessment(ETrafficLightColor.RED); 199 default: 200 return new Assessment(ETrafficLightColor.UNKNOWN); 201 } 202 } 203 204 /** 205 * Compute the test execution path. 206 */ 207 public UniformPath toUniformPath() { 208 return TestUniformPathUtils.convertToUniformPath(uniformPath); 209 } 210 211 /** 212 * Merges the {@link TestExecution}s. The result is the worst 213 * {@link ETestExecutionResult} with the maximum encountered 214 * {@link TestExecution#durationSeconds}. 215 */ 216 public static TestExecution merge(Collection<TestExecution> testExecutions) { 217 Preconditions.checkArgument(!testExecutions.isEmpty(), "Can't merge emtpy collection of test executions."); 218 219 if (testExecutions.size() == 1) { 220 return Iterables.getOnlyElement(testExecutions); 221 } 222 223 Iterator<TestExecution> it = testExecutions.iterator(); 224 TestExecution worstExecution = it.next(); 225 while (it.hasNext()) { 226 TestExecution nextExecution = it.next(); 227 Preconditions.checkArgument(nextExecution.uniformPath.equals(worstExecution.uniformPath), 228 "Can't merge test executions from separate uniform paths."); 229 if (ETestExecutionResult.worst(nextExecution.result, worstExecution.result) != worstExecution.result) { 230 worstExecution = nextExecution; 231 } 232 } 233 234 double maxDurationSeconds = testExecutions.stream().mapToDouble(TestExecution::getDurationSeconds).max() 235 .getAsDouble(); 236 237 return new TestExecution(worstExecution.uniformPath, maxDurationSeconds, worstExecution.result, 238 worstExecution.message); 239 } 240 241 /** 242 * Builder for creating {@link TestExecution} instances. 243 */ 244 public static class Builder { 245 246 private static final Pattern UNESCAPED_SLASH = Pattern.compile("(?<!\\\\)/"); 247 248 private final RelativeUniformPath uniformPath; 249 250 private double durationInSeconds = 0; 251 252 private ETestExecutionResult result; 253 254 private String failureMessage; 255 256 private Builder(RelativeUniformPath uniformPath) { 257 this.uniformPath = uniformPath; 258 } 259 260 /** 261 * Creates a builder for a fully qualified test class name and test method name. 262 * This also supports (unescaped) slashes as separator. 263 */ 264 public static Builder fromClassAndName(String fullyQualifiedClassName, String testName) { 265 // replace all slashes by a dot, so both dots and slashes build hierarchy 266 fullyQualifiedClassName = UNESCAPED_SLASH.matcher(fullyQualifiedClassName).replaceAll("."); 267 return fromPathAndName(Arrays.asList(fullyQualifiedClassName.split("\\.")), testName); 268 } 269 270 /** 271 * Creates a builder for a relative uniform path. 272 */ 273 public static Builder fromPath(RelativeUniformPath path) { 274 return new Builder(path); 275 } 276 277 /** 278 * Creates a builder for a path and test name. 279 */ 280 private static Builder fromPathAndName(List<String> pathSegments, String testName) { 281 return fromPath(RelativeUniformPath.of(pathSegments).addSuffix(UniformPath.escapeSegment(testName))); 282 } 283 284 /** 285 * Sets the failure message. Trims leading whitespace on each line. 286 */ 287 public Builder setFailureMessage(String failureMessage) { 288 if (failureMessage != null) { 289 failureMessage = StringUtils.removeWhitespaceAtBeginningOfLine(failureMessage.trim()); 290 if (!failureMessage.isEmpty()) { 291 this.failureMessage = failureMessage; 292 } 293 } 294 return this; 295 } 296 297 /** 298 * Sets the test duration. 299 */ 300 public Builder setDuration(Duration duration) { 301 this.durationInSeconds = duration.toMillis() / 1000d; 302 return this; 303 } 304 305 /** 306 * Sets the duration. 307 */ 308 public Builder setDurationInSeconds(double durationInSeconds) { 309 this.durationInSeconds = durationInSeconds; 310 return this; 311 } 312 313 /** 314 * Sets the execution result. 315 */ 316 public Builder setResult(ETestExecutionResult result) { 317 this.result = result; 318 return this; 319 } 320 321 /** 322 * Creates a new {@link TestExecution} instance from this builder. 323 */ 324 public TestExecution build() { 325 return new TestExecution(uniformPath, durationInSeconds, result, failureMessage); 326 } 327 328 /** 329 * Returns the absolute uniform path of this test execution including test 330 * namespace/path and test name. 331 */ 332 public RelativeUniformPath getAbsolutePath() { 333 return uniformPath; 334 } 335 336 } 337}