diff options
author | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2021-12-09 13:06:04 +0000 |
---|---|---|
committer | Android Build Coastguard Worker <android-build-coastguard-worker@google.com> | 2021-12-09 13:06:04 +0000 |
commit | 066f519bf8df1e6faa75206aa4a9f355a16241f0 (patch) | |
tree | 4c9540b8e3f0470aab8dfc82caa6da0143fd3361 | |
parent | 42a25926e57265fb8321c1b1c732a20ee4387069 (diff) | |
parent | e9461ba5694bde2aeaf4f215c32ac62f48096874 (diff) | |
download | modules-utils-066f519bf8df1e6faa75206aa4a9f355a16241f0.tar.gz |
Snap for 7984612 from e9461ba5694bde2aeaf4f215c32ac62f48096874 to mainline-resolv-releaseandroid-mainline-12.0.0_r80android-mainline-12.0.0_r65
Change-Id: I49f39fcec1ca0b83e464f19c2519d01832429dab
10 files changed, 1314 insertions, 0 deletions
diff --git a/java/com/android/modules/utils/testing/Android.bp b/java/com/android/modules/utils/testing/Android.bp index 28bd82c..d39bf3c 100644 --- a/java/com/android/modules/utils/testing/Android.bp +++ b/java/com/android/modules/utils/testing/Android.bp @@ -27,3 +27,35 @@ java_library { min_sdk_version: "29", visibility: ["//visibility:public"], } + +// Depend on modules-utils-testable-device-config-defaults instead of this library. +java_library { + name: "modules-utils-testable-device-config", + srcs: [ + "StaticMockFixture.java", + "StaticMockFixtureRule.java", + "TestableDeviceConfig.java", + ], + static_libs: [ + "androidx.test.rules", + "mockito-target-extended-minus-junit4", + ], + sdk_version: "module_current", + min_sdk_version: "29", + visibility: ["//visibility:public"], +} + +// Utility for mocking DeviceConfig and other device state. +// In order to use these utils successfully, both Java code and native libraries will be required. +// If your code is in a library, you will have to depend on these defaults both in the library as +// well as in the android_test that it gets linked into. +// You will also need to specify android:debuggable="true" in the test's manifest. +java_defaults { + name: "modules-utils-testable-device-config-defaults", + static_libs: ["modules-utils-testable-device-config"], + jni_libs: [ + "libdexmakerjvmtiagent", + "libstaticjvmtiagent", + ], + defaults_visibility: ["//visibility:public"], +} diff --git a/java/com/android/modules/utils/testing/StaticMockFixture.java b/java/com/android/modules/utils/testing/StaticMockFixture.java new file mode 100644 index 0000000..d800b4f --- /dev/null +++ b/java/com/android/modules/utils/testing/StaticMockFixture.java @@ -0,0 +1,56 @@ +/* + * 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.modules.utils.testing; + + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +/** + * Provides support for a set of static mocks for use within a single shared + * {@link StaticMockitoSession}. + */ +public interface StaticMockFixture { + /** + * Adds any required mock or spy classes managed by this {@link StaticMockFixture} to the + * {@link StaticMockitoSessionBuilder} provided. + * + * Call this to set up the classes that this expects to be mocked, by adding them to the + * {@link StaticMockitoSessionBuilder} using + * {@link StaticMockitoSessionBuilder#mockStatic(Class)}, + * {@link StaticMockitoSessionBuilder#spyStatic(Class)} or similar as appropriate. + * + * @param sessionBuilder the {@link StaticMockitoSessionBuilder} to which the classes should be + * added to mock, spy, or otherwise as required + * @return sessionBuilder, to allow for fluent programming + */ + StaticMockitoSessionBuilder setUpMockedClasses(StaticMockitoSessionBuilder sessionBuilder); + + /** + * Configures the behaviours of any mock or spy classes managed by this + * {@link StaticMockFixture}. + * + * Call this after {@link StaticMockitoSessionBuilder#startMocking()} has been called. + * This sets up any default behaviors for the mocks, spys, etc. + */ + void setUpMockBehaviors(); + + /** + * Tear everything down. + */ + void tearDown(); +} diff --git a/java/com/android/modules/utils/testing/StaticMockFixtureRule.java b/java/com/android/modules/utils/testing/StaticMockFixtureRule.java new file mode 100644 index 0000000..0cdc9a2 --- /dev/null +++ b/java/com/android/modules/utils/testing/StaticMockFixtureRule.java @@ -0,0 +1,141 @@ +/* + * 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.modules.utils.testing; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.mockitoSession; + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +import org.junit.AssumptionViolatedException; +import org.junit.rules.TestRule; +import org.junit.rules.TestWatcher; +import org.junit.runner.Description; +import org.junit.runners.model.Statement; +import org.mockito.quality.Strictness; + +import java.util.function.Supplier; + +/** + * <p>StaticMockFixtureRule is a {@link TestRule} that wraps one or more {@link StaticMockFixture}s + * to set them up and tear it down automatically. This works well when you have no other static + * mocks than the ones supported by their respective {@link StaticMockFixture}s.</p> + * + * <p>StaticMockFixtureRule should be defined as a rule on your test so it can clean up after + * itself. Like the following:</p> + * <pre class="prettyprint"> +* public final StaticMockFixture mStaticMockFixtures = ...; + * @Rule + * public final StaticMockFixtureRule mStaticMockFixtureRule = + * new StaticMockFixtureRule(mStaticMockFixtures); + * </pre> + */ +public class StaticMockFixtureRule implements TestRule { + private StaticMockitoSession mMockitoSession; + private StaticMockFixture[] mStaticMockFixtures; + private Supplier<? extends StaticMockFixture>[] mSupplier; + + /** + * Constructs a StaticMockFixtureRule that always uses the same {@link StaticMockFixture} + * instance(s). + * + * @param staticMockFixtures the {@link StaticMockFixture}(s) to use. + */ + public StaticMockFixtureRule(StaticMockFixture... staticMockFixtures) { + mStaticMockFixtures = staticMockFixtures; + mSupplier = null; + } + + /** + * Constructs a StaticMockFixtureRule that retrieves a new {@link StaticMockFixture} instance + * from one or more {@link Supplier<? extends StaticMockFixture >}s for each test invocation. + * + * @param supplier the {@link Supplier<? extends StaticMockFixture >}(s) that will supply the + * {@link StaticMockFixture}(s). + */ + @SafeVarargs + public StaticMockFixtureRule(Supplier<? extends StaticMockFixture>... supplier) { + mStaticMockFixtures = null; + mSupplier = supplier; + } + + @Override + public Statement apply(Statement base, Description description) { + StaticMockitoSessionBuilder sessionBuilder = getSessionBuilder(); + + if (mSupplier != null) { + mStaticMockFixtures = new StaticMockFixture[mSupplier.length]; + for (int i = 0; i < mSupplier.length; i++) { + mStaticMockFixtures[i] = mSupplier[i].get(); + } + } + + for (int i = 0; i < mStaticMockFixtures.length; i++) { + sessionBuilder = mStaticMockFixtures[i].setUpMockedClasses(sessionBuilder); + } + + mMockitoSession = sessionBuilder.startMocking(); + + for (int i = 0; i < mStaticMockFixtures.length; i++) { + mStaticMockFixtures[i].setUpMockBehaviors(); + } + + return new TestWatcher() { + @Override + protected void succeeded(Description description) { + tearDown(null); + } + + @Override + protected void skipped(AssumptionViolatedException e, Description description) { + tearDown(e); + } + + @Override + protected void failed(Throwable e, Description description) { + tearDown(e); + } + }.apply(base, description); + } + + /** + * This allows overriding the creation of the builder for a new {@link StaticMockitoSession}. + * Mainly for testing, but also useful if you have other requirements for the session. + * + * @return a new {@link StaticMockitoSessionBuilder}. + */ + public StaticMockitoSessionBuilder getSessionBuilder() { + return mockitoSession().strictness(Strictness.LENIENT); + } + + private void tearDown(Throwable e) { + mMockitoSession.finishMocking(e); + + for (int i = mStaticMockFixtures.length - 1; i >= 0; i--) { + mStaticMockFixtures[i].tearDown(); + if (mSupplier != null) { + mStaticMockFixtures[i] = null; + } + } + + if (mSupplier != null) { + mStaticMockFixtures = null; + } + + mMockitoSession = null; + } +} diff --git a/java/com/android/modules/utils/testing/TestableDeviceConfig.java b/java/com/android/modules/utils/testing/TestableDeviceConfig.java new file mode 100644 index 0000000..8f62e03 --- /dev/null +++ b/java/com/android/modules/utils/testing/TestableDeviceConfig.java @@ -0,0 +1,258 @@ +/* + * 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.modules.utils.testing; + +import static com.android.dx.mockito.inline.extended.ExtendedMockito.doAnswer; + +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyBoolean; +import static org.mockito.ArgumentMatchers.anyFloat; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyLong; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.ArgumentMatchers.nullable; +import static org.mockito.Mockito.when; + +import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; +import android.util.ArrayMap; +import android.util.Pair; + +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +import org.junit.rules.TestRule; +import org.mockito.ArgumentMatchers; +import org.mockito.Mockito; +import org.mockito.stubbing.Answer; + +import java.util.Collections; +import java.util.HashMap; +import java.util.Map; +import java.util.concurrent.ConcurrentHashMap; +import java.util.concurrent.Executor; + +/** + * TestableDeviceConfig is a {@link StaticMockFixture} that uses ExtendedMockito to replace the real + * implementation of DeviceConfig with essentially a local HashMap in the callers process. This + * allows for unit testing that do not modify the real DeviceConfig on the device at all. + */ +public final class TestableDeviceConfig implements StaticMockFixture { + + private Map<DeviceConfig.OnPropertiesChangedListener, Pair<String, Executor>> + mOnPropertiesChangedListenerMap = new HashMap<>(); + private Map<String, String> mKeyValueMap = new ConcurrentHashMap<>(); + + /** + * Clears out all local overrides. + */ + public void clearDeviceConfig() { + mKeyValueMap.clear(); + } + + /** + * {@inheritDoc} + */ + @Override + public StaticMockitoSessionBuilder setUpMockedClasses( + StaticMockitoSessionBuilder sessionBuilder) { + sessionBuilder.spyStatic(DeviceConfig.class); + return sessionBuilder; + } + + /** + * {@inheritDoc} + */ + @Override + public void setUpMockBehaviors() { + doAnswer((Answer<Void>) invocationOnMock -> { + String namespace = invocationOnMock.getArgument(0); + Executor executor = invocationOnMock.getArgument(1); + DeviceConfig.OnPropertiesChangedListener onPropertiesChangedListener = + invocationOnMock.getArgument(2); + mOnPropertiesChangedListenerMap.put( + onPropertiesChangedListener, new Pair<>(namespace, executor)); + return null; + }).when(() -> DeviceConfig.addOnPropertiesChangedListener( + anyString(), any(Executor.class), + any(DeviceConfig.OnPropertiesChangedListener.class))); + + doAnswer((Answer<Boolean>) invocationOnMock -> { + String namespace = invocationOnMock.getArgument(0); + String name = invocationOnMock.getArgument(1); + String value = invocationOnMock.getArgument(2); + mKeyValueMap.put(getKey(namespace, name), value); + for (DeviceConfig.OnPropertiesChangedListener listener : + mOnPropertiesChangedListenerMap.keySet()) { + if (namespace.equals(mOnPropertiesChangedListenerMap.get(listener).first)) { + mOnPropertiesChangedListenerMap.get(listener).second.execute( + () -> listener.onPropertiesChanged( + getProperties(namespace, name, value))); + } + } + return true; + } + ).when(() -> DeviceConfig.setProperty(anyString(), anyString(), anyString(), anyBoolean())); + + doAnswer((Answer<String>) invocationOnMock -> { + String namespace = invocationOnMock.getArgument(0); + String name = invocationOnMock.getArgument(1); + return mKeyValueMap.get(getKey(namespace, name)); + }).when(() -> DeviceConfig.getProperty(anyString(), anyString())); + + doAnswer((Answer<Properties>) invocationOnMock -> { + String namespace = invocationOnMock.getArgument(0); + final int varargStartIdx = 1; + Map<String, String> keyValues = new ArrayMap<>(); + if (invocationOnMock.getArguments().length == varargStartIdx) { + mKeyValueMap.entrySet().forEach(entry -> { + Pair<String, String> nameSpaceAndName = getNameSpaceAndName(entry.getKey()); + if (!nameSpaceAndName.first.equals(namespace)) { + return; + } + keyValues.put(nameSpaceAndName.second.toLowerCase(), entry.getValue()); + }); + } else { + for (int i = varargStartIdx; i < invocationOnMock.getArguments().length; ++i) { + String name = invocationOnMock.getArgument(i); + keyValues.put(name.toLowerCase(), mKeyValueMap.get(getKey(namespace, name))); + } + } + return getProperties(namespace, keyValues); + }).when(() -> DeviceConfig.getProperties(anyString(), ArgumentMatchers.<String>any())); + } + + /** + * {@inheritDoc} + */ + @Override + public void tearDown() { + clearDeviceConfig(); + mOnPropertiesChangedListenerMap.clear(); + } + + private static String getKey(String namespace, String name) { + return namespace + "/" + name; + } + + private Pair<String, String> getNameSpaceAndName(String key) { + final String[] values = key.split("/"); + return Pair.create(values[0], values[1]); + } + + private Properties getProperties(String namespace, String name, String value) { + return getProperties(namespace, Collections.singletonMap(name.toLowerCase(), value)); + } + + private Properties getProperties(String namespace, Map<String, String> keyValues) { + Properties properties = Mockito.mock(Properties.class); + when(properties.getNamespace()).thenReturn(namespace); + when(properties.getKeyset()).thenReturn(keyValues.keySet()); + when(properties.getBoolean(anyString(), anyBoolean())).thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + boolean defaultValue = invocation.getArgument(1); + final String value = keyValues.get(key.toLowerCase()); + if (value != null) { + return Boolean.parseBoolean(value); + } else { + return defaultValue; + } + } + ); + when(properties.getFloat(anyString(), anyFloat())).thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + float defaultValue = invocation.getArgument(1); + final String value = keyValues.get(key.toLowerCase()); + if (value != null) { + try { + return Float.parseFloat(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } else { + return defaultValue; + } + } + ); + when(properties.getInt(anyString(), anyInt())).thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + int defaultValue = invocation.getArgument(1); + final String value = keyValues.get(key.toLowerCase()); + if (value != null) { + try { + return Integer.parseInt(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } else { + return defaultValue; + } + } + ); + when(properties.getLong(anyString(), anyLong())).thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + long defaultValue = invocation.getArgument(1); + final String value = keyValues.get(key.toLowerCase()); + if (value != null) { + try { + return Long.parseLong(value); + } catch (NumberFormatException e) { + return defaultValue; + } + } else { + return defaultValue; + } + } + ); + when(properties.getString(anyString(), nullable(String.class))).thenAnswer( + invocation -> { + String key = invocation.getArgument(0); + String defaultValue = invocation.getArgument(1); + final String value = keyValues.get(key.toLowerCase()); + if (value != null) { + return value; + } else { + return defaultValue; + } + } + ); + + return properties; + } + + /** + * <p>TestableDeviceConfigRule is a {@link TestRule} that wraps a {@link TestableDeviceConfig} + * to set it up and tear it down automatically. This works well when you have no other static + * mocks.</p> + * + * <p>TestableDeviceConfigRule should be defined as a rule on your test so it can clean up after + * itself. Like the following:</p> + * <pre class="prettyprint"> + * @Rule + * public final TestableDeviceConfigRule mTestableDeviceConfigRule = + * new TestableDeviceConfigRule(); + * </pre> + */ + public static class TestableDeviceConfigRule extends StaticMockFixtureRule { + public TestableDeviceConfigRule() { + super(TestableDeviceConfig::new); + } + } +} diff --git a/javatests/com/android/modules/utils/testing/Android.bp b/javatests/com/android/modules/utils/testing/Android.bp new file mode 100644 index 0000000..89f7797 --- /dev/null +++ b/javatests/com/android/modules/utils/testing/Android.bp @@ -0,0 +1,49 @@ +// +// Copyright (C) 2021 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 { + default_applicable_licenses: ["Android-Apache-2.0"], +} + +android_test { + name: "ModulesUtilsTestingTests", + + sdk_version: "module_current", + min_sdk_version: "29", + + defaults: [ + "modules-utils-testable-device-config-defaults", + ], + + srcs: ["*.java"], + + static_libs: [ + "androidx.test.core", + "androidx.test.runner", + "androidx.test.rules", + "platform-test-annotations", + "truth-prebuilt", + ], + + libs: [ + "android.test.mock", + "android.test.runner", + ], + + test_suites: [ + "general-tests", + "mts", + ], +} diff --git a/javatests/com/android/modules/utils/testing/AndroidManifest.xml b/javatests/com/android/modules/utils/testing/AndroidManifest.xml new file mode 100644 index 0000000..d1077ce --- /dev/null +++ b/javatests/com/android/modules/utils/testing/AndroidManifest.xml @@ -0,0 +1,27 @@ +<?xml version="1.0" encoding="utf-8"?> +<!-- + ~ Copyright (C) 2021 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. + --> + +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="com.android.modules.utils.testing"> + + <application android:debuggable="true"> + <uses-library android:name="android.test.runner" /> + </application> + + <instrumentation android:name="androidx.test.runner.AndroidJUnitRunner" + android:targetPackage="com.android.modules.utils.testing"/> +</manifest> diff --git a/javatests/com/android/modules/utils/testing/MultipleStaticMocksTest.java b/javatests/com/android/modules/utils/testing/MultipleStaticMocksTest.java new file mode 100644 index 0000000..b925a6c --- /dev/null +++ b/javatests/com/android/modules/utils/testing/MultipleStaticMocksTest.java @@ -0,0 +1,163 @@ +/* + * 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.modules.utils.testing; + +import static com.google.common.truth.Truth.assertThat; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class MultipleStaticMocksTest { + @Rule + public StaticMockFixtureRule mStaticMockFixtureRule = + new StaticMockFixtureRule(AB::new, CD::new); + + private List<String> mCollected; + + @Test + public void testMultipleStaticMocks() throws Exception { + mCollected = new ArrayList<>(); + int n = 0; + + A.a(); + n = verifyCollected(n, "A.a"); + + D.b(); + n = verifyCollected(n, "D.b"); + + C.b(); + n = verifyCollected(n, "C.b"); + + B.a(); + n = verifyCollected(n, "B.a"); + } + + private int verifyCollected(int n, String... last) { + assertThat(mCollected).hasSize(n + last.length); + assertThat(mCollected.subList(n, mCollected.size())) + .containsExactlyElementsIn(last).inOrder(); + return n + last.length; + } + + private static class A { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class B { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class C { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class D { + /* package */ static void a() {} + /* package */ static void b() {} + } + + /** + * AB StaticMockFixture class that handles two mocked classes, {@link A} and {@link B}. + */ + private class AB implements StaticMockFixture { + @Override + public StaticMockitoSessionBuilder setUpMockedClasses( + StaticMockitoSessionBuilder sessionBuilder) { + sessionBuilder.spyStatic(A.class); + sessionBuilder.spyStatic(B.class); + return sessionBuilder; + } + + @Override + public void setUpMockBehaviors() { + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("A.a"); + return null; + }).when(A::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("A.b"); + return null; + }).when(A::b); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("B.a"); + return null; + }).when(B::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("B.b"); + return null; + }).when(B::b); + } + + @Override + public void tearDown() { + + } + } + + /** + * AB StaticMockFixture class that handles two mocked classes, {@link C} and {@link D}. + */ + private class CD implements StaticMockFixture { + @Override + public StaticMockitoSessionBuilder setUpMockedClasses( + StaticMockitoSessionBuilder sessionBuilder) { + sessionBuilder.spyStatic(C.class); + sessionBuilder.spyStatic(D.class); + return sessionBuilder; + } + + @Override + public void setUpMockBehaviors() { + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("C.a"); + return null; + }).when(C::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("C.b"); + return null; + }).when(C::b); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("D.a"); + return null; + }).when(D::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("D.b"); + return null; + }).when(D::b); + } + + @Override + public void tearDown() { + + } + } +} diff --git a/javatests/com/android/modules/utils/testing/StaticMockFixtureRuleTest.java b/javatests/com/android/modules/utils/testing/StaticMockFixtureRuleTest.java new file mode 100644 index 0000000..7336761 --- /dev/null +++ b/javatests/com/android/modules/utils/testing/StaticMockFixtureRuleTest.java @@ -0,0 +1,258 @@ +/* + * 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.modules.utils.testing; + +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.doNothing; +import static org.mockito.Mockito.doThrow; +import static org.mockito.Mockito.inOrder; +import static org.mockito.Mockito.verifyNoMoreInteractions; +import static org.mockito.Mockito.when; +import static org.mockito.quality.Strictness.LENIENT; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.StaticMockitoSession; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +import org.junit.After; +import org.junit.AssumptionViolatedException; +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.Description; +import org.junit.runner.RunWith; +import org.junit.runners.model.Statement; +import org.mockito.InOrder; +import org.mockito.Mock; +import org.mockito.Mockito; +import org.mockito.MockitoSession; + +import java.util.function.Supplier; + +/** Tests that StaticMockFixture manages fixtures and suppliers correctly. */ +@RunWith(AndroidJUnit4.class) +@SmallTest +public class StaticMockFixtureRuleTest { + private MockitoSession mMockitoSession; + + @Mock private StaticMockitoSessionBuilder mSessionBuilder; + @Mock private StaticMockitoSession mSession; + @Mock private StaticMockFixture mA1; + @Mock private StaticMockFixture mB1; + @Mock private StaticMockFixture mA2; + @Mock private StaticMockFixture mB2; + @Mock private Supplier<StaticMockFixture> mSupplyA; + @Mock private Supplier<StaticMockFixture> mSupplyB; + @Mock private Statement mStatement; + @Mock private Statement mSkipStatement; + @Mock private Statement mThrowStatement; + @Mock private Description mDescription; + + @Before + public void setUp() throws Throwable { + mMockitoSession = Mockito.mockitoSession() + .strictness(LENIENT) + .initMocks(this) + .startMocking(); + prepareMockBehaviours(); + } + + @After + public void tearDown() { + mMockitoSession.finishMocking(); + } + + private void prepareFixtureMocks(StaticMockFixture... mocks) { + for (StaticMockFixture mock : mocks) { + when(mock.setUpMockedClasses(any())).thenAnswer( + invocation -> invocation.getArgument(0)); + doNothing().when(mock).setUpMockBehaviors(); + } + } + + private void prepareMockBehaviours() throws Throwable { + when(mSessionBuilder.startMocking()).thenReturn(mSession); + when(mSupplyA.get()).thenReturn(mA1, mA2); + when(mSupplyB.get()).thenReturn(mB1, mB2); + prepareFixtureMocks(mA1, mA2, mB1, mB2); + when(mA1.setUpMockedClasses(any())).thenAnswer(invocation -> invocation.getArgument(0)); + doNothing().when(mA1).setUpMockBehaviors(); + when(mB1.setUpMockedClasses(any())).thenAnswer(invocation -> invocation.getArgument(0)); + doNothing().when(mB1).setUpMockBehaviors(); + doNothing().when(mStatement).evaluate(); + doThrow(new AssumptionViolatedException("bad assumption, test should be skipped")) + .when(mSkipStatement).evaluate(); + doThrow(new IllegalArgumentException("bad argument, test should be failed")) + .when(mThrowStatement).evaluate(); + doNothing().when(mA1).tearDown(); + doNothing().when(mB1).tearDown(); + } + + private InOrder mocksInOrder() { + return inOrder(mSessionBuilder, mSession, mSupplyA, mSupplyB, mA1, mA2, mB1, mB2, + mStatement, mSkipStatement, mThrowStatement, mDescription); + } + + private void verifyNoMoreImportantMockInteractions() { + verifyNoMoreInteractions(mSupplyA, mSupplyB, mA1, mA2, mB1, mB2, mStatement, + mSkipStatement, mThrowStatement); + } + + @Test + public void testRuleWorksWithExplicitFixtures() throws Throwable { + InOrder inOrder = mocksInOrder(); + + StaticMockFixtureRule rule = new StaticMockFixtureRule(mA1, mB1) { + @Override public StaticMockitoSessionBuilder getSessionBuilder() { + return mSessionBuilder; + } + }; + Statement runMe = rule.apply(mStatement, mDescription); + + inOrder.verify(mA1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mB1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mA1).setUpMockBehaviors(); + inOrder.verify(mB1).setUpMockBehaviors(); + + runMe.evaluate(); + + inOrder.verify(mStatement).evaluate(); + // note: tearDown in reverse order + inOrder.verify(mB1).tearDown(); + inOrder.verify(mA1).tearDown(); + + // Round two: use the same fixtures again. + rule.apply(mStatement, mDescription).evaluate(); + + inOrder.verify(mA1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mB1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mA1).setUpMockBehaviors(); + inOrder.verify(mB1).setUpMockBehaviors(); + inOrder.verify(mStatement).evaluate(); + // note: tearDown in reverse order + inOrder.verify(mB1).tearDown(); + inOrder.verify(mA1).tearDown(); + + verifyNoMoreImportantMockInteractions(); + } + + @Test + public void testRuleWorksWithFixtureSuppliers() throws Throwable { + InOrder inOrder = mocksInOrder(); + + StaticMockFixtureRule rule = new StaticMockFixtureRule(mSupplyA, mSupplyB) { + @Override public StaticMockitoSessionBuilder getSessionBuilder() { + return mSessionBuilder; + } + }; + Statement runMe = rule.apply(mStatement, mDescription); + + inOrder.verify(mSupplyA).get(); + inOrder.verify(mSupplyB).get(); + inOrder.verify(mA1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mB1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mA1).setUpMockBehaviors(); + inOrder.verify(mB1).setUpMockBehaviors(); + + runMe.evaluate(); + + inOrder.verify(mStatement).evaluate(); + // note: tearDown in reverse order + inOrder.verify(mB1).tearDown(); + inOrder.verify(mA1).tearDown(); + + // Round two: use the same suppliers again to retrieve different fixtures: mA2 and mB2 + rule.apply(mStatement, mDescription).evaluate(); + + inOrder.verify(mSupplyA).get(); + inOrder.verify(mSupplyB).get(); + inOrder.verify(mA2).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mB2).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mA2).setUpMockBehaviors(); + inOrder.verify(mB2).setUpMockBehaviors(); + inOrder.verify(mStatement).evaluate(); + // note: tearDown in reverse order + inOrder.verify(mB2).tearDown(); + inOrder.verify(mA2).tearDown(); + + verifyNoMoreImportantMockInteractions(); + } + + @Test + public void testTearDownOnSkippedTests() throws Throwable { + InOrder inOrder = mocksInOrder(); + + StaticMockFixtureRule rule = new StaticMockFixtureRule(mA1, mB1) { + @Override public StaticMockitoSessionBuilder getSessionBuilder() { + return mSessionBuilder; + } + }; + Statement skipStatement = rule.apply(mSkipStatement, mDescription); + + inOrder.verify(mA1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mB1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mA1).setUpMockBehaviors(); + inOrder.verify(mB1).setUpMockBehaviors(); + + try { + skipStatement.evaluate(); + fail("AssumptionViolatedException should have been thrown"); + } catch (AssumptionViolatedException e) { + // expected + } + + inOrder.verify(mSkipStatement).evaluate(); + // note: tearDown in reverse order + inOrder.verify(mB1).tearDown(); + inOrder.verify(mA1).tearDown(); + + verifyNoMoreImportantMockInteractions(); + } + + @Test + public void testTearDownOnFailedTests() throws Throwable { + InOrder inOrder = mocksInOrder(); + + StaticMockFixtureRule rule = new StaticMockFixtureRule(mA1, mB1) { + @Override public StaticMockitoSessionBuilder getSessionBuilder() { + return mSessionBuilder; + } + }; + Statement failStatement = rule.apply(mThrowStatement, mDescription); + + inOrder.verify(mA1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mB1).setUpMockedClasses(any(StaticMockitoSessionBuilder.class)); + inOrder.verify(mA1).setUpMockBehaviors(); + inOrder.verify(mB1).setUpMockBehaviors(); + + try { + failStatement.evaluate(); + fail("IllegalArgumentException should have been thrown"); + } catch (IllegalArgumentException e) { + // expected + } + + inOrder.verify(mThrowStatement).evaluate(); + // note: tearDown in reverse order + inOrder.verify(mB1).tearDown(); + inOrder.verify(mA1).tearDown(); + + verifyNoMoreImportantMockInteractions(); + } +} diff --git a/javatests/com/android/modules/utils/testing/TestableDeviceConfigAndOtherStaticMocksTest.java b/javatests/com/android/modules/utils/testing/TestableDeviceConfigAndOtherStaticMocksTest.java new file mode 100644 index 0000000..8ebe424 --- /dev/null +++ b/javatests/com/android/modules/utils/testing/TestableDeviceConfigAndOtherStaticMocksTest.java @@ -0,0 +1,172 @@ +/* + * 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.modules.utils.testing; + +import android.provider.DeviceConfig; + +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import com.android.dx.mockito.inline.extended.ExtendedMockito; +import com.android.dx.mockito.inline.extended.StaticMockitoSessionBuilder; + +import org.junit.Assert; +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.ArrayList; +import java.util.List; + +@RunWith(AndroidJUnit4.class) +@SmallTest +public class TestableDeviceConfigAndOtherStaticMocksTest { + @Rule + public StaticMockFixtureRule mStaticMockFixtureRule = + new StaticMockFixtureRule(TestableDeviceConfig::new, AB::new, CD::new); + + private List<String> mCollected; + + @Test + public void testDeviceConfigAndOtherStaticMocks() throws Exception { + mCollected = new ArrayList<>(); + int n = 0; + + String namespace = "foo"; + String flag = "bar"; + String flagValue = "new value"; + + Assert.assertNull(DeviceConfig.getProperty(namespace, flag)); + + A.a(); + verifyCollected(++n, "A.a"); + + DeviceConfig.setProperty(namespace, flag, flagValue, false); + + D.b(); + verifyCollected(++n, "D.b"); + + Assert.assertEquals(flagValue, DeviceConfig.getProperty(namespace, flag)); + + C.b(); + verifyCollected(++n, "C.b"); + + B.a(); + verifyCollected(++n, "B.a"); + } + + private void verifyCollected(int n, String last) { + Assert.assertEquals(n, mCollected.size()); + Assert.assertEquals(last, mCollected.get(n - 1)); + } + + private static class A { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class B { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class C { + /* package */ static void a() {} + /* package */ static void b() {} + } + + private static class D { + /* package */ static void a() {} + /* package */ static void b() {} + } + + /** + * AB StaticMockFixture class that handles two mocked classes, {@link A} and {@link B}. + */ + private class AB implements StaticMockFixture { + @Override + public StaticMockitoSessionBuilder setUpMockedClasses( + StaticMockitoSessionBuilder sessionBuilder) { + sessionBuilder.spyStatic(A.class); + sessionBuilder.spyStatic(B.class); + return sessionBuilder; + } + + @Override + public void setUpMockBehaviors() { + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("A.a"); + return null; + }).when(A::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("A.b"); + return null; + }).when(A::b); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("B.a"); + return null; + }).when(B::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("B.b"); + return null; + }).when(B::b); + } + + @Override + public void tearDown() { + + } + } + + /** + * AB StaticMockFixture class that handles two mocked classes, {@link C} and {@link D}. + */ + private class CD implements StaticMockFixture { + @Override + public StaticMockitoSessionBuilder setUpMockedClasses( + StaticMockitoSessionBuilder sessionBuilder) { + sessionBuilder.spyStatic(C.class); + sessionBuilder.spyStatic(D.class); + return sessionBuilder; + } + + @Override + public void setUpMockBehaviors() { + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("C.a"); + return null; + }).when(C::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("C.b"); + return null; + }).when(C::b); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("D.a"); + return null; + }).when(D::a); + ExtendedMockito.doAnswer(invocation -> { + mCollected.add("D.b"); + return null; + }).when(D::b); + } + + @Override + public void tearDown() { + + } + } +} diff --git a/javatests/com/android/modules/utils/testing/TestableDeviceConfigTest.java b/javatests/com/android/modules/utils/testing/TestableDeviceConfigTest.java new file mode 100644 index 0000000..64e007e --- /dev/null +++ b/javatests/com/android/modules/utils/testing/TestableDeviceConfigTest.java @@ -0,0 +1,158 @@ +/* + * 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.modules.utils.testing; + +import static android.provider.DeviceConfig.OnPropertiesChangedListener; + +import static com.google.common.truth.Truth.assertThat; + +import android.platform.test.annotations.Presubmit; +import android.provider.DeviceConfig; +import android.provider.DeviceConfig.Properties; + +import androidx.test.core.app.ApplicationProvider; +import androidx.test.filters.SmallTest; +import androidx.test.runner.AndroidJUnit4; + +import org.junit.Rule; +import org.junit.Test; +import org.junit.runner.RunWith; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +/** Tests that ensure appropriate settings are backed up. */ +@Presubmit +@RunWith(AndroidJUnit4.class) +@SmallTest +public class TestableDeviceConfigTest { + private static final String sNamespace = "namespace1"; + private static final String sKey = "key1"; + private static final String sValue = "value1"; + private static final long WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS = 2000; // 2 sec + + @Rule + public TestableDeviceConfig.TestableDeviceConfigRule + mTestableDeviceConfig = new TestableDeviceConfig.TestableDeviceConfigRule(); + + @Test + public void getProperty_empty() { + String result = DeviceConfig.getProperty(sNamespace, sKey); + assertThat(result).isNull(); + } + + @Test + public void setAndGetProperty_sameNamespace() { + DeviceConfig.setProperty(sNamespace, sKey, sValue, false); + String result = DeviceConfig.getProperty(sNamespace, sKey); + assertThat(result).isEqualTo(sValue); + } + + @Test + public void setAndGetProperty_differentNamespace() { + String newNamespace = "namespace2"; + DeviceConfig.setProperty(sNamespace, sKey, sValue, false); + String result = DeviceConfig.getProperty(newNamespace, sKey); + assertThat(result).isNull(); + } + + @Test + public void setAndGetProperty_multipleNamespaces() { + String newNamespace = "namespace2"; + String newValue = "value2"; + DeviceConfig.setProperty(sNamespace, sKey, sValue, false); + DeviceConfig.setProperty(newNamespace, sKey, newValue, false); + String result = DeviceConfig.getProperty(sNamespace, sKey); + assertThat(result).isEqualTo(sValue); + result = DeviceConfig.getProperty(newNamespace, sKey); + assertThat(result).isEqualTo(newValue); + } + + @Test + public void setAndGetProperty_overrideValue() { + String newValue = "value2"; + DeviceConfig.setProperty(sNamespace, sKey, sValue, false); + DeviceConfig.setProperty(sNamespace, sKey, newValue, false); + String result = DeviceConfig.getProperty(sNamespace, sKey); + assertThat(result).isEqualTo(newValue); + } + + @Test + public void getProperties_empty() { + String newKey = "key2"; + String newValue = "value2"; + DeviceConfig.setProperty(sNamespace, sKey, sValue, false); + Properties properties = DeviceConfig.getProperties(sNamespace); + assertThat(properties.getString(sKey, null)).isEqualTo(sValue); + assertThat(properties.getString(newKey, null)).isNull(); + + DeviceConfig.setProperty(sNamespace, newKey, newValue, false); + properties = DeviceConfig.getProperties(sNamespace); + assertThat(properties.getString(sKey, null)).isEqualTo(sValue); + assertThat(properties.getString(newKey, null)).isEqualTo(newValue); + + } + + @Test + public void getProperties() { + Properties properties = DeviceConfig.getProperties(sNamespace, sKey); + assertThat(properties.getString(sKey, null)).isNull(); + + DeviceConfig.setProperty(sNamespace, sKey, sValue, false); + properties = DeviceConfig.getProperties(sNamespace, sKey); + assertThat(properties.getString(sKey, null)).isEqualTo(sValue); + + String newKey = "key2"; + String newValue = "value2"; + DeviceConfig.setProperty(sNamespace, newKey, newValue, false); + properties = DeviceConfig.getProperties(sNamespace, sKey, newKey); + assertThat(properties.getString(sKey, null)).isEqualTo(sValue); + assertThat(properties.getString(newKey, null)).isEqualTo(newValue); + + String unsetKey = "key3"; + properties = DeviceConfig.getProperties(sNamespace, newKey, unsetKey); + assertThat(properties.getKeyset()).containsExactly(newKey, unsetKey); + assertThat(properties.getString(newKey, null)).isEqualTo(newValue); + assertThat(properties.getString(unsetKey, null)).isNull(); + } + + @Test + public void testListener() throws InterruptedException { + CountDownLatch countDownLatch = new CountDownLatch(1); + + OnPropertiesChangedListener changeListener = (properties) -> { + assertThat(properties.getNamespace()).isEqualTo(sNamespace); + assertThat(properties.getKeyset().size()).isEqualTo(1); + assertThat(properties.getKeyset()).contains(sKey); + assertThat(properties.getString(sKey, "bogus_value")).isEqualTo(sValue); + assertThat(properties.getString("bogus_key", "bogus_value")).isEqualTo("bogus_value"); + countDownLatch.countDown(); + }; + try { + DeviceConfig.addOnPropertiesChangedListener(sNamespace, + ApplicationProvider.getApplicationContext().getMainExecutor(), changeListener); + DeviceConfig.setProperty(sNamespace, sKey, sValue, false); + assertThat(countDownLatch.await( + WAIT_FOR_PROPERTY_CHANGE_TIMEOUT_MILLIS, TimeUnit.MILLISECONDS)).isTrue(); + } finally { + DeviceConfig.removeOnPropertiesChangedListener(changeListener); + } + } + +} + + |