Add initial Robolectric tests
Set up the infrastructure for tests and add a few initial Robolectric
tests for ThemePicker.
Bug: 118758604
Change-Id: Ie6f34c840d31d24349fd12355f93369f51168802
diff --git a/robolectric_tests/Android.mk b/robolectric_tests/Android.mk
new file mode 100644
index 0000000..3037a47
--- /dev/null
+++ b/robolectric_tests/Android.mk
@@ -0,0 +1,55 @@
+# Copyright (C) 2019 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.
+
+#############################################
+# ThenePicker Robolectric test target. #
+#############################################
+LOCAL_PATH := $(call my-dir)
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := ThemePickerRoboTests
+LOCAL_SDK_VERSION := system_current
+LOCAL_SRC_FILES := $(call all-java-files-under, src)
+LOCAL_STATIC_JAVA_LIBRARIES := \
+ androidx.test.runner \
+ androidx.test.rules \
+ mockito-robolectric-prebuilt \
+ truth-prebuilt
+LOCAL_JAVA_LIBRARIES := \
+ platform-robolectric-3.6.2-prebuilt
+
+LOCAL_JAVA_RESOURCE_DIRS := config
+
+LOCAL_INSTRUMENTATION_FOR := ThemePicker
+LOCAL_MODULE_TAGS := optional
+
+include $(BUILD_STATIC_JAVA_LIBRARY)
+
+############################################
+# Target to run the previous target. #
+############################################
+include $(CLEAR_VARS)
+
+LOCAL_MODULE := RunThemePickerRoboTests
+LOCAL_SDK_VERSION := system_current
+LOCAL_JAVA_LIBRARIES := \
+ ThemePickerRoboTests
+
+LOCAL_TEST_PACKAGE := ThemePicker
+
+LOCAL_INSTRUMENT_SOURCE_DIRS := $(dir $(LOCAL_PATH))../src \
+
+LOCAL_ROBOTEST_TIMEOUT := 36000
+
+include prebuilts/misc/common/robolectric/3.6.2/run_robotests.mk
diff --git a/robolectric_tests/config/robolectric.properties b/robolectric_tests/config/robolectric.properties
new file mode 100644
index 0000000..197c393
--- /dev/null
+++ b/robolectric_tests/config/robolectric.properties
@@ -0,0 +1,2 @@
+manifest=vendor/unbundled_google/packages/WallpaperPickerGoogle/AndroidManifest.xml
+sdk=27
diff --git a/robolectric_tests/robolectric_gradle_config/robolectric.properties b/robolectric_tests/robolectric_gradle_config/robolectric.properties
new file mode 100644
index 0000000..926e354
--- /dev/null
+++ b/robolectric_tests/robolectric_gradle_config/robolectric.properties
@@ -0,0 +1,3 @@
+# Do not include the manifest definition in this version
+# as it is specified directly in the gradle file
+sdk=27
\ No newline at end of file
diff --git a/robolectric_tests/src/com/android/customization/model/grid/GridOptionsManagerTest.java b/robolectric_tests/src/com/android/customization/model/grid/GridOptionsManagerTest.java
new file mode 100644
index 0000000..abbbaa0
--- /dev/null
+++ b/robolectric_tests/src/com/android/customization/model/grid/GridOptionsManagerTest.java
@@ -0,0 +1,73 @@
+/*
+ * Copyright (C) 2019 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.customization.model.grid;
+
+import static junit.framework.TestCase.fail;
+
+import static org.mockito.Matchers.anyBoolean;
+import static org.mockito.Mockito.verify;
+import static org.mockito.Mockito.when;
+
+import androidx.annotation.Nullable;
+
+import com.android.customization.model.CustomizationManager.Callback;
+
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+@RunWith(RobolectricTestRunner.class)
+public class GridOptionsManagerTest {
+
+ @Mock LauncherGridOptionsProvider mProvider;
+ private GridOptionsManager mManager;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ mManager = new GridOptionsManager(mProvider);
+ }
+
+ @Test
+ public void testApply() {
+ String gridName = "testName";
+ GridOption grid = new GridOption("testTitle", gridName, false, 2, 2, null, 1, "");
+ when(mProvider.applyGrid(gridName)).thenReturn(1);
+
+ mManager.apply(grid, new Callback() {
+ @Override
+ public void onSuccess() {
+ //Nothing to do here, the test passed
+ }
+
+ @Override
+ public void onError(@Nullable Throwable throwable) {
+ fail("onError was called when grid had been applied successfully");
+ }
+ });
+ }
+
+ @Test
+ public void testFetch_backgroundThread() {
+ mManager.fetchOptions(null);
+ Robolectric.flushBackgroundThreadScheduler();
+ verify(mProvider).fetch(anyBoolean());
+ }
+}
diff --git a/robolectric_tests/src/com/android/customization/model/theme/ThemeManagerTest.java b/robolectric_tests/src/com/android/customization/model/theme/ThemeManagerTest.java
new file mode 100644
index 0000000..bb5df66
--- /dev/null
+++ b/robolectric_tests/src/com/android/customization/model/theme/ThemeManagerTest.java
@@ -0,0 +1,171 @@
+/*
+ * Copyright (C) 2019 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.customization.model.theme;
+
+import static com.android.customization.model.ResourceConstants.ANDROID_PACKAGE;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_COLOR;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_FONT;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_ANDROID;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SETTINGS;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_ICON_SYSUI;
+import static com.android.customization.model.ResourceConstants.OVERLAY_CATEGORY_SHAPE;
+import static com.android.customization.model.ResourceConstants.SETTINGS_PACKAGE;
+import static com.android.customization.model.ResourceConstants.SYSUI_PACKAGE;
+import static com.android.customization.model.ResourceConstants.THEME_SETTING;
+
+import static junit.framework.TestCase.assertEquals;
+import static junit.framework.TestCase.assertTrue;
+
+import static org.junit.Assert.assertFalse;
+import static org.mockito.Matchers.anyInt;
+import static org.mockito.Matchers.anyString;
+import static org.mockito.Mockito.mock;
+import static org.mockito.Mockito.spy;
+import static org.mockito.Mockito.times;
+import static org.mockito.Mockito.verify;
+
+import android.app.Activity;
+import android.provider.Settings;
+
+import androidx.annotation.Nullable;
+import androidx.fragment.app.FragmentActivity;
+
+import com.android.customization.model.CustomizationManager.Callback;
+import com.android.customization.testutils.Condition;
+import com.android.customization.testutils.OverlayManagerMocks;
+import com.android.customization.testutils.Wait;
+import com.android.wallpaper.module.WallpaperSetter;
+
+import org.junit.After;
+import org.junit.Before;
+import org.junit.Test;
+import org.junit.runner.RunWith;
+import org.mockito.Mock;
+import org.mockito.MockitoAnnotations;
+import org.robolectric.Robolectric;
+import org.robolectric.RobolectricTestRunner;
+
+import java.util.Map;
+
+@RunWith(RobolectricTestRunner.class)
+public class ThemeManagerTest {
+
+ @Mock OverlayManagerCompat mMockOm;
+ @Mock WallpaperSetter mMockWallpaperSetter;
+ private OverlayManagerMocks mMockOmHelper;
+ private ThemeManager mThemeManager;
+ private Activity mActivity;
+
+ @Before
+ public void setUp() {
+ MockitoAnnotations.initMocks(this);
+ Activity activity = Robolectric.buildActivity(FragmentActivity.class).get();
+ mActivity = spy(activity);
+ mMockOmHelper = new OverlayManagerMocks();
+ mMockOmHelper.setUpMock(mMockOm);
+ ThemeBundleProvider provider = mock(ThemeBundleProvider.class);
+ mThemeManager = new ThemeManager(provider, activity, mMockWallpaperSetter, mMockOm);
+ }
+
+ @After
+ public void cleanUp() {
+ mMockOmHelper.clearOverlays();
+ }
+
+ @Test
+ public void testApply_DefaultTheme() {
+ mMockOmHelper.addOverlay("test.package.name_color", ANDROID_PACKAGE,
+ OVERLAY_CATEGORY_COLOR, true, 0);
+ mMockOmHelper.addOverlay("test.package.name_font", ANDROID_PACKAGE,
+ OVERLAY_CATEGORY_FONT, true, 0);
+ mMockOmHelper.addOverlay("test.package.name_shape", ANDROID_PACKAGE,
+ OVERLAY_CATEGORY_SHAPE, true, 0);
+ mMockOmHelper.addOverlay("test.package.name_icon", ANDROID_PACKAGE,
+ OVERLAY_CATEGORY_ICON_ANDROID, true, 0);
+ mMockOmHelper.addOverlay("test.package.name_settings", SETTINGS_PACKAGE,
+ OVERLAY_CATEGORY_ICON_SETTINGS, true, 0);
+ mMockOmHelper.addOverlay("test.package.name_sysui", SYSUI_PACKAGE,
+ OVERLAY_CATEGORY_ICON_SYSUI, true, 0);
+
+ ThemeBundle defaultTheme = new ThemeBundle.Builder().asDefault().build();
+
+ applyThemeAndWaitForCondition(defaultTheme, "Overlays didn't get disabled", () -> {
+ verify(mMockOm, times(6)).disableOverlay(anyString(), anyInt());
+ return true;
+ });
+
+ assertEquals("Secure Setting should be emtpy after applying default theme",
+ "",
+ Settings.Secure.getString(mActivity.getContentResolver(), THEME_SETTING));
+ }
+
+ @Test
+ public void testApply_NonDefault() {
+ final String bundleColorPackage = "test.package.name_color";
+ final String bundleFontPackage = "test.package.name_font";
+ final String otherPackage = "other.package.name_font";
+
+ mMockOmHelper.addOverlay(bundleColorPackage, ANDROID_PACKAGE,
+ OVERLAY_CATEGORY_COLOR, false, 0);
+ mMockOmHelper.addOverlay(bundleFontPackage, ANDROID_PACKAGE,
+ OVERLAY_CATEGORY_FONT, false, 0);
+ mMockOmHelper.addOverlay(otherPackage, ANDROID_PACKAGE,
+ OVERLAY_CATEGORY_FONT, false, 0);
+
+ ThemeBundle theme = new ThemeBundle.Builder()
+ .addOverlayPackage(OVERLAY_CATEGORY_COLOR, bundleColorPackage)
+ .addOverlayPackage(OVERLAY_CATEGORY_FONT, bundleFontPackage)
+ .build();
+
+ applyThemeAndWaitForCondition(theme, "Overlays didn't get enabled", () -> {
+ verify(mMockOm, times(2)).setEnabledExclusiveInCategory(anyString(), anyInt());
+ Map<String, String> overlays = mMockOm.getEnabledOverlaysForTargets(ANDROID_PACKAGE);
+ assertEquals(2, overlays.size());
+ assertTrue(bundleColorPackage + " should be enabled",
+ overlays.values().contains(bundleColorPackage));
+ assertTrue(bundleFontPackage + " should be enabled",
+ overlays.values().contains(bundleFontPackage));
+ assertFalse(otherPackage + " should not be enabled",
+ overlays.values().contains(otherPackage));
+ return true;
+ });
+
+ assertEquals("Secure Setting was not properly set after applying theme",
+ theme.getSerializedPackages(),
+ Settings.Secure.getString(mActivity.getContentResolver(), THEME_SETTING));
+ }
+
+ private void applyThemeAndWaitForCondition(ThemeBundle theme, String message,
+ Condition condition) {
+ boolean[] done = {false, false};
+ mThemeManager.apply(theme, new Callback() {
+ @Override
+ public void onSuccess() {
+ done[0] = true;
+ }
+
+ @Override
+ public void onError(@Nullable Throwable throwable) {
+ done[0] = true;
+ done[1] = true;
+ }
+ });
+ // TODO: refactor these tests so we can get rid of the long wait.
+ Wait.atMost(message, () -> done[0] && condition.isTrue(), 1000);
+ // done[1] is only set to true in the onError callback.
+ assertFalse(done[1]);
+ }
+}
diff --git a/robolectric_tests/src/com/android/customization/testutils/Condition.java b/robolectric_tests/src/com/android/customization/testutils/Condition.java
new file mode 100644
index 0000000..f013555
--- /dev/null
+++ b/robolectric_tests/src/com/android/customization/testutils/Condition.java
@@ -0,0 +1,20 @@
+/*
+ * Copyright (C) 2019 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.customization.testutils;
+
+public interface Condition {
+ boolean isTrue() throws Throwable;
+}
diff --git a/robolectric_tests/src/com/android/customization/testutils/OverlayManagerMocks.java b/robolectric_tests/src/com/android/customization/testutils/OverlayManagerMocks.java
new file mode 100644
index 0000000..ed77224
--- /dev/null
+++ b/robolectric_tests/src/com/android/customization/testutils/OverlayManagerMocks.java
@@ -0,0 +1,135 @@
+/*
+ * Copyright (C) 2019 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.customization.testutils;
+
+import static org.mockito.ArgumentMatchers.any;
+import static org.mockito.ArgumentMatchers.anyInt;
+import static org.mockito.ArgumentMatchers.anyString;
+import static org.mockito.Mockito.when;
+
+import android.content.om.OverlayManager;
+import android.text.TextUtils;
+
+import com.android.customization.model.theme.OverlayManagerCompat;
+
+import org.mockito.stubbing.Answer;
+
+import java.util.Arrays;
+import java.util.HashSet;
+import java.util.Map;
+import java.util.Set;
+import java.util.function.Function;
+import java.util.stream.Collectors;
+
+/**
+ * Helper class to provide mock implementation for OverlayManager, to use, create a Mockito Mock
+ * for OverlayManager and call {@link #setUpMock(OverlayManager)} with it, then use
+ * {@link #addOverlay(String, String, String, boolean, int)} to add fake OverlayInfo to be returned
+ * by the mocked OverlayManager.
+ */
+public class OverlayManagerMocks {
+ private static class MockOverlay {
+ final String packageName;
+ final String targetPackage;
+ final String category;
+
+ public MockOverlay(String packageName, String targetPackage, String category) {
+ this.packageName = packageName;
+ this.targetPackage = targetPackage;
+ this.category = category;
+ }
+
+ @Override
+ public boolean equals(Object obj) {
+ return obj instanceof MockOverlay
+ && TextUtils.equals(((MockOverlay) obj).packageName, packageName)
+ && TextUtils.equals(((MockOverlay) obj).targetPackage, targetPackage)
+ && TextUtils.equals(((MockOverlay) obj).category, category);
+ }
+ }
+
+ private Set<MockOverlay> mAllOverlays = new HashSet<>();
+ private Set<MockOverlay> mEnabledOverlays = new HashSet<>();
+
+ private boolean setEnabled(String packageName, boolean enable, int userId) {
+ if (packageName == null) {
+ return false;
+ }
+ Set<MockOverlay> packageOverlays = mAllOverlays.stream()
+ .filter(mockOverlay -> mockOverlay.packageName.equals(packageName)).collect(
+ Collectors.toSet());;
+ if (packageOverlays.isEmpty()) {
+ return false;
+ }
+ if (enable) {
+ mEnabledOverlays.addAll(packageOverlays);
+ } else {
+ mEnabledOverlays.removeAll(packageOverlays);
+ }
+ return true;
+ }
+
+ public void addOverlay(String packageName, String targetPackage, String category,
+ boolean enabled, int userId) {
+ MockOverlay overlay = new MockOverlay(packageName, targetPackage, category);
+ mAllOverlays.add(overlay);
+ if (enabled) {
+ mEnabledOverlays.add(overlay);
+ }
+ }
+
+ public void clearOverlays() {
+ mAllOverlays.clear();
+ mEnabledOverlays.clear();
+ }
+
+ public void setUpMock(OverlayManagerCompat mockOverlayManager) {
+ when(mockOverlayManager.getEnabledPackageName(anyString(), anyString())).then(
+ (Answer<String>) inv ->
+ mEnabledOverlays.stream().filter(
+ mockOverlay ->
+ mockOverlay.targetPackage.equals(inv.getArgument(0))
+ && mockOverlay.category.equals(inv.getArgument(1)))
+ .map(overlay -> overlay.packageName).findFirst().orElse(null));
+
+
+ when(mockOverlayManager.disableOverlay(anyString(), anyInt())).then(
+ (Answer<Boolean>) invocation ->
+ setEnabled(invocation.getArgument(0),
+ false,
+ invocation.getArgument(1)));
+
+ when(mockOverlayManager.setEnabledExclusiveInCategory(anyString(), anyInt())).then(
+ (Answer<Boolean>) invocation ->
+ setEnabled(
+ invocation.getArgument(0),
+ true,
+ invocation.getArgument(1)));
+
+ when(mockOverlayManager.getEnabledOverlaysForTargets(any())).then(
+ (Answer<Map<String, String>>) inv ->
+ mEnabledOverlays.stream().filter(
+ overlay ->
+ Arrays.asList(inv.getArguments())
+ .contains(overlay.targetPackage))
+ .collect(Collectors.toMap(
+ overlay ->
+ overlay.category,
+ (Function<MockOverlay, String>) overlay ->
+ overlay.packageName))
+ );
+ }
+}
\ No newline at end of file
diff --git a/robolectric_tests/src/com/android/customization/testutils/Wait.java b/robolectric_tests/src/com/android/customization/testutils/Wait.java
new file mode 100644
index 0000000..54650ba
--- /dev/null
+++ b/robolectric_tests/src/com/android/customization/testutils/Wait.java
@@ -0,0 +1,58 @@
+/*
+ * Copyright (C) 2019 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.customization.testutils;
+
+
+import android.os.SystemClock;
+
+import org.junit.Assert;
+
+/**
+ * A utility class for waiting for a condition to be true.
+ */
+public class Wait {
+
+ private static final long DEFAULT_SLEEP_MS = 200;
+
+ public static void atMost(String message, Condition condition, long timeout) {
+ atMost(message, condition, timeout, DEFAULT_SLEEP_MS);
+ }
+
+ public static void atMost(String message, Condition condition, long timeout, long sleepMillis) {
+ long endTime = SystemClock.uptimeMillis() + timeout;
+ while (SystemClock.uptimeMillis() < endTime) {
+ try {
+ if (condition.isTrue()) {
+ return;
+ }
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ SystemClock.sleep(sleepMillis);
+ }
+
+ // Check once more before returning false.
+ try {
+ if (condition.isTrue()) {
+ return;
+ }
+ } catch (Throwable t) {
+ throw new RuntimeException(t);
+ }
+ Assert.fail(message);
+ }
+}
+