• 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.FileConvertible;
34 import com.google.android.libraries.mobiledatadownload.file.common.internal.ForwardingOutputStream;
35 import com.google.android.libraries.mobiledatadownload.file.common.testing.AlwaysThrowsTransform;
36 import com.google.android.libraries.mobiledatadownload.file.common.testing.FileDescriptorLeakChecker;
37 import com.google.android.libraries.mobiledatadownload.file.spi.Backend;
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.FileOutputStream;
44 import java.io.IOException;
45 import java.io.OutputStream;
46 import java.util.concurrent.ExecutorService;
47 import java.util.concurrent.Executors;
48 import org.junit.Before;
49 import org.junit.Rule;
50 import org.junit.Test;
51 import org.junit.rules.TemporaryFolder;
52 import org.junit.runner.RunWith;
53 import org.junit.runners.JUnit4;
54 
55 @RunWith(JUnit4.class)
56 public final class WriteFileOpenerAndroidTest {
57 
58   private final String bigContent = makeContentThatExceedsOsBufferSize();
59   private final String smallContent = "content";
60   private SynchronousFileStorage storage;
61   private ExecutorService executor = Executors.newCachedThreadPool();
62   private final Context context = ApplicationProvider.getApplicationContext();
63 
64   @Rule public TemporaryFolder tmpFolder = new TemporaryFolder();
65   @Rule public FileDescriptorLeakChecker leakChecker = new FileDescriptorLeakChecker();
66 
67   @Before
setUpStorage()68   public void setUpStorage() throws Exception {
69     storage =
70         new SynchronousFileStorage(
71             ImmutableList.of(new JavaFileBackend()),
72             ImmutableList.of(new CompressTransform(), new AlwaysThrowsTransform()));
73   }
74 
75   @Test
compressAndWriteToPipe()76   public void compressAndWriteToPipe() throws Exception {
77     Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
78     File pipedFile;
79     try (WriteFileOpener.FileCloser piped =
80         storage.open(
81             uri, WriteFileOpener.create().withFallbackToPipeUsingExecutor(executor, context))) {
82       pipedFile = piped.file();
83       assertThat(pipedFile.getAbsolutePath()).endsWith(".fifo");
84       writeFileToSink(new FileOutputStream(pipedFile), bigContent);
85     }
86     assertThat(readFileFromSource(storage.open(uri, ReadStreamOpener.create())))
87         .isEqualTo(bigContent);
88     assertThat(pipedFile.exists()).isFalse();
89   }
90 
91   @Test
compressButDontWriteToPipe_shouldNotLeak()92   public void compressButDontWriteToPipe_shouldNotLeak() throws Exception {
93     Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
94     File pipedFile;
95     try (WriteFileOpener.FileCloser piped =
96         storage.open(
97             uri, WriteFileOpener.create().withFallbackToPipeUsingExecutor(executor, context))) {
98       pipedFile = piped.file();
99       assertThat(pipedFile.getAbsolutePath()).endsWith(".fifo");
100     }
101     assertThat(pipedFile.exists()).isFalse();
102   }
103 
104   @Test
staleFifo_isDeletedAndReplaced()105   public void staleFifo_isDeletedAndReplaced() throws Exception {
106     Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
107 
108     String staleFifoName = ".mobstore-WriteFileOpener-" + Process.myPid() + "-0.fifo";
109     File staleFifo = new File(context.getCacheDir(), staleFifoName);
110     Os.mkfifo(staleFifo.getAbsolutePath(), OsConstants.S_IRUSR | OsConstants.S_IWUSR);
111 
112     File pipedFile;
113     try (WriteFileOpener.FileCloser piped =
114         storage.open(
115             uri, WriteFileOpener.create().withFallbackToPipeUsingExecutor(executor, context))) {
116       pipedFile = piped.file();
117       assertThat(pipedFile).isEqualTo(staleFifo);
118       writeFileToSink(new FileOutputStream(pipedFile), bigContent);
119     }
120 
121     assertThat(readFileFromSource(storage.open(uri, ReadStreamOpener.create())))
122         .isEqualTo(bigContent);
123     assertThat(staleFifo.exists()).isFalse();
124   }
125 
126   @Test
multipleStreams_shouldCreateMultipleFifos()127   public void multipleStreams_shouldCreateMultipleFifos() throws Exception {
128     Uri uri0 = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
129     Uri uri1 = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
130     Uri uri2 = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
131 
132     WriteFileOpener.FileCloser piped0 =
133         storage.open(
134             uri0, WriteFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
135     WriteFileOpener.FileCloser piped1 =
136         storage.open(
137             uri1, WriteFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
138     WriteFileOpener.FileCloser piped2 =
139         storage.open(
140             uri2, WriteFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
141 
142     assertThat(piped0.file().getAbsolutePath()).endsWith("-0.fifo");
143     assertThat(piped1.file().getAbsolutePath()).endsWith("-1.fifo");
144     assertThat(piped2.file().getAbsolutePath()).endsWith("-2.fifo");
145 
146     writeFileToSink(new FileOutputStream(piped0.file()), bigContent + "0");
147     writeFileToSink(new FileOutputStream(piped1.file()), bigContent + "1");
148     writeFileToSink(new FileOutputStream(piped2.file()), bigContent + "2");
149 
150     piped0.close();
151     piped1.close();
152     piped2.close();
153 
154     assertThat(readFileFromSource(storage.open(uri0, ReadStreamOpener.create())))
155         .isEqualTo(bigContent + "0");
156     assertThat(readFileFromSource(storage.open(uri1, ReadStreamOpener.create())))
157         .isEqualTo(bigContent + "1");
158     assertThat(readFileFromSource(storage.open(uri2, ReadStreamOpener.create())))
159         .isEqualTo(bigContent + "2");
160 
161     assertThat(piped0.file().exists()).isFalse();
162     assertThat(piped1.file().exists()).isFalse();
163     assertThat(piped2.file().exists()).isFalse();
164   }
165 
166   @Test
compressAndWriteToPipeWithoutExecutor_shouldFail()167   public void compressAndWriteToPipeWithoutExecutor_shouldFail() throws Exception {
168     Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
169     assertThrows(IOException.class, () -> storage.open(uri, WriteFileOpener.create()));
170   }
171 
172   @Test
writeBigContentWithException_shouldThrowEPipeAndPropagate()173   public void writeBigContentWithException_shouldThrowEPipeAndPropagate() throws Exception {
174     Uri uri =
175         uriToNewTempFile().build().buildUpon().encodedFragment("transform=alwaysthrows").build();
176     WriteFileOpener.FileCloser piped =
177         storage.open(
178             uri, WriteFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
179     // Throws EPIPE while writing.
180     assertThrows(
181         IOException.class, () -> writeFileToSink(new FileOutputStream(piped.file()), bigContent));
182     // Throws underlying exception when closing.
183     assertThrows(IOException.class, () -> piped.close());
184     assertThat(piped.file().exists()).isFalse();
185   }
186 
187   @Test
writeSmallContentWithException_shouldPropagate()188   public void writeSmallContentWithException_shouldPropagate() throws Exception {
189     Uri uri =
190         uriToNewTempFile().build().buildUpon().encodedFragment("transform=alwaysthrows").build();
191     WriteFileOpener.FileCloser piped =
192         storage.open(
193             uri, WriteFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
194     // Small content is buffered and pump failure is is not visible.
195     writeFileToSink(new FileOutputStream(piped.file()), smallContent);
196     // Throws underlying exception when closing.
197     assertThrows(IOException.class, () -> piped.close());
198     assertThat(piped.file().exists()).isFalse();
199   }
200 
201   @Test
writeToPlainFile()202   public void writeToPlainFile() throws Exception {
203     Uri uri = uriToNewTempFile().build(); // No transforms.
204     try (WriteFileOpener.FileCloser direct =
205         storage.open(
206             uri, WriteFileOpener.create().withFallbackToPipeUsingExecutor(executor, context))) {
207       assertThat(direct.file().getAbsolutePath()).startsWith(tmpFolder.getRoot().toString());
208       writeFileToSink(new FileOutputStream(direct.file()), bigContent);
209       assertThat(readFileFromSource(storage.open(uri, ReadStreamOpener.create())))
210           .isEqualTo(bigContent);
211     }
212   }
213 
214   @Test
writeToPlainFile_shouldNotPrematurelyCloseStream()215   public void writeToPlainFile_shouldNotPrematurelyCloseStream() throws Exception {
216     // No transforms, write to stub test backend
217     storage = new SynchronousFileStorage(ImmutableList.of(new BufferingBackend()));
218     File file = tmpFolder.newFile();
219     Uri uri = Uri.parse("buffer:///" + file.getAbsolutePath());
220 
221     try (WriteFileOpener.FileCloser direct = storage.open(uri, WriteFileOpener.create())) {
222       writeFileToSink(new FileOutputStream(direct.file()), bigContent);
223     }
224     assertThat(readFileFromSource(new FileInputStream(file))).isEqualTo(bigContent);
225   }
226 
uriToNewTempFile()227   private FileUri.Builder uriToNewTempFile() throws Exception {
228     return FileUri.builder().fromFile(tmpFolder.newFile());
229   }
230 
231   /** A backend that uses temporary files to buffer IO operations. */
232   private static class BufferingBackend implements Backend {
233     @Override
name()234     public String name() {
235       return "buffer";
236     }
237 
238     @Override
openForWrite(Uri uri)239     public OutputStream openForWrite(Uri uri) throws IOException {
240       File tempFile = new File(uri.getPath() + ".tmp");
241       File finalFile = new File(uri.getPath());
242       return new BufferingOutputStream(new FileOutputStream(tempFile), tempFile, finalFile);
243     }
244 
245     private static class BufferingOutputStream extends ForwardingOutputStream
246         implements FileConvertible {
247       private final File tempFile;
248       private final File finalFile;
249 
BufferingOutputStream(OutputStream stream, File tempFile, File finalFile)250       BufferingOutputStream(OutputStream stream, File tempFile, File finalFile) {
251         super(stream);
252         this.tempFile = tempFile;
253         this.finalFile = finalFile;
254       }
255 
256       @Override
toFile()257       public File toFile() {
258         return tempFile;
259       }
260 
261       @Override
close()262       public void close() throws IOException {
263         out.close();
264         tempFile.renameTo(finalFile);
265       }
266     }
267   }
268 }
269