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

Commit a640c164 authored by Jeff Sharkey's avatar Jeff Sharkey
Browse files

Baseline Testables updates for Ravenwood.

The `testables` library offers utilities like `TestableContext` and
`TestableLooper` that are heavily used by SystemUI tests.

As part of onboarding SystemUI tests in Ravenwood, we need these
testables to have baseline functionality, even though Ravenwood
doesn't have full `Context` support yet, which we do by quietly
deferring features (like SettingsProvider interactions) when
running under Ravenwood, along with TODOs to circle back as we add
more support under Ravenwood.

For `TestableLooper` we need to defer any `Looper.getMainLooper()`
interactions until the actual test attempts to use the Looper, since
the RavenwoodRule won't have created a main looper before a test
is running.

Bug: 319647875
Test: atest SystemUiRavenTests
Change-Id: I0e38acef814bf9baa0fe61c5478f77a555f312b6
parent c93d27ce
Loading
Loading
Loading
Loading
+33 −10
Original line number Diff line number Diff line
@@ -62,8 +62,9 @@ import java.util.ArrayList;
 */
public class TestableContext extends ContextWrapper implements TestRule {

    private final TestableContentResolver mTestableContentResolver;
    private final TestableSettingsProvider mSettingsProvider;
    private TestableContentResolver mTestableContentResolver;
    private TestableSettingsProvider mSettingsProvider;
    private RuntimeException mSettingsProviderFailure;

