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