blob: 7992b770769148f21f00bd9e4f60f15a128e8a70 [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.createFile;
import static com.google.common.truth.Truth.assertThat;
import static java.nio.charset.StandardCharsets.UTF_8;
import static org.junit.Assert.assertThrows;
import android.net.Uri;
import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage;
import com.google.android.libraries.mobiledatadownload.file.backends.JavaFileBackend;
import com.google.android.libraries.mobiledatadownload.file.behaviors.SyncingBehavior;
import com.google.android.libraries.mobiledatadownload.file.common.testing.TemporaryUri;
import com.google.android.libraries.mobiledatadownload.file.common.testing.WritesThrowTransform;
import com.google.android.libraries.mobiledatadownload.file.transforms.CompressTransform;
import com.google.android.libraries.mobiledatadownload.file.transforms.TransformProtoFragments;
import com.google.common.base.Ascii;
import com.google.common.io.ByteStreams;
import com.google.mobiledatadownload.TransformProto;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.util.Arrays;
import java.util.concurrent.CountDownLatch;
import org.junit.Rule;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.mockito.Mockito;
import org.robolectric.RobolectricTestRunner;
@RunWith(RobolectricTestRunner.class)
public final class StreamMutationOpenerTest {
@Rule public TemporaryUri tmpUri = new TemporaryUri();
public SynchronousFileStorage storageWithTransform() throws Exception {
return new SynchronousFileStorage(
Arrays.asList(new JavaFileBackend()),
Arrays.asList(new CompressTransform(), new WritesThrowTransform()));
}
@Test
public void okIfFileDoesNotExist() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri dirUri = tmpUri.newDirectoryUri();
Uri uri = dirUri.buildUpon().appendPath("testfile").build();
String content = "content";
assertThat(storage.children(dirUri)).isEmpty();
try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
byte[] read = ByteStreams.toByteArray(in);
assertThat(read).hasLength(0);
out.write(content.getBytes(UTF_8));
return true;
});
}
ReadByteArrayOpener opener = ReadByteArrayOpener.create();
String actual = new String(storage.open(uri, opener), UTF_8);
assertThat(actual).isEqualTo(content);
}
@Test
public void willFailToOverwriteDirectory() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri uri = tmpUri.newDirectoryUri();
String content = "content";
try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
assertThrows(
IOException.class,
() ->
mutator.mutate(
(InputStream in, OutputStream out) -> {
out.write(content.getBytes(UTF_8));
return true;
}));
}
}
@Test
public void canMutate() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri uri = tmpUri.newUri();
String content = "content";
String expected = Ascii.toUpperCase(content);
createFile(storage, uri, content);
try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
String read = new String(ByteStreams.toByteArray(in), UTF_8);
out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
return true;
});
}
ReadByteArrayOpener opener = ReadByteArrayOpener.create();
String actual = new String(storage.open(uri, opener), UTF_8);
assertThat(actual).isEqualTo(expected);
}
@Test
public void canMutate_butNotCommit() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri uri = tmpUri.newUri();
String content = "content";
createFile(storage, uri, content);
try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
String read = new String(ByteStreams.toByteArray(in), UTF_8);
out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
return false;
});
}
ReadByteArrayOpener opener = ReadByteArrayOpener.create();
String actual = new String(storage.open(uri, opener), UTF_8);
assertThat(actual).isEqualTo(content); // Unchanged.
}
@Test
public void canMutate_repeatedly() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri uri = tmpUri.newUri();
String content = "content";
String expected = "TNETNOC";
createFile(storage, uri, content);
try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
String read = new String(ByteStreams.toByteArray(in), UTF_8);
out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
return true;
});
mutator.mutate(
(InputStream in, OutputStream out) -> {
String read = new String(ByteStreams.toByteArray(in), UTF_8);
out.write(new StringBuilder(read).reverse().toString().getBytes(UTF_8));
return true;
});
}
ReadByteArrayOpener opener = ReadByteArrayOpener.create();
String actual = new String(storage.open(uri, opener), UTF_8);
assertThat(actual).isEqualTo(expected);
}
@Test
public void canMutate_withSync() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri uri = tmpUri.newUri();
String content = "content";
String expected = Ascii.toUpperCase(content);
storage.open(uri, WriteStringOpener.create(content));
SyncingBehavior syncing = Mockito.spy(new SyncingBehavior());
try (StreamMutationOpener.Mutator mutator =
storage.open(uri, StreamMutationOpener.create().withBehaviors(syncing))) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
String read = new String(ByteStreams.toByteArray(in), UTF_8);
out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
return true;
});
}
Mockito.verify(syncing).sync();
String actual = storage.open(uri, ReadStringOpener.create());
assertThat(actual).isEqualTo(expected);
}
@Test
public void okIfFileDoesNotExist_withExclusiveLock() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri dirUri = tmpUri.newDirectoryUri();
Uri uri = dirUri.buildUpon().appendPath("testfile").build();
String content = "content";
LockFileOpener locking = LockFileOpener.createExclusive();
try (StreamMutationOpener.Mutator mutator =
storage.open(uri, StreamMutationOpener.create().withLocking(locking))) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
assertThat(storage.open(uri, LockFileOpener.createExclusive().nonBlocking(true)))
.isNull();
assertThat(storage.open(uri, LockFileOpener.createReadOnlyShared().nonBlocking(true)))
.isNull();
byte[] read = ByteStreams.toByteArray(in);
assertThat(read).hasLength(0);
out.write(content.getBytes(UTF_8));
return true;
});
}
ReadByteArrayOpener opener = ReadByteArrayOpener.create();
String actual = new String(storage.open(uri, opener), UTF_8);
assertThat(actual).isEqualTo(content);
}
@Test
public void canMutate_withExclusiveLock() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri uri = tmpUri.newUri();
String content = "content";
String expected = Ascii.toUpperCase(content);
createFile(storage, uri, content);
LockFileOpener locking = LockFileOpener.createExclusive();
try (StreamMutationOpener.Mutator mutator =
storage.open(uri, StreamMutationOpener.create().withLocking(locking))) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
assertThat(storage.open(uri, LockFileOpener.createExclusive().nonBlocking(true)))
.isNull();
assertThat(storage.open(uri, LockFileOpener.createReadOnlyShared().nonBlocking(true)))
.isNull();
String read = new String(ByteStreams.toByteArray(in), UTF_8);
out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
return true;
});
}
ReadByteArrayOpener opener = ReadByteArrayOpener.create();
String actual = new String(storage.open(uri, opener), UTF_8);
assertThat(actual).isEqualTo(expected);
}
@Test
public void rollsBack_afterIOException() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri dirUri = tmpUri.newDirectoryUri();
Uri uri = dirUri.buildUpon().appendPath("testfile").build();
String content = "content";
createFile(storage, uri, content);
assertThat(storage.children(dirUri)).hasSize(1);
Uri uriForPartialWrite =
uri.buildUpon().encodedFragment("transform=writethrows(write_length=1)").build();
try (StreamMutationOpener.Mutator mutator =
storage.open(uriForPartialWrite, StreamMutationOpener.create())) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
assertThat(storage.children(dirUri)).hasSize(2);
String read = new String(ByteStreams.toByteArray(in), UTF_8);
out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
throw new IOException("something went wrong");
});
} catch (IOException ex) {
// Ignore.
}
assertThat(storage.children(dirUri)).hasSize(1);
ReadByteArrayOpener opener = ReadByteArrayOpener.create();
String actual = new String(storage.open(uri, opener), UTF_8);
assertThat(actual).isEqualTo(content); // Still original content.
}
@Test
public void rollsBack_afterRuntimeException() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri dirUri = tmpUri.newDirectoryUri();
Uri uri = dirUri.buildUpon().appendPath("testfile").build();
String content = "content";
createFile(storage, uri, content);
assertThat(storage.children(dirUri)).hasSize(1);
try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
assertThat(storage.children(dirUri)).hasSize(2);
String read = new String(ByteStreams.toByteArray(in), UTF_8);
out.write(Ascii.toUpperCase(read).getBytes(UTF_8));
throw new RuntimeException("something went wrong");
});
} catch (IOException ex) {
// Ignore RuntimeException wrapped in IOException.
}
assertThat(storage.children(dirUri)).hasSize(1);
ReadByteArrayOpener opener = ReadByteArrayOpener.create();
String actual = new String(storage.open(uri, opener), UTF_8);
assertThat(actual).isEqualTo(content); // Still original content.
}
@Test
public void okIfStreamsAreWrapped() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri uri = tmpUri.newUri();
// Write path
try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
try (DataOutputStream dos = new DataOutputStream(out)) {
dos.writeLong(42);
}
return true;
});
}
// Read path (slightly-overloaded use of StreamMutationOpener, since we're not doing a mutation)
try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
try (DataInputStream dis = new DataInputStream(in)) {
assertThat(dis.readLong()).isEqualTo(42);
}
return true;
});
}
}
@Test
public void canMutate_withTransforms() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri dirUri = tmpUri.newDirectoryUri();
Uri uri =
TransformProtoFragments.addOrReplaceTransform(
dirUri.buildUpon().appendPath("testfile").build(),
TransformProto.Transform.newBuilder()
.setCompress(TransformProto.CompressTransform.getDefaultInstance())
.build());
String content = "content";
String expected = Ascii.toUpperCase(content);
createFile(storage, uri, content);
try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
String read = new String(ByteStreams.toByteArray(in), UTF_8);
byte[] plaintext = Ascii.toUpperCase(read).getBytes(UTF_8);
out.write(plaintext);
out.flush();
// Check that the tmpfile is compressed.
Uri tmp = null;
for (Uri childUri : storage.children(dirUri)) {
if (childUri.getPath().contains(".mobstore_tmp")) {
tmp = childUri;
break;
}
}
assertThat(tmp).isNotNull();
byte[] compressed = storage.open(tmp, ReadByteArrayOpener.create());
assertThat(compressed.length).isGreaterThan(0);
assertThat(compressed).isNotEqualTo(plaintext);
return true;
});
}
ReadByteArrayOpener opener = ReadByteArrayOpener.create();
String actual = new String(storage.open(uri, opener), UTF_8);
assertThat(actual).isEqualTo(expected);
}
@Test
public void multiThreadWithoutLock_lacksIsolation() throws Exception {
SynchronousFileStorage storage = storageWithTransform();
Uri uri = tmpUri.newUri();
CountDownLatch latch = new CountDownLatch(1);
CountDownLatch latch2 = new CountDownLatch(1);
CountDownLatch latch3 = new CountDownLatch(1);
Thread thread =
new Thread(
() -> {
try (StreamMutationOpener.Mutator mutator =
storage.open(uri, StreamMutationOpener.create())) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
latch.countDown();
out.write("other-thread".getBytes(UTF_8));
try {
latch2.await();
} catch (InterruptedException ex) {
throw new IOException(ex);
}
return true;
});
latch3.countDown();
} catch (Exception ex) {
throw new RuntimeException(ex);
}
});
thread.setDaemon(true);
thread.start();
latch.await();
try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) {
mutator.mutate(
(InputStream in, OutputStream out) -> {
out.write("this-thread".getBytes(UTF_8));
return true;
});
}
ReadByteArrayOpener opener = ReadByteArrayOpener.create();
assertThat(new String(storage.open(uri, opener), UTF_8)).isEqualTo("this-thread");
latch2.countDown();
latch3.await();
assertThat(new String(storage.open(uri, opener), UTF_8)).isEqualTo("other-thread");
}
}