/*
* Copyright (c) 2017 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.entity;
import static com.googlecode.objectify.ObjectifyService.ofy;
import com.android.vts.util.TimeUtil;
import com.android.vts.util.UrlUtil;
import com.android.vts.util.UrlUtil.LinkDisplay;
import com.google.appengine.api.datastore.Entity;
import com.google.appengine.api.datastore.Key;
import com.google.appengine.api.datastore.KeyFactory;
import com.google.gson.Gson;
import com.google.gson.JsonArray;
import com.google.gson.JsonElement;
import com.google.gson.JsonObject;
import com.google.gson.JsonPrimitive;
import com.googlecode.objectify.annotation.Cache;
import com.googlecode.objectify.annotation.Id;
import com.googlecode.objectify.annotation.Ignore;
import com.googlecode.objectify.annotation.Index;
import com.googlecode.objectify.annotation.OnLoad;
import com.googlecode.objectify.annotation.Parent;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.function.Supplier;
import java.util.logging.Level;
import java.util.logging.Logger;
import java.util.stream.Stream;
import lombok.Getter;
import lombok.NoArgsConstructor;
import lombok.Setter;
import org.apache.commons.lang3.math.NumberUtils;
@com.googlecode.objectify.annotation.Entity(name = "TestRun")
@Cache
@NoArgsConstructor
/** Entity describing test run information. */
public class TestRunEntity implements DashboardEntity {
protected static final Logger logger = Logger.getLogger(TestRunEntity.class.getName());
/** Enum for classifying test run types. */
public enum TestRunType {
OTHER(0),
PRESUBMIT(1),
POSTSUBMIT(2);
private final int value;
private TestRunType(int value) {
this.value = value;
}
/**
* Get the ordinal representation of the type.
*
* @return The value associated with the test run type.
*/
public int getNumber() {
return value;
}
/**
* Convert an ordinal value to a TestRunType.
*
* @param value The orginal value to parse.
* @return a TestRunType value.
*/
public static TestRunType fromNumber(int value) {
if (value == 1) {
return TestRunType.PRESUBMIT;
} else if (value == 2) {
return TestRunType.POSTSUBMIT;
} else {
return TestRunType.OTHER;
}
}
/**
* Determine the test run type based on the build ID.
*
*
Postsubmit runs are expected to have integer build IDs, while presubmit runs are
* integers prefixed by the character P. All other runs (e.g. local builds) are classified
* as OTHER.
*
* @param buildId The build ID.
* @return the TestRunType.
*/
public static TestRunType fromBuildId(String buildId) {
if (buildId.toLowerCase().startsWith("p")) {
if (NumberUtils.isParsable(buildId.substring(1))) {
return TestRunType.PRESUBMIT;
} else {
return TestRunType.OTHER;
}
} else if (NumberUtils.isParsable(buildId)) {
return TestRunType.POSTSUBMIT;
} else {
return TestRunType.OTHER;
}
}
}
public static final String KIND = "TestRun";
// Property keys
public static final String TEST_NAME = "testName";
public static final String TYPE = "type";
public static final String START_TIMESTAMP = "startTimestamp";
public static final String END_TIMESTAMP = "endTimestamp";
public static final String TEST_BUILD_ID = "testBuildId";
public static final String HOST_NAME = "hostName";
public static final String PASS_COUNT = "passCount";
public static final String FAIL_COUNT = "failCount";
public static final String HAS_CODE_COVERAGE = "hasCodeCoverage";
public static final String HAS_COVERAGE = "hasCoverage";
public static final String TEST_CASE_IDS = "testCaseIds";
public static final String LOG_LINKS = "logLinks";
public static final String API_COVERAGE_KEY_LIST = "apiCoverageKeyList";
public static final String TOTAL_API_COUNT = "totalApiCount";
public static final String COVERED_API_COUNT = "coveredApiCount";
@Ignore private Key key;
@Id @Getter @Setter private Long id;
@Parent @Getter @Setter private com.googlecode.objectify.Key> testRunParent;
@Index @Getter @Setter private long type;
@Index @Getter @Setter private long startTimestamp;
@Index @Getter @Setter private long endTimestamp;
@Index @Getter @Setter private String testBuildId;
@Index @Getter @Setter private String testName;
@Index @Getter @Setter private String hostName;
@Index @Getter @Setter private long passCount;
@Index @Getter @Setter private long failCount;
@Index private boolean hasCoverage;
@Index @Getter @Setter private boolean hasCodeCoverage;
@Ignore private com.googlecode.objectify.Key codeCoverageEntityKey;
@Index @Getter @Setter private long coveredLineCount;
@Index @Getter @Setter private long totalLineCount;
@Getter @Setter private List testCaseIds;
@Getter @Setter private List logLinks;
/**
* Create a TestRunEntity object describing a test run.
*
* @param parentKey The key to the parent TestEntity.
* @param type The test run type (e.g. presubmit, postsubmit, other)
* @param startTimestamp The time in microseconds when the test run started.
* @param endTimestamp The time in microseconds when the test run ended.
* @param testBuildId The build ID of the VTS test build.
* @param hostName The name of host machine.
* @param passCount The number of passing test cases in the run.
* @param failCount The number of failing test cases in the run.
*/
public TestRunEntity(
Key parentKey,
long type,
long startTimestamp,
long endTimestamp,
String testBuildId,
String hostName,
long passCount,
long failCount,
boolean hasCodeCoverage,
List testCaseIds,
List logLinks) {
this.id = startTimestamp;
this.key = KeyFactory.createKey(parentKey, KIND, startTimestamp);
this.type = type;
this.startTimestamp = startTimestamp;
this.endTimestamp = endTimestamp;
this.testBuildId = testBuildId;
this.hostName = hostName;
this.passCount = passCount;
this.failCount = failCount;
this.hasCodeCoverage = hasCodeCoverage;
this.testName = parentKey.getName();
this.testCaseIds = testCaseIds;
this.logLinks = logLinks;
this.testRunParent = com.googlecode.objectify.Key.create(TestEntity.class, testName);
this.codeCoverageEntityKey = getCodeCoverageEntityKey();
}
/**
* Called after the POJO is populated with data through objecitfy library
*/
@OnLoad
private void onLoad() {
if (Objects.isNull(this.hasCodeCoverage)) {
this.hasCodeCoverage = this.hasCoverage;
this.save();
}
}
public Entity toEntity() {
Entity testRunEntity = new Entity(this.key);
testRunEntity.setProperty(TEST_NAME, this.testName);
testRunEntity.setProperty(TYPE, this.type);
testRunEntity.setProperty(START_TIMESTAMP, this.startTimestamp);
testRunEntity.setUnindexedProperty(END_TIMESTAMP, this.endTimestamp);
testRunEntity.setProperty(TEST_BUILD_ID, this.testBuildId.toLowerCase());
testRunEntity.setProperty(HOST_NAME, this.hostName.toLowerCase());
testRunEntity.setProperty(PASS_COUNT, this.passCount);
testRunEntity.setProperty(FAIL_COUNT, this.failCount);
testRunEntity.setProperty(HAS_CODE_COVERAGE, this.hasCodeCoverage);
testRunEntity.setUnindexedProperty(TEST_CASE_IDS, this.testCaseIds);
if (this.logLinks != null && this.logLinks.size() > 0) {
testRunEntity.setUnindexedProperty(LOG_LINKS, this.logLinks);
}
return testRunEntity;
}
/** Saving function for the instance of this class */
@Override
public com.googlecode.objectify.Key save() {
return ofy().save().entity(this).now();
}
/**
* Get key info from appengine based library.
*/
public Key getKey() {
Key parentKey = KeyFactory.createKey(TestEntity.KIND, testName);
return KeyFactory.createKey(parentKey, KIND, startTimestamp);
}
/** Getter hasCodeCoverage value */
public boolean getHasCodeCoverage() {
return this.hasCodeCoverage;
}
/** Getter DateTime string from startTimestamp */
public String getStartDateTime() {
return TimeUtil.getDateTimeString(this.startTimestamp);
}
/** Getter DateTime string from startTimestamp */
public String getEndDateTime() {
return TimeUtil.getDateTimeString(this.endTimestamp);
}
/** find TestRun entity by ID and test name */
public static TestRunEntity getByTestNameId(String testName, long id) {
com.googlecode.objectify.Key testKey =
com.googlecode.objectify.Key.create(TestEntity.class, testName);
return ofy().load().type(TestRunEntity.class).parent(testKey).id(id).now();
}
/** Get CodeCoverageEntity Key to generate Key by combining key info */
private com.googlecode.objectify.Key getCodeCoverageEntityKey() {
com.googlecode.objectify.Key testRunKey = this.getOfyKey();
return com.googlecode.objectify.Key.create(
testRunKey, CodeCoverageEntity.class, this.startTimestamp);
}
/** Get ApiCoverageEntity Key from the parent key */
public com.googlecode.objectify.Key getOfyKey() {
com.googlecode.objectify.Key testKey =
com.googlecode.objectify.Key.create(
TestEntity.class, this.testName);
com.googlecode.objectify.Key testRunKey =
com.googlecode.objectify.Key.create(
testKey, TestRunEntity.class, this.startTimestamp);
return testRunKey;
}
/** Get ApiCoverageEntity from key info */
public Optional> getApiCoverageEntityList() {
com.googlecode.objectify.Key testRunKey = this.getOfyKey();
List apiCoverageEntityList =
ofy().load().type(ApiCoverageEntity.class).ancestor(testRunKey).list();
return Optional.ofNullable(apiCoverageEntityList);
}
/**
* Get CodeCoverageEntity instance from codeCoverageEntityKey value.
*/
public CodeCoverageEntity getCodeCoverageEntity() {
if (this.hasCodeCoverage) {
CodeCoverageEntity codeCoverageEntity =
ofy().load()
.type(CodeCoverageEntity.class)
.filterKey(this.codeCoverageEntityKey)
.first()
.now();
if (Objects.isNull(codeCoverageEntity)) {
codeCoverageEntity =
new CodeCoverageEntity(
this.getKey(), coveredLineCount, totalLineCount);
codeCoverageEntity.save();
return codeCoverageEntity;
} else {
return codeCoverageEntity;
}
} else {
logger.log(
Level.WARNING,
"The hasCodeCoverage value is false. Please check the code coverage entity key");
return null;
}
}
/**
* Convert an Entity object to a TestRunEntity.
*
* @param e The entity to process.
* @return TestRunEntity object with the properties from e processed, or null if incompatible.
*/
@SuppressWarnings("unchecked")
public static TestRunEntity fromEntity(Entity e) {
if (!e.getKind().equals(KIND)
|| !e.hasProperty(TYPE)
|| !e.hasProperty(START_TIMESTAMP)
|| !e.hasProperty(END_TIMESTAMP)
|| !e.hasProperty(TEST_BUILD_ID)
|| !e.hasProperty(HOST_NAME)
|| !e.hasProperty(PASS_COUNT)
|| !e.hasProperty(FAIL_COUNT)) {
logger.log(Level.WARNING, "Missing test run attributes in entity: " + e.toString());
return null;
}
try {
long type = (long) e.getProperty(TYPE);
long startTimestamp = (long) e.getProperty(START_TIMESTAMP);
long endTimestamp = (long) e.getProperty(END_TIMESTAMP);
String testBuildId = (String) e.getProperty(TEST_BUILD_ID);
String hostName = (String) e.getProperty(HOST_NAME);
long passCount = (long) e.getProperty(PASS_COUNT);
long failCount = (long) e.getProperty(FAIL_COUNT);
boolean hasCodeCoverage = false;
if (e.hasProperty(HAS_CODE_COVERAGE)) {
hasCodeCoverage = (boolean) e.getProperty(HAS_CODE_COVERAGE);
} else {
hasCodeCoverage = (boolean) e.getProperty(HAS_COVERAGE);
}
List testCaseIds = (List) e.getProperty(TEST_CASE_IDS);
if (Objects.isNull(testCaseIds)) {
testCaseIds = new ArrayList<>();
}
List links = new ArrayList<>();
if (e.hasProperty(LOG_LINKS)) {
links = (List) e.getProperty(LOG_LINKS);
}
return new TestRunEntity(
e.getKey().getParent(),
type,
startTimestamp,
endTimestamp,
testBuildId,
hostName,
passCount,
failCount,
hasCodeCoverage,
testCaseIds,
links);
} catch (ClassCastException exception) {
// Invalid cast
logger.log(Level.WARNING, "Error parsing test run entity.", exception);
}
return null;
}
/** Get JsonFormat logLinks */
public JsonElement getJsonLogLinks() {
List logLinks = this.getLogLinks();
List links = new ArrayList<>();
if (logLinks != null && logLinks.size() > 0) {
for (String rawUrl : logLinks) {
UrlUtil.LinkDisplay validatedLink = UrlUtil.processUrl(rawUrl);
if (validatedLink == null) {
logger.log(Level.WARNING, "Invalid logging URL : " + rawUrl);
continue;
}
String[] logInfo = new String[] {validatedLink.name, validatedLink.url};
links.add(new Gson().toJsonTree(logInfo));
}
}
return new Gson().toJsonTree(links);
}
public JsonObject toJson() {
Map testCoverageStatusMap = TestCoverageStatusEntity
.getTestCoverageStatusMap();
JsonObject json = new JsonObject();
json.add(TEST_NAME, new JsonPrimitive(this.testName));
json.add(TEST_BUILD_ID, new JsonPrimitive(this.testBuildId));
json.add(HOST_NAME, new JsonPrimitive(this.hostName));
json.add(PASS_COUNT, new JsonPrimitive(this.passCount));
json.add(FAIL_COUNT, new JsonPrimitive(this.failCount));
json.add(START_TIMESTAMP, new JsonPrimitive(this.startTimestamp));
json.add(END_TIMESTAMP, new JsonPrimitive(this.endTimestamp));
// Overwrite the coverage value with newly update value from user decision
if (this.hasCodeCoverage) {
CodeCoverageEntity codeCoverageEntity = this.getCodeCoverageEntity();
if (testCoverageStatusMap.containsKey(this.testName)) {
TestCoverageStatusEntity testCoverageStatusEntity =
testCoverageStatusMap.get(this.testName);
if (testCoverageStatusEntity.getUpdatedCoveredLineCount() > 0) {
codeCoverageEntity.setCoveredLineCount(
testCoverageStatusEntity.getUpdatedCoveredLineCount());
}
if (testCoverageStatusEntity.getUpdatedTotalLineCount() > 0) {
codeCoverageEntity.setTotalLineCount(
testCoverageStatusEntity.getUpdatedTotalLineCount());
}
}
long totalLineCount = codeCoverageEntity.getTotalLineCount();
long coveredLineCount = codeCoverageEntity.getCoveredLineCount();
if (totalLineCount > 0 && coveredLineCount >= 0) {
json.add(CodeCoverageEntity.COVERED_LINE_COUNT, new JsonPrimitive(coveredLineCount));
json.add(CodeCoverageEntity.TOTAL_LINE_COUNT, new JsonPrimitive(totalLineCount));
}
}
Optional> apiCoverageEntityOptionList =
this.getApiCoverageEntityList();
if (apiCoverageEntityOptionList.isPresent()) {
List apiCoverageEntityList = apiCoverageEntityOptionList.get();
Supplier> apiCoverageStreamSupplier =
() -> apiCoverageEntityList.stream();
int totalHalApi =
apiCoverageStreamSupplier.get().mapToInt(data -> data.getHalApi().size()).sum();
if (totalHalApi > 0) {
int coveredHalApi =
apiCoverageStreamSupplier
.get()
.mapToInt(data -> data.getCoveredHalApi().size())
.sum();
JsonArray apiCoverageKeyArray =
apiCoverageStreamSupplier
.get()
.map(data -> new JsonPrimitive(data.getUrlSafeKey()))
.collect(JsonArray::new, JsonArray::add, JsonArray::addAll);
json.add(API_COVERAGE_KEY_LIST, apiCoverageKeyArray);
json.add(COVERED_API_COUNT, new JsonPrimitive(coveredHalApi));
json.add(TOTAL_API_COUNT, new JsonPrimitive(totalHalApi));
}
}
List logLinks = this.getLogLinks();
if (logLinks != null && logLinks.size() > 0) {
List links = new ArrayList<>();
for (String rawUrl : logLinks) {
LinkDisplay validatedLink = UrlUtil.processUrl(rawUrl);
if (validatedLink == null) {
logger.log(Level.WARNING, "Invalid logging URL : " + rawUrl);
continue;
}
String[] logInfo = new String[] {validatedLink.name, validatedLink.url};
links.add(new Gson().toJsonTree(logInfo));
}
if (links.size() > 0) {
json.add(this.LOG_LINKS, new Gson().toJsonTree(links));
}
}
return json;
}
}