1 /*
2  * Copyright (c) 2016 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.ApiCoverageEntity;
20 import com.android.vts.entity.BranchEntity;
21 import com.android.vts.entity.BuildTargetEntity;
22 import com.android.vts.entity.CodeCoverageEntity;
23 import com.android.vts.entity.CoverageEntity;
24 import com.android.vts.entity.DashboardEntity;
25 import com.android.vts.entity.DeviceInfoEntity;
26 import com.android.vts.entity.HalApiEntity;
27 import com.android.vts.entity.ProfilingPointRunEntity;
28 import com.android.vts.entity.TestCaseRunEntity;
29 import com.android.vts.entity.TestEntity;
30 import com.android.vts.entity.TestPlanEntity;
31 import com.android.vts.entity.TestPlanRunEntity;
32 import com.android.vts.entity.TestRunEntity;
33 import com.android.vts.proto.VtsReportMessage;
34 import com.android.vts.proto.VtsReportMessage.DashboardPostMessage;
35 import com.android.vts.proto.VtsReportMessage.TestPlanReportMessage;
36 import com.android.vts.proto.VtsReportMessage.TestReportMessage;
37 import com.google.api.client.googleapis.auth.oauth2.GoogleCredential;
38 import com.google.api.client.http.javanet.NetHttpTransport;
39 import com.google.api.client.json.jackson.JacksonFactory;
40 import com.google.api.services.oauth2.Oauth2;
41 import com.google.api.services.oauth2.model.Tokeninfo;
42 
43 import java.io.IOException;
44 import java.util.ArrayList;
45 import java.util.Arrays;
46 import java.util.HashSet;
47 import java.util.List;
48 import java.util.Map;
49 import java.util.Set;
50 import java.util.stream.Collectors;
51 import javax.servlet.ServletConfig;
52 import javax.servlet.ServletException;
53 import javax.servlet.http.HttpServletRequest;
54 import javax.servlet.http.HttpServletResponse;
55 
56 import com.google.appengine.api.datastore.Key;
57 import lombok.extern.slf4j.Slf4j;
58 import org.apache.commons.codec.binary.Base64;
59 
60 import static com.googlecode.objectify.ObjectifyService.ofy;
61 
62 @Slf4j
63 /** REST endpoint for posting data to the Dashboard. */
64 public class DatastoreRestServlet extends BaseApiServlet {
65     private static String SERVICE_CLIENT_ID;
66     private static final String SERVICE_NAME = "VTS Dashboard";
67 
68     @Override
init(ServletConfig cfg)69     public void init(ServletConfig cfg) throws ServletException {
70         super.init(cfg);
71 
72         SERVICE_CLIENT_ID = this.systemConfigProp.getProperty("appengine.serviceClientID");
73     }
74 
75     @Override
doPost(HttpServletRequest request, HttpServletResponse response)76     public void doPost(HttpServletRequest request, HttpServletResponse response)
77             throws IOException {
78         // Retrieve the params
79         DashboardPostMessage postMessage;
80         try {
81             String payload = request.getReader().lines().collect(Collectors.joining());
82             byte[] value = Base64.decodeBase64(payload);
83             postMessage = DashboardPostMessage.parseFrom(value);
84         } catch (IOException e) {
85             response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
86             log.error("Invalid proto: " + e.getLocalizedMessage());
87             return;
88         }
89 
90         String resultMsg = "";
91         // Verify service account access token.
92         if (postMessage.hasAccessToken()) {
93             String accessToken = postMessage.getAccessToken();
94             log.debug("accessToken => " + accessToken);
95 
96             GoogleCredential credential = new GoogleCredential().setAccessToken(accessToken);
97             Oauth2 oauth2 =
98                     new Oauth2.Builder(new NetHttpTransport(), new JacksonFactory(), credential)
99                             .setApplicationName(SERVICE_NAME)
100                             .build();
101             Tokeninfo tokenInfo = oauth2.tokeninfo().setAccessToken(accessToken).execute();
102             if (tokenInfo.getIssuedTo().equals(SERVICE_CLIENT_ID)) {
103                 for (TestReportMessage testReportMessage : postMessage.getTestReportList()) {
104                     this.insertTestReport(testReportMessage);
105                 }
106 
107                 for (TestPlanReportMessage planReportMessage :
108                         postMessage.getTestPlanReportList()) {
109                     this.insertTestPlanReport(planReportMessage);
110                 }
111 
112                 response.setStatus(HttpServletResponse.SC_OK);
113                 resultMsg = "Success!!";
114             } else {
115                 log.warn("service_client_id didn't match!");
116                 log.debug("SERVICE_CLIENT_ID => " + tokenInfo.getIssuedTo());
117                 resultMsg = "Your SERVICE_CLIENT_ID is incorrect!";
118                 response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
119             }
120         } else {
121             log.error("postMessage do not contain any accessToken!");
122             resultMsg = "Your message do not have access token!";
123             response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
124         }
125         response.setContentType("application/json");
126         response.setCharacterEncoding("UTF-8");
127         response.getWriter().write("{'result_msg': " + resultMsg + "}");
128     }
129 
130     /**
131      * Upload data from a test report message
132      *
133      * @param report The test report containing data to upload.
134      */
insertTestReport(TestReportMessage report)135     private void insertTestReport(TestReportMessage report) {
136 
137         if (!report.hasStartTimestamp()
138                 || !report.hasEndTimestamp()
139                 || !report.hasTest()
140                 || !report.hasHostInfo()
141                 || !report.hasBuildInfo()) {
142             // missing information
143             log.error("Missing information in report !");
144             return;
145         }
146 
147         List<TestEntity> testEntityList = new ArrayList<>();
148         List<TestRunEntity> testRunEntityList = new ArrayList<>();
149         List<BranchEntity> branchEntityList = new ArrayList<>();
150         List<BuildTargetEntity> buildTargetEntityList = new ArrayList<>();
151         List<CoverageEntity> coverageEntityList = new ArrayList<>();
152         List<CodeCoverageEntity> codeCoverageEntityList = new ArrayList<>();
153         List<DeviceInfoEntity> deviceInfoEntityList = new ArrayList<>();
154         List<ProfilingPointRunEntity> profilingPointRunEntityList = new ArrayList<>();
155         List<TestCaseRunEntity> testCaseRunEntityList = new ArrayList<>();
156         List<ApiCoverageEntity> apiCoverageEntityList = new ArrayList<>();
157 
158         List<?> allEntityList =
159                 Arrays.asList(
160                         testEntityList,
161                         branchEntityList,
162                         buildTargetEntityList,
163                         coverageEntityList,
164                         codeCoverageEntityList,
165                         deviceInfoEntityList,
166                         profilingPointRunEntityList,
167                         testCaseRunEntityList,
168                         apiCoverageEntityList,
169                         testRunEntityList);
170 
171         long passCount = 0;
172         long failCount = 0;
173         long coveredLineCount = 0;
174         long totalLineCount = 0;
175 
176         Set<Key> buildTargetKeys = new HashSet<>();
177         Set<Key> branchKeys = new HashSet<>();
178         List<Key> profilingPointKeyList = new ArrayList<>();
179         List<String> linkList = new ArrayList<>();
180 
181         long startTimestamp = report.getStartTimestamp();
182         long endTimestamp = report.getEndTimestamp();
183         String testName = report.getTest().toStringUtf8();
184         String testBuildId = report.getBuildInfo().getId().toStringUtf8();
185         String hostName = report.getHostInfo().getHostname().toStringUtf8();
186 
187         TestEntity testEntity = new TestEntity(testName);
188 
189         com.googlecode.objectify.Key testRunKey =
190                 testEntity.getTestRunKey(report.getStartTimestamp());
191 
192         testEntityList.add(testEntity);
193 
194         int testCaseRunEntityIndex = 0;
195         testCaseRunEntityList.add(new TestCaseRunEntity());
196         // Process test cases
197         for (VtsReportMessage.TestCaseReportMessage testCase : report.getTestCaseList()) {
198             String testCaseName = testCase.getName().toStringUtf8();
199             VtsReportMessage.TestCaseResult result = testCase.getTestResult();
200             // Track global pass/fail counts
201             if (result == VtsReportMessage.TestCaseResult.TEST_CASE_RESULT_PASS) {
202                 ++passCount;
203             } else if (result != VtsReportMessage.TestCaseResult.TEST_CASE_RESULT_SKIP) {
204                 ++failCount;
205             }
206             if (testCase.getSystraceCount() > 0
207                     && testCase.getSystraceList().get(0).getUrlCount() > 0) {
208                 String systraceLink = testCase.getSystraceList().get(0).getUrl(0).toStringUtf8();
209                 linkList.add(systraceLink);
210             }
211 
212             // Process coverage data for test case
213             for (VtsReportMessage.CoverageReportMessage coverage : testCase.getCoverageList()) {
214                 CoverageEntity coverageEntity =
215                         CoverageEntity.fromCoverageReport(testRunKey, testCaseName, coverage);
216                 if (coverageEntity == null) {
217                     log.warn("Invalid coverage report in test run " + testRunKey);
218                 } else {
219                     coveredLineCount += coverageEntity.getCoveredCount();
220                     totalLineCount += coverageEntity.getTotalCount();
221                     coverageEntityList.add(coverageEntity);
222                 }
223             }
224 
225             // Process profiling data for test case
226             for (VtsReportMessage.ProfilingReportMessage profiling : testCase.getProfilingList()) {
227                 ProfilingPointRunEntity profilingPointRunEntity =
228                         ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling);
229                 if (profilingPointRunEntity == null) {
230                     log.warn("Invalid profiling report in test run " + testRunKey);
231                 } else {
232                     profilingPointRunEntityList.add(profilingPointRunEntity);
233                     profilingPointKeyList.add(profilingPointRunEntity.getKey());
234                     testEntity.setHasProfilingData(true);
235                 }
236             }
237 
238             TestCaseRunEntity testCaseRunEntity = testCaseRunEntityList.get(testCaseRunEntityIndex);
239             if (!testCaseRunEntity.addTestCase(testCaseName, result.getNumber())) {
240                 testCaseRunEntity = new TestCaseRunEntity();
241                 testCaseRunEntity.addTestCase(testCaseName, result.getNumber());
242                 testCaseRunEntityList.add(testCaseRunEntity);
243                 testCaseRunEntityIndex++;
244             }
245         }
246 
247         // Process device information
248         long testRunType = 0;
249         for (VtsReportMessage.AndroidDeviceInfoMessage device : report.getDeviceInfoList()) {
250             DeviceInfoEntity deviceInfoEntity =
251                     DeviceInfoEntity.fromDeviceInfoMessage(testRunKey, device);
252             if (deviceInfoEntity == null) {
253                 log.warn("Invalid device info in test run " + testRunKey);
254             } else {
255                 // Run type on devices must be the same, else set to OTHER
256                 TestRunEntity.TestRunType runType =
257                         TestRunEntity.TestRunType.fromBuildId(deviceInfoEntity.getBuildId());
258                 if (runType == null) {
259                     testRunType = TestRunEntity.TestRunType.OTHER.getNumber();
260                 } else {
261                     testRunType = runType.getNumber();
262                 }
263                 deviceInfoEntityList.add(deviceInfoEntity);
264                 BuildTargetEntity target = new BuildTargetEntity(deviceInfoEntity.getBuildFlavor());
265                 if (buildTargetKeys.add(target.getKey())) {
266                     buildTargetEntityList.add(target);
267                 }
268                 BranchEntity branch = new BranchEntity(deviceInfoEntity.getBranch());
269                 if (branchKeys.add(branch.getKey())) {
270                     branchEntityList.add(branch);
271                 }
272             }
273         }
274 
275         // Overall run type should be determined by the device builds unless test build is OTHER
276         if (testRunType == TestRunEntity.TestRunType.OTHER.getNumber()) {
277             testRunType = TestRunEntity.TestRunType.fromBuildId(testBuildId).getNumber();
278         } else if (TestRunEntity.TestRunType.fromBuildId(testBuildId)
279                 == TestRunEntity.TestRunType.OTHER) {
280             testRunType = TestRunEntity.TestRunType.OTHER.getNumber();
281         }
282 
283         // Process global coverage data
284         for (VtsReportMessage.CoverageReportMessage coverage : report.getCoverageList()) {
285             CoverageEntity coverageEntity =
286                     CoverageEntity.fromCoverageReport(testRunKey, new String(), coverage);
287             if (coverageEntity == null) {
288                 log.warn("Invalid coverage report in test run " + testRunKey);
289             } else {
290                 coveredLineCount += coverageEntity.getCoveredCount();
291                 totalLineCount += coverageEntity.getTotalCount();
292                 coverageEntityList.add(coverageEntity);
293             }
294         }
295 
296         // Process global API coverage data
297         for (VtsReportMessage.ApiCoverageReportMessage apiCoverage : report.getApiCoverageList()) {
298             VtsReportMessage.HalInterfaceMessage halInterfaceMessage =
299                     apiCoverage.getHalInterface();
300             List<String> halApiList =
301                     apiCoverage
302                             .getHalApiList()
303                             .stream()
304                             .map(h -> h.toStringUtf8())
305                             .collect(Collectors.toList());
306             List<String> coveredHalApiList =
307                     apiCoverage
308                             .getCoveredHalApiList()
309                             .stream()
310                             .map(h -> h.toStringUtf8())
311                             .collect(Collectors.toList());
312             ApiCoverageEntity apiCoverageEntity =
313                     new ApiCoverageEntity(
314                             testRunKey,
315                             halInterfaceMessage.getHalPackageName().toStringUtf8(),
316                             halInterfaceMessage.getHalVersionMajor(),
317                             halInterfaceMessage.getHalVersionMinor(),
318                             halInterfaceMessage.getHalInterfaceName().toStringUtf8(),
319                             halApiList,
320                             coveredHalApiList);
321             apiCoverageEntityList.add(apiCoverageEntity);
322         }
323 
324         // Process global profiling data
325         for (VtsReportMessage.ProfilingReportMessage profiling : report.getProfilingList()) {
326             ProfilingPointRunEntity profilingPointRunEntity =
327                     ProfilingPointRunEntity.fromProfilingReport(testRunKey, profiling);
328             if (profilingPointRunEntity == null) {
329                 log.warn("Invalid profiling report in test run " + testRunKey);
330             } else {
331                 profilingPointRunEntityList.add(profilingPointRunEntity);
332                 profilingPointKeyList.add(profilingPointRunEntity.getKey());
333                 testEntity.setHasProfilingData(true);
334             }
335         }
336 
337         // Process log data
338         for (VtsReportMessage.LogMessage log : report.getLogList()) {
339             if (log.hasUrl()) {
340                 linkList.add(log.getUrl().toStringUtf8());
341             }
342         }
343         // Process url resource
344         for (VtsReportMessage.UrlResourceMessage resource : report.getLinkResourceList()) {
345             if (resource.hasUrl()) {
346                 linkList.add(resource.getUrl().toStringUtf8());
347             }
348         }
349 
350         boolean hasCodeCoverage = totalLineCount > 0 && coveredLineCount >= 0;
351         TestRunEntity testRunEntity =
352                 new TestRunEntity(
353                         testEntity.getOldKey(),
354                         testRunType,
355                         startTimestamp,
356                         endTimestamp,
357                         testBuildId,
358                         hostName,
359                         passCount,
360                         failCount,
361                         hasCodeCoverage,
362                         new ArrayList<>(),
363                         linkList);
364         testRunEntityList.add(testRunEntity);
365 
366         CodeCoverageEntity codeCoverageEntity =
367                 new CodeCoverageEntity(
368                         testRunEntity.getId(),
369                         testRunEntity.getKey(),
370                         coveredLineCount,
371                         totalLineCount);
372         codeCoverageEntityList.add(codeCoverageEntity);
373 
374         ofy().transact(
375                         () -> {
376                             List<Long> testCaseIds = new ArrayList<>();
377                             for (Object entity : allEntityList) {
378                                 if (entity instanceof List) {
379                                     List listEntity = (List) entity;
380                                     if (listEntity.size() > 0
381                                             && listEntity.get(0) instanceof TestCaseRunEntity) {
382                                         Map<
383                                                         com.googlecode.objectify.Key<
384                                                                 TestCaseRunEntity>,
385                                                         TestCaseRunEntity>
386                                                 testCaseRunEntityMap =
387                                                         DashboardEntity.saveAll(
388                                                                 testCaseRunEntityList,
389                                                                 this
390                                                                         .MAX_ENTITY_SIZE_PER_TRANSACTION);
391 
392                                         testCaseIds =
393                                                 testCaseRunEntityMap
394                                                         .values()
395                                                         .stream()
396                                                         .map(
397                                                                 testCaseRunEntity ->
398                                                                         testCaseRunEntity.getId())
399                                                         .collect(Collectors.toList());
400                                     } else if (listEntity.size() > 0
401                                             && listEntity.get(0) instanceof TestRunEntity) {
402                                         testRunEntityList.get(0).setTestCaseIds(testCaseIds);
403                                         DashboardEntity.saveAll(
404                                                 testRunEntityList,
405                                                 this.MAX_ENTITY_SIZE_PER_TRANSACTION);
406                                     } else {
407                                         List<DashboardEntity> dashboardEntityList =
408                                                 (List<DashboardEntity>) entity;
409                                         DashboardEntity.saveAll(
410                                                 dashboardEntityList,
411                                                 this.MAX_ENTITY_SIZE_PER_TRANSACTION);
412                                     }
413                                 }
414                             }
415                         });
416     }
417 
418     /**
419      * Upload data from a test plan report message
420      *
421      * @param report The test plan report containing data to upload.
422      */
insertTestPlanReport(TestPlanReportMessage report)423     private void insertTestPlanReport(TestPlanReportMessage report) {
424         List<DeviceInfoEntity> deviceInfoEntityList = new ArrayList<>();
425         List<HalApiEntity> halApiEntityList = new ArrayList<>();
426 
427         List allEntityList = Arrays.asList(deviceInfoEntityList, halApiEntityList);
428 
429         List<String> testModules = report.getTestModuleNameList();
430         List<Long> testTimes = report.getTestModuleStartTimestampList();
431         if (testModules.size() != testTimes.size() || !report.hasTestPlanName()) {
432             log.error("TestPlanReportMessage is missing information.");
433             return;
434         }
435 
436         String testPlanName = report.getTestPlanName();
437         TestPlanEntity testPlanEntity = new TestPlanEntity(testPlanName);
438         List<com.googlecode.objectify.Key<TestRunEntity>> testRunKeyList = new ArrayList<>();
439         for (int index = 0; index < testModules.size(); index++) {
440             String test = testModules.get(index);
441             long time = testTimes.get(index);
442             com.googlecode.objectify.Key testKey =
443                     com.googlecode.objectify.Key.create(TestEntity.class, test);
444             com.googlecode.objectify.Key testRunKey =
445                     com.googlecode.objectify.Key.create(testKey, TestRunEntity.class, time);
446             testRunKeyList.add(testRunKey);
447         }
448 
449         Map<com.googlecode.objectify.Key<TestRunEntity>, TestRunEntity> testRunEntityMap =
450                 ofy().load().keys(() -> testRunKeyList.iterator());
451 
452         long passCount = 0;
453         long failCount = 0;
454         long startTimestamp = -1;
455         long endTimestamp = -1;
456         String testBuildId = null;
457         long testType = -1;
458         Set<DeviceInfoEntity> deviceInfoEntitySet = new HashSet<>();
459         for (TestRunEntity testRunEntity : testRunEntityMap.values()) {
460             passCount += testRunEntity.getPassCount();
461             failCount += testRunEntity.getFailCount();
462             if (startTimestamp < 0 || testRunEntity.getStartTimestamp() < startTimestamp) {
463                 startTimestamp = testRunEntity.getStartTimestamp();
464             }
465             if (endTimestamp < 0 || testRunEntity.getEndTimestamp() > endTimestamp) {
466                 endTimestamp = testRunEntity.getEndTimestamp();
467             }
468             testType = testRunEntity.getType();
469             testBuildId = testRunEntity.getTestBuildId();
470 
471             List<DeviceInfoEntity> deviceInfoEntityListWithTestRunKey =
472                     ofy().load()
473                             .type(DeviceInfoEntity.class)
474                             .ancestor(testRunEntity.getOfyKey())
475                             .list();
476 
477             for (DeviceInfoEntity deviceInfoEntity : deviceInfoEntityListWithTestRunKey) {
478                 deviceInfoEntitySet.add(deviceInfoEntity);
479             }
480         }
481 
482         if (startTimestamp < 0 || testBuildId == null || testType == -1) {
483             log.debug("startTimestamp => " + startTimestamp);
484             log.debug("testBuildId => " + testBuildId);
485             log.debug("type => " + testType);
486             log.error("Couldn't infer test run information from runs.");
487             return;
488         }
489 
490         TestPlanRunEntity testPlanRunEntity =
491                 new TestPlanRunEntity(
492                         testPlanEntity.getKey(),
493                         testPlanName,
494                         testType,
495                         startTimestamp,
496                         endTimestamp,
497                         testBuildId,
498                         passCount,
499                         failCount,
500                         0L,
501                         0L,
502                         testRunKeyList);
503 
504         // Create the device infos.
505         for (DeviceInfoEntity device : deviceInfoEntitySet) {
506             deviceInfoEntityList.add(device.copyWithParent(testPlanRunEntity.getOfyKey()));
507         }
508 
509         // Process global HAL API coverage data
510         for (VtsReportMessage.ApiCoverageReportMessage apiCoverage : report.getHalApiReportList()) {
511             VtsReportMessage.HalInterfaceMessage halInterfaceMessage =
512                     apiCoverage.getHalInterface();
513             List<String> halApiList =
514                     apiCoverage
515                             .getHalApiList()
516                             .stream()
517                             .map(h -> h.toStringUtf8())
518                             .collect(Collectors.toList());
519             List<String> coveredHalApiList =
520                     apiCoverage
521                             .getCoveredHalApiList()
522                             .stream()
523                             .map(h -> h.toStringUtf8())
524                             .collect(Collectors.toList());
525             HalApiEntity halApiEntity =
526                     new HalApiEntity(
527                             testPlanRunEntity.getOfyKey(),
528                             halInterfaceMessage.getHalReleaseLevel().toStringUtf8(),
529                             halInterfaceMessage.getHalPackageName().toStringUtf8(),
530                             halInterfaceMessage.getHalVersionMajor(),
531                             halInterfaceMessage.getHalVersionMinor(),
532                             halInterfaceMessage.getHalInterfaceName().toStringUtf8(),
533                             halApiList,
534                             coveredHalApiList);
535             halApiEntityList.add(halApiEntity);
536         }
537 
538         ofy().transact(
539                         () -> {
540                             testPlanEntity.save();
541                             testPlanRunEntity.save();
542                             for (Object entity : allEntityList) {
543                                 List<DashboardEntity> dashboardEntityList =
544                                         (List<DashboardEntity>) entity;
545                                 Map<com.googlecode.objectify.Key<DashboardEntity>, DashboardEntity>
546                                         mapInfo =
547                                                 DashboardEntity.saveAll(
548                                                         dashboardEntityList,
549                                                         this.MAX_ENTITY_SIZE_PER_TRANSACTION);
550                             }
551                         });
552 
553         // Add the task to calculate total number API list.
554         testPlanRunEntity.addCoverageApiTask();
555     }
556 }
557