• 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.makeContentThatExceedsOsBufferSize;
19 import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.readFileFromSource;
20 import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.writeFileToSink;
21 import static com.google.common.truth.Truth.assertThat;
22 import static org.junit.Assert.assertThrows;
23 
24 import android.content.Context;
25 import android.net.Uri;
26 import android.os.Process;
27 import android.system.Os;
28 import android.system.OsConstants;
29 import androidx.test.core.app.ApplicationProvider;
30 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
31 import com.google.android.libraries.mobiledatadownload.file.backends.FileUri;
32 import com.google.android.libraries.mobiledatadownload.file.backends.JavaFileBackend;
33 import com.google.android.libraries.mobiledatadownload.file.common.UnsupportedFileStorageOperation;
34 import com.google.android.libraries.mobiledatadownload.file.common.testing.AlwaysThrowsTransform;
35 import com.google.android.libraries.mobiledatadownload.file.common.testing.FileDescriptorLeakChecker;
36 import com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils;
37 import com.google.android.libraries.mobiledatadownload.file.samples.ByteCountingMonitor;
38 import com.google.android.libraries.mobiledatadownload.file.transforms.CompressTransform;
39 import com.google.android.libraries.mobiledatadownload.file.transforms.TransformProtos;
40 import com.google.common.collect.ImmutableList;
41 import java.io.File;
42 import java.io.FileInputStream;
43 import java.io.IOException;
44 import java.io.InputStream;
45 import java.util.concurrent.ExecutorService;
46 import java.util.concurrent.Executors;
47 import org.junit.Before;
48 import org.junit.Rule;
49 import org.junit.Test;
50 import org.junit.rules.TemporaryFolder;
51 import org.junit.runner.RunWith;
52 import org.junit.runners.JUnit4;
53 
54 @RunWith(JUnit4.class)
55 public final class ReadFileOpenerAndroidTest {
56 
57   private final String smallContent = "content";
58   private final String bigContent = makeContentThatExceedsOsBufferSize();
59   private SynchronousFileStorage storage;
60   private ExecutorService executor = Executors.newCachedThreadPool();
61   private final Context context = ApplicationProvider.getApplicationContext();
62 
63   @Rule public TemporaryFolder tmpFolder = new TemporaryFolder();
64   @Rule public FileDescriptorLeakChecker leakChecker = new FileDescriptorLeakChecker();
65 
66   @Before
setUpStorage()67   public void setUpStorage() throws Exception {
68     storage =
69         new SynchronousFileStorage(
70             ImmutableList.of(new JavaFileBackend()),
71             ImmutableList.of(new CompressTransform(), new AlwaysThrowsTransform()));
72   }
73 
74   @Test
compressAndReadBigContentFromPipe()75   public void compressAndReadBigContentFromPipe() throws Exception {
76     Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
77     writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
78     ReadFileOpener opener =
79         ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context);
80     File piped = storage.open(uri, opener);
81     assertThat(piped.getAbsolutePath()).endsWith(".fifo");
82     try (FileInputStream in = new FileInputStream(piped)) {
83       assertThat(readFileFromSource(in)).isEqualTo(bigContent);
84     }
85     assertThat(piped.exists()).isFalse();
86   }
87 
88   @Test
compressAndReadSmallContentFromPipe()89   public void compressAndReadSmallContentFromPipe() throws Exception {
90     Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
91     writeFileToSink(storage.open(uri, WriteStreamOpener.create()), smallContent);
92     ReadFileOpener opener =
93         ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context);
94     File piped = storage.open(uri, opener);
95     try (FileInputStream in = new FileInputStream(piped)) {
96       assertThat(readFileFromSource(in)).isEqualTo(smallContent);
97     }
98     assertThat(piped.exists()).isFalse();
99   }
100 
101   @Test
compressWithPartialReadFromPipe_shouldNotLeak()102   public void compressWithPartialReadFromPipe_shouldNotLeak() throws Exception {
103     Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
104     writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
105     ReadFileOpener opener =
106         ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context);
107     File piped = storage.open(uri, opener);
108     assertThat(piped.getAbsolutePath()).endsWith(".fifo");
109     try (InputStream in = new FileInputStream(piped)) {
110       in.read(); // Just read 1 byte.
111     }
112     assertThrows(IOException.class, () -> opener.waitForPump());
113     assertThat(piped.exists()).isFalse();
114   }
115 
116   @Test
compressAndReadFromPipeWithoutExecutor_shouldFail()117   public void compressAndReadFromPipeWithoutExecutor_shouldFail() throws Exception {
118     Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
119     writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
120     assertThrows(IOException.class, () -> storage.open(uri, ReadFileOpener.create()));
121   }
122 
123   @Test
readFromPlainFile()124   public void readFromPlainFile() throws Exception {
125     Uri uri = uriToNewTempFile().build(); // No transforms.
126     writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
127     File direct =
128         storage.open(
129             uri, ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
130     try (FileInputStream in = new FileInputStream(direct)) {
131       assertThat(direct.getAbsolutePath()).startsWith(tmpFolder.getRoot().toString());
132       assertThat(readFileFromSource(in)).isEqualTo(bigContent);
133     }
134     assertThat(direct.exists()).isTrue();
135   }
136 
137   @Test
readingFromPipeWithException_shouldReturnEmptyPipe()138   public void readingFromPipeWithException_shouldReturnEmptyPipe() throws Exception {
139     // A previous implementation had a race condition where it was possible to read from
140     // an unrelated file descriptor if an exception was thrown in background pump thread.
141     FileUri.Builder uriBuilder = uriToNewTempFile();
142     writeFileToSink(storage.open(uriBuilder.build(), WriteStreamOpener.create()), bigContent);
143     ReadFileOpener opener =
144         ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context);
145     File file =
146         storage.open(
147             uriBuilder.build().buildUpon().encodedFragment("transform=alwaysthrows").build(),
148             opener);
149     try (FileInputStream in = new FileInputStream(file)) {
150       assertThat(readFileFromSource(in)).isEmpty();
151     }
152     assertThrows(IOException.class, () -> opener.waitForPump());
153     assertThat(file.exists()).isFalse();
154   }
155 
156   @Test
multipleStreams_shouldCreateMultipleFifos()157   public void multipleStreams_shouldCreateMultipleFifos() throws Exception {
158     Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
159     writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
160     File piped0 =
161         storage.open(
162             uri, ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
163     File piped1 =
164         storage.open(
165             uri, ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
166     File piped2 =
167         storage.open(
168             uri, ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
169     assertThat(piped0.getAbsolutePath()).endsWith("-0.fifo");
170     assertThat(piped1.getAbsolutePath()).endsWith("-1.fifo");
171     assertThat(piped2.getAbsolutePath()).endsWith("-2.fifo");
172     try (FileInputStream in0 = new FileInputStream(piped0);
173         FileInputStream in1 = new FileInputStream(piped1);
174         FileInputStream in2 = new FileInputStream(piped2)) {
175       assertThat(readFileFromSource(in2)).isEqualTo(bigContent);
176       assertThat(readFileFromSource(in0)).isEqualTo(bigContent);
177       assertThat(readFileFromSource(in1)).isEqualTo(bigContent);
178     }
179     assertThat(piped0.exists()).isFalse();
180     assertThat(piped1.exists()).isFalse();
181     assertThat(piped2.exists()).isFalse();
182   }
183 
184   @Test
staleFifo_isDeletedAndReplaced()185   public void staleFifo_isDeletedAndReplaced() throws Exception {
186     Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
187     writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
188     ReadFileOpener opener =
189         ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context);
190     String staleFifoName = ".mobstore-ReadFileOpener-" + Process.myPid() + "-0.fifo";
191     File staleFifo = new File(context.getCacheDir(), staleFifoName);
192     Os.mkfifo(staleFifo.getAbsolutePath(), OsConstants.S_IRUSR | OsConstants.S_IWUSR);
193 
194     File piped = storage.open(uri, opener);
195     assertThat(piped).isEqualTo(staleFifo);
196     try (FileInputStream in = new FileInputStream(piped)) {
197       assertThat(readFileFromSource(in)).isEqualTo(bigContent);
198     }
199     assertThat(piped.exists()).isFalse();
200   }
201 
202   @Test
shortCircuit_succeedsWithSimplePath()203   public void shortCircuit_succeedsWithSimplePath() throws Exception {
204     Uri uri = uriToNewTempFile().build();
205     writeFileToSink(storage.open(uri, WriteStreamOpener.create()), smallContent);
206     ReadFileOpener opener = ReadFileOpener.create().withShortCircuit();
207     File file = storage.open(uri, opener);
208     assertThat(readFileFromSource(new FileInputStream(file))).isEqualTo(smallContent);
209   }
210 
211   @Test
shortCircuit_isRejectedWithTransforms()212   public void shortCircuit_isRejectedWithTransforms() throws Exception {
213     Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
214     ReadFileOpener opener = ReadFileOpener.create().withShortCircuit();
215     assertThrows(UnsupportedFileStorageOperation.class, () -> storage.open(uri, opener));
216   }
217 
218   @Test
shortCircuit_succeedsWithMonitors()219   public void shortCircuit_succeedsWithMonitors() throws Exception {
220     SynchronousFileStorage storageWithMonitor =
221         new SynchronousFileStorage(
222             ImmutableList.of(new JavaFileBackend()),
223             ImmutableList.of(),
224             ImmutableList.of(new ByteCountingMonitor()));
225     Uri uri = uriToNewTempFile().build();
226     byte[] content = StreamUtils.makeArrayOfBytesContent();
227     StreamUtils.createFile(storageWithMonitor, uri, content);
228 
229     ReadFileOpener opener = ReadFileOpener.create().withShortCircuit();
230     File file = storageWithMonitor.open(uri, opener);
231     assertThat(StreamUtils.readFileInBytesFromSource(new FileInputStream(file))).isEqualTo(content);
232   }
233 
234   // TODO(b/69319355): replace with TemporaryUri
uriToNewTempFile()235   private FileUri.Builder uriToNewTempFile() throws Exception {
236     return FileUri.builder().fromFile(tmpFolder.newFile());
237   }
238 }
239