blob: 8626f5b5dfb2a607c7834620173fe3774be0bbb5 [file] [log] [blame]
/*
* Copyright 2022 Google LLC
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package com.google.android.libraries.mobiledatadownload.file.openers;
import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.makeContentThatExceedsOsBufferSize;
import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.readFileFromSource;
import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.writeFileToSink;
import static com.google.common.truth.Truth.assertThat;
import static org.junit.Assert.assertThrows;
import android.content.Context;
import android.net.Uri;
import android.os.Process;
import android.system.Os;
import android.system.OsConstants;
import androidx.test.core.app.ApplicationProvider;
import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
import com.google.android.libraries.mobiledatadownload.file.backends.FileUri;
import com.google.android.libraries.mobiledatadownload.file.backends.JavaFileBackend;
import com.google.android.libraries.mobiledatadownload.file.common.UnsupportedFileStorageOperation;
import com.google.android.libraries.mobiledatadownload.file.common.testing.AlwaysThrowsTransform;
import com.google.android.libraries.mobiledatadownload.file.common.testing.FileDescriptorLeakChecker;
import com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils;
import com.google.android.libraries.mobiledatadownload.file.samples.ByteCountingMonitor;
import com.google.android.libraries.mobiledatadownload.file.transforms.CompressTransform;
import com.google.android.libraries.mobiledatadownload.file.transforms.TransformProtos;
import com.google.common.collect.ImmutableList;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import org.junit.Before;
import org.junit.Rule;
import org.junit.Test;
import org.junit.rules.TemporaryFolder;
import org.junit.runner.RunWith;
import org.junit.runners.JUnit4;
@RunWith(JUnit4.class)
public final class ReadFileOpenerAndroidTest {
private final String smallContent = "content";
private final String bigContent = makeContentThatExceedsOsBufferSize();
private SynchronousFileStorage storage;
private ExecutorService executor = Executors.newCachedThreadPool();
private final Context context = ApplicationProvider.getApplicationContext();
@Rule public TemporaryFolder tmpFolder = new TemporaryFolder();
@Rule public FileDescriptorLeakChecker leakChecker = new FileDescriptorLeakChecker();
@Before
public void setUpStorage() throws Exception {
storage =
new SynchronousFileStorage(
ImmutableList.of(new JavaFileBackend()),
ImmutableList.of(new CompressTransform(), new AlwaysThrowsTransform()));
}
@Test
public void compressAndReadBigContentFromPipe() throws Exception {
Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
ReadFileOpener opener =
ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context);
File piped = storage.open(uri, opener);
assertThat(piped.getAbsolutePath()).endsWith(".fifo");
try (FileInputStream in = new FileInputStream(piped)) {
assertThat(readFileFromSource(in)).isEqualTo(bigContent);
}
assertThat(piped.exists()).isFalse();
}
@Test
public void compressAndReadSmallContentFromPipe() throws Exception {
Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
writeFileToSink(storage.open(uri, WriteStreamOpener.create()), smallContent);
ReadFileOpener opener =
ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context);
File piped = storage.open(uri, opener);
try (FileInputStream in = new FileInputStream(piped)) {
assertThat(readFileFromSource(in)).isEqualTo(smallContent);
}
assertThat(piped.exists()).isFalse();
}
@Test
public void compressWithPartialReadFromPipe_shouldNotLeak() throws Exception {
Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
ReadFileOpener opener =
ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context);
File piped = storage.open(uri, opener);
assertThat(piped.getAbsolutePath()).endsWith(".fifo");
try (InputStream in = new FileInputStream(piped)) {
in.read(); // Just read 1 byte.
}
assertThrows(IOException.class, () -> opener.waitForPump());
assertThat(piped.exists()).isFalse();
}
@Test
public void compressAndReadFromPipeWithoutExecutor_shouldFail() throws Exception {
Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
assertThrows(IOException.class, () -> storage.open(uri, ReadFileOpener.create()));
}
@Test
public void readFromPlainFile() throws Exception {
Uri uri = uriToNewTempFile().build(); // No transforms.
writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
File direct =
storage.open(
uri, ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
try (FileInputStream in = new FileInputStream(direct)) {
assertThat(direct.getAbsolutePath()).startsWith(tmpFolder.getRoot().toString());
assertThat(readFileFromSource(in)).isEqualTo(bigContent);
}
assertThat(direct.exists()).isTrue();
}
@Test
public void readingFromPipeWithException_shouldReturnEmptyPipe() throws Exception {
// A previous implementation had a race condition where it was possible to read from
// an unrelated file descriptor if an exception was thrown in background pump thread.
FileUri.Builder uriBuilder = uriToNewTempFile();
writeFileToSink(storage.open(uriBuilder.build(), WriteStreamOpener.create()), bigContent);
ReadFileOpener opener =
ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context);
File file =
storage.open(
uriBuilder.build().buildUpon().encodedFragment("transform=alwaysthrows").build(),
opener);
try (FileInputStream in = new FileInputStream(file)) {
assertThat(readFileFromSource(in)).isEmpty();
}
assertThrows(IOException.class, () -> opener.waitForPump());
assertThat(file.exists()).isFalse();
}
@Test
public void multipleStreams_shouldCreateMultipleFifos() throws Exception {
Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
File piped0 =
storage.open(
uri, ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
File piped1 =
storage.open(
uri, ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
File piped2 =
storage.open(
uri, ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context));
assertThat(piped0.getAbsolutePath()).endsWith("-0.fifo");
assertThat(piped1.getAbsolutePath()).endsWith("-1.fifo");
assertThat(piped2.getAbsolutePath()).endsWith("-2.fifo");
try (FileInputStream in0 = new FileInputStream(piped0);
FileInputStream in1 = new FileInputStream(piped1);
FileInputStream in2 = new FileInputStream(piped2)) {
assertThat(readFileFromSource(in2)).isEqualTo(bigContent);
assertThat(readFileFromSource(in0)).isEqualTo(bigContent);
assertThat(readFileFromSource(in1)).isEqualTo(bigContent);
}
assertThat(piped0.exists()).isFalse();
assertThat(piped1.exists()).isFalse();
assertThat(piped2.exists()).isFalse();
}
@Test
public void staleFifo_isDeletedAndReplaced() throws Exception {
Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
writeFileToSink(storage.open(uri, WriteStreamOpener.create()), bigContent);
ReadFileOpener opener =
ReadFileOpener.create().withFallbackToPipeUsingExecutor(executor, context);
String staleFifoName = ".mobstore-ReadFileOpener-" + Process.myPid() + "-0.fifo";
File staleFifo = new File(context.getCacheDir(), staleFifoName);
Os.mkfifo(staleFifo.getAbsolutePath(), OsConstants.S_IRUSR | OsConstants.S_IWUSR);
File piped = storage.open(uri, opener);
assertThat(piped).isEqualTo(staleFifo);
try (FileInputStream in = new FileInputStream(piped)) {
assertThat(readFileFromSource(in)).isEqualTo(bigContent);
}
assertThat(piped.exists()).isFalse();
}
@Test
public void shortCircuit_succeedsWithSimplePath() throws Exception {
Uri uri = uriToNewTempFile().build();
writeFileToSink(storage.open(uri, WriteStreamOpener.create()), smallContent);
ReadFileOpener opener = ReadFileOpener.create().withShortCircuit();
File file = storage.open(uri, opener);
assertThat(readFileFromSource(new FileInputStream(file))).isEqualTo(smallContent);
}
@Test
public void shortCircuit_isRejectedWithTransforms() throws Exception {
Uri uri = uriToNewTempFile().withTransform(TransformProtos.DEFAULT_COMPRESS_SPEC).build();
ReadFileOpener opener = ReadFileOpener.create().withShortCircuit();
assertThrows(UnsupportedFileStorageOperation.class, () -> storage.open(uri, opener));
}
@Test
public void shortCircuit_succeedsWithMonitors() throws Exception {
SynchronousFileStorage storageWithMonitor =
new SynchronousFileStorage(
ImmutableList.of(new JavaFileBackend()),
ImmutableList.of(),
ImmutableList.of(new ByteCountingMonitor()));
Uri uri = uriToNewTempFile().build();
byte[] content = StreamUtils.makeArrayOfBytesContent();
StreamUtils.createFile(storageWithMonitor, uri, content);
ReadFileOpener opener = ReadFileOpener.create().withShortCircuit();
File file = storageWithMonitor.open(uri, opener);
assertThat(StreamUtils.readFileInBytesFromSource(new FileInputStream(file))).isEqualTo(content);
}
// TODO(b/69319355): replace with TemporaryUri
private FileUri.Builder uriToNewTempFile() throws Exception {
return FileUri.builder().fromFile(tmpFolder.newFile());
}
}