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.common.testing; 17 18 import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.appendFile; 19 import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.createFile; 20 import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.makeArrayOfBytesContent; 21 import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.readFileInBytes; 22 import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.readFileInBytesFromSource; 23 import static com.google.android.libraries.mobiledatadownload.file.common.testing.StreamUtils.writeFileToSink; 24 import static com.google.common.truth.Truth.assertThat; 25 import static org.junit.Assert.assertThrows; 26 import static org.junit.Assume.assumeTrue; 27 28 import android.net.Uri; 29 import com.google.android.libraries.mobiledatadownload.file.SynchronousFileStorage; 30 import com.google.android.libraries.mobiledatadownload.file.common.FileConvertible; 31 import com.google.android.libraries.mobiledatadownload.file.common.MalformedUriException; 32 import com.google.android.libraries.mobiledatadownload.file.openers.AppendStreamOpener; 33 import com.google.android.libraries.mobiledatadownload.file.openers.ReadStreamOpener; 34 import com.google.android.libraries.mobiledatadownload.file.openers.WriteStreamOpener; 35 import com.google.android.libraries.mobiledatadownload.file.spi.Backend; 36 import com.google.common.collect.ImmutableList; 37 import com.google.common.collect.Lists; 38 import com.google.common.primitives.Bytes; 39 import java.io.File; 40 import java.io.FileInputStream; 41 import java.io.FileNotFoundException; 42 import java.io.FileOutputStream; 43 import java.io.IOException; 44 import java.io.InputStream; 45 import java.io.OutputStream; 46 import java.util.Arrays; 47 import java.util.List; 48 import org.junit.Before; 49 import org.junit.Rule; 50 import org.junit.Test; 51 import org.junit.rules.TestName; 52 53 /** 54 * Base class for {@code Backend} tests that exercises common behavior expected of all 55 * implementations. Concrete test cases must specify a test runner, extend from this class, and 56 * implement the abstract setup methods. Subclasses are free to add additional test methods in order 57 * to exercise backend-specific behavior using the provided {@link #storage}. 58 * 59 * <p>If the backend under test does not support a specific feature, the test subclass should 60 * override the appropriate {@code supportsX()} and return false in order to skip the associated 61 * unit tests. NOTE: this is adopted from Guava and seems like the least-bad strategy. 62 * 63 * <p>Abstract setup methods may be called before the {@code @Before} methods of the subclass, and 64 * so should not depend on them. {@code @BeforeClass}, lazy initialization, and static 65 * initialization are viable alternatives. 66 */ 67 public abstract class BackendTestBase { 68 69 private SynchronousFileStorage storage; 70 private static final byte[] TEST_CONTENT = makeArrayOfBytesContent(); 71 private static final byte[] OTHER_CONTENT = makeArrayOfBytesContent(6); 72 73 @Rule public TestName testName = new TestName(); 74 75 /** Returns the concrete {@code Backend} instance to be tested. */ backend()76 protected abstract Backend backend(); 77 78 /** Enables unit tests verifying {@link Backend#openForAppend}. */ supportsAppend()79 protected boolean supportsAppend() { 80 return true; 81 } 82 83 /** Enables unit tests verifying {@link Backend#rename}. */ supportsRename()84 protected boolean supportsRename() { 85 return true; 86 } 87 88 /** 89 * Enables unit tests verifying {@link Backend#createDirectory}, {@link Backend#isDirectory}, 90 * {@link Backend#deleteDirectory}, {@link Backend#children}, and writing to a subdirectory uri. 91 */ supportsDirectories()92 protected boolean supportsDirectories() { 93 return true; 94 } 95 96 /** Enables unit tests verifying that {@link FileConvertible} can be returned directly. */ supportsFileConvertible()97 protected boolean supportsFileConvertible() { 98 return true; 99 } 100 101 /** Enable unit tests verifying {@link Backend#toFile}. */ supportsToFile()102 protected boolean supportsToFile() { 103 return true; 104 } 105 106 /** 107 * Returns a URI to a temporary directory for writing test data to. The {@code Backend} should be 108 * able to {@code openForWrite} a file to this directory without any additional setup code. 109 */ legalUriBase()110 protected abstract Uri legalUriBase() throws IOException; 111 112 /** 113 * Returns a list of URIs for which {@code Backend.openForRead(uri)} is expected to throw {@code 114 * MalformedUriException} without any additional setup code. 115 */ illegalUrisToRead()116 protected List<Uri> illegalUrisToRead() { 117 return ImmutableList.of(); 118 } 119 120 /** 121 * Returns a list of URIs for which {@code Backend.openForWrite(uri)} is expected to throw {@code 122 * MalformedUriException} without any additional setup code. 123 */ illegalUrisToWrite()124 protected List<Uri> illegalUrisToWrite() { 125 return ImmutableList.of(); 126 } 127 128 /** 129 * Returns a list of URIs for which {@code Backend.openForAppend(uri)} is expected to throw {@code 130 * MalformedUriException} without any additional setup code. 131 */ illegalUrisToAppend()132 protected List<Uri> illegalUrisToAppend() { 133 return ImmutableList.of(); 134 } 135 136 @Before setUpStorage()137 public final void setUpStorage() { 138 assertThat(backend()).isNotNull(); 139 storage = new SynchronousFileStorage(ImmutableList.of(backend()), ImmutableList.of()); 140 } 141 142 /** Returns the storage instance used in testing. */ storage()143 protected SynchronousFileStorage storage() { 144 return storage; 145 } 146 147 @Test name_returnsNonEmptyString()148 public void name_returnsNonEmptyString() { 149 assertThat(backend().name()).isNotEmpty(); 150 } 151 152 @Test openForRead_withMissingFile_throwsFileNotFound()153 public void openForRead_withMissingFile_throwsFileNotFound() throws Exception { 154 Uri uri = uriForTestMethod(); 155 assertThrows(FileNotFoundException.class, () -> storage.open(uri, ReadStreamOpener.create())); 156 } 157 158 @Test openForRead_withIllegalUri_throwsIllegalArgumentException()159 public void openForRead_withIllegalUri_throwsIllegalArgumentException() throws Exception { 160 for (Uri uri : illegalUrisToRead()) { 161 assertThrows(MalformedUriException.class, () -> storage.open(uri, ReadStreamOpener.create())); 162 } 163 } 164 165 @Test openForRead_readsWrittenContent()166 public void openForRead_readsWrittenContent() throws Exception { 167 Uri uri = uriForTestMethod(); 168 createFile(storage, uri, TEST_CONTENT); 169 assertThat(readFileInBytes(storage, uri)).isEqualTo(TEST_CONTENT); 170 } 171 172 @Test openForRead_returnsFileConvertible()173 public void openForRead_returnsFileConvertible() throws Exception { 174 assumeTrue(supportsFileConvertible()); 175 176 Uri uri = uriForTestMethod(); 177 createFile(storage(), uri, TEST_CONTENT); 178 179 InputStream stream = backend().openForRead(uri); 180 assertThat(stream).isInstanceOf(FileConvertible.class); 181 File file = ((FileConvertible) stream).toFile(); 182 assertThat(readFileInBytesFromSource(new FileInputStream(file))).isEqualTo(TEST_CONTENT); 183 } 184 185 @Test openForWrite_withIllegalUri_throwsIllegalArgumentException()186 public void openForWrite_withIllegalUri_throwsIllegalArgumentException() throws Exception { 187 for (Uri uri : illegalUrisToWrite()) { 188 assertThrows( 189 MalformedUriException.class, () -> storage.open(uri, WriteStreamOpener.create())); 190 } 191 } 192 193 @Test openForWrite_withFailedDirectoryCreation_throwsException()194 public void openForWrite_withFailedDirectoryCreation_throwsException() throws Exception { 195 assumeTrue(supportsDirectories()); 196 197 Uri parent = uriForTestMethod(); 198 createFile(storage, parent, TEST_CONTENT); 199 200 Uri child = parent.buildUpon().appendPath("child").build(); 201 assertThrows(IOException.class, () -> storage.open(child, WriteStreamOpener.create())); 202 } 203 204 @Test openForWrite_overwritesExistingContent()205 public void openForWrite_overwritesExistingContent() throws Exception { 206 Uri uri = uriForTestMethod(); 207 createFile(storage, uri, OTHER_CONTENT); 208 209 createFile(storage, uri, TEST_CONTENT); 210 assertThat(readFileInBytes(storage, uri)).isEqualTo(TEST_CONTENT); 211 } 212 213 @Test openForWrite_createsParentDirectory()214 public void openForWrite_createsParentDirectory() throws Exception { 215 assumeTrue(supportsDirectories()); 216 217 Uri parent = uriForTestMethod(); 218 Uri child = parent.buildUpon().appendPath("child").build(); 219 220 createFile(storage, child, TEST_CONTENT); 221 assertThat(storage.isDirectory(parent)).isTrue(); 222 assertThat(storage.exists(child)).isTrue(); 223 } 224 225 @Test openForWrite_returnsFileConvertible()226 public void openForWrite_returnsFileConvertible() throws Exception { 227 assumeTrue(supportsFileConvertible()); 228 229 Uri uri = uriForTestMethod(); 230 try (OutputStream stream = backend().openForWrite(uri)) { 231 assertThat(stream).isInstanceOf(FileConvertible.class); 232 File file = ((FileConvertible) stream).toFile(); 233 writeFileToSink(new FileOutputStream(file), TEST_CONTENT); 234 } 235 assertThat(readFileInBytes(storage(), uri)).isEqualTo(TEST_CONTENT); 236 } 237 238 @Test openForAppend_withIllegalUri_throwsIllegalArgumentException()239 public void openForAppend_withIllegalUri_throwsIllegalArgumentException() throws Exception { 240 assumeTrue(supportsAppend()); 241 242 for (Uri uri : illegalUrisToAppend()) { 243 assertThrows( 244 MalformedUriException.class, () -> storage.open(uri, AppendStreamOpener.create())); 245 } 246 } 247 248 @Test openForAppend_appendsContent()249 public void openForAppend_appendsContent() throws Exception { 250 assumeTrue(supportsAppend()); 251 252 Uri uri = uriForTestMethod(); 253 createFile(storage, uri, OTHER_CONTENT); 254 255 appendFile(storage, uri, TEST_CONTENT); 256 assertThat(readFileInBytes(storage, uri)).isEqualTo(Bytes.concat(OTHER_CONTENT, TEST_CONTENT)); 257 } 258 259 @Test openForAppend_returnsFileConvertible()260 public void openForAppend_returnsFileConvertible() throws Exception { 261 assumeTrue(supportsAppend()); 262 assumeTrue(supportsFileConvertible()); 263 264 Uri uri = uriForTestMethod(); 265 createFile(storage, uri, OTHER_CONTENT); 266 try (OutputStream stream = backend().openForAppend(uri)) { 267 assertThat(stream).isInstanceOf(FileConvertible.class); 268 File file = ((FileConvertible) stream).toFile(); 269 writeFileToSink(new FileOutputStream(file, /* append= */ true), TEST_CONTENT); 270 } 271 assertThat(readFileInBytes(storage(), uri)) 272 .isEqualTo(Bytes.concat(OTHER_CONTENT, TEST_CONTENT)); 273 } 274 275 @Test deleteFile_deletesFile()276 public void deleteFile_deletesFile() throws Exception { 277 Uri uri = uriForTestMethod(); 278 createFile(storage, uri, TEST_CONTENT); 279 280 storage.deleteFile(uri); 281 assertThat(storage.exists(uri)).isFalse(); 282 } 283 284 @Test deleteFile_onDirectory_throwsFileNotFound()285 public void deleteFile_onDirectory_throwsFileNotFound() throws Exception { 286 assumeTrue(supportsDirectories()); 287 288 Uri uri = uriForTestMethod(); 289 storage.createDirectory(uri); 290 291 assertThrows(FileNotFoundException.class, () -> storage.deleteFile(uri)); 292 } 293 294 @Test deleteFile_onMissingFile_throwsFileNotFound()295 public void deleteFile_onMissingFile_throwsFileNotFound() throws Exception { 296 Uri uri = uriForTestMethod(); 297 assertThrows(FileNotFoundException.class, () -> storage.deleteFile(uri)); 298 } 299 300 @Test rename_renamesFile()301 public void rename_renamesFile() throws Exception { 302 assumeTrue(supportsRename()); 303 304 Uri uri1 = uriForTestMethodWithSuffix("1"); 305 Uri uri2 = uriForTestMethodWithSuffix("2"); 306 Uri uri3 = uriForTestMethodWithSuffix("3"); 307 createFile(storage, uri1, OTHER_CONTENT); 308 createFile(storage, uri2, TEST_CONTENT); 309 310 storage.rename(uri2, uri3); 311 storage.rename(uri1, uri2); 312 assertThat(storage.exists(uri1)).isFalse(); 313 assertThat(readFileInBytes(storage, uri2)).isEqualTo(OTHER_CONTENT); 314 assertThat(readFileInBytes(storage, uri3)).isEqualTo(TEST_CONTENT); 315 } 316 317 @Test rename_renamesDirectory()318 public void rename_renamesDirectory() throws Exception { 319 assumeTrue(supportsRename()); 320 assumeTrue(supportsDirectories()); 321 322 Uri uriA = uriForTestMethodWithSuffix("a"); 323 Uri uriB = uriForTestMethodWithSuffix("b"); 324 325 storage.createDirectory(uriA); 326 storage.rename(uriA, uriB); 327 assertThat(storage.isDirectory(uriA)).isFalse(); 328 assertThat(storage.isDirectory(uriB)).isTrue(); 329 } 330 331 @Test exists_returnsTrueIfFileExists()332 public void exists_returnsTrueIfFileExists() throws Exception { 333 Uri uri = uriForTestMethod(); 334 assertThat(storage.exists(uri)).isFalse(); 335 336 createFile(storage, uri, TEST_CONTENT); 337 assertThat(storage.exists(uri)).isTrue(); 338 } 339 340 @Test exists_returnsTrueIfDirectoryExists()341 public void exists_returnsTrueIfDirectoryExists() throws Exception { 342 assumeTrue(supportsDirectories()); 343 344 Uri uri = uriForTestMethod(); 345 assertThat(storage.exists(uri)).isFalse(); 346 347 storage.createDirectory(uri); 348 assertThat(storage.exists(uri)).isTrue(); 349 } 350 351 @Test isDirectory_returnsTrueIfDirectoryExists()352 public void isDirectory_returnsTrueIfDirectoryExists() throws Exception { 353 assumeTrue(supportsDirectories()); 354 355 Uri uri = uriForTestMethod(); 356 assertThat(storage.isDirectory(uri)).isFalse(); 357 358 storage.createDirectory(uri); 359 assertThat(storage.isDirectory(uri)).isTrue(); 360 } 361 362 @Test isDirectory_returnsFalseIfDoesntExist()363 public void isDirectory_returnsFalseIfDoesntExist() throws Exception { 364 assumeTrue(supportsDirectories()); 365 366 Uri uri = uriForTestMethod(); 367 assertThat(storage.isDirectory(uri)).isFalse(); 368 } 369 370 @Test isDirectory_returnsFalseIfIsFile()371 public void isDirectory_returnsFalseIfIsFile() throws Exception { 372 assumeTrue(supportsDirectories()); 373 374 Uri uri = uriForTestMethod(); 375 createFile(storage, uri, TEST_CONTENT); 376 assertThat(storage.isDirectory(uri)).isFalse(); 377 } 378 379 @Test createDirectory_createsDirectory()380 public void createDirectory_createsDirectory() throws Exception { 381 assumeTrue(supportsDirectories()); 382 383 Uri uri = uriForTestMethod(); 384 storage.createDirectory(uri); 385 assertThat(storage.isDirectory(uri)).isTrue(); 386 } 387 388 @Test createDirectory_createsParentDirectory()389 public void createDirectory_createsParentDirectory() throws Exception { 390 assumeTrue(supportsDirectories()); 391 392 Uri parent = uriForTestMethod(); 393 Uri child = parent.buildUpon().appendPath("child").build(); 394 395 storage.createDirectory(child); 396 assertThat(storage.isDirectory(child)).isTrue(); 397 assertThat(storage.isDirectory(parent)).isTrue(); 398 } 399 400 @Test fileSize_withMissingFile_returnsZero()401 public void fileSize_withMissingFile_returnsZero() throws Exception { 402 Uri uri = uriForTestMethod(); 403 assertThat(storage.fileSize(uri)).isEqualTo(0); 404 } 405 406 @Test fileSize_returnsSizeOfFile()407 public void fileSize_returnsSizeOfFile() throws Exception { 408 Uri uri = uriForTestMethod(); 409 410 backend().openForWrite(uri).close(); 411 assertThat(storage.fileSize(uri)).isEqualTo(0); 412 413 createFile(storage, uri, TEST_CONTENT); 414 assertThat(storage.fileSize(uri)).isEqualTo(TEST_CONTENT.length); 415 } 416 417 @Test fileSize_withDirReturns0()418 public void fileSize_withDirReturns0() throws Exception { 419 assumeTrue(supportsDirectories()); 420 421 Uri uri = uriForTestMethod(); 422 storage.createDirectory(uri); 423 assertThat(storage.fileSize(uri)).isEqualTo(0); 424 } 425 426 @Test deleteDirectory_shouldDeleteEmptyDirectory()427 public void deleteDirectory_shouldDeleteEmptyDirectory() throws Exception { 428 assumeTrue(supportsDirectories()); 429 430 Uri uri = uriForTestMethod(); 431 assertThat(storage.isDirectory(uri)).isFalse(); 432 storage.createDirectory(uri); 433 assertThat(storage.isDirectory(uri)).isTrue(); 434 storage.deleteDirectory(uri); 435 assertThat(storage.isDirectory(uri)).isFalse(); 436 } 437 438 @Test deleteDirectory_shouldNOTDeleteNonEmptyDirectory()439 public void deleteDirectory_shouldNOTDeleteNonEmptyDirectory() throws Exception { 440 assumeTrue(supportsDirectories()); 441 442 Uri uri = uriForTestMethod(); 443 storage.createDirectory(uri); 444 Uri fileUri = uri.buildUpon().appendPath("file").build(); 445 createFile(storage, fileUri, TEST_CONTENT); 446 447 assertThat(storage.isDirectory(uri)).isTrue(); 448 assertThrows(IOException.class, () -> storage.deleteDirectory(uri)); 449 assertThat(storage.isDirectory(uri)).isTrue(); 450 451 storage.deleteFile(fileUri); 452 storage.deleteDirectory(uri); 453 assertThat(storage.isDirectory(uri)).isFalse(); 454 } 455 456 @Test deleteDirectory_onFileShouldThrow()457 public void deleteDirectory_onFileShouldThrow() throws Exception { 458 assumeTrue(supportsDirectories()); 459 460 Uri uri = uriForTestMethod(); 461 createFile(storage, uri, TEST_CONTENT); 462 463 assertThrows(IOException.class, () -> storage.deleteDirectory(uri)); 464 } 465 466 @Test children_withEmptyDirectoryShouldReturnEmpty()467 public void children_withEmptyDirectoryShouldReturnEmpty() throws Exception { 468 assumeTrue(supportsDirectories()); 469 470 Uri uri = uriForTestMethod(); 471 storage.createDirectory(uri); 472 473 assertThat(storage.children(uri)).isEmpty(); 474 } 475 476 @Test children_onNotFoundShouldThrow()477 public void children_onNotFoundShouldThrow() throws Exception { 478 assumeTrue(supportsDirectories()); 479 480 Uri uri = uriForTestMethod(); 481 482 assertThrows(IOException.class, () -> storage.children(uri)); 483 } 484 485 @Test children_onFileShouldThrow()486 public void children_onFileShouldThrow() throws Exception { 487 assumeTrue(supportsDirectories()); 488 489 Uri uri = uriForTestMethod(); 490 createFile(storage, uri, TEST_CONTENT); 491 492 assertThrows(IOException.class, () -> storage.children(uri)); 493 } 494 495 @Test children_shouldReturnFilesAndSubDirectories()496 public void children_shouldReturnFilesAndSubDirectories() throws Exception { 497 assumeTrue(supportsDirectories()); 498 499 Uri uri = uriForTestMethod(); 500 storage.createDirectory(uri); 501 502 List<Uri> fileUris = 503 Arrays.asList( 504 uri.buildUpon().appendPath("file1").build(), 505 uri.buildUpon().appendPath("file2").build(), 506 uri.buildUpon().appendPath("file3").build()); 507 for (Uri file : fileUris) { 508 createFile(storage, file, TEST_CONTENT); 509 } 510 511 List<Uri> subdirUris = 512 Arrays.asList( 513 uri.buildUpon().appendPath("dir1").build(), 514 uri.buildUpon().appendPath("dir2").build(), 515 uri.buildUpon().appendPath("dir3").build()); 516 for (Uri subdir : subdirUris) { 517 storage.createDirectory(subdir); 518 } 519 List<Uri> subdirUrisWithTrailingSlashes = 520 Lists.transform(subdirUris, u -> Uri.parse(u.toString() + "/")); 521 522 List<Uri> expected = Lists.newArrayList(); 523 expected.addAll(fileUris); 524 expected.addAll(subdirUrisWithTrailingSlashes); 525 assertThat(storage.children(uri)).containsExactlyElementsIn(expected); 526 } 527 528 @Test toFile_converts()529 public void toFile_converts() throws Exception { 530 assumeTrue(supportsToFile()); 531 532 Uri uri = uriForTestMethod(); 533 createFile(storage, uri, TEST_CONTENT); 534 File file = backend().toFile(uri); 535 assertThat(readFileInBytesFromSource(new FileInputStream(file))).isEqualTo(TEST_CONTENT); 536 } 537 538 /** Returns a URI in the test directory unique to the current test method. */ uriForTestMethod()539 protected Uri uriForTestMethod() throws IOException { 540 return legalUriBase().buildUpon().appendPath(testName.getMethodName()).build(); 541 } 542 543 /** Returns a URI in the test directory unique to the current test method and {@code suffix}. */ uriForTestMethodWithSuffix(String suffix)544 private Uri uriForTestMethodWithSuffix(String suffix) throws IOException { 545 return legalUriBase().buildUpon().appendPath(testName.getMethodName() + "-" + suffix).build(); 546 } 547 } 548