diff options
author | Jason Monk <jmonk@google.com> | 2017-04-27 13:46:48 +0000 |
---|---|---|
committer | Android (Google) Code Review <android-gerrit@google.com> | 2017-04-27 13:46:54 +0000 |
commit | 2cddbcaa0f271d4a89cb61b017fc7fcde96e4378 (patch) | |
tree | 7ba038f6deae97ec0ef9ae67449a63c0cb452d0c /tests/testables/src | |
parent | 2e0e7f3c94f39e5c6629152d2abfd618a5a7407b (diff) | |
parent | f06a317039a6502252c2b4b1a878520d166a38c6 (diff) |
Merge "Guard against incorrect context use." into oc-dev
Diffstat (limited to 'tests/testables/src')
3 files changed, 127 insertions, 326 deletions
diff --git a/tests/testables/src/android/testing/TestableContext.java b/tests/testables/src/android/testing/TestableContext.java index cb5d4cb8242f..630a287c6f4a 100644 --- a/tests/testables/src/android/testing/TestableContext.java +++ b/tests/testables/src/android/testing/TestableContext.java @@ -43,7 +43,7 @@ import org.junit.runners.model.Statement; * <ul> * <li>System services can be mocked out with {@link #addMockSystemService}</li> * <li>Service binding can be mocked out with {@link #addMockService}</li> - * <li>Settings support {@link TestableSettings}</li> + * <li>Settings support {@link TestableSettingsProvider}</li> * <li>Has support for {@link LeakCheck} for services and receivers</li> * </ul> * @@ -59,7 +59,7 @@ import org.junit.runners.model.Statement; public class TestableContext extends ContextWrapper implements TestRule { private final TestableContentResolver mTestableContentResolver; - private final TestableSettings mSettingsProvider; + private final TestableSettingsProvider mSettingsProvider; private ArrayMap<String, Object> mMockSystemServices; private ArrayMap<ComponentName, IBinder> mMockServices; @@ -79,9 +79,8 @@ public class TestableContext extends ContextWrapper implements TestRule { mTestableContentResolver = new TestableContentResolver(base); ContentProviderClient settings = base.getContentResolver() .acquireContentProviderClient(Settings.AUTHORITY); - mSettingsProvider = TestableSettings.getFakeSettingsProvider(settings, - mTestableContentResolver); - mTestableContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider.getProvider()); + mSettingsProvider = TestableSettingsProvider.getFakeSettingsProvider(settings); + mTestableContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider); mReceiver = check != null ? check.getTracker("receiver") : null; mService = check != null ? check.getTracker("service") : null; mComponent = check != null ? check.getTracker("component") : null; @@ -129,7 +128,7 @@ public class TestableContext extends ContextWrapper implements TestRule { return super.getSystemService(name); } - public TestableSettings getSettingsProvider() { + TestableSettingsProvider getSettingsProvider() { return mSettingsProvider; } @@ -236,12 +235,12 @@ public class TestableContext extends ContextWrapper implements TestRule { return new TestWatcher() { @Override protected void succeeded(Description description) { - mSettingsProvider.clearOverrides(); + mSettingsProvider.clearValuesAndCheck(TestableContext.this); } @Override protected void failed(Throwable e, Description description) { - mSettingsProvider.clearOverrides(); + mSettingsProvider.clearValuesAndCheck(TestableContext.this); } }.apply(base, description); } diff --git a/tests/testables/src/android/testing/TestableSettings.java b/tests/testables/src/android/testing/TestableSettings.java deleted file mode 100644 index d19f1ef60b2e..000000000000 --- a/tests/testables/src/android/testing/TestableSettings.java +++ /dev/null @@ -1,318 +0,0 @@ -/* - * Copyright (C) 2017 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 android.testing; - -import android.content.ContentProvider; -import android.content.ContentProviderClient; -import android.content.ContentResolver; -import android.os.Bundle; -import android.os.RemoteException; -import android.provider.Settings; -import android.support.annotation.VisibleForTesting; -import android.test.mock.MockContentProvider; -import android.testing.TestableSettings.SettingOverrider.Builder; -import android.util.ArrayMap; -import android.util.ArraySet; -import android.util.Log; - -import java.util.ArrayList; -import java.util.List; -import java.util.Map; -import java.util.Set; - -/** - * Allows calls to android.provider.Settings to be tested easier. A SettingOverride - * can be acquired and a set of specific settings can be set to a value (and not changed - * in the system when set), so that they can be tested without breaking the test device. - * <p> - * To use, in the before method acquire the override add all settings that will affect if - * your test passes or not. - * - * <pre class="prettyprint"> - * {@literal - * mSettingOverride = mTestableContext.getSettingsProvider().acquireOverridesBuilder() - * .addSetting("secure", Secure.USER_SETUP_COMPLETE, "0") - * .build(); - * } - * </pre> - * - * Then in the after free up the settings. - * - * <pre class="prettyprint"> - * {@literal - * mSettingOverride.release(); - * } - * </pre> - */ -public class TestableSettings { - - private static final String TAG = "TestableSettings"; - private static final boolean DEBUG = false; - - // Number of times to try to acquire a setting if in use. - private static final int MAX_TRIES = 10; - // Time to wait for each setting. WAIT_TIMEOUT * MAX_TRIES will be the maximum wait time - // for a setting. - private static final long WAIT_TIMEOUT = 1000; - - private static TestableSettingsProvider sInstance; - - private final TestableSettingsProvider mProvider; - - private TestableSettings(TestableSettingsProvider provider) { - mProvider = provider; - } - - public Builder acquireOverridesBuilder() { - return new Builder(this); - } - - public void clearOverrides() { - List<SettingOverrider> overrides = mProvider.mOwners.remove(this); - if (overrides != null) { - overrides.forEach(override -> override.ensureReleased()); - } - } - - private void acquireSettings(SettingOverrider overridder, Set<String> keys) - throws AcquireTimeoutException { - mProvider.acquireSettings(overridder, keys, this); - } - - ContentProvider getProvider() { - return mProvider; - } - - @VisibleForTesting - Object getLock() { - return mProvider.mOverrideMap; - } - - public static class SettingOverrider { - private final Set<String> mValidKeys; - private final Map<String, String> mValueMap = new ArrayMap<>(); - private final TestableSettings mSettings; - private boolean mReleased; - public Throwable mObtain; - - private SettingOverrider(Set<String> keys, TestableSettings provider) { - mValidKeys = new ArraySet<>(keys); - mSettings = provider; - } - - private void ensureReleased() { - if (!mReleased) { - release(); - } - } - - public void release() { - mSettings.mProvider.releaseSettings(mValidKeys); - mReleased = true; - } - - private void putDirect(String key, String value) { - mValueMap.put(key, value); - } - - public void put(String table, String key, String value) { - if (!mValidKeys.contains(key(table, key))) { - throw new IllegalArgumentException("Key " + table + " " + key - + " not acquired for this overrider"); - } - mValueMap.put(key(table, key), value); - } - - public void remove(String table, String key) { - if (!mValidKeys.contains(key(table, key))) { - throw new IllegalArgumentException("Key " + table + " " + key - + " not acquired for this overrider"); - } - mValueMap.remove(key(table, key)); - } - - public String get(String table, String key) { - if (!mValidKeys.contains(key(table, key))) { - throw new IllegalArgumentException("Key " + table + " " + key - + " not acquired for this overrider"); - } - Log.d(TAG, "Get " + table + " " + key + " " + mValueMap.get(key(table, key))); - return mValueMap.get(key(table, key)); - } - - public static class Builder { - private final TestableSettings mProvider; - private Set<String> mKeys = new ArraySet<>(); - private Map<String, String> mValues = new ArrayMap<>(); - - private Builder(TestableSettings provider) { - mProvider = provider; - } - - public Builder addSetting(String table, String key) { - mKeys.add(key(table, key)); - return this; - } - - public Builder addSetting(String table, String key, String value) { - addSetting(table, key); - mValues.put(key(table, key), value); - return this; - } - - public SettingOverrider build() throws AcquireTimeoutException { - SettingOverrider overrider = new SettingOverrider(mKeys, mProvider); - mProvider.acquireSettings(overrider, mKeys); - mValues.forEach((key, value) -> overrider.putDirect(key, value)); - return overrider; - } - } - } - - private static class TestableSettingsProvider extends MockContentProvider { - - private final Map<String, SettingOverrider> mOverrideMap = new ArrayMap<>(); - private final Map<Object, List<SettingOverrider>> mOwners = new ArrayMap<>(); - - private final ContentProviderClient mSettings; - private final ContentResolver mResolver; - - public TestableSettingsProvider(ContentProviderClient settings, ContentResolver resolver) { - mSettings = settings; - mResolver = resolver; - } - - private void releaseSettings(Set<String> keys) { - synchronized (mOverrideMap) { - for (String key : keys) { - if (DEBUG) Log.d(TAG, "Releasing " + key); - mOverrideMap.remove(key); - } - if (DEBUG) Log.d(TAG, "Notifying"); - mOverrideMap.notify(); - } - } - - private boolean checkKeysLocked(Set<String> keys, boolean shouldThrow) - throws AcquireTimeoutException { - for (String key : keys) { - if (mOverrideMap.containsKey(key)) { - if (shouldThrow) { - if (DEBUG) Log.e(TAG, "Lock obtained at", - mOverrideMap.get(key).mObtain); - throw new AcquireTimeoutException("Could not acquire " + key, - mOverrideMap.get(key).mObtain); - } - return false; - } - } - return true; - } - - private void acquireSettings(SettingOverrider overridder, Set<String> keys, - Object owner) throws AcquireTimeoutException { - synchronized (mOwners) { - List<SettingOverrider> list = mOwners.get(owner); - if (list == null) { - list = new ArrayList<>(); - mOwners.put(owner, list); - } - list.add(overridder); - } - synchronized (mOverrideMap) { - for (int i = 0; i < MAX_TRIES; i++) { - if (checkKeysLocked(keys, false)) break; - try { - if (DEBUG) Log.d(TAG, "Waiting for contention to finish"); - mOverrideMap.wait(WAIT_TIMEOUT); - } catch (InterruptedException e) { - } - } - overridder.mObtain = new Throwable(); - checkKeysLocked(keys, true); - for (String key : keys) { - if (DEBUG) Log.d(TAG, "Acquiring " + key); - mOverrideMap.put(key, overridder); - } - } - } - - public Bundle call(String method, String arg, Bundle extras) { - // Methods are "GET_system", "GET_global", "PUT_secure", etc. - final String[] commands = method.split("_", 2); - final String op = commands[0]; - final String table = commands[1]; - - synchronized (mOverrideMap) { - SettingOverrider overrider = mOverrideMap.get(key(table, arg)); - if (overrider == null) { - // Fall through to real settings. - try { - if (DEBUG) Log.d(TAG, "Falling through to real settings " + method); - // TODO: Add our own version of caching to handle this. - Bundle call = mSettings.call(method, arg, extras); - call.remove(Settings.CALL_METHOD_TRACK_GENERATION_KEY); - return call; - } catch (RemoteException e) { - throw new RuntimeException(e); - } - } - String value; - Bundle out = new Bundle(); - switch (op) { - case "GET": - value = overrider.get(table, arg); - if (value != null) { - out.putString(Settings.NameValueTable.VALUE, value); - } - break; - case "PUT": - value = extras.getString(Settings.NameValueTable.VALUE, null); - if (value != null) { - overrider.put(table, arg, value); - } else { - overrider.remove(table, arg); - } - break; - default: - throw new UnsupportedOperationException("Unknown command " + method); - } - return out; - } - } - } - - public static class AcquireTimeoutException extends Exception { - public AcquireTimeoutException(String str, Throwable cause) { - super(str, cause); - } - } - - private static String key(String table, String key) { - return table + "_" + key; - } - - /** - * Since the settings provider is cached inside android.provider.Settings, this must - * be gotten statically to ensure there is only one instance referenced. - */ - public static TestableSettings getFakeSettingsProvider(ContentProviderClient settings, - ContentResolver resolver) { - if (sInstance == null) { - sInstance = new TestableSettingsProvider(settings, resolver); - } - return new TestableSettings(sInstance); - } -} diff --git a/tests/testables/src/android/testing/TestableSettingsProvider.java b/tests/testables/src/android/testing/TestableSettingsProvider.java new file mode 100644 index 000000000000..13056cf677d6 --- /dev/null +++ b/tests/testables/src/android/testing/TestableSettingsProvider.java @@ -0,0 +1,120 @@ +/* + * Copyright (C) 2017 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 android.testing; + +import android.content.ContentProviderClient; +import android.content.Context; +import android.os.Bundle; +import android.os.RemoteException; +import android.provider.Settings; +import android.test.mock.MockContentProvider; +import android.util.Log; + +import java.util.HashMap; + +import static org.junit.Assert.*; + +/** + * Allows calls to android.provider.Settings to be tested easier. + * + * This provides a simple copy-on-write implementation of settings that gets cleared + * at the end of each test. + */ +public class TestableSettingsProvider extends MockContentProvider { + + private static final String TAG = "TestableSettingsProvider"; + private static final boolean DEBUG = false; + private static final String MY_UNIQUE_KEY = "Key_" + TestableSettingsProvider.class.getName(); + private static TestableSettingsProvider sInstance; + + private final ContentProviderClient mSettings; + + private final HashMap<String, String> mValues = new HashMap<>(); + + private TestableSettingsProvider(ContentProviderClient settings) { + mSettings = settings; + } + + void clearValuesAndCheck(Context context) { + mValues.put(key("global", MY_UNIQUE_KEY), MY_UNIQUE_KEY); + mValues.put(key("secure", MY_UNIQUE_KEY), MY_UNIQUE_KEY); + mValues.put(key("system", MY_UNIQUE_KEY), MY_UNIQUE_KEY); + + // Verify that if any test is using TestableContext, they all have the correct settings + // provider. + assertEquals("Incorrect settings provider, test using incorrect Context?", MY_UNIQUE_KEY, + Settings.Global.getString(context.getContentResolver(), MY_UNIQUE_KEY)); + assertEquals("Incorrect settings provider, test using incorrect Context?", MY_UNIQUE_KEY, + Settings.Secure.getString(context.getContentResolver(), MY_UNIQUE_KEY)); + assertEquals("Incorrect settings provider, test using incorrect Context?", MY_UNIQUE_KEY, + Settings.System.getString(context.getContentResolver(), MY_UNIQUE_KEY)); + + mValues.clear(); + } + + public Bundle call(String method, String arg, Bundle extras) { + // Methods are "GET_system", "GET_global", "PUT_secure", etc. + final String[] commands = method.split("_", 2); + final String op = commands[0]; + final String table = commands[1]; + + String k = key(table, arg); + String value; + Bundle out = new Bundle(); + switch (op) { + case "GET": + if (mValues.containsKey(k)) { + value = mValues.get(k); + if (value != null) { + out.putString(Settings.NameValueTable.VALUE, value); + } + } else { + // Fall through to real settings. + try { + if (DEBUG) Log.d(TAG, "Falling through to real settings " + method); + // TODO: Add our own version of caching to handle this. + Bundle call = mSettings.call(method, arg, extras); + call.remove(Settings.CALL_METHOD_TRACK_GENERATION_KEY); + return call; + } catch (RemoteException e) { + throw new RuntimeException(e); + } + } + break; + case "PUT": + value = extras.getString(Settings.NameValueTable.VALUE, null); + mValues.put(k, value); + break; + default: + throw new UnsupportedOperationException("Unknown command " + method); + } + return out; + } + + private static String key(String table, String key) { + return table + "_" + key; + } + + /** + * Since the settings provider is cached inside android.provider.Settings, this must + * be gotten statically to ensure there is only one instance referenced. + */ + static TestableSettingsProvider getFakeSettingsProvider(ContentProviderClient settings) { + if (sInstance == null) { + sInstance = new TestableSettingsProvider(settings); + } + return sInstance; + } +} |