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}