/** * Copyright 2016 Google Inc. All Rights Reserved. * *

Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file * except in compliance with the License. You may obtain a copy of the License at * *

http://www.apache.org/licenses/LICENSE-2.0 * *

Unless required by applicable law or agreed to in writing, software distributed under the * License is distributed on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either * express or implied. See the License for the specific language governing permissions and * limitations under the License. */ package com.android.vts.util; import com.android.vts.entity.BranchEntity; import com.android.vts.entity.BuildTargetEntity; import com.android.vts.entity.CoverageEntity; import com.android.vts.entity.DeviceInfoEntity; import com.android.vts.entity.ProfilingPointRunEntity; import com.android.vts.entity.TestCaseRunEntity; import com.android.vts.entity.TestEntity; import com.android.vts.entity.TestPlanEntity; import com.android.vts.entity.TestPlanRunEntity; import com.android.vts.entity.TestRunEntity; import com.android.vts.entity.TestRunEntity.TestRunType; import com.android.vts.job.VtsAlertJobServlet; import com.android.vts.job.VtsCoverageAlertJobServlet; import com.android.vts.job.VtsProfilingStatsJobServlet; import com.android.vts.proto.VtsReportMessage.AndroidDeviceInfoMessage; import com.android.vts.proto.VtsReportMessage.CoverageReportMessage; import com.android.vts.proto.VtsReportMessage.LogMessage; import com.android.vts.proto.VtsReportMessage.ProfilingReportMessage; import com.android.vts.proto.VtsReportMessage.TestCaseReportMessage; import com.android.vts.proto.VtsReportMessage.TestCaseResult; import com.android.vts.proto.VtsReportMessage.TestPlanReportMessage; import com.android.vts.proto.VtsReportMessage.TestReportMessage; import com.android.vts.proto.VtsReportMessage.UrlResourceMessage; import com.google.appengine.api.datastore.DatastoreFailureException; import com.google.appengine.api.datastore.DatastoreService; import com.google.appengine.api.datastore.DatastoreServiceFactory; import com.google.appengine.api.datastore.DatastoreTimeoutException; import com.google.appengine.api.datastore.Entity; import com.google.appengine.api.datastore.EntityNotFoundException; import com.google.appengine.api.datastore.FetchOptions; import com.google.appengine.api.datastore.Key; import com.google.appengine.api.datastore.KeyFactory; import com.google.appengine.api.datastore.Query; import com.google.appengine.api.datastore.Query.Filter; import com.google.appengine.api.datastore.Query.FilterOperator; import com.google.appengine.api.datastore.Query.FilterPredicate; import com.google.appengine.api.datastore.Transaction; import com.google.appengine.api.datastore.TransactionOptions; import com.google.common.collect.Lists; import java.io.IOException; import java.util.ArrayList; import java.util.Arrays; import java.util.ConcurrentModificationException; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import java.util.logging.Level; import java.util.logging.Logger; /** DatastoreHelper, a helper class for interacting with Cloud Datastore. */ public class DatastoreHelper { /** The default kind name for datastore */ public static final String NULL_ENTITY_KIND = "nullEntity"; public static final int MAX_WRITE_RETRIES = 5; /** * This variable is for maximum number of entities per transaction You can find the detail here * (https://cloud.google.com/datastore/docs/concepts/limits) */ public static final int MAX_ENTITY_SIZE_PER_TRANSACTION = 300; protected static final Logger logger = Logger.getLogger(DatastoreHelper.class.getName()); private static final DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); /** * Get query fetch options for large batches of entities. * * @return FetchOptions with a large chunk and prefetch size. */ public static FetchOptions getLargeBatchOptions() { return FetchOptions.Builder.withChunkSize(1000).prefetchSize(1000); } /** * Returns true if there are data points newer than lowerBound in the results table. * * @param parentKey The parent key to use in the query. * @param kind The query entity kind. * @param lowerBound The (exclusive) lower time bound, long, microseconds. * @return boolean True if there are newer data points. * @throws IOException */ public static boolean hasNewer(Key parentKey, String kind, Long lowerBound) throws IOException { if (lowerBound == null || lowerBound <= 0) return false; DatastoreService datastore = DatastoreServiceFactory.getDatastoreService(); Key startKey = KeyFactory.createKey(parentKey, kind, lowerBound); Filter startFilter = new FilterPredicate( Entity.KEY_RESERVED_PROPERTY, FilterOperator.GREATER_THAN, startKey); Query q = new Query(kind).setAncestor(parentKey).setFilter(startFilter).setKeysOnly(); return datastore.prepare(q).countEntities(FetchOptions.Builder.withLimit(1)) > 0; } /** * Returns true if there are data points older than upperBound in the table. * * @param parentKey The parent key to use in the query. * @param kind The query entity kind. * @param upperBound The (exclusive) upper time bound, long, microseconds. * @return boolean True if there are older data points. * @throws IOException */ public static boolean hasOlder(Key parentKey, String kind, Long upperBound) throws IOException { if (upperBound == null || upperBound <= 0) return false; Key endKey = KeyFactory.createKey(parentKey, kind, upperBound); Filter endFilter = new FilterPredicate(Entity.KEY_RESERVED_PROPERTY, FilterOperator.LESS_THAN, endKey); Query q = new Query(kind).setAncestor(parentKey).setFilter(endFilter).setKeysOnly(); return datastore.prepare(q).countEntities(FetchOptions.Builder.withLimit(1)) > 0; } /** * Get all of the devices branches. * * @return a list of all branches. */ public static List getAllBranches() { Query query = new Query(BranchEntity.KIND).setKeysOnly(); List branches = new ArrayList<>(); for (Entity e : datastore.prepare(query).asIterable(getLargeBatchOptions())) { branches.add(e.getKey().getName()); } return branches; } /** * Get all of the device build flavors. * * @return a list of all device build flavors. */ public static List getAllBuildFlavors() { Query query = new Query(BuildTargetEntity.KIND).setKeysOnly(); List devices = new ArrayList<>(); for (Entity e : datastore.prepare(query).asIterable(getLargeBatchOptions())) { devices.add(e.getKey().getName()); } return devices; } /** * Upload data from a test report message * * @param report The test report containing data to upload. */ public static void insertTestReport(TestReportMessage report) { List testEntityList = new ArrayList<>(); List branchEntityList = new ArrayList<>(); List buildTargetEntityList = new ArrayList<>(); List coverageEntityList = new ArrayList<>(); List profilingPointRunEntityList = new ArrayList<>(); if (!report.hasStartTimestamp() || !report.hasEndTimestamp() || !report.hasTest() || !report.hasHostInfo() || !report.hasBuildInfo()) { // missing information return; } long startTimestamp = report.getStartTimestamp(); long endTimestamp = report.getEndTimestamp(); String testName = report.getTest().toStringUtf8(); String testBuildId = report.getBuildInfo().getId().toStringUtf8(); String hostName = report.getHostInfo().getHostname().toStringUtf8(); TestEntity testEntity = new TestEntity(testName); Key testRunKey = KeyFactory.createKey( testEntity.key, TestRunEntity.KIND, report.getStartTimestamp()); long passCount = 0; long failCount = 0; long coveredLineCount = 0; long totalLineCount = 0; Set buildTargetKeys = new HashSet<>(); Set branchKeys = new HashSet<>(); List testCases = new ArrayList<>(); List profilingPointKeys = new ArrayList<>(); List links = new ArrayList<>(); // Process test cases for (TestCaseReportMessage testCase : report.getTestCaseList()) { String testCaseName = testCase.getName().toStringUtf8(); TestCaseResult result = testCase.getTestResult(); // Track global pass/fail counts if (result == TestCaseResult.TEST_CASE_RESULT_PASS) { ++passCount; } else if (result != TestCaseResult.TEST_CASE_RESULT_SKIP) { ++failCount; } if (testCase.getSystraceCount() > 0 && testCase.getSystraceList().get(0).getUrlCount() > 0) { String systraceLink = testCase.getSystraceList().get(0).getUrl(0).toStringUtf8(); links.add(systraceLink); } // Process coverage data for test case for (CoverageReportMessage coverage : testCase.getCoverageList()) { CoverageEntity coverageEntity = CoverageEntity.fromCoverageReport(testRunKey, testCaseName, coverage); if (coverageEntity == null) { logger.log(Level.WARNING, "Invalid coverage report in test run " + testRunKey); } else { coveredLineCount += coverageEntity.coveredLineCount; totalLineCount += coverageEntity.totalLineCount; coverageEntityList.add(coverageEntity.toEntity()); } } // Process profiling data for test case for (ProfilingReportMessage profiling : testCase.getProfilingList()) { ProfilingPointRunEntity profilingPointRunEntity = ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling); if (profilingPointRunEntity == null) { logger.log(Level.WARNING, "Invalid profiling report in test run " + testRunKey); } else { profilingPointRunEntityList.add(profilingPointRunEntity.toEntity()); profilingPointKeys.add(profilingPointRunEntity.key); testEntity.setHasProfilingData(true); } } int lastIndex = testCases.size() - 1; if (lastIndex < 0 || testCases.get(lastIndex).isFull()) { testCases.add(new TestCaseRunEntity()); ++lastIndex; } TestCaseRunEntity testCaseEntity = testCases.get(lastIndex); testCaseEntity.addTestCase(testCaseName, result.getNumber()); } List testCasePuts = new ArrayList<>(); for (TestCaseRunEntity testCaseEntity : testCases) { testCasePuts.add(testCaseEntity.toEntity()); } List testCaseKeys = datastore.put(testCasePuts); List testCaseIds = new ArrayList<>(); for (Key key : testCaseKeys) { testCaseIds.add(key.getId()); } // Process device information TestRunType testRunType = null; for (AndroidDeviceInfoMessage device : report.getDeviceInfoList()) { DeviceInfoEntity deviceInfoEntity = DeviceInfoEntity.fromDeviceInfoMessage(testRunKey, device); if (deviceInfoEntity == null) { logger.log(Level.WARNING, "Invalid device info in test run " + testRunKey); } else { // Run type on devices must be the same, else set to OTHER TestRunType runType = TestRunType.fromBuildId(deviceInfoEntity.buildId); if (testRunType == null) { testRunType = runType; } else if (runType != testRunType) { testRunType = TestRunType.OTHER; } testEntityList.add(deviceInfoEntity.toEntity()); BuildTargetEntity target = new BuildTargetEntity(deviceInfoEntity.buildFlavor); if (buildTargetKeys.add(target.key)) { buildTargetEntityList.add(target.toEntity()); } BranchEntity branch = new BranchEntity(deviceInfoEntity.branch); if (branchKeys.add(branch.key)) { branchEntityList.add(branch.toEntity()); } } } // Overall run type should be determined by the device builds unless test build is OTHER if (testRunType == null) { testRunType = TestRunType.fromBuildId(testBuildId); } else if (TestRunType.fromBuildId(testBuildId) == TestRunType.OTHER) { testRunType = TestRunType.OTHER; } // Process global coverage data for (CoverageReportMessage coverage : report.getCoverageList()) { CoverageEntity coverageEntity = CoverageEntity.fromCoverageReport(testRunKey, new String(), coverage); if (coverageEntity == null) { logger.log(Level.WARNING, "Invalid coverage report in test run " + testRunKey); } else { coveredLineCount += coverageEntity.coveredLineCount; totalLineCount += coverageEntity.totalLineCount; coverageEntityList.add(coverageEntity.toEntity()); } } // Process global profiling data for (ProfilingReportMessage profiling : report.getProfilingList()) { ProfilingPointRunEntity profilingPointRunEntity = ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling); if (profilingPointRunEntity == null) { logger.log(Level.WARNING, "Invalid profiling report in test run " + testRunKey); } else { profilingPointRunEntityList.add(profilingPointRunEntity.toEntity()); profilingPointKeys.add(profilingPointRunEntity.key); testEntity.setHasProfilingData(true); } } // Process log data for (LogMessage log : report.getLogList()) { if (log.hasUrl()) links.add(log.getUrl().toStringUtf8()); } // Process url resource for (UrlResourceMessage resource : report.getLinkResourceList()) { if (resource.hasUrl()) links.add(resource.getUrl().toStringUtf8()); } TestRunEntity testRunEntity = new TestRunEntity( testEntity.key, testRunType, startTimestamp, endTimestamp, testBuildId, hostName, passCount, failCount, testCaseIds, links, coveredLineCount, totalLineCount); testEntityList.add(testRunEntity.toEntity()); Entity test = testEntity.toEntity(); if (datastoreTransactionalRetry(test, testEntityList)) { List> auxiliaryEntityList = Arrays.asList( profilingPointRunEntityList, coverageEntityList, branchEntityList, buildTargetEntityList); int indexCount = 0; for (List entityList : auxiliaryEntityList) { switch (indexCount) { case 0: case 1: if (entityList.size() > MAX_ENTITY_SIZE_PER_TRANSACTION) { List> partitionedList = Lists.partition(entityList, MAX_ENTITY_SIZE_PER_TRANSACTION); partitionedList.forEach( subEntityList -> { datastoreTransactionalRetry( new Entity(NULL_ENTITY_KIND), subEntityList); }); } else { datastoreTransactionalRetry(new Entity(NULL_ENTITY_KIND), entityList); } break; case 2: case 3: datastoreTransactionalRetryWithXG( new Entity(NULL_ENTITY_KIND), entityList, true); break; default: break; } indexCount++; } if (testRunEntity.type == TestRunType.POSTSUBMIT) { VtsAlertJobServlet.addTask(testRunKey); if (testRunEntity.hasCoverage) { VtsCoverageAlertJobServlet.addTask(testRunKey); } if (profilingPointKeys.size() > 0) { VtsProfilingStatsJobServlet.addTasks(profilingPointKeys); } } else { logger.log( Level.WARNING, "The alert email was not sent as testRunEntity type is not POSTSUBMIT!" + " \n " + " testRunEntity type => " + testRunEntity.type); } } } /** * Upload data from a test plan report message * * @param report The test plan report containing data to upload. */ public static void insertTestPlanReport(TestPlanReportMessage report) { List testEntityList = new ArrayList<>(); List testModules = report.getTestModuleNameList(); List testTimes = report.getTestModuleStartTimestampList(); if (testModules.size() != testTimes.size() || !report.hasTestPlanName()) { logger.log(Level.WARNING, "TestPlanReportMessage is missing information."); return; } String testPlanName = report.getTestPlanName(); Entity testPlanEntity = new TestPlanEntity(testPlanName).toEntity(); List testRunKeys = new ArrayList<>(); for (int i = 0; i < testModules.size(); i++) { String test = testModules.get(i); long time = testTimes.get(i); Key parentKey = KeyFactory.createKey(TestEntity.KIND, test); Key testRunKey = KeyFactory.createKey(parentKey, TestRunEntity.KIND, time); testRunKeys.add(testRunKey); } Map testRuns = datastore.get(testRunKeys); long passCount = 0; long failCount = 0; long startTimestamp = -1; long endTimestamp = -1; String testBuildId = null; TestRunType type = null; Set deviceInfoEntitySet = new HashSet<>(); for (Key testRunKey : testRuns.keySet()) { TestRunEntity testRun = TestRunEntity.fromEntity(testRuns.get(testRunKey)); if (testRun == null) { continue; // not a valid test run } passCount += testRun.passCount; failCount += testRun.failCount; if (startTimestamp < 0 || testRunKey.getId() < startTimestamp) { startTimestamp = testRunKey.getId(); } if (endTimestamp < 0 || testRun.endTimestamp > endTimestamp) { endTimestamp = testRun.endTimestamp; } if (type == null) { type = testRun.type; } else if (type != testRun.type) { type = TestRunType.OTHER; } testBuildId = testRun.testBuildId; Query deviceInfoQuery = new Query(DeviceInfoEntity.KIND).setAncestor(testRunKey); for (Entity deviceInfoEntity : datastore.prepare(deviceInfoQuery).asIterable()) { DeviceInfoEntity device = DeviceInfoEntity.fromEntity(deviceInfoEntity); if (device == null) { continue; // invalid entity } deviceInfoEntitySet.add(device); } } if (startTimestamp < 0 || testBuildId == null || type == null) { logger.log(Level.WARNING, "Couldn't infer test run information from runs."); return; } TestPlanRunEntity testPlanRun = new TestPlanRunEntity( testPlanEntity.getKey(), testPlanName, type, startTimestamp, endTimestamp, testBuildId, passCount, failCount, testRunKeys); // Create the device infos. for (DeviceInfoEntity device : deviceInfoEntitySet) { testEntityList.add(device.copyWithParent(testPlanRun.key).toEntity()); } testEntityList.add(testPlanRun.toEntity()); datastoreTransactionalRetry(testPlanEntity, testEntityList); } /** * Datastore Transactional process for data insertion with MAX_WRITE_RETRIES times and withXG of * false value * * @param entity The entity that you want to insert to datastore. * @param entityList The list of entity for using datastore put method. */ private static boolean datastoreTransactionalRetry(Entity entity, List entityList) { return datastoreTransactionalRetryWithXG(entity, entityList, false); } /** * Datastore Transactional process for data insertion with MAX_WRITE_RETRIES times * * @param entity The entity that you want to insert to datastore. * @param entityList The list of entity for using datastore put method. */ private static boolean datastoreTransactionalRetryWithXG( Entity entity, List entityList, boolean withXG) { int retries = 0; while (true) { Transaction txn; if (withXG) { TransactionOptions options = TransactionOptions.Builder.withXG(withXG); txn = datastore.beginTransaction(options); } else { txn = datastore.beginTransaction(); } try { // Check if test already exists in the database if (!entity.getKind().equalsIgnoreCase(NULL_ENTITY_KIND)) { try { if (entity.getKind().equalsIgnoreCase("Test")) { Entity datastoreEntity = datastore.get(entity.getKey()); TestEntity datastoreTestEntity = TestEntity.fromEntity(datastoreEntity); if (datastoreTestEntity == null || !datastoreTestEntity.equals(entity)) { entityList.add(entity); } } else if (entity.getKind().equalsIgnoreCase("TestPlan")) { datastore.get(entity.getKey()); } else { datastore.get(entity.getKey()); } } catch (EntityNotFoundException e) { entityList.add(entity); } } datastore.put(txn, entityList); txn.commit(); break; } catch (ConcurrentModificationException | DatastoreFailureException | DatastoreTimeoutException e) { entityList.remove(entity); logger.log( Level.WARNING, "Retrying insert kind: " + entity.getKind() + " key: " + entity.getKey()); if (retries++ >= MAX_WRITE_RETRIES) { logger.log( Level.SEVERE, "Exceeded maximum retries kind: " + entity.getKind() + " key: " + entity.getKey()); return false; } } finally { if (txn.isActive()) { logger.log( Level.WARNING, "Transaction rollback forced for : " + entity.getKind()); txn.rollback(); } } } return true; } }