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