• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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