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.createFile; 19 import static com.google.common.truth.Truth.assertThat; 20 import static java.nio.charset.StandardCharsets.UTF_8; 21 import static org.junit.Assert.assertThrows; 22 23 import android.net.Uri; 24 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 25 import com.google.android.libraries.mobiledatadownload.file.backends.JavaFileBackend; 26 import com.google.android.libraries.mobiledatadownload.file.behaviors.SyncingBehavior; 27 import com.google.android.libraries.mobiledatadownload.file.common.testing.TemporaryUri; 28 import com.google.android.libraries.mobiledatadownload.file.common.testing.WritesThrowTransform; 29 import com.google.android.libraries.mobiledatadownload.file.transforms.CompressTransform; 30 import com.google.android.libraries.mobiledatadownload.file.transforms.TransformProtoFragments; 31 import com.google.common.base.Ascii; 32 import com.google.common.io.ByteStreams; 33 import com.google.mobiledatadownload.TransformProto; 34 import java.io.DataInputStream; 35 import java.io.DataOutputStream; 36 import java.io.IOException; 37 import java.io.InputStream; 38 import java.io.OutputStream; 39 import java.util.Arrays; 40 import java.util.concurrent.CountDownLatch; 41 import org.junit.Rule; 42 import org.junit.Test; 43 import org.junit.runner.RunWith; 44 import org.mockito.Mockito; 45 import org.robolectric.RobolectricTestRunner; 46 47 @RunWith(RobolectricTestRunner.class) 48 public final class StreamMutationOpenerTest { 49 50 @Rule public TemporaryUri tmpUri = new TemporaryUri(); 51 storageWithTransform()52 public SynchronousFileStorage storageWithTransform() throws Exception { 53 return new SynchronousFileStorage( 54 Arrays.asList(new JavaFileBackend()), 55 Arrays.asList(new CompressTransform(), new WritesThrowTransform())); 56 } 57 58 @Test okIfFileDoesNotExist()59 public void okIfFileDoesNotExist() throws Exception { 60 SynchronousFileStorage storage = storageWithTransform(); 61 Uri dirUri = tmpUri.newDirectoryUri(); 62 Uri uri = dirUri.buildUpon().appendPath("testfile").build(); 63 String content = "content"; 64 65 assertThat(storage.children(dirUri)).isEmpty(); 66 try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) { 67 mutator.mutate( 68 (InputStream in, OutputStream out) -> { 69 byte[] read = ByteStreams.toByteArray(in); 70 assertThat(read).hasLength(0); 71 out.write(content.getBytes(UTF_8)); 72 return true; 73 }); 74 } 75 76 ReadByteArrayOpener opener = ReadByteArrayOpener.create(); 77 String actual = new String(storage.open(uri, opener), UTF_8); 78 79 assertThat(actual).isEqualTo(content); 80 } 81 82 @Test willFailToOverwriteDirectory()83 public void willFailToOverwriteDirectory() throws Exception { 84 SynchronousFileStorage storage = storageWithTransform(); 85 Uri uri = tmpUri.newDirectoryUri(); 86 String content = "content"; 87 88 try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) { 89 assertThrows( 90 IOException.class, 91 () -> 92 mutator.mutate( 93 (InputStream in, OutputStream out) -> { 94 out.write(content.getBytes(UTF_8)); 95 return true; 96 })); 97 } 98 } 99 100 @Test canMutate()101 public void canMutate() throws Exception { 102 SynchronousFileStorage storage = storageWithTransform(); 103 Uri uri = tmpUri.newUri(); 104 String content = "content"; 105 String expected = Ascii.toUpperCase(content); 106 createFile(storage, uri, content); 107 108 try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) { 109 mutator.mutate( 110 (InputStream in, OutputStream out) -> { 111 String read = new String(ByteStreams.toByteArray(in), UTF_8); 112 out.write(Ascii.toUpperCase(read).getBytes(UTF_8)); 113 return true; 114 }); 115 } 116 117 ReadByteArrayOpener opener = ReadByteArrayOpener.create(); 118 String actual = new String(storage.open(uri, opener), UTF_8); 119 120 assertThat(actual).isEqualTo(expected); 121 } 122 123 @Test canMutate_butNotCommit()124 public void canMutate_butNotCommit() throws Exception { 125 SynchronousFileStorage storage = storageWithTransform(); 126 Uri uri = tmpUri.newUri(); 127 String content = "content"; 128 createFile(storage, uri, content); 129 130 try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) { 131 mutator.mutate( 132 (InputStream in, OutputStream out) -> { 133 String read = new String(ByteStreams.toByteArray(in), UTF_8); 134 out.write(Ascii.toUpperCase(read).getBytes(UTF_8)); 135 return false; 136 }); 137 } 138 139 ReadByteArrayOpener opener = ReadByteArrayOpener.create(); 140 String actual = new String(storage.open(uri, opener), UTF_8); 141 142 assertThat(actual).isEqualTo(content); // Unchanged. 143 } 144 145 @Test canMutate_repeatedly()146 public void canMutate_repeatedly() throws Exception { 147 SynchronousFileStorage storage = storageWithTransform(); 148 Uri uri = tmpUri.newUri(); 149 String content = "content"; 150 String expected = "TNETNOC"; 151 createFile(storage, uri, content); 152 153 try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) { 154 mutator.mutate( 155 (InputStream in, OutputStream out) -> { 156 String read = new String(ByteStreams.toByteArray(in), UTF_8); 157 out.write(Ascii.toUpperCase(read).getBytes(UTF_8)); 158 return true; 159 }); 160 mutator.mutate( 161 (InputStream in, OutputStream out) -> { 162 String read = new String(ByteStreams.toByteArray(in), UTF_8); 163 out.write(new StringBuilder(read).reverse().toString().getBytes(UTF_8)); 164 return true; 165 }); 166 } 167 168 ReadByteArrayOpener opener = ReadByteArrayOpener.create(); 169 String actual = new String(storage.open(uri, opener), UTF_8); 170 171 assertThat(actual).isEqualTo(expected); 172 } 173 174 @Test canMutate_withSync()175 public void canMutate_withSync() throws Exception { 176 SynchronousFileStorage storage = storageWithTransform(); 177 Uri uri = tmpUri.newUri(); 178 String content = "content"; 179 String expected = Ascii.toUpperCase(content); 180 storage.open(uri, WriteStringOpener.create(content)); 181 182 SyncingBehavior syncing = Mockito.spy(new SyncingBehavior()); 183 try (StreamMutationOpener.Mutator mutator = 184 storage.open(uri, StreamMutationOpener.create().withBehaviors(syncing))) { 185 mutator.mutate( 186 (InputStream in, OutputStream out) -> { 187 String read = new String(ByteStreams.toByteArray(in), UTF_8); 188 out.write(Ascii.toUpperCase(read).getBytes(UTF_8)); 189 return true; 190 }); 191 } 192 Mockito.verify(syncing).sync(); 193 194 String actual = storage.open(uri, ReadStringOpener.create()); 195 assertThat(actual).isEqualTo(expected); 196 } 197 198 @Test okIfFileDoesNotExist_withExclusiveLock()199 public void okIfFileDoesNotExist_withExclusiveLock() throws Exception { 200 SynchronousFileStorage storage = storageWithTransform(); 201 Uri dirUri = tmpUri.newDirectoryUri(); 202 Uri uri = dirUri.buildUpon().appendPath("testfile").build(); 203 String content = "content"; 204 205 LockFileOpener locking = LockFileOpener.createExclusive(); 206 try (StreamMutationOpener.Mutator mutator = 207 storage.open(uri, StreamMutationOpener.create().withLocking(locking))) { 208 mutator.mutate( 209 (InputStream in, OutputStream out) -> { 210 assertThat(storage.open(uri, LockFileOpener.createExclusive().nonBlocking(true))) 211 .isNull(); 212 assertThat(storage.open(uri, LockFileOpener.createReadOnlyShared().nonBlocking(true))) 213 .isNull(); 214 byte[] read = ByteStreams.toByteArray(in); 215 assertThat(read).hasLength(0); 216 out.write(content.getBytes(UTF_8)); 217 return true; 218 }); 219 } 220 221 ReadByteArrayOpener opener = ReadByteArrayOpener.create(); 222 String actual = new String(storage.open(uri, opener), UTF_8); 223 224 assertThat(actual).isEqualTo(content); 225 } 226 227 @Test canMutate_withExclusiveLock()228 public void canMutate_withExclusiveLock() throws Exception { 229 SynchronousFileStorage storage = storageWithTransform(); 230 Uri uri = tmpUri.newUri(); 231 String content = "content"; 232 String expected = Ascii.toUpperCase(content); 233 createFile(storage, uri, content); 234 235 LockFileOpener locking = LockFileOpener.createExclusive(); 236 try (StreamMutationOpener.Mutator mutator = 237 storage.open(uri, StreamMutationOpener.create().withLocking(locking))) { 238 mutator.mutate( 239 (InputStream in, OutputStream out) -> { 240 assertThat(storage.open(uri, LockFileOpener.createExclusive().nonBlocking(true))) 241 .isNull(); 242 assertThat(storage.open(uri, LockFileOpener.createReadOnlyShared().nonBlocking(true))) 243 .isNull(); 244 String read = new String(ByteStreams.toByteArray(in), UTF_8); 245 out.write(Ascii.toUpperCase(read).getBytes(UTF_8)); 246 return true; 247 }); 248 } 249 250 ReadByteArrayOpener opener = ReadByteArrayOpener.create(); 251 String actual = new String(storage.open(uri, opener), UTF_8); 252 253 assertThat(actual).isEqualTo(expected); 254 } 255 256 @Test rollsBack_afterIOException()257 public void rollsBack_afterIOException() throws Exception { 258 SynchronousFileStorage storage = storageWithTransform(); 259 Uri dirUri = tmpUri.newDirectoryUri(); 260 Uri uri = dirUri.buildUpon().appendPath("testfile").build(); 261 String content = "content"; 262 createFile(storage, uri, content); 263 264 assertThat(storage.children(dirUri)).hasSize(1); 265 266 Uri uriForPartialWrite = 267 uri.buildUpon().encodedFragment("transform=writethrows(write_length=1)").build(); 268 try (StreamMutationOpener.Mutator mutator = 269 storage.open(uriForPartialWrite, StreamMutationOpener.create())) { 270 mutator.mutate( 271 (InputStream in, OutputStream out) -> { 272 assertThat(storage.children(dirUri)).hasSize(2); 273 String read = new String(ByteStreams.toByteArray(in), UTF_8); 274 out.write(Ascii.toUpperCase(read).getBytes(UTF_8)); 275 throw new IOException("something went wrong"); 276 }); 277 } catch (IOException ex) { 278 // Ignore. 279 } 280 assertThat(storage.children(dirUri)).hasSize(1); 281 282 ReadByteArrayOpener opener = ReadByteArrayOpener.create(); 283 String actual = new String(storage.open(uri, opener), UTF_8); 284 285 assertThat(actual).isEqualTo(content); // Still original content. 286 } 287 288 @Test rollsBack_afterRuntimeException()289 public void rollsBack_afterRuntimeException() throws Exception { 290 SynchronousFileStorage storage = storageWithTransform(); 291 Uri dirUri = tmpUri.newDirectoryUri(); 292 Uri uri = dirUri.buildUpon().appendPath("testfile").build(); 293 String content = "content"; 294 createFile(storage, uri, content); 295 296 assertThat(storage.children(dirUri)).hasSize(1); 297 try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) { 298 mutator.mutate( 299 (InputStream in, OutputStream out) -> { 300 assertThat(storage.children(dirUri)).hasSize(2); 301 String read = new String(ByteStreams.toByteArray(in), UTF_8); 302 out.write(Ascii.toUpperCase(read).getBytes(UTF_8)); 303 throw new RuntimeException("something went wrong"); 304 }); 305 } catch (IOException ex) { 306 // Ignore RuntimeException wrapped in IOException. 307 } 308 assertThat(storage.children(dirUri)).hasSize(1); 309 310 ReadByteArrayOpener opener = ReadByteArrayOpener.create(); 311 String actual = new String(storage.open(uri, opener), UTF_8); 312 313 assertThat(actual).isEqualTo(content); // Still original content. 314 } 315 316 @Test okIfStreamsAreWrapped()317 public void okIfStreamsAreWrapped() throws Exception { 318 SynchronousFileStorage storage = storageWithTransform(); 319 Uri uri = tmpUri.newUri(); 320 321 // Write path 322 try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) { 323 mutator.mutate( 324 (InputStream in, OutputStream out) -> { 325 try (DataOutputStream dos = new DataOutputStream(out)) { 326 dos.writeLong(42); 327 } 328 return true; 329 }); 330 } 331 332 // Read path (slightly-overloaded use of StreamMutationOpener, since we're not doing a mutation) 333 try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) { 334 mutator.mutate( 335 (InputStream in, OutputStream out) -> { 336 try (DataInputStream dis = new DataInputStream(in)) { 337 assertThat(dis.readLong()).isEqualTo(42); 338 } 339 return true; 340 }); 341 } 342 } 343 344 @Test canMutate_withTransforms()345 public void canMutate_withTransforms() throws Exception { 346 SynchronousFileStorage storage = storageWithTransform(); 347 Uri dirUri = tmpUri.newDirectoryUri(); 348 Uri uri = 349 TransformProtoFragments.addOrReplaceTransform( 350 dirUri.buildUpon().appendPath("testfile").build(), 351 TransformProto.Transform.newBuilder() 352 .setCompress(TransformProto.CompressTransform.getDefaultInstance()) 353 .build()); 354 355 String content = "content"; 356 String expected = Ascii.toUpperCase(content); 357 createFile(storage, uri, content); 358 359 try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) { 360 mutator.mutate( 361 (InputStream in, OutputStream out) -> { 362 String read = new String(ByteStreams.toByteArray(in), UTF_8); 363 byte[] plaintext = Ascii.toUpperCase(read).getBytes(UTF_8); 364 out.write(plaintext); 365 out.flush(); 366 367 // Check that the tmpfile is compressed. 368 Uri tmp = null; 369 for (Uri childUri : storage.children(dirUri)) { 370 if (childUri.getPath().contains(".mobstore_tmp")) { 371 tmp = childUri; 372 break; 373 } 374 } 375 assertThat(tmp).isNotNull(); 376 byte[] compressed = storage.open(tmp, ReadByteArrayOpener.create()); 377 assertThat(compressed.length).isGreaterThan(0); 378 assertThat(compressed).isNotEqualTo(plaintext); 379 return true; 380 }); 381 } 382 383 ReadByteArrayOpener opener = ReadByteArrayOpener.create(); 384 String actual = new String(storage.open(uri, opener), UTF_8); 385 386 assertThat(actual).isEqualTo(expected); 387 } 388 389 @Test multiThreadWithoutLock_lacksIsolation()390 public void multiThreadWithoutLock_lacksIsolation() throws Exception { 391 SynchronousFileStorage storage = storageWithTransform(); 392 Uri uri = tmpUri.newUri(); 393 CountDownLatch latch = new CountDownLatch(1); 394 CountDownLatch latch2 = new CountDownLatch(1); 395 CountDownLatch latch3 = new CountDownLatch(1); 396 Thread thread = 397 new Thread( 398 () -> { 399 try (StreamMutationOpener.Mutator mutator = 400 storage.open(uri, StreamMutationOpener.create())) { 401 mutator.mutate( 402 (InputStream in, OutputStream out) -> { 403 latch.countDown(); 404 out.write("other-thread".getBytes(UTF_8)); 405 try { 406 latch2.await(); 407 } catch (InterruptedException ex) { 408 throw new IOException(ex); 409 } 410 return true; 411 }); 412 latch3.countDown(); 413 } catch (Exception ex) { 414 throw new RuntimeException(ex); 415 } 416 }); 417 thread.setDaemon(true); 418 thread.start(); 419 latch.await(); 420 421 try (StreamMutationOpener.Mutator mutator = storage.open(uri, StreamMutationOpener.create())) { 422 mutator.mutate( 423 (InputStream in, OutputStream out) -> { 424 out.write("this-thread".getBytes(UTF_8)); 425 return true; 426 }); 427 } 428 429 ReadByteArrayOpener opener = ReadByteArrayOpener.create(); 430 assertThat(new String(storage.open(uri, opener), UTF_8)).isEqualTo("this-thread"); 431 432 latch2.countDown(); 433 latch3.await(); 434 435 assertThat(new String(storage.open(uri, opener), UTF_8)).isEqualTo("other-thread"); 436 } 437 } 438