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.file.common.testing; 17 18 import android.net.Uri; 19 import android.util.Pair; 20 import com.google.android.libraries.mobiledatadownload.file.backends.JavaFileBackend; 21 import com.google.android.libraries.mobiledatadownload.file.common.GcParam; 22 import com.google.android.libraries.mobiledatadownload.file.common.LockScope; 23 import com.google.android.libraries.mobiledatadownload.file.common.internal.ForwardingOutputStream; 24 import com.google.android.libraries.mobiledatadownload.file.spi.Backend; 25 import com.google.errorprone.annotations.concurrent.GuardedBy; 26 import java.io.Closeable; 27 import java.io.File; 28 import java.io.IOException; 29 import java.io.InputStream; 30 import java.io.OutputStream; 31 import java.util.EnumMap; 32 import java.util.Map; 33 import java.util.concurrent.CountDownLatch; 34 import org.checkerframework.checker.nullness.qual.Nullable; 35 36 /** A Fake Backend for testing. It allows overriding certain behavior. */ 37 public class FakeFileBackend implements Backend { 38 private final Backend delegate; 39 40 @GuardedBy("failureMap") 41 private final @Nullable Map<OperationType, IOException> failureMap = 42 new EnumMap<>(OperationType.class); 43 44 @GuardedBy("suspensionMap") 45 private final @Nullable Map<OperationType, CountDownLatch> suspensionMap = 46 new EnumMap<>(OperationType.class); 47 48 /** Available operation types. */ 49 public enum OperationType { 50 ALL, 51 READ, // openForRead, openForNativeRead 52 WRITE, // openForWrite, openForAppend 53 QUERY, // exists, isDirectory, fileSize, children, getGcParam, toFile 54 MANAGE, // delete, rename, createDirectory, setGcParam 55 WRITE_STREAM, // openForWrite/Append return streams that fail 56 EXISTS, // exists 57 } 58 59 /** 60 * Creates a {@link FakeFileBackend} that delegates to {@link JavaFileBackend} (file:// URIs). Use 61 * with {@link TemporaryUri} to avoid file path collisions. 62 */ FakeFileBackend()63 public FakeFileBackend() { 64 this(new JavaFileBackend()); 65 } 66 67 /** Creates a {@link FakeFileBackend} that delegates to {@code delegate}. */ FakeFileBackend(Backend delegate)68 public FakeFileBackend(Backend delegate) { 69 this.delegate = delegate; 70 } 71 setFailure(OperationType type, IOException ex)72 public void setFailure(OperationType type, IOException ex) { 73 synchronized (failureMap) { 74 if (type == OperationType.ALL) { 75 for (OperationType t : OperationType.values()) { 76 failureMap.put(t, ex); 77 } 78 } else { 79 failureMap.put(type, ex); 80 } 81 } 82 } 83 clearFailure(OperationType type)84 public void clearFailure(OperationType type) { 85 synchronized (failureMap) { 86 if (type == OperationType.ALL) { 87 failureMap.clear(); 88 } else { 89 failureMap.remove(type); 90 } 91 } 92 } 93 setSuspension(OperationType type)94 public void setSuspension(OperationType type) { 95 synchronized (suspensionMap) { 96 if (type == OperationType.ALL) { 97 for (OperationType t : OperationType.values()) { 98 suspensionMap.put(t, new CountDownLatch(1)); 99 } 100 } else { 101 suspensionMap.put(type, new CountDownLatch(1)); 102 } 103 } 104 } 105 clearSuspension(OperationType type)106 public void clearSuspension(OperationType type) { 107 synchronized (suspensionMap) { 108 if (type == OperationType.ALL) { 109 for (CountDownLatch latch : suspensionMap.values()) { 110 latch.countDown(); 111 } 112 suspensionMap.clear(); 113 } else if (suspensionMap.containsKey(type)) { 114 suspensionMap.get(type).countDown(); 115 suspensionMap.remove(type); 116 } 117 } 118 } 119 throwIf(OperationType type)120 private void throwIf(OperationType type) throws IOException { 121 IOException ioException; 122 synchronized (failureMap) { 123 ioException = failureMap.get(type); 124 } 125 if (ioException != null) { 126 throw ioException; 127 } 128 } 129 suspendIf(OperationType type)130 private void suspendIf(OperationType type) throws IOException { 131 CountDownLatch latch; 132 synchronized (suspensionMap) { 133 latch = suspensionMap.get(type); 134 } 135 if (latch != null) { 136 try { 137 latch.await(); 138 } catch (InterruptedException ex) { 139 Thread.currentThread().interrupt(); 140 throw new IOException( 141 "Thread interrupted while CountDownLatch for suspended operation is waiting ", ex); 142 } 143 } 144 } 145 throwOrSuspendIf(OperationType type)146 private void throwOrSuspendIf(OperationType type) throws IOException { 147 throwIf(type); 148 suspendIf(type); 149 } 150 151 @Override name()152 public String name() { 153 return delegate.name(); 154 } 155 156 @Override openForRead(Uri uri)157 public InputStream openForRead(Uri uri) throws IOException { 158 throwOrSuspendIf(OperationType.READ); 159 return delegate.openForRead(uri); 160 } 161 162 @Override openForNativeRead(Uri uri)163 public Pair<Uri, Closeable> openForNativeRead(Uri uri) throws IOException { 164 throwOrSuspendIf(OperationType.READ); 165 return delegate.openForNativeRead(uri); 166 } 167 168 @Override openForWrite(Uri uri)169 public OutputStream openForWrite(Uri uri) throws IOException { 170 throwOrSuspendIf(OperationType.WRITE); 171 OutputStream out = delegate.openForWrite(uri); 172 IOException ioException; 173 synchronized (failureMap) { 174 ioException = failureMap.get(OperationType.WRITE_STREAM); 175 } 176 if (ioException != null) { 177 return new FailingOutputStream(out, ioException); 178 } 179 return out; 180 } 181 182 @Override openForAppend(Uri uri)183 public OutputStream openForAppend(Uri uri) throws IOException { 184 throwOrSuspendIf(OperationType.WRITE); 185 OutputStream out = delegate.openForAppend(uri); 186 IOException ioException; 187 synchronized (failureMap) { 188 ioException = failureMap.get(OperationType.WRITE_STREAM); 189 } 190 if (ioException != null) { 191 return new FailingOutputStream(out, ioException); 192 } 193 return out; 194 } 195 196 @Override deleteFile(Uri uri)197 public void deleteFile(Uri uri) throws IOException { 198 throwOrSuspendIf(OperationType.MANAGE); 199 delegate.deleteFile(uri); 200 } 201 202 @Override deleteDirectory(Uri uri)203 public void deleteDirectory(Uri uri) throws IOException { 204 throwOrSuspendIf(OperationType.MANAGE); 205 delegate.deleteDirectory(uri); 206 } 207 208 @Override rename(Uri from, Uri to)209 public void rename(Uri from, Uri to) throws IOException { 210 throwOrSuspendIf(OperationType.MANAGE); 211 delegate.rename(from, to); 212 } 213 214 @Override exists(Uri uri)215 public boolean exists(Uri uri) throws IOException { 216 throwOrSuspendIf(OperationType.EXISTS); 217 throwOrSuspendIf(OperationType.QUERY); 218 return delegate.exists(uri); 219 } 220 221 @Override isDirectory(Uri uri)222 public boolean isDirectory(Uri uri) throws IOException { 223 throwOrSuspendIf(OperationType.QUERY); 224 return delegate.isDirectory(uri); 225 } 226 227 @Override createDirectory(Uri uri)228 public void createDirectory(Uri uri) throws IOException { 229 throwOrSuspendIf(OperationType.MANAGE); 230 delegate.createDirectory(uri); 231 } 232 233 @Override fileSize(Uri uri)234 public long fileSize(Uri uri) throws IOException { 235 throwOrSuspendIf(OperationType.QUERY); 236 return delegate.fileSize(uri); 237 } 238 239 @Override children(Uri parentUri)240 public Iterable<Uri> children(Uri parentUri) throws IOException { 241 throwOrSuspendIf(OperationType.QUERY); 242 return delegate.children(parentUri); 243 } 244 245 @Override getGcParam(Uri uri)246 public GcParam getGcParam(Uri uri) throws IOException { 247 throwOrSuspendIf(OperationType.QUERY); 248 return delegate.getGcParam(uri); 249 } 250 251 @Override setGcParam(Uri uri, GcParam param)252 public void setGcParam(Uri uri, GcParam param) throws IOException { 253 throwOrSuspendIf(OperationType.MANAGE); 254 delegate.setGcParam(uri, param); 255 } 256 257 @Override toFile(Uri uri)258 public File toFile(Uri uri) throws IOException { 259 throwOrSuspendIf(OperationType.QUERY); 260 return delegate.toFile(uri); 261 } 262 263 @Override lockScope()264 public LockScope lockScope() throws IOException { 265 return delegate.lockScope(); 266 } 267 268 static class FailingOutputStream extends ForwardingOutputStream { 269 private final IOException exception; 270 FailingOutputStream(OutputStream delegate, IOException exception)271 FailingOutputStream(OutputStream delegate, IOException exception) { 272 super(delegate); 273 this.exception = exception; 274 } 275 276 @Override write(byte[] b)277 public void write(byte[] b) throws IOException { 278 throw exception; 279 } 280 281 @Override write(byte[] b, int off, int len)282 public void write(byte[] b, int off, int len) throws IOException { 283 throw exception; 284 } 285 } 286 } 287