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