Donate to e Foundation | Murena handsets with /e/OS | Own a part of Murena! Learn more

Commit f06a3170 authored by Jason Monk's avatar Jason Monk
Browse files

Guard against incorrect context use.

Instead of just having random tests fail, fail all the tests with a
slightly more useful message.

Also remove all the code around acquiring and contention, now that
we have a test rule in place for TestableContext, we can just use
that for cleanup and have a simple copy-on-write provider.

Test: runtest -x frameworks/base/tests/testables && runtest systemui
Change-Id: I907da23730a4a96cfa2bb112100a06980f01b078
Fixes: 37302051
parent 41bf42a1
Loading
Loading
Loading
Loading
+2 −3
Original line number Diff line number Diff line
@@ -47,9 +47,8 @@ public class DozeConfigurationTest extends SysuiTestCase {
            return;
        }

        mContext.getSettingsProvider().acquireOverridesBuilder()
                .addSetting("secure", Settings.Secure.DOZE_ALWAYS_ON, null)
                .build();
        Settings.Secure.putString(mContext.getContentResolver(), Settings.Secure.DOZE_ALWAYS_ON,
                null);

        assertFalse(mDozeConfig.alwaysOnEnabled(UserHandle.USER_CURRENT));
    }
+4 −8
Original line number Diff line number Diff line
@@ -18,7 +18,6 @@ import android.test.suitebuilder.annotation.SmallTest;

import com.android.settingslib.Utils;
import com.android.systemui.statusbar.policy.NetworkController.IconState;
import android.testing.TestableSettings.SettingOverrider;

import org.junit.Test;
import org.junit.runner.RunWith;
@@ -92,11 +91,10 @@ public class NetworkControllerWifiTest extends NetworkControllerBaseTest {
                        attr);

        // Must set the Settings value before instantiating the NetworkControllerImpl due to bugs in
        // TestableSettings.
        SettingOverrider settingsOverrider =
                mContext.getSettingsProvider().acquireOverridesBuilder()
                        .addSetting("global", Settings.Global.NETWORK_SCORING_UI_ENABLED, "1")
                        .build();
        // TestableSettingsProvider.
        Settings.Global.putString(mContext.getContentResolver(),
                Settings.Global.NETWORK_SCORING_UI_ENABLED,
                "1");
        super.setUp(); // re-instantiate NetworkControllImpl now that setting has been updated
        setupNetworkScoreManager();

@@ -131,8 +129,6 @@ public class NetworkControllerWifiTest extends NetworkControllerBaseTest {
        assertEquals("SD Badge is set",
                Utils.getWifiBadgeResource(NetworkBadging.BADGING_SD),
                iconState.iconOverlay);

        settingsOverrider.release();
    }

    private void setupNetworkScoreManager() {
+7 −8
Original line number Diff line number Diff line
@@ -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);
    }
+0 −318
Original line number Diff line number Diff line
/*
 * 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);
    }
}
+120 −0
Original line number Diff line number Diff line
/*
 * 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;
    }
}
Loading