1 /* 2 * Copyright (c) 2017 Google Inc. All Rights Reserved. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); you 5 * may not use this file except in compliance with the License. You may 6 * 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 13 * implied. See the License for the specific language governing 14 * permissions and limitations under the License. 15 */ 16 17 package com.android.vts.entity; 18 19 import static com.googlecode.objectify.ObjectifyService.ofy; 20 21 import com.android.vts.util.TimeUtil; 22 import com.android.vts.util.UrlUtil; 23 import com.android.vts.util.UrlUtil.LinkDisplay; 24 import com.google.appengine.api.datastore.Entity; 25 import com.google.appengine.api.datastore.Key; 26 import com.google.appengine.api.datastore.KeyFactory; 27 import com.google.gson.Gson; 28 import com.google.gson.JsonArray; 29 import com.google.gson.JsonElement; 30 import com.google.gson.JsonObject; 31 import com.google.gson.JsonPrimitive; 32 import com.googlecode.objectify.annotation.Cache; 33 import com.googlecode.objectify.annotation.Id; 34 import com.googlecode.objectify.annotation.Ignore; 35 import com.googlecode.objectify.annotation.Index; 36 import com.googlecode.objectify.annotation.OnLoad; 37 import com.googlecode.objectify.annotation.Parent; 38 import java.util.ArrayList; 39 import java.util.List; 40 import java.util.Map; 41 import java.util.Objects; 42 import java.util.Optional; 43 import java.util.function.Supplier; 44 import java.util.logging.Level; 45 import java.util.logging.Logger; 46 import java.util.stream.Stream; 47 import lombok.Getter; 48 import lombok.NoArgsConstructor; 49 import lombok.Setter; 50 import org.apache.commons.lang3.math.NumberUtils; 51 52 @com.googlecode.objectify.annotation.Entity(name = "TestRun") 53 @Cache 54 @NoArgsConstructor 55 /** Entity describing test run information. */ 56 public class TestRunEntity implements DashboardEntity { 57 protected static final Logger logger = Logger.getLogger(TestRunEntity.class.getName()); 58 59 /** Enum for classifying test run types. */ 60 public enum TestRunType { 61 OTHER(0), 62 PRESUBMIT(1), 63 POSTSUBMIT(2); 64 65 private final int value; 66 TestRunType(int value)67 private TestRunType(int value) { 68 this.value = value; 69 } 70 71 /** 72 * Get the ordinal representation of the type. 73 * 74 * @return The value associated with the test run type. 75 */ getNumber()76 public int getNumber() { 77 return value; 78 } 79 80 /** 81 * Convert an ordinal value to a TestRunType. 82 * 83 * @param value The orginal value to parse. 84 * @return a TestRunType value. 85 */ fromNumber(int value)86 public static TestRunType fromNumber(int value) { 87 if (value == 1) { 88 return TestRunType.PRESUBMIT; 89 } else if (value == 2) { 90 return TestRunType.POSTSUBMIT; 91 } else { 92 return TestRunType.OTHER; 93 } 94 } 95 96 /** 97 * Determine the test run type based on the build ID. 98 * 99 * <p>Postsubmit runs are expected to have integer build IDs, while presubmit runs are 100 * integers prefixed by the character P. All other runs (e.g. local builds) are classified 101 * as OTHER. 102 * 103 * @param buildId The build ID. 104 * @return the TestRunType. 105 */ fromBuildId(String buildId)106 public static TestRunType fromBuildId(String buildId) { 107 if (buildId.toLowerCase().startsWith("p")) { 108 if (NumberUtils.isParsable(buildId.substring(1))) { 109 return TestRunType.PRESUBMIT; 110 } else { 111 return TestRunType.OTHER; 112 } 113 } else if (NumberUtils.isParsable(buildId)) { 114 return TestRunType.POSTSUBMIT; 115 } else { 116 return TestRunType.OTHER; 117 } 118 } 119 } 120 121 public static final String KIND = "TestRun"; 122 123 // Property keys 124 public static final String TEST_NAME = "testName"; 125 public static final String TYPE = "type"; 126 public static final String START_TIMESTAMP = "startTimestamp"; 127 public static final String END_TIMESTAMP = "endTimestamp"; 128 public static final String TEST_BUILD_ID = "testBuildId"; 129 public static final String HOST_NAME = "hostName"; 130 public static final String PASS_COUNT = "passCount"; 131 public static final String FAIL_COUNT = "failCount"; 132 public static final String HAS_CODE_COVERAGE = "hasCodeCoverage"; 133 public static final String HAS_COVERAGE = "hasCoverage"; 134 public static final String TEST_CASE_IDS = "testCaseIds"; 135 public static final String LOG_LINKS = "logLinks"; 136 public static final String API_COVERAGE_KEY_LIST = "apiCoverageKeyList"; 137 public static final String TOTAL_API_COUNT = "totalApiCount"; 138 public static final String COVERED_API_COUNT = "coveredApiCount"; 139 140 @Ignore private Key key; 141 142 @Id @Getter @Setter private Long id; 143 144 @Parent @Getter @Setter private com.googlecode.objectify.Key<?> testRunParent; 145 146 @Index @Getter @Setter private long type; 147 148 @Index @Getter @Setter private long startTimestamp; 149 150 @Index @Getter @Setter private long endTimestamp; 151 152 @Index @Getter @Setter private String testBuildId; 153 154 @Index @Getter @Setter private String testName; 155 156 @Index @Getter @Setter private String hostName; 157 158 @Index @Getter @Setter private long passCount; 159 160 @Index @Getter @Setter private long failCount; 161 162 @Index private boolean hasCoverage; 163 164 @Index @Getter @Setter private boolean hasCodeCoverage; 165 166 @Ignore private com.googlecode.objectify.Key<CodeCoverageEntity> codeCoverageEntityKey; 167 168 @Index @Getter @Setter private long coveredLineCount; 169 170 @Index @Getter @Setter private long totalLineCount; 171 172 @Getter @Setter private List<Long> testCaseIds; 173 174 @Getter @Setter private List<String> logLinks; 175 176 /** 177 * Create a TestRunEntity object describing a test run. 178 * 179 * @param parentKey The key to the parent TestEntity. 180 * @param type The test run type (e.g. presubmit, postsubmit, other) 181 * @param startTimestamp The time in microseconds when the test run started. 182 * @param endTimestamp The time in microseconds when the test run ended. 183 * @param testBuildId The build ID of the VTS test build. 184 * @param hostName The name of host machine. 185 * @param passCount The number of passing test cases in the run. 186 * @param failCount The number of failing test cases in the run. 187 */ TestRunEntity( Key parentKey, long type, long startTimestamp, long endTimestamp, String testBuildId, String hostName, long passCount, long failCount, boolean hasCodeCoverage, List<Long> testCaseIds, List<String> logLinks)188 public TestRunEntity( 189 Key parentKey, 190 long type, 191 long startTimestamp, 192 long endTimestamp, 193 String testBuildId, 194 String hostName, 195 long passCount, 196 long failCount, 197 boolean hasCodeCoverage, 198 List<Long> testCaseIds, 199 List<String> logLinks) { 200 this.id = startTimestamp; 201 this.key = KeyFactory.createKey(parentKey, KIND, startTimestamp); 202 this.type = type; 203 this.startTimestamp = startTimestamp; 204 this.endTimestamp = endTimestamp; 205 this.testBuildId = testBuildId; 206 this.hostName = hostName; 207 this.passCount = passCount; 208 this.failCount = failCount; 209 this.hasCodeCoverage = hasCodeCoverage; 210 this.testName = parentKey.getName(); 211 this.testCaseIds = testCaseIds; 212 this.logLinks = logLinks; 213 214 this.testRunParent = com.googlecode.objectify.Key.create(TestEntity.class, testName); 215 this.codeCoverageEntityKey = getCodeCoverageEntityKey(); 216 } 217 218 /** 219 * Called after the POJO is populated with data through objecitfy library 220 */ 221 @OnLoad onLoad()222 private void onLoad() { 223 if (Objects.isNull(this.hasCodeCoverage)) { 224 this.hasCodeCoverage = this.hasCoverage; 225 this.save(); 226 } 227 } 228 toEntity()229 public Entity toEntity() { 230 Entity testRunEntity = new Entity(this.key); 231 testRunEntity.setProperty(TEST_NAME, this.testName); 232 testRunEntity.setProperty(TYPE, this.type); 233 testRunEntity.setProperty(START_TIMESTAMP, this.startTimestamp); 234 testRunEntity.setUnindexedProperty(END_TIMESTAMP, this.endTimestamp); 235 testRunEntity.setProperty(TEST_BUILD_ID, this.testBuildId.toLowerCase()); 236 testRunEntity.setProperty(HOST_NAME, this.hostName.toLowerCase()); 237 testRunEntity.setProperty(PASS_COUNT, this.passCount); 238 testRunEntity.setProperty(FAIL_COUNT, this.failCount); 239 testRunEntity.setProperty(HAS_CODE_COVERAGE, this.hasCodeCoverage); 240 testRunEntity.setUnindexedProperty(TEST_CASE_IDS, this.testCaseIds); 241 if (this.logLinks != null && this.logLinks.size() > 0) { 242 testRunEntity.setUnindexedProperty(LOG_LINKS, this.logLinks); 243 } 244 return testRunEntity; 245 } 246 247 /** Saving function for the instance of this class */ 248 @Override save()249 public com.googlecode.objectify.Key<TestRunEntity> save() { 250 return ofy().save().entity(this).now(); 251 } 252 253 /** 254 * Get key info from appengine based library. 255 */ getKey()256 public Key getKey() { 257 Key parentKey = KeyFactory.createKey(TestEntity.KIND, testName); 258 return KeyFactory.createKey(parentKey, KIND, startTimestamp); 259 } 260 261 /** Getter hasCodeCoverage value */ getHasCodeCoverage()262 public boolean getHasCodeCoverage() { 263 return this.hasCodeCoverage; 264 } 265 266 /** Getter DateTime string from startTimestamp */ getStartDateTime()267 public String getStartDateTime() { 268 return TimeUtil.getDateTimeString(this.startTimestamp); 269 } 270 271 /** Getter DateTime string from startTimestamp */ getEndDateTime()272 public String getEndDateTime() { 273 return TimeUtil.getDateTimeString(this.endTimestamp); 274 } 275 276 /** find TestRun entity by ID and test name */ getByTestNameId(String testName, long id)277 public static TestRunEntity getByTestNameId(String testName, long id) { 278 com.googlecode.objectify.Key testKey = 279 com.googlecode.objectify.Key.create(TestEntity.class, testName); 280 return ofy().load().type(TestRunEntity.class).parent(testKey).id(id).now(); 281 } 282 283 /** Get CodeCoverageEntity Key to generate Key by combining key info */ getCodeCoverageEntityKey()284 private com.googlecode.objectify.Key getCodeCoverageEntityKey() { 285 com.googlecode.objectify.Key testRunKey = this.getOfyKey(); 286 return com.googlecode.objectify.Key.create( 287 testRunKey, CodeCoverageEntity.class, this.startTimestamp); 288 } 289 290 /** Get ApiCoverageEntity Key from the parent key */ getOfyKey()291 public com.googlecode.objectify.Key getOfyKey() { 292 com.googlecode.objectify.Key testKey = 293 com.googlecode.objectify.Key.create( 294 TestEntity.class, this.testName); 295 com.googlecode.objectify.Key testRunKey = 296 com.googlecode.objectify.Key.create( 297 testKey, TestRunEntity.class, this.startTimestamp); 298 return testRunKey; 299 } 300 301 /** Get ApiCoverageEntity from key info */ getApiCoverageEntityList()302 public Optional<List<ApiCoverageEntity>> getApiCoverageEntityList() { 303 com.googlecode.objectify.Key testRunKey = this.getOfyKey(); 304 List<ApiCoverageEntity> apiCoverageEntityList = 305 ofy().load().type(ApiCoverageEntity.class).ancestor(testRunKey).list(); 306 return Optional.ofNullable(apiCoverageEntityList); 307 } 308 309 /** 310 * Get CodeCoverageEntity instance from codeCoverageEntityKey value. 311 */ getCodeCoverageEntity()312 public CodeCoverageEntity getCodeCoverageEntity() { 313 if (this.hasCodeCoverage) { 314 CodeCoverageEntity codeCoverageEntity = 315 ofy().load() 316 .type(CodeCoverageEntity.class) 317 .filterKey(this.codeCoverageEntityKey) 318 .first() 319 .now(); 320 if (Objects.isNull(codeCoverageEntity)) { 321 codeCoverageEntity = 322 new CodeCoverageEntity( 323 this.getKey(), coveredLineCount, totalLineCount); 324 codeCoverageEntity.save(); 325 return codeCoverageEntity; 326 } else { 327 return codeCoverageEntity; 328 } 329 } else { 330 logger.log( 331 Level.WARNING, 332 "The hasCodeCoverage value is false. Please check the code coverage entity key"); 333 return null; 334 } 335 } 336 337 /** 338 * Convert an Entity object to a TestRunEntity. 339 * 340 * @param e The entity to process. 341 * @return TestRunEntity object with the properties from e processed, or null if incompatible. 342 */ 343 @SuppressWarnings("unchecked") fromEntity(Entity e)344 public static TestRunEntity fromEntity(Entity e) { 345 if (!e.getKind().equals(KIND) 346 || !e.hasProperty(TYPE) 347 || !e.hasProperty(START_TIMESTAMP) 348 || !e.hasProperty(END_TIMESTAMP) 349 || !e.hasProperty(TEST_BUILD_ID) 350 || !e.hasProperty(HOST_NAME) 351 || !e.hasProperty(PASS_COUNT) 352 || !e.hasProperty(FAIL_COUNT)) { 353 logger.log(Level.WARNING, "Missing test run attributes in entity: " + e.toString()); 354 return null; 355 } 356 try { 357 long type = (long) e.getProperty(TYPE); 358 long startTimestamp = (long) e.getProperty(START_TIMESTAMP); 359 long endTimestamp = (long) e.getProperty(END_TIMESTAMP); 360 String testBuildId = (String) e.getProperty(TEST_BUILD_ID); 361 String hostName = (String) e.getProperty(HOST_NAME); 362 long passCount = (long) e.getProperty(PASS_COUNT); 363 long failCount = (long) e.getProperty(FAIL_COUNT); 364 boolean hasCodeCoverage = false; 365 if (e.hasProperty(HAS_CODE_COVERAGE)) { 366 hasCodeCoverage = (boolean) e.getProperty(HAS_CODE_COVERAGE); 367 } else { 368 hasCodeCoverage = (boolean) e.getProperty(HAS_COVERAGE); 369 } 370 List<Long> testCaseIds = (List<Long>) e.getProperty(TEST_CASE_IDS); 371 if (Objects.isNull(testCaseIds)) { 372 testCaseIds = new ArrayList<>(); 373 } 374 List<String> links = new ArrayList<>(); 375 if (e.hasProperty(LOG_LINKS)) { 376 links = (List<String>) e.getProperty(LOG_LINKS); 377 } 378 return new TestRunEntity( 379 e.getKey().getParent(), 380 type, 381 startTimestamp, 382 endTimestamp, 383 testBuildId, 384 hostName, 385 passCount, 386 failCount, 387 hasCodeCoverage, 388 testCaseIds, 389 links); 390 } catch (ClassCastException exception) { 391 // Invalid cast 392 logger.log(Level.WARNING, "Error parsing test run entity.", exception); 393 } 394 return null; 395 } 396 397 /** Get JsonFormat logLinks */ getJsonLogLinks()398 public JsonElement getJsonLogLinks() { 399 List<String> logLinks = this.getLogLinks(); 400 List<JsonElement> links = new ArrayList<>(); 401 if (logLinks != null && logLinks.size() > 0) { 402 for (String rawUrl : logLinks) { 403 UrlUtil.LinkDisplay validatedLink = UrlUtil.processUrl(rawUrl); 404 if (validatedLink == null) { 405 logger.log(Level.WARNING, "Invalid logging URL : " + rawUrl); 406 continue; 407 } 408 String[] logInfo = new String[] {validatedLink.name, validatedLink.url}; 409 links.add(new Gson().toJsonTree(logInfo)); 410 } 411 } 412 return new Gson().toJsonTree(links); 413 } 414 toJson()415 public JsonObject toJson() { 416 Map<String, TestCoverageStatusEntity> testCoverageStatusMap = TestCoverageStatusEntity 417 .getTestCoverageStatusMap(); 418 419 JsonObject json = new JsonObject(); 420 json.add(TEST_NAME, new JsonPrimitive(this.testName)); 421 json.add(TEST_BUILD_ID, new JsonPrimitive(this.testBuildId)); 422 json.add(HOST_NAME, new JsonPrimitive(this.hostName)); 423 json.add(PASS_COUNT, new JsonPrimitive(this.passCount)); 424 json.add(FAIL_COUNT, new JsonPrimitive(this.failCount)); 425 json.add(START_TIMESTAMP, new JsonPrimitive(this.startTimestamp)); 426 json.add(END_TIMESTAMP, new JsonPrimitive(this.endTimestamp)); 427 428 // Overwrite the coverage value with newly update value from user decision 429 if (this.hasCodeCoverage) { 430 CodeCoverageEntity codeCoverageEntity = this.getCodeCoverageEntity(); 431 if (testCoverageStatusMap.containsKey(this.testName)) { 432 TestCoverageStatusEntity testCoverageStatusEntity = 433 testCoverageStatusMap.get(this.testName); 434 435 if (testCoverageStatusEntity.getUpdatedCoveredLineCount() > 0) { 436 codeCoverageEntity.setCoveredLineCount( 437 testCoverageStatusEntity.getUpdatedCoveredLineCount()); 438 } 439 if (testCoverageStatusEntity.getUpdatedTotalLineCount() > 0) { 440 codeCoverageEntity.setTotalLineCount( 441 testCoverageStatusEntity.getUpdatedTotalLineCount()); 442 } 443 } 444 445 long totalLineCount = codeCoverageEntity.getTotalLineCount(); 446 long coveredLineCount = codeCoverageEntity.getCoveredLineCount(); 447 if (totalLineCount > 0 && coveredLineCount >= 0) { 448 json.add(CodeCoverageEntity.COVERED_LINE_COUNT, new JsonPrimitive(coveredLineCount)); 449 json.add(CodeCoverageEntity.TOTAL_LINE_COUNT, new JsonPrimitive(totalLineCount)); 450 } 451 } 452 453 Optional<List<ApiCoverageEntity>> apiCoverageEntityOptionList = 454 this.getApiCoverageEntityList(); 455 if (apiCoverageEntityOptionList.isPresent()) { 456 List<ApiCoverageEntity> apiCoverageEntityList = apiCoverageEntityOptionList.get(); 457 Supplier<Stream<ApiCoverageEntity>> apiCoverageStreamSupplier = 458 () -> apiCoverageEntityList.stream(); 459 int totalHalApi = 460 apiCoverageStreamSupplier.get().mapToInt(data -> data.getHalApi().size()).sum(); 461 if (totalHalApi > 0) { 462 int coveredHalApi = 463 apiCoverageStreamSupplier 464 .get() 465 .mapToInt(data -> data.getCoveredHalApi().size()) 466 .sum(); 467 JsonArray apiCoverageKeyArray = 468 apiCoverageStreamSupplier 469 .get() 470 .map(data -> new JsonPrimitive(data.getUrlSafeKey())) 471 .collect(JsonArray::new, JsonArray::add, JsonArray::addAll); 472 473 json.add(API_COVERAGE_KEY_LIST, apiCoverageKeyArray); 474 json.add(COVERED_API_COUNT, new JsonPrimitive(coveredHalApi)); 475 json.add(TOTAL_API_COUNT, new JsonPrimitive(totalHalApi)); 476 } 477 } 478 479 List<String> logLinks = this.getLogLinks(); 480 if (logLinks != null && logLinks.size() > 0) { 481 List<JsonElement> links = new ArrayList<>(); 482 for (String rawUrl : logLinks) { 483 LinkDisplay validatedLink = UrlUtil.processUrl(rawUrl); 484 if (validatedLink == null) { 485 logger.log(Level.WARNING, "Invalid logging URL : " + rawUrl); 486 continue; 487 } 488 String[] logInfo = new String[] {validatedLink.name, validatedLink.url}; 489 links.add(new Gson().toJsonTree(logInfo)); 490 } 491 if (links.size() > 0) { 492 json.add(this.LOG_LINKS, new Gson().toJsonTree(links)); 493 } 494 } 495 return json; 496 } 497 } 498