    private ArrayList<MockServiceResolver> mMockServiceResolvers;
    private ArrayMap<String, Object> mMockSystemServices;
@@ -83,12 +84,24 @@ public class TestableContext extends ContextWrapper implements TestRule {

    public TestableContext(Context base, LeakCheck check) {
        super(base);
        mTestableContentResolver = new TestableContentResolver(base);

        // Configure TestableSettingsProvider when possible; if we fail to initialize some
        // underlying infrastructure then remember the error and report it later when a test
        // attempts to interact with it
        try {
            ContentProviderClient settings = base.getContentResolver()
                    .acquireContentProviderClient(Settings.AUTHORITY);
            mSettingsProvider = TestableSettingsProvider.getFakeSettingsProvider(settings);
            mTestableContentResolver = new TestableContentResolver(base);
            mTestableContentResolver.addProvider(Settings.AUTHORITY, mSettingsProvider);
            mSettingsProvider.clearValuesAndCheck(TestableContext.this);
            mSettingsProviderFailure = null;
        } catch (Throwable t) {
            mTestableContentResolver = null;
            mSettingsProvider = null;
            mSettingsProviderFailure = new RuntimeException(
                    "Failed to initialize TestableSettingsProvider", t);
        }
        mReceiver = check != null ? check.getTracker("receiver") : null;
        mService = check != null ? check.getTracker("service") : null;
        mComponent = check != null ? check.getTracker("component") : null;
@@ -171,11 +184,17 @@ public class TestableContext extends ContextWrapper implements TestRule {
    }

    TestableSettingsProvider getSettingsProvider() {
        if (mSettingsProviderFailure != null) {
            throw mSettingsProviderFailure;
        }
        return mSettingsProvider;
    }

    @Override
    public TestableContentResolver getContentResolver() {
        if (mSettingsProviderFailure != null) {
            throw mSettingsProviderFailure;
        }
        return mTestableContentResolver;
    }

@@ -515,13 +534,17 @@ public class TestableContext extends ContextWrapper implements TestRule {
        return new TestWatcher() {
            @Override
            protected void succeeded(Description description) {
                if (mSettingsProvider != null) {
                    mSettingsProvider.clearValuesAndCheck(TestableContext.this);
                }
            }

            @Override
            protected void failed(Throwable e, Description description) {
                if (mSettingsProvider != null) {
                    mSettingsProvider.clearValuesAndCheck(TestableContext.this);
                }
            }
        }.apply(base, description);
    }
}
+60 −37
Original line number Diff line number Diff line
@@ -32,6 +32,7 @@ import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import java.lang.reflect.Field;
import java.util.Map;
import java.util.Objects;
import java.util.concurrent.atomic.AtomicBoolean;

/**
@@ -76,7 +77,7 @@ public class TestableLooper {
    }

    private TestableLooper(TestLooperManager wrapper, Looper l) {
        mQueueWrapper = wrapper;
        mQueueWrapper = Objects.requireNonNull(wrapper);
        setupQueue(l);
    }

@@ -282,65 +283,94 @@ public class TestableLooper {
        return InstrumentationRegistry.getInstrumentation().acquireLooperManager(l);
    }

    private static final Map<Object, TestableLooper> sLoopers = new ArrayMap<>();
    private static final Map<Object, TestableLooperHolder> sLoopers = new ArrayMap<>();

    /**
     * For use with {@link RunWithLooper}, used to get the TestableLooper that was
     * automatically created for this test.
     */
    public static TestableLooper get(Object test) {
        return sLoopers.get(test);
        final TestableLooperHolder looperHolder = sLoopers.get(test);
        return (looperHolder != null) ? looperHolder.mTestableLooper : null;
    }

    public static void remove(Object test) {
        sLoopers.remove(test);
    }

    static class LooperFrameworkMethod extends FrameworkMethod {
    /**
     * Holder object that contains {@link TestableLooper} so that its initialization can be
     * deferred until a test case is actually run, instead of forcing it to be created at
     * {@link FrameworkMethod} construction time.
     *
     * This deferral is important because some test environments may configure
     * {@link Looper#getMainLooper()} as part of a {@code Rule} instead of assuming it's globally
     * initialized and unconditionally available.
     */
    private static class TestableLooperHolder {
        private final boolean mSetAsMain;
        private final Object mTest;

        private TestableLooper mTestableLooper;
        private Looper mLooper;
        private Handler mHandler;
        private HandlerThread mHandlerThread;

        private final TestableLooper mTestableLooper;
        private final Looper mLooper;
        private final Handler mHandler;
        public TestableLooperHolder(boolean setAsMain, Object test) {
            mSetAsMain = setAsMain;
            mTest = test;
        }

        public LooperFrameworkMethod(FrameworkMethod base, boolean setAsMain, Object test) {
            super(base.getMethod());
        public void ensureInit() {
            if (mLooper != null) return;
            try {
                mLooper = setAsMain ? Looper.getMainLooper() : createLooper();
                mLooper = mSetAsMain ? Looper.getMainLooper() : createLooper();
                mTestableLooper = new TestableLooper(mLooper, false);
                if (!setAsMain) {
                    mTestableLooper.getLooper().getThread().setName(test.getClass().getName());
                if (!mSetAsMain) {
                    mTestableLooper.getLooper().getThread().setName(mTest.getClass().getName());
                }
            } catch (Exception e) {
                throw new RuntimeException(e);
            }
            sLoopers.put(test, mTestableLooper);
            mHandler = new Handler(mLooper);
        }

        public LooperFrameworkMethod(TestableLooper other, FrameworkMethod base) {
        private Looper createLooper() {
            // TODO: Find way to share these.
            mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName());
            mHandlerThread.start();
            return mHandlerThread.getLooper();
        }
    }

    static class LooperFrameworkMethod extends FrameworkMethod {
        private TestableLooperHolder mLooperHolder;

        public LooperFrameworkMethod(FrameworkMethod base, TestableLooperHolder looperHolder) {
            super(base.getMethod());
            mLooper = other.mLooper;
            mTestableLooper = other;
            mHandler = Handler.createAsync(mLooper);
            mLooperHolder = looperHolder;
        }

        public static FrameworkMethod get(FrameworkMethod base, boolean setAsMain, Object test) {
            if (sLoopers.containsKey(test)) {
                return new LooperFrameworkMethod(sLoopers.get(test), base);
            TestableLooperHolder looperHolder = sLoopers.get(test);
            if (looperHolder == null) {
                looperHolder = new TestableLooperHolder(setAsMain, test);
                sLoopers.put(test, looperHolder);
            }
            return new LooperFrameworkMethod(base, setAsMain, test);
            return new LooperFrameworkMethod(base, looperHolder);
        }

        @Override
        public Object invokeExplosively(Object target, Object... params) throws Throwable {
            if (Looper.myLooper() == mLooper) {
            mLooperHolder.ensureInit();
            if (Looper.myLooper() == mLooperHolder.mLooper) {
                // Already on the right thread from another statement, just execute then.
                return super.invokeExplosively(target, params);
            }
            boolean set = mTestableLooper.mQueueWrapper == null;
            boolean set = mLooperHolder.mTestableLooper.mQueueWrapper == null;
            if (set) {
                mTestableLooper.mQueueWrapper = acquireLooperManager(mLooper);
                mLooperHolder.mTestableLooper.mQueueWrapper = acquireLooperManager(
                        mLooperHolder.mLooper);
            }
            try {
                Object[] ret = new Object[1];
@@ -352,11 +382,11 @@ public class TestableLooper {
                        throw new LooperException(throwable);
                    }
                };
                Message m = Message.obtain(mHandler, execute);
                Message m = Message.obtain(mLooperHolder.mHandler, execute);

                // Dispatch our message.
                try {
                    mTestableLooper.mQueueWrapper.execute(m);
                    mLooperHolder.mTestableLooper.mQueueWrapper.execute(m);
                } catch (LooperException e) {
                    throw e.getSource();
                } catch (RuntimeException re) {
@@ -373,27 +403,20 @@ public class TestableLooper {
                return ret[0];
            } finally {
                if (set) {
                    mTestableLooper.mQueueWrapper.release();
                    mTestableLooper.mQueueWrapper = null;
                    if (HOLD_MAIN_THREAD && mLooper == Looper.getMainLooper()) {
                    mLooperHolder.mTestableLooper.mQueueWrapper.release();
                    mLooperHolder.mTestableLooper.mQueueWrapper = null;
                    if (HOLD_MAIN_THREAD && mLooperHolder.mLooper == Looper.getMainLooper()) {
                        TestableInstrumentation.releaseMain();
                    }
                }
            }
        }

        private Looper createLooper() {
            // TODO: Find way to share these.
            mHandlerThread = new HandlerThread(TestableLooper.class.getSimpleName());
            mHandlerThread.start();
            return mHandlerThread.getLooper();
        }

        @Override
        protected void finalize() throws Throwable {
            super.finalize();
            if (mHandlerThread != null) {
                mHandlerThread.quit();
            if (mLooperHolder.mHandlerThread != null) {
                mLooperHolder.mHandlerThread.quit();
            }
        }