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.api; 18 19 import com.android.vts.entity.BranchEntity; 20 import com.android.vts.entity.BuildTargetEntity; 21 import com.android.vts.entity.CodeCoverageEntity; 22 import com.android.vts.entity.CoverageEntity; 23 import com.android.vts.entity.DeviceInfoEntity; 24 import com.android.vts.entity.ProfilingPointRunEntity; 25 import com.android.vts.entity.TestCaseRunEntity; 26 import com.android.vts.entity.TestEntity; 27 import com.android.vts.entity.TestPlanEntity; 28 import com.android.vts.entity.TestPlanRunEntity; 29 import com.android.vts.entity.TestRunEntity; 30 import com.android.vts.entity.TestStatusEntity; 31 import com.android.vts.entity.TestStatusEntity.TestCaseReference; 32 import com.android.vts.entity.TestSuiteFileEntity; 33 import com.android.vts.entity.TestSuiteResultEntity; 34 import com.google.appengine.api.datastore.DatastoreFailureException; 35 import com.google.appengine.api.datastore.DatastoreService; 36 import com.google.appengine.api.datastore.DatastoreServiceFactory; 37 import com.google.appengine.api.datastore.DatastoreTimeoutException; 38 import com.google.appengine.api.datastore.Entity; 39 import com.google.appengine.api.datastore.EntityNotFoundException; 40 import com.google.appengine.api.datastore.Key; 41 import com.google.appengine.api.datastore.KeyFactory; 42 import com.google.appengine.api.datastore.Query; 43 import com.google.appengine.api.datastore.Transaction; 44 import com.google.appengine.api.users.User; 45 import com.google.appengine.api.users.UserService; 46 import com.google.appengine.api.users.UserServiceFactory; 47 import com.google.appengine.api.utils.SystemProperty; 48 import com.google.gson.Gson; 49 import com.google.gson.GsonBuilder; 50 51 import java.io.FileNotFoundException; 52 import java.io.IOException; 53 import java.io.InputStream; 54 import java.io.InputStreamReader; 55 import java.io.Reader; 56 import java.nio.file.FileSystems; 57 import java.nio.file.Path; 58 import java.nio.file.Paths; 59 import java.time.Instant; 60 import java.time.temporal.ChronoUnit; 61 import java.util.Arrays; 62 import java.util.ArrayList; 63 import java.util.ConcurrentModificationException; 64 import java.util.HashMap; 65 import java.util.HashSet; 66 import java.util.List; 67 import java.util.Map; 68 import java.util.Properties; 69 import java.util.Random; 70 import java.util.Set; 71 import java.util.logging.Level; 72 import java.util.logging.Logger; 73 import java.util.stream.IntStream; 74 import javax.servlet.ServletConfig; 75 import javax.servlet.ServletException; 76 import javax.servlet.http.HttpServlet; 77 import javax.servlet.http.HttpServletRequest; 78 import javax.servlet.http.HttpServletResponse; 79 80 /** Servlet for handling requests to add mock data in datastore. */ 81 public class TestDataForDevServlet extends HttpServlet { 82 protected static final Logger logger = Logger.getLogger(TestDataForDevServlet.class.getName()); 83 84 /** Google Cloud Storage project's default directory name for suite test result files */ 85 private static String GCS_SUITE_TEST_FOLDER_NAME; 86 87 /** datastore instance to save the test data into datastore through datastore library. */ 88 private DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); 89 /** 90 * Gson is a Java library that can be used to convert Java Objects into their JSON 91 * representation. It can also be used to convert a JSON string to an equivalent Java object. 92 */ 93 private Gson gson = new GsonBuilder().create(); 94 95 /** System Configuration Property class */ 96 protected Properties systemConfigProp = new Properties(); 97 98 @Override init(ServletConfig cfg)99 public void init(ServletConfig cfg) throws ServletException { 100 super.init(cfg); 101 102 try { 103 InputStream defaultInputStream = 104 TestDataForDevServlet.class 105 .getClassLoader() 106 .getResourceAsStream("config.properties"); 107 systemConfigProp.load(defaultInputStream); 108 109 GCS_SUITE_TEST_FOLDER_NAME = systemConfigProp.getProperty("gcs.suiteTestFolderName"); 110 } catch (FileNotFoundException e) { 111 e.printStackTrace(); 112 } catch (IOException e) { 113 e.printStackTrace(); 114 } 115 } 116 117 /** 118 * TestReportData class for mapping test-report-data.json. This internal class's each fields 119 * will be automatically mapped to test-report-data.json file through Gson 120 */ 121 private class TestReportDataObject { 122 private List<Test> testList; 123 124 private class Test { 125 private List<TestRun> testRunList; 126 127 private class TestRun { 128 private String testName; 129 private int type; 130 private long startTimestamp; 131 private long endTimestamp; 132 private String testBuildId; 133 private String hostName; 134 private long passCount; 135 private long failCount; 136 private boolean hasCoverage; 137 private long coveredLineCount; 138 private long totalLineCount; 139 private List<Long> testCaseIds; 140 private List<Long> failingTestcaseIds; 141 private List<Integer> failingTestcaseOffsets; 142 private List<String> links; 143 144 private List<Coverage> coverageList; 145 private List<Profiling> profilingList; 146 private List<TestCaseRun> testCaseRunList; 147 private List<DeviceInfo> deviceInfoList; 148 private List<BuildTarget> buildTargetList; 149 private List<Branch> branchList; 150 151 private class Coverage { 152 private String group; 153 private long coveredLineCount; 154 private long totalLineCount; 155 private String filePath; 156 private String projectName; 157 private String projectVersion; 158 private List<Long> lineCoverage; 159 } 160 161 private class Profiling { 162 private String name; 163 private int type; 164 private int regressionMode; 165 private List<String> labels; 166 private List<Long> values; 167 private String xLabel; 168 private String yLabel; 169 private List<String> options; 170 } 171 172 private class TestCaseRun { 173 private List<String> testCaseNames; 174 private List<Integer> results; 175 } 176 177 private class DeviceInfo { 178 private String branch; 179 private String product; 180 private String buildFlavor; 181 private String buildId; 182 private String abiBitness; 183 private String abiName; 184 } 185 186 private class BuildTarget { 187 private String targetName; 188 } 189 190 private class Branch { 191 private String branchName; 192 } 193 } 194 } 195 196 @Override toString()197 public String toString() { 198 return "(" + testList + ")"; 199 } 200 } 201 202 private class TestPlanReportDataObject { 203 private List<TestPlan> testPlanList; 204 205 private class TestPlan { 206 207 private String testPlanName; 208 private List<String> testModules; 209 private List<Long> testTimes; 210 } 211 212 @Override toString()213 public String toString() { 214 return "(" + testPlanList + ")"; 215 } 216 } 217 generateSuiteTestData( HttpServletRequest request, HttpServletResponse response)218 private Map<String, Object> generateSuiteTestData( 219 HttpServletRequest request, HttpServletResponse response) { 220 Map<String, Object> resultMap = new HashMap<>(); 221 String fileSeparator = FileSystems.getDefault().getSeparator(); 222 Random rand = new Random(); 223 List<String> branchList = Arrays.asList("master", "oc_mr", "oc"); 224 List<String> targetList = 225 Arrays.asList( 226 "sailfish-userdebug", 227 "marlin-userdebug", 228 "taimen-userdebug", 229 "walleye-userdebug", 230 "aosp_arm_a-userdebug"); 231 branchList.forEach( 232 branch -> 233 targetList.forEach( 234 target -> 235 IntStream.range(0, 10) 236 .forEach( 237 idx -> { 238 String year = 239 String.format( 240 "%04d", 2010 + idx); 241 String month = 242 String.format( 243 "%02d", 244 rand.nextInt(12)); 245 String day = 246 String.format( 247 "%02d", 248 rand.nextInt(30)); 249 String fileName = 250 String.format( 251 "%02d%02d%02d.bin", 252 rand.nextInt(23) + 1, 253 rand.nextInt(59) + 1, 254 rand.nextInt(59) + 1); 255 256 List<String> pathList = 257 Arrays.asList( 258 GCS_SUITE_TEST_FOLDER_NAME 259 == "" 260 ? "suite_result" 261 : GCS_SUITE_TEST_FOLDER_NAME, 262 year, 263 month, 264 day, 265 fileName); 266 267 Path pathInfo = 268 Paths.get( 269 String.join( 270 fileSeparator, 271 pathList)); 272 273 TestSuiteFileEntity 274 newTestSuiteFileEntity = 275 new TestSuiteFileEntity( 276 pathInfo 277 .toString()); 278 279 com.googlecode.objectify.Key< 280 TestSuiteFileEntity> 281 testSuiteFileParent = 282 com.googlecode.objectify 283 .Key.create( 284 TestSuiteFileEntity 285 .class, 286 newTestSuiteFileEntity 287 .getFilePath()); 288 289 290 TestSuiteResultEntity 291 testSuiteResultEntity = 292 new TestSuiteResultEntity( 293 testSuiteFileParent, 294 Instant.now() 295 .minus( 296 rand 297 .nextInt( 298 100), 299 ChronoUnit 300 .DAYS) 301 .getEpochSecond(), 302 Instant.now() 303 .minus( 304 rand 305 .nextInt( 306 100), 307 ChronoUnit 308 .DAYS) 309 .getEpochSecond(), 310 1, 311 idx / 2 == 0 312 ? false 313 : true, 314 pathInfo 315 .toString(), 316 idx / 2 == 0 317 ? "/error/infra/log" 318 : "", 319 "Test Place Name -" 320 + idx, 321 "Suite Test Plan", 322 "Suite Version " 323 + idx, 324 "Suite Test Name", 325 "Suite Build Number " 326 + idx, 327 rand.nextInt(), 328 rand.nextInt(), 329 branch, 330 target, 331 Long.toString( 332 Math 333 .abs( 334 rand 335 .nextLong())), 336 "Build System Fingerprint " 337 + idx, 338 "Build Vendor Fingerprint " 339 + idx, 340 rand.nextInt(), 341 rand.nextInt()); 342 343 testSuiteResultEntity.save(newTestSuiteFileEntity); 344 }))); 345 resultMap.put("result", "successfully generated!"); 346 return resultMap; 347 } 348 349 @Override doGet(HttpServletRequest request, HttpServletResponse response)350 public void doGet(HttpServletRequest request, HttpServletResponse response) throws IOException { 351 String requestUri = request.getRequestURI(); 352 String requestArgs = request.getQueryString(); 353 354 Map<String, Object> resultMap = new HashMap<>(); 355 String pathInfo = requestUri.replace("/api/test_data/", ""); 356 switch (pathInfo) { 357 case "suite": 358 resultMap = this.generateSuiteTestData(request, response); 359 break; 360 default: 361 throw new IllegalArgumentException("Invalid path info of URL"); 362 } 363 364 String json = new Gson().toJson(resultMap); 365 response.setStatus(HttpServletResponse.SC_OK); 366 response.setContentType("application/json"); 367 response.setCharacterEncoding("UTF-8"); 368 response.getWriter().write(json); 369 } 370 371 /** Add mock data to local dev datastore. */ 372 @Override doPost(HttpServletRequest request, HttpServletResponse response)373 public void doPost(HttpServletRequest request, HttpServletResponse response) 374 throws IOException { 375 if (SystemProperty.environment.value() == SystemProperty.Environment.Value.Production) { 376 response.setStatus(HttpServletResponse.SC_NOT_ACCEPTABLE); 377 return; 378 } 379 380 UserService userService = UserServiceFactory.getUserService(); 381 User currentUser = userService.getCurrentUser(); 382 383 String pathInfo = request.getPathInfo(); 384 String[] pathParts = pathInfo.split("/"); 385 if (pathParts.length > 1) { 386 // Read the json output 387 Reader postJsonReader = new InputStreamReader(request.getInputStream()); 388 Gson gson = new GsonBuilder().create(); 389 390 String testType = pathParts[1]; 391 if (testType.equalsIgnoreCase("report")) { 392 TestReportDataObject trdObj = 393 gson.fromJson(postJsonReader, TestReportDataObject.class); 394 logger.log(Level.INFO, "trdObj => " + trdObj); 395 trdObj.testList.forEach( 396 test -> { 397 test.testRunList.forEach( 398 testRun -> { 399 TestEntity testEntity = new TestEntity(testRun.testName); 400 401 Key testRunKey = 402 KeyFactory.createKey( 403 testEntity.getOldKey(), 404 TestRunEntity.KIND, 405 testRun.startTimestamp); 406 407 List<TestCaseReference> failingTestCases = 408 new ArrayList<>(); 409 for (int idx = 0; 410 idx < testRun.failingTestcaseIds.size(); 411 idx++) { 412 failingTestCases.add( 413 new TestCaseReference( 414 testRun.failingTestcaseIds.get(idx), 415 testRun.failingTestcaseOffsets.get( 416 idx))); 417 } 418 419 TestStatusEntity testStatusEntity = 420 new TestStatusEntity( 421 testRun.testName, 422 testRun.startTimestamp, 423 (int) testRun.passCount, 424 failingTestCases.size(), 425 failingTestCases); 426 datastore.put(testStatusEntity.toEntity()); 427 428 testRun.coverageList.forEach( 429 testRunCoverage -> { 430 CoverageEntity coverageEntity = 431 new CoverageEntity( 432 testRunKey, 433 testRunCoverage.group, 434 testRunCoverage 435 .coveredLineCount, 436 testRunCoverage.totalLineCount, 437 testRunCoverage.filePath, 438 testRunCoverage.projectName, 439 testRunCoverage.projectVersion, 440 testRunCoverage.lineCoverage); 441 datastore.put(coverageEntity.toEntity()); 442 }); 443 444 testRun.profilingList.forEach( 445 testRunProfile -> { 446 ProfilingPointRunEntity profilingEntity = 447 new ProfilingPointRunEntity( 448 testRunKey, 449 testRunProfile.name, 450 testRunProfile.type, 451 testRunProfile.regressionMode, 452 testRunProfile.labels, 453 testRunProfile.values, 454 testRunProfile.xLabel, 455 testRunProfile.yLabel, 456 testRunProfile.options); 457 datastore.put(profilingEntity.toEntity()); 458 }); 459 460 TestCaseRunEntity testCaseEntity = new TestCaseRunEntity(); 461 testRun.testCaseRunList.forEach( 462 testCaseRun -> { 463 for (int idx = 0; 464 idx < testCaseRun.testCaseNames.size(); 465 idx++) { 466 testCaseEntity.addTestCase( 467 testCaseRun.testCaseNames.get(idx), 468 testCaseRun.results.get(idx)); 469 } 470 }); 471 datastore.put(testCaseEntity.toEntity()); 472 473 testRun.deviceInfoList.forEach( 474 deviceInfo -> { 475 DeviceInfoEntity deviceInfoEntity = 476 new DeviceInfoEntity( 477 testRunKey, 478 deviceInfo.branch, 479 deviceInfo.product, 480 deviceInfo.buildFlavor, 481 deviceInfo.buildId, 482 deviceInfo.abiBitness, 483 deviceInfo.abiName); 484 ; 485 datastore.put(deviceInfoEntity.toEntity()); 486 }); 487 488 testRun.buildTargetList.forEach( 489 buildTarget -> { 490 BuildTargetEntity buildTargetEntity = 491 new BuildTargetEntity( 492 buildTarget.targetName); 493 buildTargetEntity.save(); 494 }); 495 496 testRun.branchList.forEach( 497 branch -> { 498 BranchEntity branchEntity = 499 new BranchEntity(branch.branchName); 500 branchEntity.save(); 501 }); 502 503 boolean hasCodeCoverage = 504 testRun.totalLineCount > 0 505 && testRun.coveredLineCount >= 0; 506 TestRunEntity testRunEntity = 507 new TestRunEntity( 508 testEntity.getOldKey(), 509 testRun.type, 510 testRun.startTimestamp, 511 testRun.endTimestamp, 512 testRun.testBuildId, 513 testRun.hostName, 514 testRun.passCount, 515 testRun.failCount, 516 hasCodeCoverage, 517 testRun.testCaseIds, 518 testRun.links); 519 datastore.put(testRunEntity.toEntity()); 520 521 CodeCoverageEntity codeCoverageEntity = 522 new CodeCoverageEntity( 523 testRunEntity.getKey(), 524 testRun.coveredLineCount, 525 testRun.totalLineCount); 526 datastore.put(codeCoverageEntity.toEntity()); 527 528 Entity newTestEntity = testEntity.toEntity(); 529 530 Transaction txn = datastore.beginTransaction(); 531 try { 532 // Check if test already exists in the datastore 533 try { 534 Entity oldTest = 535 datastore.get(testEntity.getOldKey()); 536 TestEntity oldTestEntity = 537 TestEntity.fromEntity(oldTest); 538 if (oldTestEntity == null 539 || !oldTestEntity.equals(testEntity)) { 540 datastore.put(newTestEntity); 541 } 542 } catch (EntityNotFoundException e) { 543 datastore.put(newTestEntity); 544 } 545 txn.commit(); 546 547 } catch (ConcurrentModificationException 548 | DatastoreFailureException 549 | DatastoreTimeoutException e) { 550 logger.log( 551 Level.WARNING, 552 "Retrying test run insert: " 553 + newTestEntity.getKey()); 554 } finally { 555 if (txn.isActive()) { 556 logger.log( 557 Level.WARNING, 558 "Transaction rollback forced for run: " 559 + testRunEntity.getKey()); 560 txn.rollback(); 561 } 562 } 563 }); 564 }); 565 } else { 566 TestPlanReportDataObject tprdObj = 567 gson.fromJson(postJsonReader, TestPlanReportDataObject.class); 568 tprdObj.testPlanList.forEach( 569 testPlan -> { 570 Entity testPlanEntity = 571 new TestPlanEntity(testPlan.testPlanName).toEntity(); 572 List<Key> testRunKeys = new ArrayList<>(); 573 for (int idx = 0; idx < testPlan.testModules.size(); idx++) { 574 String test = testPlan.testModules.get(idx); 575 long time = testPlan.testTimes.get(idx); 576 Key parentKey = KeyFactory.createKey(TestEntity.KIND, test); 577 Key testRunKey = 578 KeyFactory.createKey(parentKey, TestRunEntity.KIND, time); 579 testRunKeys.add(testRunKey); 580 } 581 Map<Key, Entity> testRuns = datastore.get(testRunKeys); 582 long passCount = 0; 583 long failCount = 0; 584 long startTimestamp = -1; 585 long endTimestamp = -1; 586 String testBuildId = null; 587 long type = 0; 588 Set<DeviceInfoEntity> devices = new HashSet<>(); 589 for (Key testRunKey : testRuns.keySet()) { 590 TestRunEntity testRun = 591 TestRunEntity.fromEntity(testRuns.get(testRunKey)); 592 if (testRun == null) { 593 continue; // not a valid test run 594 } 595 passCount += testRun.getPassCount(); 596 failCount += testRun.getFailCount(); 597 if (startTimestamp < 0 || testRunKey.getId() < startTimestamp) { 598 startTimestamp = testRunKey.getId(); 599 } 600 if (endTimestamp < 0 || testRun.getEndTimestamp() > endTimestamp) { 601 endTimestamp = testRun.getEndTimestamp(); 602 } 603 type = testRun.getType(); 604 testBuildId = testRun.getTestBuildId(); 605 Query deviceInfoQuery = 606 new Query(DeviceInfoEntity.KIND).setAncestor(testRunKey); 607 for (Entity deviceInfoEntity : 608 datastore.prepare(deviceInfoQuery).asIterable()) { 609 DeviceInfoEntity device = 610 DeviceInfoEntity.fromEntity(deviceInfoEntity); 611 if (device == null) { 612 continue; // invalid entity 613 } 614 devices.add(device); 615 } 616 } 617 if (startTimestamp < 0 || testBuildId == null || type == 0) { 618 logger.log( 619 Level.WARNING, 620 "Couldn't infer test run information from runs."); 621 return; 622 } 623 TestPlanRunEntity testPlanRun = 624 new TestPlanRunEntity( 625 testPlanEntity.getKey(), 626 testPlan.testPlanName, 627 type, 628 startTimestamp, 629 endTimestamp, 630 testBuildId, 631 passCount, 632 failCount, 633 0L, 634 0L, 635 testRunKeys); 636 637 // Create the device infos. 638 for (DeviceInfoEntity device : devices) { 639 datastore.put( 640 device.copyWithParent(testPlanRun.getOfyKey()).toEntity()); 641 } 642 datastore.put(testPlanRun.toEntity()); 643 644 Transaction txn = datastore.beginTransaction(); 645 try { 646 // Check if test already exists in the database 647 try { 648 datastore.get(testPlanEntity.getKey()); 649 } catch (EntityNotFoundException e) { 650 datastore.put(testPlanEntity); 651 } 652 txn.commit(); 653 } catch (ConcurrentModificationException 654 | DatastoreFailureException 655 | DatastoreTimeoutException e) { 656 logger.log( 657 Level.WARNING, 658 "Retrying test plan insert: " + testPlanEntity.getKey()); 659 } finally { 660 if (txn.isActive()) { 661 logger.log( 662 Level.WARNING, 663 "Transaction rollback forced for plan run: " 664 + testPlanRun.key); 665 txn.rollback(); 666 } 667 } 668 }); 669 } 670 } else { 671 logger.log(Level.WARNING, "URL path parameter is omitted!"); 672 } 673 674 response.setStatus(HttpServletResponse.SC_OK); 675 } 676 } 677