1 /* 2 * Copyright 2022 Google LLC 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 package com.google.android.libraries.mobiledatadownload.testing; 17 18 import com.google.android.libraries.mobiledatadownload.downloader.DownloadRequest; 19 import com.google.android.libraries.mobiledatadownload.downloader.FileDownloader; 20 import com.google.common.base.Optional; 21 import com.google.common.flogger.GoogleLogger; 22 import com.google.common.util.concurrent.FutureCallback; 23 import com.google.common.util.concurrent.Futures; 24 import com.google.common.util.concurrent.ListenableFuture; 25 import com.google.common.util.concurrent.ListeningExecutorService; 26 import java.util.concurrent.CountDownLatch; 27 28 /** 29 * File Downloader that allows control over how long the download takes. 30 * 31 * <p>An optional delegate FileDownloader can be provided to perform downloading while maintaining 32 * control over the download state. 33 * 34 * <h3>The states of a download</h3> 35 * 36 * <p>The following "states" are defined and using the BlockingFileDownloader will allow the 37 * download to be paused so assertions can be made: 38 * 39 * <ol> 40 * <li>Idle - The download has not started yet. 41 * <li>In Progress - The download has started, but has not completed yet. 42 * <li>Finishing* - The download is finishing (Only applicable when delegate is provided). 43 * <li>Finished - The download has finished. 44 * </ol> 45 * 46 * <h5>The "Finishing" state</h5> 47 * 48 * <p>The Finishing state is a special state only applicable when a delegate FileDownloader is 49 * provided. The Finishing state can be considered most like the In Progress state, the main 50 * distinction being that no action is being performed during the In Progress state, but the 51 * delegate is running during the Finishing state. 52 * 53 * <p><em>Why not just run the delegate during In Progress?</em> 54 * 55 * <p>Because the delgate could be performing actual work (i.e. network calls, I/O), there could be 56 * some variability introduced that causes test assertions to become flaky. The In Progress is 57 * reserved to effectively be a Frozen state that ensure no work is being done. This ensures that 58 * test assertions remain consistent. 59 * 60 * <h5>Controlling the states</h5> 61 * 62 * <p>After creating an instance of BlockingFileDownloader, the following example shows how the 63 * BlockingFileDownloader can control the state as well as when assertions can be made. 64 * 65 * <pre>{@code 66 * // Before the call to MDD's downloadFileGroup, the state is considered "Idle" and assertions 67 * // can be made at this time. 68 * 69 * mdd.downloadFileGroup(...); 70 * 71 * // After calling downloadFileGroup, the state is in a transition period from "Idle" to 72 * // "In Progress." assertions should not be made during this time. 73 * 74 * blockingFileDownloader.waitForDownloadStarted(); 75 * 76 * // The above method blocks until the "In Progress" state has been reached. Assertions can be made 77 * // after this call for the "In Progress" state. If no assertions need to be made during this 78 * // state, the above call can be skipped. 79 * 80 * blockingFileDownloader.finishDownloading(); 81 * 82 * // The above method moves the state from "In Progress" to "Finishing" (if a delegate is provided) 83 * // or "Finished" (if no delegate is provided). This is another transition period, so assertions 84 * // should not be made during this time. 85 * 86 * blockingFileDownloader.waitForDownloadCompleted(); 87 * 88 * // The above method ensures the state has changed from "In Progress"/"Finishing" to "Finished." 89 * // After this point, assertions can be made safely again. 90 * // 91 * // Optionally, the future returned from mdd.downloadFileGroup can be awaited instead, since that 92 * // also ensures the download has completed. 93 * }</pre> 94 */ 95 public final class BlockingFileDownloader implements FileDownloader { 96 private static final GoogleLogger logger = GoogleLogger.forEnclosingClass(); 97 98 private final ListeningExecutorService downloadExecutor; 99 private final Optional<FileDownloader> delegateFileDownloaderOptional; 100 101 private CountDownLatch downloadInProgressLatch = new CountDownLatch(1); 102 private CountDownLatch downloadFinishingLatch = new CountDownLatch(1); 103 private CountDownLatch delegateInProgressLatch = new CountDownLatch(1); 104 private CountDownLatch downloadFinishedLatch = new CountDownLatch(1); 105 BlockingFileDownloader(ListeningExecutorService downloadExecutor)106 public BlockingFileDownloader(ListeningExecutorService downloadExecutor) { 107 this.downloadExecutor = downloadExecutor; 108 this.delegateFileDownloaderOptional = Optional.absent(); 109 } 110 BlockingFileDownloader( ListeningExecutorService downloadExecutor, FileDownloader delegateFileDownloader)111 public BlockingFileDownloader( 112 ListeningExecutorService downloadExecutor, FileDownloader delegateFileDownloader) { 113 this.downloadExecutor = downloadExecutor; 114 this.delegateFileDownloaderOptional = Optional.of(delegateFileDownloader); 115 } 116 117 @Override startDownloading(DownloadRequest downloadRequest)118 public ListenableFuture<Void> startDownloading(DownloadRequest downloadRequest) { 119 ListenableFuture<Void> downloadFuture = 120 Futures.submitAsync( 121 () -> { 122 logger.atInfo().log("Download Started, state changed to: In Progress"); 123 downloadInProgressLatch.countDown(); 124 125 logger.atInfo().log("Waiting for download to continue"); 126 downloadFinishingLatch.await(); 127 128 ListenableFuture<Void> result; 129 if (delegateFileDownloaderOptional.isPresent()) { 130 logger.atInfo().log("Download State Changed to: Finishing (delegate in progress)"); 131 // Delegate was provided, so perform its download 132 result = delegateFileDownloaderOptional.get().startDownloading(downloadRequest); 133 delegateInProgressLatch.countDown(); 134 } else { 135 result = Futures.immediateVoidFuture(); 136 } 137 138 return result; 139 }, 140 downloadExecutor); 141 142 // Add a callback to ensure the state transitions from "In Progress"/"Finishing" to "Finished." 143 Futures.addCallback( 144 downloadFuture, 145 new FutureCallback<Void>() { 146 @Override 147 public void onSuccess(Void unused) { 148 BlockingFileDownloader.logger.atInfo().log("Download State Changed to: Finished"); 149 downloadFinishedLatch.countDown(); 150 } 151 152 @Override 153 public void onFailure(Throwable t) { 154 BlockingFileDownloader.logger 155 .atSevere() 156 .withCause(t) 157 .log("Download State Changed to: Finished (with error)"); 158 downloadFinishedLatch.countDown(); 159 } 160 }, 161 downloadExecutor); 162 163 return downloadFuture; 164 } 165 166 /** Blocks the caller thread until the download has moved to the "In Progress" state. */ waitForDownloadStarted()167 public void waitForDownloadStarted() throws InterruptedException { 168 downloadInProgressLatch.await(); 169 } 170 171 /** Blocks the caller thread until the download has moved to the "Finishing" state. */ waitForDownloadCompleted()172 public void waitForDownloadCompleted() throws InterruptedException { 173 downloadFinishedLatch.await(); 174 } 175 176 /** 177 * Blocks the caller thread until the delegate downloader has been invoked. This method will never 178 * complete if no delegate was provided. 179 */ waitForDelegateStarted()180 public void waitForDelegateStarted() throws InterruptedException { 181 delegateInProgressLatch.await(); 182 } 183 184 /** 185 * Finishes the current download lifecycle. 186 * 187 * <p>The in progress latch is triggered if it hasn't yet been triggered to prevent a deadlock 188 * from occurring. 189 */ finishDownloading()190 public void finishDownloading() { 191 if (downloadInProgressLatch.getCount() > 0) { 192 downloadInProgressLatch.countDown(); 193 } 194 downloadFinishingLatch.countDown(); 195 } 196 197 /** 198 * Resets the current state of the download lifecycle. 199 * 200 * <p>An existing download cycle is finished before resetting the state to prevent a deadlock from 201 * occurring. 202 */ resetState()203 public void resetState() { 204 // Force a finish download if it was previously in progress to prevent deadlock. 205 if (downloadFinishedLatch.getCount() > 0) { 206 finishDownloading(); 207 } 208 downloadFinishedLatch.countDown(); 209 210 logger.atInfo().log("Reset State back to: Idle"); 211 downloadInProgressLatch = new CountDownLatch(1); 212 downloadFinishingLatch = new CountDownLatch(1); 213 downloadFinishedLatch = new CountDownLatch(1); 214 delegateInProgressLatch = new CountDownLatch(1); 215 } 216 } 217