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