• 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.openers;
17 
18 import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.createFile;
19 import static com.google.common.truth.Truth.assertThat;
20 import static java.nio.charset.StandardCharsets.UTF_8;
21 import static org.junit.Assert.assertThrows;
22 
23 import android.net.Uri;
24 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
25 import com.google.android.libraries.mobiledatadownload.file.backends.JavaFileBackend;
26 import com.google.android.libraries.mobiledatadownload.file.behaviors.SyncingBehavior;
27 import com.google.android.libraries.mobiledatadownload.file.common.testing.TemporaryUri;
28 import com.google.android.libraries.mobiledatadownload.file.common.testing.WritesThrowTransform;
29 import com.google.android.libraries.mobiledatadownload.file.transforms.CompressTransform;
30 import com.google.android.libraries.mobiledatadownload.file.transforms.TransformProtoFragments;
31 import com.google.common.base.Ascii;
32 import com.google.common.io.ByteStreams;
33 import com.google.mobiledatadownload.TransformProto;
34 import java.io.DataInputStream;
35 import java.io.DataOutputStream;
36 import java.io.IOException;
37 import java.io.InputStream;
38 import java.io.OutputStream;
39 import java.util.Arrays;
40 import java.util.concurrent.CountDownLatch;
41 import org.junit.Rule;
42 import org.junit.Test;
43 import org.junit.runner.RunWith;
44 import org.mockito.Mockito;
45 import org.robolectric.RobolectricTestRunner;
46 
47 @RunWith(RobolectricTestRunner.class)
48 public final class StreamMutationOpenerTest {
49 
50   @Rule public TemporaryUri tmpUri = new TemporaryUri();
51 
storageWithTransform()52   public SynchronousFileStorage storageWithTransform() throws Exception {
53     return new SynchronousFileStorage(
54         Arrays.asList(new JavaFileBackend()),
55         Arrays.asList(new CompressTransform(), new WritesThrowTransform()));
56   }
57 
58   @Test
okIfFileDoesNotExist()59   public void okIfFileDoesNotExist() throws Exception {
60     SynchronousFileStorage storage = storageWithTransform();
61     Uri dirUri = tmpUri.newDirectoryUri();
62     Uri uri = dirUri.buildUpon().appendPath("testfile").build();
63     String content = "content";
64 
65     assertThat(storage.children(dirUri)).isEmpty();
66     try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
67       mutator.mutate(
68           (InputStream in, OutputStream out) -> {
69             byte[] read = ByteStreams.toByteArray(in);
70             assertThat(read).hasLength(0);
71             out.write(content.getBytes(UTF_8));
72             return true;
73           });
74     }
75 
76     ReadByteArrayOpener opener = ReadByteArrayOpener.create();
77     String actual = new String(storage.open(uri, opener), UTF_8);
78 
79     assertThat(actual).isEqualTo(content);
80   }
81 
82   @Test
willFailToOverwriteDirectory()83   public void willFailToOverwriteDirectory() throws Exception {
84     SynchronousFileStorage storage = storageWithTransform();
85     Uri uri = tmpUri.newDirectoryUri();
86     String content = "content";
87 
88     try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
89       assertThrows(
90           IOException.class,
91           () ->
92               mutator.mutate(
93                   (InputStream in, OutputStream out) -> {
94                     out.write(content.getBytes(UTF_8));
95                     return true;
96                   }));
97     }
98   }
99 
100   @Test
canMutate()101   public void canMutate() throws Exception {
102     SynchronousFileStorage storage = storageWithTransform();
103     Uri uri = tmpUri.newUri();
104     String content = "content";
105     String expected = Ascii.toUpperCase(content);
106     createFile(storage, uri, content);
107 
108     try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
109       mutator.mutate(
110           (InputStream in, OutputStream out) -> {
111             String read = new String(ByteStreams.toByteArray(in), UTF_8);
112             out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
113             return true;
114           });
115     }
116 
117     ReadByteArrayOpener opener = ReadByteArrayOpener.create();
118     String actual = new String(storage.open(uri, opener), UTF_8);
119 
120     assertThat(actual).isEqualTo(expected);
121   }
122 
123   @Test
canMutate_butNotCommit()124   public void canMutate_butNotCommit() throws Exception {
125     SynchronousFileStorage storage = storageWithTransform();
126     Uri uri = tmpUri.newUri();
127     String content = "content";
128     createFile(storage, uri, content);
129 
130     try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
131       mutator.mutate(
132           (InputStream in, OutputStream out) -> {
133             String read = new String(ByteStreams.toByteArray(in), UTF_8);
134             out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
135             return false;
136           });
137     }
138 
139     ReadByteArrayOpener opener = ReadByteArrayOpener.create();
140     String actual = new String(storage.open(uri, opener), UTF_8);
141 
142     assertThat(actual).isEqualTo(content); // Unchanged.
143   }
144 
145   @Test
canMutate_repeatedly()146   public void canMutate_repeatedly() throws Exception {
147     SynchronousFileStorage storage = storageWithTransform();
148     Uri uri = tmpUri.newUri();
149     String content = "content";
150     String expected = "TNETNOC";
151     createFile(storage, uri, content);
152 
153     try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
154       mutator.mutate(
155           (InputStream in, OutputStream out) -> {
156             String read = new String(ByteStreams.toByteArray(in), UTF_8);
157             out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
158             return true;
159           });
160       mutator.mutate(
161           (InputStream in, OutputStream out) -> {
162             String read = new String(ByteStreams.toByteArray(in), UTF_8);
163             out.write(new StringBuilder(read).reverse().toString().getBytes(UTF_8));
164             return true;
165           });
166     }
167 
168     ReadByteArrayOpener opener = ReadByteArrayOpener.create();
169     String actual = new String(storage.open(uri, opener), UTF_8);
170 
171     assertThat(actual).isEqualTo(expected);
172   }
173 
174   @Test
canMutate_withSync()175   public void canMutate_withSync() throws Exception {
176     SynchronousFileStorage storage = storageWithTransform();
177     Uri uri = tmpUri.newUri();
178     String content = "content";
179     String expected = Ascii.toUpperCase(content);
180     storage.open(uri, WriteStringOpener.create(content));
181 
182     SyncingBehavior syncing = Mockito.spy(new SyncingBehavior());
183     try (StreamMutationOpener.Mutator mutator =
184         storage.open(uri, StreamMutationOpener.create().withBehaviors(syncing))) {
185       mutator.mutate(
186           (InputStream in, OutputStream out) -> {
187             String read = new String(ByteStreams.toByteArray(in), UTF_8);
188             out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
189             return true;
190           });
191     }
192     Mockito.verify(syncing).sync();
193 
194     String actual = storage.open(uri, ReadStringOpener.create());
195     assertThat(actual).isEqualTo(expected);
196   }
197 
198   @Test
okIfFileDoesNotExist_withExclusiveLock()199   public void okIfFileDoesNotExist_withExclusiveLock() throws Exception {
200     SynchronousFileStorage storage = storageWithTransform();
201     Uri dirUri = tmpUri.newDirectoryUri();
202     Uri uri = dirUri.buildUpon().appendPath("testfile").build();
203     String content = "content";
204 
205     LockFileOpener locking = LockFileOpener.createExclusive();
206     try (StreamMutationOpener.Mutator mutator =
207         storage.open(uri, StreamMutationOpener.create().withLocking(locking))) {
208       mutator.mutate(
209           (InputStream in, OutputStream out) -> {
210             assertThat(storage.open(uri, LockFileOpener.createExclusive().nonBlocking(true)))
211                 .isNull();
212             assertThat(storage.open(uri, LockFileOpener.createReadOnlyShared().nonBlocking(true)))
213                 .isNull();
214             byte[] read = ByteStreams.toByteArray(in);
215             assertThat(read).hasLength(0);
216             out.write(content.getBytes(UTF_8));
217             return true;
218           });
219     }
220 
221     ReadByteArrayOpener opener = ReadByteArrayOpener.create();
222     String actual = new String(storage.open(uri, opener), UTF_8);
223 
224     assertThat(actual).isEqualTo(content);
225   }
226 
227   @Test
canMutate_withExclusiveLock()228   public void canMutate_withExclusiveLock() throws Exception {
229     SynchronousFileStorage storage = storageWithTransform();
230     Uri uri = tmpUri.newUri();
231     String content = "content";
232     String expected = Ascii.toUpperCase(content);
233     createFile(storage, uri, content);
234 
235     LockFileOpener locking = LockFileOpener.createExclusive();
236     try (StreamMutationOpener.Mutator mutator =
237         storage.open(uri, StreamMutationOpener.create().withLocking(locking))) {
238       mutator.mutate(
239           (InputStream in, OutputStream out) -> {
240             assertThat(storage.open(uri, LockFileOpener.createExclusive().nonBlocking(true)))
241                 .isNull();
242             assertThat(storage.open(uri, LockFileOpener.createReadOnlyShared().nonBlocking(true)))
243                 .isNull();
244             String read = new String(ByteStreams.toByteArray(in), UTF_8);
245             out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
246             return true;
247           });
248     }
249 
250     ReadByteArrayOpener opener = ReadByteArrayOpener.create();
251     String actual = new String(storage.open(uri, opener), UTF_8);
252 
253     assertThat(actual).isEqualTo(expected);
254   }
255 
256   @Test
rollsBack_afterIOException()257   public void rollsBack_afterIOException() throws Exception {
258     SynchronousFileStorage storage = storageWithTransform();
259     Uri dirUri = tmpUri.newDirectoryUri();
260     Uri uri = dirUri.buildUpon().appendPath("testfile").build();
261     String content = "content";
262     createFile(storage, uri, content);
263 
264     assertThat(storage.children(dirUri)).hasSize(1);
265 
266     Uri uriForPartialWrite =
267         uri.buildUpon().encodedFragment("transform=writethrows(write_length=1)").build();
268     try (StreamMutationOpener.Mutator mutator =
269         storage.open(uriForPartialWrite, StreamMutationOpener.create())) {
270       mutator.mutate(
271           (InputStream in, OutputStream out) -> {
272             assertThat(storage.children(dirUri)).hasSize(2);
273             String read = new String(ByteStreams.toByteArray(in), UTF_8);
274             out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
275             throw new IOException("something went wrong");
276           });
277     } catch (IOException ex) {
278       // Ignore.
279     }
280     assertThat(storage.children(dirUri)).hasSize(1);
281 
282     ReadByteArrayOpener opener = ReadByteArrayOpener.create();
283     String actual = new String(storage.open(uri, opener), UTF_8);
284 
285     assertThat(actual).isEqualTo(content); // Still original content.
286   }
287 
288   @Test
rollsBack_afterRuntimeException()289   public void rollsBack_afterRuntimeException() throws Exception {
290     SynchronousFileStorage storage = storageWithTransform();
291     Uri dirUri = tmpUri.newDirectoryUri();
292     Uri uri = dirUri.buildUpon().appendPath("testfile").build();
293     String content = "content";
294     createFile(storage, uri, content);
295 
296     assertThat(storage.children(dirUri)).hasSize(1);
297     try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
298       mutator.mutate(
299           (InputStream in, OutputStream out) -> {
300             assertThat(storage.children(dirUri)).hasSize(2);
301             String read = new String(ByteStreams.toByteArray(in), UTF_8);
302             out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
303             throw new RuntimeException("something went wrong");
304           });
305     } catch (IOException ex) {
306       // Ignore RuntimeException wrapped in IOException.
307     }
308     assertThat(storage.children(dirUri)).hasSize(1);
309 
310     ReadByteArrayOpener opener = ReadByteArrayOpener.create();
311     String actual = new String(storage.open(uri, opener), UTF_8);
312 
313     assertThat(actual).isEqualTo(content); // Still original content.
314   }
315 
316   @Test
okIfStreamsAreWrapped()317   public void okIfStreamsAreWrapped() throws Exception {
318     SynchronousFileStorage storage = storageWithTransform();
319     Uri uri = tmpUri.newUri();
320 
321     // Write path
322     try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
323       mutator.mutate(
324           (InputStream in, OutputStream out) -> {
325             try (DataOutputStream dos = new DataOutputStream(out)) {
326               dos.writeLong(42);
327             }
328             return true;
329           });
330     }
331 
332     // Read path (slightly-overloaded use of StreamMutationOpener, since we're not doing a mutation)
333     try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
334       mutator.mutate(
335           (InputStream in, OutputStream out) -> {
336             try (DataInputStream dis = new DataInputStream(in)) {
337               assertThat(dis.readLong()).isEqualTo(42);
338             }
339             return true;
340           });
341     }
342   }
343 
344   @Test
canMutate_withTransforms()345   public void canMutate_withTransforms() throws Exception {
346     SynchronousFileStorage storage = storageWithTransform();
347     Uri dirUri = tmpUri.newDirectoryUri();
348     Uri uri =
349         TransformProtoFragments.addOrReplaceTransform(
350             dirUri.buildUpon().appendPath("testfile").build(),
351             TransformProto.Transform.newBuilder()
352                 .setCompress(TransformProto.CompressTransform.getDefaultInstance())
353                 .build());
354 
355     String content = "content";
356     String expected = Ascii.toUpperCase(content);
357     createFile(storage, uri, content);
358 
359     try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
360       mutator.mutate(
361           (InputStream in, OutputStream out) -> {
362             String read = new String(ByteStreams.toByteArray(in), UTF_8);
363             byte[] plaintext = Ascii.toUpperCase(read).getBytes(UTF_8);
364             out.write(plaintext);
365             out.flush();
366 
367             // Check that the tmpfile is compressed.
368             Uri tmp = null;
369             for (Uri childUri : storage.children(dirUri)) {
370               if (childUri.getPath().contains(".mobstore_tmp")) {
371                 tmp = childUri;
372                 break;
373               }
374             }
375             assertThat(tmp).isNotNull();
376             byte[] compressed = storage.open(tmp, ReadByteArrayOpener.create());
377             assertThat(compressed.length).isGreaterThan(0);
378             assertThat(compressed).isNotEqualTo(plaintext);
379             return true;
380           });
381     }
382 
383     ReadByteArrayOpener opener = ReadByteArrayOpener.create();
384     String actual = new String(storage.open(uri, opener), UTF_8);
385 
386     assertThat(actual).isEqualTo(expected);
387   }
388 
389   @Test
multiThreadWithoutLock_lacksIsolation()390   public void multiThreadWithoutLock_lacksIsolation() throws Exception {
391     SynchronousFileStorage storage = storageWithTransform();
392     Uri uri = tmpUri.newUri();
393     CountDownLatch latch = new CountDownLatch(1);
394     CountDownLatch latch2 = new CountDownLatch(1);
395     CountDownLatch latch3 = new CountDownLatch(1);
396     Thread thread =
397         new Thread(
398             () -> {
399               try (StreamMutationOpener.Mutator mutator =
400                   storage.open(uri, StreamMutationOpener.create())) {
401                 mutator.mutate(
402                     (InputStream in, OutputStream out) -> {
403                       latch.countDown();
404                       out.write("other-thread".getBytes(UTF_8));
405                       try {
406                         latch2.await();
407                       } catch (InterruptedException ex) {
408                         throw new IOException(ex);
409                       }
410                       return true;
411                     });
412                 latch3.countDown();
413               } catch (Exception ex) {
414                 throw new RuntimeException(ex);
415               }
416             });
417     thread.setDaemon(true);
418     thread.start();
419     latch.await();
420 
421     try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
422       mutator.mutate(
423           (InputStream in, OutputStream out) -> {
424             out.write("this-thread".getBytes(UTF_8));
425             return true;
426           });
427     }
428 
429     ReadByteArrayOpener opener = ReadByteArrayOpener.create();
430     assertThat(new String(storage.open(uri, opener), UTF_8)).isEqualTo("this-thread");
431 
432     latch2.countDown();
433     latch3.await();
434 
435     assertThat(new String(storage.open(uri, opener), UTF_8)).isEqualTo("other-thread");
436   }
437 }
438