1 /*
2  * Copyright (C) 2023 The Android Open Source Project
3  *
4  * Licensed under the Apache License, Version 2.0 (the "License");
5  * you may not use this file except in compliance with the License.
6  * You may 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 implied.
13  * See the License for the specific language governing permissions and
14  * limitations under the License.
15  */
16 
17 package android.healthconnect.cts.database;
18 
19 import com.android.ddmlib.testrunner.RemoteAndroidTestRunner;
20 import com.android.tradefed.device.DeviceNotAvailableException;
21 import com.android.tradefed.device.ITestDevice;
22 import com.android.tradefed.log.LogUtil;
23 import com.android.tradefed.result.CollectingTestListener;
24 import com.android.tradefed.result.TestDescription;
25 import com.android.tradefed.result.TestResult;
26 import com.android.tradefed.result.TestRunResult;
27 import com.android.tradefed.util.FileUtil;
28 
29 import java.io.BufferedOutputStream;
30 import java.io.File;
31 import java.io.FileOutputStream;
32 import java.io.IOException;
33 import java.io.InputStream;
34 import java.io.OutputStream;
35 import java.rmi.RemoteException;
36 import java.util.HashMap;
37 import java.util.List;
38 import java.util.Map;
39 import java.util.concurrent.TimeUnit;
40 
41 public class DatabaseTestUtils {
42     public static final String INSTALL_ARG_FORCE_QUERYABLE = "--force-queryable";
43 
44     public static final String HC_APEX_RESOURCE_PATH_PREFIX = "/HealthConnectApexFiles/";
45     public static final String HC_CTS_RESOURCE_PATH_PREFIX = "/HealthConnectCtsApkFiles/";
46     public static final String TEST_RUNNER = "androidx.test.runner.AndroidJUnitRunner";
47 
getCurrentHcDatabaseVersion(ITestDevice device)48     public static int getCurrentHcDatabaseVersion(ITestDevice device)
49             throws DeviceNotAvailableException {
50         String currentDbVersion =
51                 device.executeShellCommand(
52                         "cd ~; sqlite3 data/system_ce/0/healthconnect/healthconnect.db"
53                                 + " \"PRAGMA user_version;\"");
54         // To remove any extra white spaces on the sides.
55         currentDbVersion = currentDbVersion.strip();
56         LogUtil.CLog.d("Current Database version  " + currentDbVersion);
57         return Integer.parseInt(currentDbVersion);
58     }
59 
deleteHcDatabase(ITestDevice device)60     public static void deleteHcDatabase(ITestDevice device) throws DeviceNotAvailableException {
61         String result =
62                 device.executeShellCommand(
63                         "cd ~; rm /data/system_ce/0/healthconnect/healthconnect.db;");
64         // Deleted successfully.
65         if (!result.isBlank()) {
66             throw new IllegalArgumentException("Failed to remove healthconnect.db : " + result);
67         }
68     }
69 
70     /**
71      * Installs package using the packageFilename in Resources.
72      *
73      * <p>Since this method can be used for both HC apex files and CTS apk files, pass true in
74      * {@code isHcApex} to notify that the resource to be installed is an apex otherwise pass false.
75      */
assertInstallSucceeds( ITestDevice device, String packageFilenameInResources, boolean isHcApex)76     public static void assertInstallSucceeds(
77             ITestDevice device, String packageFilenameInResources, boolean isHcApex)
78             throws Exception {
79         String installResult =
80                 installPackageFromResource(device, packageFilenameInResources, isHcApex);
81         if (installResult != null) {
82             throw new IllegalArgumentException(
83                     "Failed to install " + packageFilenameInResources + ": " + installResult);
84         }
85     }
86 
assertUninstallSucceeds(ITestDevice device, String packageName)87     public static void assertUninstallSucceeds(ITestDevice device, String packageName)
88             throws DeviceNotAvailableException {
89         String uninstallResult = device.uninstallPackage(packageName);
90         if (uninstallResult != null) {
91             throw new IllegalArgumentException(
92                     "Failed to install " + uninstallResult + ": " + uninstallResult);
93         }
94     }
95 
96     /** Fetches the package from resources and installs it for the current user. */
installPackageFromResource( ITestDevice device, String apkFilenameInResources, boolean isHcApex)97     public static String installPackageFromResource(
98             ITestDevice device, String apkFilenameInResources, boolean isHcApex)
99             throws IOException {
100         // ITestDevice.installPackage API requires the APK to be installed to be a File. We thus
101         // copy the requested resource into a temporary file, attempt to install it, and delete the
102         // file during cleanup.
103         File apkFile = null;
104         try {
105             apkFile = getFileFromResource(apkFilenameInResources, isHcApex);
106             // Install package for current user.
107             return device.installPackageForUser(
108                     apkFile, true, device.getCurrentUser(), INSTALL_ARG_FORCE_QUERYABLE);
109         } catch (DeviceNotAvailableException e) {
110             throw new RemoteException("Device is not available, please connect a device.", e);
111         } finally {
112             cleanUpFile(apkFile);
113         }
114     }
115 
getFileFromResource(String filenameInResources, boolean isHcApex)116     public static File getFileFromResource(String filenameInResources, boolean isHcApex)
117             throws IOException, IllegalArgumentException {
118         final String fullResourceName;
119         if (isHcApex) {
120             fullResourceName = HC_APEX_RESOURCE_PATH_PREFIX + filenameInResources;
121         } else {
122             fullResourceName = HC_CTS_RESOURCE_PATH_PREFIX + filenameInResources;
123         }
124         File tempDir = FileUtil.createTempDir("HcHostSideTests");
125         File file = new File(tempDir, filenameInResources);
126         InputStream in = DatabaseTestUtils.class.getResourceAsStream(fullResourceName);
127         if (in == null) {
128             throw new IllegalArgumentException("Resource not found: " + fullResourceName);
129         }
130         OutputStream out = new BufferedOutputStream(new FileOutputStream(file));
131         byte[] buf = new byte[65536];
132         int chunkSize;
133         while ((chunkSize = in.read(buf)) != -1) {
134             out.write(buf, 0, chunkSize);
135         }
136         out.close();
137         return file;
138     }
139 
cleanUpFile(File file)140     static void cleanUpFile(File file) {
141         if (file != null && file.exists()) {
142             file.delete();
143         }
144     }
145 
146     /**
147      * Run a device side test.
148      *
149      * @param pkgName Test package name, such as "android.healthconnect.cts".
150      * @param testClassName Test class name; either a fully qualified name, or "." + a class name.
151      * @param testMethodName Test method name.
152      */
runDeviceTests( ITestDevice device, String pkgName, String testClassName, String testMethodName)153     public static String runDeviceTests(
154             ITestDevice device, String pkgName, String testClassName, String testMethodName)
155             throws DeviceNotAvailableException {
156         if (testClassName != null && testClassName.startsWith(".")) {
157             testClassName = pkgName + testClassName;
158         }
159 
160         RemoteAndroidTestRunner testRunner =
161                 new RemoteAndroidTestRunner(pkgName, TEST_RUNNER, device.getIDevice());
162         testRunner.setMaxTimeout(1800, TimeUnit.SECONDS);
163         if (testClassName != null && testMethodName != null) {
164             testRunner.setMethodName(testClassName, testMethodName);
165         } else if (testClassName != null) {
166             testRunner.setClassName(testClassName);
167         }
168 
169         CollectingTestListener listener = new CollectingTestListener();
170         assert (device.runInstrumentationTests(testRunner, listener));
171 
172         final TestRunResult result = listener.getCurrentRunResults();
173         if (result.isRunFailure()) {
174             throw new AssertionError(
175                     "Failed to successfully run device tests for "
176                             + result.getName()
177                             + ": "
178                             + result.getRunFailureMessage());
179         }
180         if (result.getNumTests() == 0) {
181             throw new AssertionError("No tests were run on the device");
182         }
183 
184         if (result.hasFailedTests()) {
185             // build a meaningful error message
186             StringBuilder errorBuilder = new StringBuilder("On-device tests failed:\n");
187             for (Map.Entry<TestDescription, TestResult> resultEntry :
188                     result.getTestResults().entrySet()) {
189                 if (!resultEntry
190                         .getValue()
191                         .getStatus()
192                         .equals(com.android.ddmlib.testrunner.TestResult.TestStatus.PASSED)) {
193                     errorBuilder.append(resultEntry.getKey().toString());
194                     errorBuilder.append(":\n");
195                     errorBuilder.append(resultEntry.getValue().getStackTrace());
196                 }
197             }
198             throw new AssertionError(errorBuilder.toString());
199         }
200         return result.getTextSummary();
201     }
202 
203     /**
204      * Checks the deletion of tables of the previous version of the database in the current version
205      * of the database.
206      */
checkExistingTableDeletion( HashMap<String, TableInfo> mTableListPreviousVersion, HashMap<String, TableInfo> mTableListCurrentVersion, List<String> deletionOfTable)207     public static void checkExistingTableDeletion(
208             HashMap<String, TableInfo> mTableListPreviousVersion,
209             HashMap<String, TableInfo> mTableListCurrentVersion,
210             List<String> deletionOfTable) {
211 
212         for (String tableName : mTableListPreviousVersion.keySet()) {
213 
214             if (!mTableListCurrentVersion.containsKey(tableName)) {
215                 deletionOfTable.add("Table: " + tableName + " has been deleted from the database");
216             }
217         }
218     }
219 
220     /**
221      * Checks for the modifications in the primary keys of the database between previous and current
222      * version.
223      */
checkPrimaryKeyModification( HashMap<String, TableInfo> mTableListPreviousVersion, HashMap<String, TableInfo> mTableListCurrentVersion, List<String> modificationOfPrimaryKey)224     public static void checkPrimaryKeyModification(
225             HashMap<String, TableInfo> mTableListPreviousVersion,
226             HashMap<String, TableInfo> mTableListCurrentVersion,
227             List<String> modificationOfPrimaryKey) {
228 
229         for (String tableName : mTableListPreviousVersion.keySet()) {
230 
231             if (mTableListCurrentVersion.containsKey(tableName)) {
232 
233                 List<String> primaryKeyPreviousVersion =
234                         mTableListPreviousVersion.get(tableName).getPrimaryKey();
235                 List<String> primaryKeyCurrentVersion =
236                         mTableListCurrentVersion.get(tableName).getPrimaryKey();
237 
238                 for (String pk : primaryKeyPreviousVersion) {
239                     if (!primaryKeyCurrentVersion.contains(pk)) {
240                         modificationOfPrimaryKey.add(
241                                 "Primary key column: "
242                                         + pk
243                                         + " has been deleted from the table: "
244                                         + tableName);
245                     }
246                 }
247 
248                 for (String pk : primaryKeyCurrentVersion) {
249                     if (!primaryKeyPreviousVersion.contains(pk)) {
250                         modificationOfPrimaryKey.add(
251                                 "Primary key column: "
252                                         + pk
253                                         + " has been added to the table: "
254                                         + tableName);
255                     }
256                 }
257             }
258         }
259     }
260 
261     /**
262      * Checks for the modifications in the columns of each table of the database between previous
263      * and current version.
264      */
checkColumnModification( HashMap<String, TableInfo> mTableListPreviousVersion, HashMap<String, TableInfo> mTableListCurrentVersion, List<String> modificationOfColumn)265     public static void checkColumnModification(
266             HashMap<String, TableInfo> mTableListPreviousVersion,
267             HashMap<String, TableInfo> mTableListCurrentVersion,
268             List<String> modificationOfColumn) {
269 
270         for (String tableName : mTableListPreviousVersion.keySet()) {
271 
272             if (mTableListCurrentVersion.containsKey(tableName)) {
273 
274                 HashMap<String, ColumnInfo> columnInfoPreviousVersion =
275                         mTableListPreviousVersion.get(tableName).getColumnInfoMapping();
276                 HashMap<String, ColumnInfo> columnInfoCurrentVersion =
277                         mTableListCurrentVersion.get(tableName).getColumnInfoMapping();
278 
279                 for (String columnName : columnInfoPreviousVersion.keySet()) {
280                     ColumnInfo column1 = columnInfoPreviousVersion.get(columnName);
281 
282                     if (columnInfoCurrentVersion.containsKey(columnName)) {
283                         ColumnInfo column2 = columnInfoCurrentVersion.get(columnName);
284                         column1.checkColumnDiff(column2, modificationOfColumn, tableName);
285                     } else {
286                         modificationOfColumn.add(
287                                 "Column: "
288                                         + columnName
289                                         + " has been deleted from the table: "
290                                         + tableName);
291                     }
292                 }
293 
294                 for (String columnName : columnInfoCurrentVersion.keySet()) {
295 
296                     if (!columnInfoPreviousVersion.containsKey(columnName)) {
297                         ColumnInfo columnInfo = columnInfoCurrentVersion.get(columnName);
298 
299                         if (columnInfo.getConstraints().contains(ColumnInfo.UNIQUE_CONSTRAINT)) {
300                             modificationOfColumn.add(
301                                     "UNIQUE constraint is not allowed for the new column: "
302                                             + columnName
303                                             + " of table: "
304                                             + tableName);
305                         }
306 
307                         if (columnInfo.getConstraints().contains(ColumnInfo.NOT_NULL_CONSTRAINT)) {
308                             modificationOfColumn.add(
309                                     "NOT NULL constraint is not allowed for the new column: "
310                                             + columnName
311                                             + " of table: "
312                                             + tableName);
313                         }
314 
315                         if (columnInfo
316                                 .getConstraints()
317                                 .contains(ColumnInfo.AUTO_INCREMENT_CONSTRAINT)) {
318                             modificationOfColumn.add(
319                                     "AUTOINCREMENT constraint is not allowed for the new column: "
320                                             + columnName
321                                             + " of table: "
322                                             + tableName);
323                         }
324 
325                         if (!columnInfo.getCheckConstraints().isEmpty()) {
326                             modificationOfColumn.add(
327                                     "Check constraints are not allowed for the new column: "
328                                             + columnName
329                                             + " of table: "
330                                             + tableName);
331                         }
332                     }
333                 }
334             }
335         }
336     }
337 
338     /**
339      * Checks for the modifications in the foreign keys of each table of the database between
340      * previous and current version.
341      */
checkForeignKeyModification( HashMap<String, TableInfo> mTableListPreviousVersion, HashMap<String, TableInfo> mTableListCurrentVersion, List<String> modificationOfForeignKey)342     public static void checkForeignKeyModification(
343             HashMap<String, TableInfo> mTableListPreviousVersion,
344             HashMap<String, TableInfo> mTableListCurrentVersion,
345             List<String> modificationOfForeignKey) {
346 
347         for (String tableName : mTableListPreviousVersion.keySet()) {
348 
349             if (mTableListCurrentVersion.containsKey(tableName)) {
350 
351                 HashMap<String, ForeignKeyInfo> foreignKeyListPreviousVersion =
352                         mTableListPreviousVersion.get(tableName).getForeignKeyMapping();
353                 HashMap<String, ForeignKeyInfo> foreignKeyListCurrentVersion =
354                         mTableListCurrentVersion.get(tableName).getForeignKeyMapping();
355 
356                 for (String foreignKeyName : foreignKeyListPreviousVersion.keySet()) {
357 
358                     if (foreignKeyListCurrentVersion.containsKey(foreignKeyName)) {
359 
360                         ForeignKeyInfo foreignInfo1 =
361                                 foreignKeyListPreviousVersion.get(foreignKeyName);
362                         ForeignKeyInfo foreignInfo2 =
363                                 foreignKeyListCurrentVersion.get(foreignKeyName);
364 
365                         foreignInfo1.checkForeignKeyDiff(
366                                 foreignInfo2, modificationOfForeignKey, tableName);
367                     } else {
368                         modificationOfForeignKey.add(
369                                 "Foreign Key: "
370                                         + foreignKeyName
371                                         + " has been deleted from the table: "
372                                         + tableName);
373                     }
374                 }
375 
376                 for (String foreignKeyName : foreignKeyListCurrentVersion.keySet()) {
377 
378                     if (!foreignKeyListPreviousVersion.containsKey(foreignKeyName)) {
379 
380                         ForeignKeyInfo foreignKeyInfo =
381                                 foreignKeyListCurrentVersion.get(foreignKeyName);
382                         String referTableName = foreignKeyInfo.getForeignKeyTableName();
383                         List<Integer> constraintListOfReferencedColumn =
384                                 mTableListCurrentVersion
385                                         .get(referTableName)
386                                         .getColumnInfoMapping()
387                                         .get(foreignKeyInfo.getForeignKeyReferredColumnName())
388                                         .getConstraints();
389                         if (!mTableListCurrentVersion
390                                         .get(referTableName)
391                                         .getPrimaryKey()
392                                         .contains(foreignKeyInfo.getForeignKeyReferredColumnName())
393                                 && !constraintListOfReferencedColumn.contains(
394                                         ColumnInfo.UNIQUE_CONSTRAINT)) {
395                             modificationOfForeignKey.add(
396                                     "New Foreign key : "
397                                             + foreignKeyName
398                                             + " of  table: "
399                                             + tableName
400                                             + " has neither been made on primary key of "
401                                             + "referenced table: "
402                                             + referTableName
403                                             + " nor UNIQUE constraint ");
404                         }
405                     }
406                 }
407             }
408         }
409     }
410 
411     /**
412      * Checks for the modifications in the indexes of each table of the database between previous
413      * and current version.
414      */
checkIndexModification( HashMap<String, TableInfo> mTableListPreviousVersion, HashMap<String, TableInfo> mTableListCurrentVersion, List<String> modificationOfIndex)415     public static void checkIndexModification(
416             HashMap<String, TableInfo> mTableListPreviousVersion,
417             HashMap<String, TableInfo> mTableListCurrentVersion,
418             List<String> modificationOfIndex) {
419 
420         for (String tableName : mTableListPreviousVersion.keySet()) {
421 
422             if (mTableListCurrentVersion.containsKey(tableName)) {
423 
424                 HashMap<String, IndexInfo> indexInfoPreviousVersion =
425                         mTableListPreviousVersion.get(tableName).getIndexInfoMapping();
426                 HashMap<String, IndexInfo> indexInfoCurrentVersion =
427                         mTableListCurrentVersion.get(tableName).getIndexInfoMapping();
428 
429                 for (String indexName : indexInfoPreviousVersion.keySet()) {
430                     IndexInfo index1 = indexInfoPreviousVersion.get(indexName);
431 
432                     if (indexInfoCurrentVersion.containsKey(indexName)) {
433 
434                         IndexInfo index2 = indexInfoCurrentVersion.get(indexName);
435                         index1.checkIndexDiff(index2, modificationOfIndex, tableName);
436                     } else {
437                         modificationOfIndex.add(
438                                 "Index : "
439                                         + indexName
440                                         + " has been deleted from table "
441                                         + tableName);
442                     }
443                 }
444             }
445         }
446     }
447 
448     /**
449      * Checks for the addition of new tables in the current version of the database.
450      *
451      * <p>The only way by which a new table can interact with older ones is with the help of foreign
452      * key So, we need a check to ensure that the table to which the foreign key is being mapped is
453      * primary key of that table.
454      */
checkNewTableAddition( HashMap<String, TableInfo> mTableListPreviousVersion, HashMap<String, TableInfo> mTableListCurrentVersion, List<String> additionOfTable)455     public static void checkNewTableAddition(
456             HashMap<String, TableInfo> mTableListPreviousVersion,
457             HashMap<String, TableInfo> mTableListCurrentVersion,
458             List<String> additionOfTable) {
459 
460         for (String tableName : mTableListCurrentVersion.keySet()) {
461 
462             if (!mTableListPreviousVersion.containsKey(tableName)) {
463 
464                 HashMap<String, ForeignKeyInfo> foreignKeyList =
465                         mTableListCurrentVersion.get(tableName).getForeignKeyMapping();
466 
467                 for (String foreignKeyName : foreignKeyList.keySet()) {
468 
469                     ForeignKeyInfo foreignKeyInfo = foreignKeyList.get(foreignKeyName);
470                     String referTableName = foreignKeyInfo.getForeignKeyTableName();
471                     List<Integer> constraintListOfReferencedColumn =
472                             mTableListCurrentVersion
473                                     .get(referTableName)
474                                     .getColumnInfoMapping()
475                                     .get(foreignKeyInfo.getForeignKeyReferredColumnName())
476                                     .getConstraints();
477                     /**
478                      * Checking whether the column to which foreign key has been mapped is primary
479                      * key of the referenced table or not.
480                      */
481                     if (!mTableListCurrentVersion
482                                     .get(referTableName)
483                                     .getPrimaryKey()
484                                     .contains(foreignKeyInfo.getForeignKeyReferredColumnName())
485                             && !constraintListOfReferencedColumn.contains(
486                                     ColumnInfo.UNIQUE_CONSTRAINT)) {
487                         additionOfTable.add(
488                                 "Foreign key : "
489                                         + foreignKeyName
490                                         + " of new table: "
491                                         + tableName
492                                         + " has neither been made on primary key of referenced"
493                                         + " table: "
494                                         + referTableName
495                                         + " nor UNIQUE constraint ");
496                     }
497                 }
498             }
499         }
500     }
501 
502     /**
503      * @return true if apex version file is present in resources otherwise false.
504      */
isFilePresentInResources(String filenameInResources)505     public static boolean isFilePresentInResources(String filenameInResources) {
506         final String fullResourceName = HC_APEX_RESOURCE_PATH_PREFIX + filenameInResources;
507         InputStream in = DatabaseTestUtils.class.getResourceAsStream(fullResourceName);
508         return in != null;
509     }
510 }
511 
512