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