summaryrefslogtreecommitdiff
path: root/adservices/tests/unittest/service-core/src/com/android/adservices/data/RoomSchemaMigrationGuardrailTest.java
diff options
context:
space:
mode:
Diffstat (limited to 'adservices/tests/unittest/service-core/src/com/android/adservices/data/RoomSchemaMigrationGuardrailTest.java')
-rw-r--r--adservices/tests/unittest/service-core/src/com/android/adservices/data/RoomSchemaMigrationGuardrailTest.java181
1 files changed, 181 insertions, 0 deletions
diff --git a/adservices/tests/unittest/service-core/src/com/android/adservices/data/RoomSchemaMigrationGuardrailTest.java b/adservices/tests/unittest/service-core/src/com/android/adservices/data/RoomSchemaMigrationGuardrailTest.java
new file mode 100644
index 0000000000..9e6c5724ee
--- /dev/null
+++ b/adservices/tests/unittest/service-core/src/com/android/adservices/data/RoomSchemaMigrationGuardrailTest.java
@@ -0,0 +1,181 @@
+/*
+ * Copyright (C) 2022 The Android Open Source Project
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.android.adservices.data;
+
+import android.annotation.NonNull;
+import android.content.Context;
+
+import androidx.room.RoomDatabase;
+import androidx.room.migration.bundle.EntityBundle;
+import androidx.room.migration.bundle.FieldBundle;
+import androidx.room.migration.bundle.SchemaBundle;
+import androidx.test.platform.app.InstrumentationRegistry;
+
+import com.android.adservices.data.adselection.AdSelectionDatabase;
+import com.android.adservices.data.customaudience.CustomAudienceDatabase;
+
+import com.google.common.collect.ImmutableList;
+
+import org.junit.Test;
+
+import java.io.IOException;
+import java.io.InputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+
+/** This UT is a guardrail to schema migration managed by Room. */
+public class RoomSchemaMigrationGuardrailTest {
+ private static final Context CONTEXT =
+ InstrumentationRegistry.getInstrumentation().getTargetContext();
+ private static final List<Class<? extends RoomDatabase>> DATABASE_CLASSES =
+ ImmutableList.of(CustomAudienceDatabase.class, AdSelectionDatabase.class);
+ private static final List<DatabaseWithVersion> BYPASS_DATABASE_VERSIONS_NEW_FIELD_ONLY =
+ ImmutableList.of(new DatabaseWithVersion(CustomAudienceDatabase.class, 2));
+
+ @Test
+ public void validateDatabaseMigration() throws IOException {
+ List<String> errors = new ArrayList<>();
+ List<DatabaseWithVersion> databaseClassesWithNewestVersion =
+ validateAndGetDatabaseClassesWithNewestVersionNumber(errors);
+ for (DatabaseWithVersion databaseWithVersion : databaseClassesWithNewestVersion) {
+ validateNewFieldOnly(databaseWithVersion, errors);
+ }
+ if (!errors.isEmpty()) {
+ throw new RuntimeException(
+ String.format(
+ "Finish validating room databases with error \n %s",
+ String.join("\n", errors)));
+ }
+ }
+
+ private List<DatabaseWithVersion> validateAndGetDatabaseClassesWithNewestVersionNumber(
+ List<String> errors) throws IOException {
+ ImmutableList.Builder<DatabaseWithVersion> result = new ImmutableList.Builder<>();
+ for (Class<? extends RoomDatabase> clazz : DATABASE_CLASSES) {
+ try {
+ final int newestDatabaseVersion = getNewestDatabaseVersion(clazz);
+ result.add(new DatabaseWithVersion(clazz, newestDatabaseVersion));
+ } catch (Exception e) {
+ errors.add(
+ String.format(
+ "Fail to get database schema for %s, with error %s.",
+ clazz.getCanonicalName(), e.getMessage()));
+ }
+ }
+ return result.build();
+ }
+
+ private int getNewestDatabaseVersion(Class<? extends RoomDatabase> database)
+ throws IOException {
+ return Arrays.stream(CONTEXT.getAssets().list(database.getCanonicalName()))
+ .map(p -> p.split("\\.")[0])
+ .mapToInt(Integer::parseInt)
+ .max()
+ .orElseThrow();
+ }
+
+ private void validateNewFieldOnly(DatabaseWithVersion databaseWithVersion, List<String> errors)
+ throws IOException {
+ // Custom audience table v1 to v2 is violating the policy. Skip it.
+ if (BYPASS_DATABASE_VERSIONS_NEW_FIELD_ONLY.contains(databaseWithVersion)) {
+ return;
+ }
+ int newestDatabaseVersion = databaseWithVersion.mVersion;
+ Class<? extends RoomDatabase> roomDatabaseClass = databaseWithVersion.mRoomDatabaseClass;
+ if (databaseWithVersion.mVersion == 1) {
+ return;
+ }
+
+ SchemaBundle oldSchemaBundle = loadSchema(roomDatabaseClass, newestDatabaseVersion - 1);
+ SchemaBundle newSchemaBundle = loadSchema(roomDatabaseClass, newestDatabaseVersion);
+
+ Map<String, EntityBundle> oldTables =
+ oldSchemaBundle.getDatabase().getEntitiesByTableName();
+ Map<String, EntityBundle> newTables =
+ newSchemaBundle.getDatabase().getEntitiesByTableName();
+
+ // We don't care new table in a new DB version. So iterate through the old version.
+ for (Map.Entry<String, EntityBundle> e : oldTables.entrySet()) {
+ String tableName = e.getKey();
+
+ // table in old version must show in new.
+ if (!newTables.containsKey(tableName)) {
+ errors.add(
+ String.format(
+ "New version DB is missing table %s present in old version",
+ tableName));
+ continue;
+ }
+
+ EntityBundle oldEntityBundle = e.getValue();
+ EntityBundle newEntityBundle = newTables.get(tableName);
+
+ for (FieldBundle oldFieldBundle : oldEntityBundle.getFields()) {
+ if (!newEntityBundle.getFields().contains(oldFieldBundle)) {
+ errors.add(
+ String.format(
+ "Table %s and field %s: Missing field in new version or"
+ + " mismatch field in new and old version.",
+ tableName, oldEntityBundle));
+ }
+ }
+ }
+ }
+
+ private SchemaBundle loadSchema(Class<? extends RoomDatabase> database, int version)
+ throws IOException {
+ InputStream input =
+ CONTEXT.getAssets().open(database.getCanonicalName() + "/" + version + ".json");
+ return SchemaBundle.deserialize(input);
+ }
+
+ private static class DatabaseWithVersion {
+ @NonNull private final Class<? extends RoomDatabase> mRoomDatabaseClass;
+ private final int mVersion;
+
+ DatabaseWithVersion(@NonNull Class<? extends RoomDatabase> roomDatabaseClass, int version) {
+ mRoomDatabaseClass = roomDatabaseClass;
+ mVersion = version;
+ }
+
+ @Override
+ public boolean equals(Object o) {
+ if (this == o) return true;
+ if (!(o instanceof DatabaseWithVersion)) return false;
+ DatabaseWithVersion that = (DatabaseWithVersion) o;
+ return mVersion == that.mVersion && mRoomDatabaseClass.equals(that.mRoomDatabaseClass);
+ }
+
+ @Override
+ public int hashCode() {
+ return Objects.hash(mRoomDatabaseClass, mVersion);
+ }
+
+ @Override
+ public String toString() {
+ return "DatabaseWithVersion{"
+ + "mRoomDatabaseClass="
+ + mRoomDatabaseClass
+ + ", mVersion="
+ + mVersion
+ + '}';
+ }
+ }
+}