1/*
2 * Copyright (C) 2017 The Android Open Source Project
3 *
4 * Licensed under the Apache License, Version 2.0 (the "License");
5 * you may not use this file except in compliance with the License.
6 * You may obtain a copy of the License at
7 *
8 *      http://www.apache.org/licenses/LICENSE-2.0
9 *
10 * Unless required by applicable law or agreed to in writing, software
11 * distributed under the License is distributed on an "AS IS" BASIS,
12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
13 * See the License for the specific language governing permissions and
14 * limitations under the License.
15 */
16
17package androidx.lifecycle;
18
19import static org.hamcrest.CoreMatchers.is;
20import static org.hamcrest.MatcherAssert.assertThat;
21import static org.mockito.Matchers.any;
22import static org.mockito.Matchers.anyInt;
23import static org.mockito.Mockito.mock;
24import static org.mockito.Mockito.never;
25import static org.mockito.Mockito.reset;
26import static org.mockito.Mockito.spy;
27import static org.mockito.Mockito.timeout;
28import static org.mockito.Mockito.verify;
29
30import androidx.annotation.NonNull;
31import androidx.annotation.Nullable;
32import androidx.arch.core.executor.ArchTaskExecutor;
33import androidx.arch.core.executor.TaskExecutor;
34import androidx.arch.core.executor.TaskExecutorWithFakeMainThread;
35import androidx.lifecycle.util.InstantTaskExecutor;
36
37import org.junit.After;
38import org.junit.Before;
39import org.junit.Test;
40import org.junit.runner.RunWith;
41import org.junit.runners.JUnit4;
42import org.mockito.ArgumentCaptor;
43
44import java.util.Collections;
45import java.util.concurrent.Executor;
46import java.util.concurrent.Semaphore;
47import java.util.concurrent.TimeUnit;
48import java.util.concurrent.atomic.AtomicInteger;
49
50@RunWith(JUnit4.class)
51public class ComputableLiveDataTest {
52    private TaskExecutor mTaskExecutor;
53    private TestLifecycleOwner mLifecycleOwner;
54
55    @Before
56    public void setup() {
57        mLifecycleOwner = new TestLifecycleOwner();
58    }
59
60    @Before
61    public void swapExecutorDelegate() {
62        mTaskExecutor = spy(new InstantTaskExecutor());
63        ArchTaskExecutor.getInstance().setDelegate(mTaskExecutor);
64    }
65
66    @After
67    public void removeExecutorDelegate() {
68        ArchTaskExecutor.getInstance().setDelegate(null);
69    }
70
71    @Test
72    public void noComputeWithoutObservers() {
73        final TestComputable computable = new TestComputable();
74        verify(mTaskExecutor, never()).executeOnDiskIO(computable.mRefreshRunnable);
75        verify(mTaskExecutor, never()).executeOnDiskIO(computable.mInvalidationRunnable);
76    }
77
78    @Test
79    public void noConcurrentCompute() throws InterruptedException {
80        TaskExecutorWithFakeMainThread executor = new TaskExecutorWithFakeMainThread(2);
81        ArchTaskExecutor.getInstance().setDelegate(executor);
82        try {
83            // # of compute calls
84            final Semaphore computeCounter = new Semaphore(0);
85            // available permits for computation
86            final Semaphore computeLock = new Semaphore(0);
87            final TestComputable computable = new TestComputable(1, 2) {
88                @Override
89                protected Integer compute() {
90                    try {
91                        computeCounter.release(1);
92                        computeLock.tryAcquire(1, 20, TimeUnit.SECONDS);
93                    } catch (InterruptedException e) {
94                        throw new AssertionError(e);
95                    }
96                    return super.compute();
97                }
98            };
99            final ArgumentCaptor<Integer> captor = ArgumentCaptor.forClass(Integer.class);
100            //noinspection unchecked
101            final Observer<Integer> observer = mock(Observer.class);
102            executor.postToMainThread(new Runnable() {
103                @Override
104                public void run() {
105                    computable.getLiveData().observeForever(observer);
106                    verify(observer, never()).onChanged(anyInt());
107                }
108            });
109            // wait for first compute call
110            assertThat(computeCounter.tryAcquire(1, 2, TimeUnit.SECONDS), is(true));
111            // re-invalidate while in compute
112            computable.invalidate();
113            computable.invalidate();
114            computable.invalidate();
115            computable.invalidate();
116            // ensure another compute call does not arrive
117            assertThat(computeCounter.tryAcquire(1, 2, TimeUnit.SECONDS), is(false));
118            // allow computation to finish
119            computeLock.release(2);
120            // wait for the second result, first will be skipped due to invalidation during compute
121            verify(observer, timeout(2000)).onChanged(captor.capture());
122            assertThat(captor.getAllValues(), is(Collections.singletonList(2)));
123            reset(observer);
124            // allow all computations to run, there should not be any.
125            computeLock.release(100);
126            // unfortunately, Mockito.after is not available in 1.9.5
127            executor.drainTasks(2);
128            // assert no other results arrive
129            verify(observer, never()).onChanged(anyInt());
130        } finally {
131            ArchTaskExecutor.getInstance().setDelegate(null);
132        }
133    }
134
135    @Test
136    public void addingObserverShouldTriggerAComputation() {
137        TestComputable computable = new TestComputable(1);
138        mLifecycleOwner.handleEvent(Lifecycle.Event.ON_CREATE);
139        final AtomicInteger mValue = new AtomicInteger(-1);
140        computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
141            @Override
142            public void onChanged(@Nullable Integer integer) {
143                //noinspection ConstantConditions
144                mValue.set(integer);
145            }
146        });
147        verify(mTaskExecutor, never()).executeOnDiskIO(any(Runnable.class));
148        assertThat(mValue.get(), is(-1));
149        mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
150        verify(mTaskExecutor).executeOnDiskIO(computable.mRefreshRunnable);
151        assertThat(mValue.get(), is(1));
152    }
153
154    @Test
155    public void customExecutor() {
156        Executor customExecutor = mock(Executor.class);
157        TestComputable computable = new TestComputable(customExecutor, 1);
158        mLifecycleOwner.handleEvent(Lifecycle.Event.ON_CREATE);
159        computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
160            @Override
161            public void onChanged(@Nullable Integer integer) {
162                // ignored
163            }
164        });
165        verify(mTaskExecutor, never()).executeOnDiskIO(any(Runnable.class));
166        verify(customExecutor, never()).execute(any(Runnable.class));
167
168        mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
169
170        verify(mTaskExecutor, never()).executeOnDiskIO(computable.mRefreshRunnable);
171        verify(customExecutor).execute(computable.mRefreshRunnable);
172    }
173
174    @Test
175    public void invalidationShouldNotReTriggerComputationIfObserverIsInActive() {
176        TestComputable computable = new TestComputable(1, 2);
177        mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
178        final AtomicInteger mValue = new AtomicInteger(-1);
179        computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
180            @Override
181            public void onChanged(@Nullable Integer integer) {
182                //noinspection ConstantConditions
183                mValue.set(integer);
184            }
185        });
186        assertThat(mValue.get(), is(1));
187        mLifecycleOwner.handleEvent(Lifecycle.Event.ON_STOP);
188        computable.invalidate();
189        reset(mTaskExecutor);
190        verify(mTaskExecutor, never()).executeOnDiskIO(computable.mRefreshRunnable);
191        assertThat(mValue.get(), is(1));
192    }
193
194    @Test
195    public void invalidationShouldReTriggerQueryIfObserverIsActive() {
196        TestComputable computable = new TestComputable(1, 2);
197        mLifecycleOwner.handleEvent(Lifecycle.Event.ON_START);
198        final AtomicInteger mValue = new AtomicInteger(-1);
199        computable.getLiveData().observe(mLifecycleOwner, new Observer<Integer>() {
200            @Override
201            public void onChanged(@Nullable Integer integer) {
202                //noinspection ConstantConditions
203                mValue.set(integer);
204            }
205        });
206        assertThat(mValue.get(), is(1));
207        computable.invalidate();
208        assertThat(mValue.get(), is(2));
209    }
210
211    static class TestComputable extends ComputableLiveData<Integer> {
212        final int[] mValues;
213        AtomicInteger mValueCounter = new AtomicInteger();
214
215        TestComputable(@NonNull Executor executor, int... values) {
216            super(executor);
217            mValues = values;
218        }
219
220        TestComputable(int... values) {
221            mValues = values;
222        }
223
224        @Override
225        protected Integer compute() {
226            return mValues[mValueCounter.getAndIncrement()];
227        }
228    }
229
230    static class TestLifecycleOwner implements LifecycleOwner {
231        private LifecycleRegistry mLifecycle;
232
233        TestLifecycleOwner() {
234            mLifecycle = new LifecycleRegistry(this);
235        }
236
237        @Override
238        public Lifecycle getLifecycle() {
239            return mLifecycle;
240        }
241
242        void handleEvent(Lifecycle.Event event) {
243            mLifecycle.handleLifecycleEvent(event);
244        }
245    }
246}
247