1 /* 2 * Copyright (C) 2018 The Android Open Source Project 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 package com.android.tradefed.result; 17 18 import com.android.ddmlib.testrunner.TestResult.TestStatus; 19 import com.android.tradefed.log.LogUtil.CLog; 20 import com.android.tradefed.metrics.proto.MetricMeasurement.Metric; 21 import com.android.tradefed.util.proto.TfMetricProtoUtil; 22 23 import java.util.ArrayList; 24 import java.util.Arrays; 25 import java.util.HashMap; 26 import java.util.LinkedHashMap; 27 import java.util.LinkedHashSet; 28 import java.util.List; 29 import java.util.Map; 30 import java.util.Set; 31 32 /** 33 * Holds results from a single test run. 34 * 35 * <p>Maintains an accurate count of tests, and tracks incomplete tests. 36 * 37 * <p>Not thread safe! The test* callbacks must be called in order 38 */ 39 public class TestRunResult { 40 private String mTestRunName; 41 // Uses a LinkedHashMap to have predictable iteration order 42 private Map<TestDescription, TestResult> mTestResults = 43 new LinkedHashMap<TestDescription, TestResult>(); 44 // Store the metrics for the run 45 private Map<String, String> mRunMetrics = new HashMap<>(); 46 private HashMap<String, Metric> mRunProtoMetrics = new HashMap<>(); 47 // Log files associated with the test run itself (testRunStart / testRunEnd). 48 private Map<String, LogFile> mRunLoggedFiles; 49 private boolean mIsRunComplete = false; 50 private long mElapsedTime = 0; 51 52 private TestResult mCurrentTestResult; 53 54 /** represents sums of tests in each TestStatus state. Indexed by TestStatus.ordinal() */ 55 private int[] mStatusCounts = new int[TestStatus.values().length]; 56 /** tracks if mStatusCounts is accurate, or if it needs to be recalculated */ 57 private boolean mIsCountDirty = true; 58 59 private String mRunFailureError = null; 60 61 private boolean mAggregateMetrics = false; 62 63 /** Create an empty{@link TestRunResult}. */ TestRunResult()64 public TestRunResult() { 65 mTestRunName = "not started"; 66 mRunLoggedFiles = new LinkedHashMap<String, LogFile>(); 67 } 68 setAggregateMetrics(boolean metricAggregation)69 public void setAggregateMetrics(boolean metricAggregation) { 70 mAggregateMetrics = metricAggregation; 71 } 72 73 /** @return the test run name */ getName()74 public String getName() { 75 return mTestRunName; 76 } 77 78 /** Returns a map of the test results. */ getTestResults()79 public Map<TestDescription, TestResult> getTestResults() { 80 return mTestResults; 81 } 82 83 /** @return a {@link Map} of the test run metrics. */ getRunMetrics()84 public Map<String, String> getRunMetrics() { 85 return mRunMetrics; 86 } 87 88 /** @return a {@link Map} of the test run metrics with the new proto format. */ getRunProtoMetrics()89 public HashMap<String, Metric> getRunProtoMetrics() { 90 return mRunProtoMetrics; 91 } 92 93 /** Gets the set of completed tests. */ getCompletedTests()94 public Set<TestDescription> getCompletedTests() { 95 List completedStatuses = new ArrayList<TestStatus>(); 96 for (TestStatus s : TestStatus.values()) { 97 if (!s.equals(TestStatus.INCOMPLETE)) { 98 completedStatuses.add(s); 99 } 100 } 101 return getTestsInState(completedStatuses); 102 } 103 104 /** Gets the set of failed tests. */ getFailedTests()105 public Set<TestDescription> getFailedTests() { 106 return getTestsInState(Arrays.asList(TestStatus.FAILURE)); 107 } 108 109 /** Gets the set of tests in given statuses. */ getTestsInState(List<TestStatus> statuses)110 private Set<TestDescription> getTestsInState(List<TestStatus> statuses) { 111 Set<TestDescription> tests = new LinkedHashSet<>(); 112 for (Map.Entry<TestDescription, TestResult> testEntry : getTestResults().entrySet()) { 113 TestStatus status = testEntry.getValue().getStatus(); 114 if (statuses.contains(status)) { 115 tests.add(testEntry.getKey()); 116 } 117 } 118 return tests; 119 } 120 121 /** @return <code>true</code> if test run failed. */ isRunFailure()122 public boolean isRunFailure() { 123 return mRunFailureError != null; 124 } 125 126 /** @return <code>true</code> if test run finished. */ isRunComplete()127 public boolean isRunComplete() { 128 return mIsRunComplete; 129 } 130 setRunComplete(boolean runComplete)131 public void setRunComplete(boolean runComplete) { 132 mIsRunComplete = runComplete; 133 } 134 135 /** Gets the number of tests in given state for this run. */ getNumTestsInState(TestStatus status)136 public int getNumTestsInState(TestStatus status) { 137 if (mIsCountDirty) { 138 // clear counts 139 for (int i = 0; i < mStatusCounts.length; i++) { 140 mStatusCounts[i] = 0; 141 } 142 // now recalculate 143 for (TestResult r : mTestResults.values()) { 144 mStatusCounts[r.getStatus().ordinal()]++; 145 } 146 mIsCountDirty = false; 147 } 148 return mStatusCounts[status.ordinal()]; 149 } 150 151 /** Gets the number of tests in this run. */ getNumTests()152 public int getNumTests() { 153 return mTestResults.size(); 154 } 155 156 /** Gets the number of complete tests in this run ie with status != incomplete. */ getNumCompleteTests()157 public int getNumCompleteTests() { 158 return getNumTests() - getNumTestsInState(TestStatus.INCOMPLETE); 159 } 160 161 /** @return <code>true</code> if test run had any failed or error tests. */ hasFailedTests()162 public boolean hasFailedTests() { 163 return getNumAllFailedTests() > 0; 164 } 165 166 /** Return total number of tests in a failure state (failed, assumption failure) */ getNumAllFailedTests()167 public int getNumAllFailedTests() { 168 return getNumTestsInState(TestStatus.FAILURE); 169 } 170 171 /** Returns the current run elapsed time. */ getElapsedTime()172 public long getElapsedTime() { 173 return mElapsedTime; 174 } 175 176 /** Return the run failure error message, <code>null</code> if run did not fail. */ getRunFailureMessage()177 public String getRunFailureMessage() { 178 return mRunFailureError; 179 } 180 181 /** 182 * Notify that a test run started. 183 * 184 * @param runName the name associated to the test run for tracking purpose. 185 * @param testCount the number of test cases associated with the test count. 186 */ testRunStarted(String runName, int testCount)187 public void testRunStarted(String runName, int testCount) { 188 mTestRunName = runName; 189 mIsRunComplete = false; 190 // Do not reset mRunFailureError since for re-run we want to preserve previous failures. 191 } 192 testStarted(TestDescription test)193 public void testStarted(TestDescription test) { 194 testStarted(test, System.currentTimeMillis()); 195 } 196 testStarted(TestDescription test, long startTime)197 public void testStarted(TestDescription test, long startTime) { 198 mCurrentTestResult = new TestResult(); 199 mCurrentTestResult.setStartTime(startTime); 200 addTestResult(test, mCurrentTestResult); 201 } 202 addTestResult(TestDescription test, TestResult testResult)203 private void addTestResult(TestDescription test, TestResult testResult) { 204 mIsCountDirty = true; 205 mTestResults.put(test, testResult); 206 } 207 updateTestResult(TestDescription test, TestStatus status, String trace)208 private void updateTestResult(TestDescription test, TestStatus status, String trace) { 209 TestResult r = mTestResults.get(test); 210 if (r == null) { 211 CLog.d("received test event without test start for %s", test); 212 r = new TestResult(); 213 } 214 r.setStatus(status); 215 r.setStackTrace(trace); 216 addTestResult(test, r); 217 } 218 testFailed(TestDescription test, String trace)219 public void testFailed(TestDescription test, String trace) { 220 updateTestResult(test, TestStatus.FAILURE, trace); 221 } 222 testAssumptionFailure(TestDescription test, String trace)223 public void testAssumptionFailure(TestDescription test, String trace) { 224 updateTestResult(test, TestStatus.ASSUMPTION_FAILURE, trace); 225 } 226 testIgnored(TestDescription test)227 public void testIgnored(TestDescription test) { 228 updateTestResult(test, TestStatus.IGNORED, null); 229 } 230 testEnded(TestDescription test, HashMap<String, Metric> testMetrics)231 public void testEnded(TestDescription test, HashMap<String, Metric> testMetrics) { 232 testEnded(test, System.currentTimeMillis(), testMetrics); 233 } 234 testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics)235 public void testEnded(TestDescription test, long endTime, HashMap<String, Metric> testMetrics) { 236 TestResult result = mTestResults.get(test); 237 if (result == null) { 238 result = new TestResult(); 239 } 240 if (result.getStatus().equals(TestStatus.INCOMPLETE)) { 241 result.setStatus(TestStatus.PASSED); 242 } 243 result.setEndTime(endTime); 244 result.setMetrics(TfMetricProtoUtil.compatibleConvert(testMetrics)); 245 result.setProtoMetrics(testMetrics); 246 addTestResult(test, result); 247 mCurrentTestResult = null; 248 } 249 testRunFailed(String errorMessage)250 public void testRunFailed(String errorMessage) { 251 mRunFailureError = errorMessage; 252 } 253 testRunStopped(long elapsedTime)254 public void testRunStopped(long elapsedTime) { 255 mElapsedTime += elapsedTime; 256 mIsRunComplete = true; 257 } 258 testRunEnded(long elapsedTime, Map<String, String> runMetrics)259 public void testRunEnded(long elapsedTime, Map<String, String> runMetrics) { 260 if (mAggregateMetrics) { 261 for (Map.Entry<String, String> entry : runMetrics.entrySet()) { 262 String existingValue = mRunMetrics.get(entry.getKey()); 263 String combinedValue = combineValues(existingValue, entry.getValue()); 264 mRunMetrics.put(entry.getKey(), combinedValue); 265 } 266 } else { 267 mRunMetrics.putAll(runMetrics); 268 } 269 // Also add to the new interface: 270 mRunProtoMetrics.putAll(TfMetricProtoUtil.upgradeConvert(runMetrics)); 271 272 mElapsedTime += elapsedTime; 273 mIsRunComplete = true; 274 } 275 276 /** New interface using the new proto metrics. */ testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics)277 public void testRunEnded(long elapsedTime, HashMap<String, Metric> runMetrics) { 278 // Internally store the information as backward compatible format 279 testRunEnded(elapsedTime, TfMetricProtoUtil.compatibleConvert(runMetrics)); 280 // Store the new format directly too. 281 // TODO: See if aggregation should/can be done with the new format. 282 mRunProtoMetrics.putAll(runMetrics); 283 284 // TODO: when old format is deprecated, do not forget to uncomment the next two lines 285 // mElapsedTime += elapsedTime; 286 // mIsRunComplete = true; 287 } 288 289 /** 290 * Combine old and new metrics value 291 * 292 * @param existingValue 293 * @param newValue 294 * @return the combination of the two string as Long or Double value. 295 */ combineValues(String existingValue, String newValue)296 private String combineValues(String existingValue, String newValue) { 297 if (existingValue != null) { 298 try { 299 Long existingLong = Long.parseLong(existingValue); 300 Long newLong = Long.parseLong(newValue); 301 return Long.toString(existingLong + newLong); 302 } catch (NumberFormatException e) { 303 // not a long, skip to next 304 } 305 try { 306 Double existingDouble = Double.parseDouble(existingValue); 307 Double newDouble = Double.parseDouble(newValue); 308 return Double.toString(existingDouble + newDouble); 309 } catch (NumberFormatException e) { 310 // not a double either, fall through 311 } 312 } 313 // default to overriding existingValue 314 return newValue; 315 } 316 317 /** Returns a user friendly string describing results. */ getTextSummary()318 public String getTextSummary() { 319 StringBuilder builder = new StringBuilder(); 320 builder.append(String.format("Total tests %d, ", getNumTests())); 321 for (TestStatus status : TestStatus.values()) { 322 int count = getNumTestsInState(status); 323 // only add descriptive state for states that have non zero values, to avoid cluttering 324 // the response 325 if (count > 0) { 326 builder.append(String.format("%s %d, ", status.toString().toLowerCase(), count)); 327 } 328 } 329 return builder.toString(); 330 } 331 332 /** 333 * Information about a file being logged are stored and associated to the test case or test run 334 * in progress. 335 * 336 * @param dataName the name referencing the data. 337 * @param logFile The {@link LogFile} object representing where the object was saved and and 338 * information about it. 339 */ testLogSaved(String dataName, LogFile logFile)340 public void testLogSaved(String dataName, LogFile logFile) { 341 if (mCurrentTestResult != null) { 342 // We have a test case in progress, we can associate the log to it. 343 mCurrentTestResult.addLoggedFile(dataName, logFile); 344 } else { 345 mRunLoggedFiles.put(dataName, logFile); 346 } 347 } 348 349 /** Returns a copy of the map containing all the logged file associated with that test case. */ getRunLoggedFiles()350 public Map<String, LogFile> getRunLoggedFiles() { 351 return new LinkedHashMap<>(mRunLoggedFiles); 352 } 353 354 355 /** 356 * Merge multiple TestRunResults of the same testRunName. If a testcase shows up in multiple 357 * TestRunResults but has different results (e.g. "boottest-device" runs three times with result 358 * FAIL-FAIL-PASS), we concatenate all the stack traces from the FAILED runs and trust the final 359 * run result for status, metrics, log files, start/end time. 360 * 361 * @param testRunResults A list of TestRunResult to merge. 362 * @return the final TestRunResult containing the merged data from the testRunResults. 363 */ merge(List<TestRunResult> testRunResults)364 public static TestRunResult merge(List<TestRunResult> testRunResults) { 365 if (testRunResults.isEmpty()) { 366 return null; 367 } 368 TestRunResult finalRunResult = new TestRunResult(); 369 370 String testRunName = testRunResults.get(0).getName(); 371 Map<String, String> finalRunMetrics = new HashMap<>(); 372 HashMap<String, Metric> finalRunProtoMetrics = new HashMap<>(); 373 Map<String, LogFile> finalRunLoggedFiles = new HashMap<>(); 374 Map<TestDescription, TestResult> finalTestResults = 375 new HashMap<TestDescription, TestResult>(); 376 377 for (TestRunResult eachRunResult : testRunResults) { 378 // Check all mTestRunNames are the same. 379 if (!testRunName.equals(eachRunResult.getName())) { 380 throw new IllegalArgumentException( 381 String.format( 382 "Unabled to merge TestRunResults: The run results names are " 383 + "different (%s, %s)", 384 testRunName, eachRunResult.getName())); 385 } 386 // Keep the last TestRunResult's RunMetrics, ProtoMetrics and logFiles. 387 // TODO: Currently we keep a single item when multiple TestRunResult have the same 388 // keys. In the future, we may want to improve this logic. 389 finalRunMetrics.putAll(eachRunResult.getRunMetrics()); 390 finalRunProtoMetrics.putAll(eachRunResult.getRunProtoMetrics()); 391 finalRunLoggedFiles.putAll(eachRunResult.getRunLoggedFiles()); 392 // TODO: We are not handling the TestResult log files in the merging logic (different 393 // from the TestRunResult log files). Need to improve in the future. 394 for (Map.Entry<TestDescription, TestResult> testResultEntry : 395 eachRunResult.getTestResults().entrySet()) { 396 if (!finalTestResults.containsKey(testResultEntry.getKey())) { 397 TestResult newResult = TestResult.clone(testResultEntry.getValue()); 398 finalTestResults.put(testResultEntry.getKey(), newResult); 399 } else { 400 /** 401 * Merge the same testcase's TestResults. - Test status is the final run's 402 * status, - Test stack trace is the concatenation of each TestResult's stack 403 * traces. - Test start time is the first TestResult's start time. - Test end 404 * time is the last TestResult's end time. - Test metrics is the first 405 * TestResult's metrics. 406 */ 407 TestResult existingResult = finalTestResults.get(testResultEntry.getKey()); 408 // If the test passes, then it doesn't have stack trace. 409 if (testResultEntry.getValue().getStackTrace() != null) { 410 if (existingResult.getStackTrace() != null) { 411 String stackTrace = 412 String.format( 413 "%s\n%s", 414 existingResult.getStackTrace(), 415 testResultEntry.getValue().getStackTrace()); 416 existingResult.setStackTrace(stackTrace); 417 } else { 418 existingResult.setStackTrace( 419 testResultEntry.getValue().getStackTrace()); 420 } 421 } 422 existingResult.setStatus(testResultEntry.getValue().getStatus()); 423 existingResult.setEndTime(testResultEntry.getValue().getEndTime()); 424 finalTestResults.put(testResultEntry.getKey(), existingResult); 425 } 426 } 427 } 428 finalRunResult.mTestRunName = testRunName; 429 finalRunResult.mRunMetrics = finalRunMetrics; 430 finalRunResult.mRunProtoMetrics = finalRunProtoMetrics; 431 finalRunResult.mRunLoggedFiles = finalRunLoggedFiles; 432 finalRunResult.mTestResults = finalTestResults; 433 return finalRunResult; 434 } 435 } 436