LCOV - code coverage report
Current view: top level - libs/remote_storage/tests/common - tests.rs (source / functions) Coverage Total Hit
Test: 691a4c28fe7169edd60b367c52d448a0a6605f1f.info Lines: 14.0 % 215 30
Test Date: 2024-05-10 13:18:37 Functions: 50.0 % 48 24

            Line data    Source code
       1              : use anyhow::Context;
       2              : use camino::Utf8Path;
       3              : use remote_storage::ListingMode;
       4              : use remote_storage::RemotePath;
       5              : use std::sync::Arc;
       6              : use std::{collections::HashSet, num::NonZeroU32};
       7              : use test_context::test_context;
       8              : use tokio_util::sync::CancellationToken;
       9              : use tracing::debug;
      10              : 
      11              : use crate::common::{download_to_vec, upload_stream, wrap_stream};
      12              : 
      13              : use super::{
      14              :     MaybeEnabledStorage, MaybeEnabledStorageWithSimpleTestBlobs, MaybeEnabledStorageWithTestBlobs,
      15              : };
      16              : 
      17              : /// Tests that S3 client can list all prefixes, even if the response come paginated and requires multiple S3 queries.
      18              : /// Uses real S3 and requires [`ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME`] and related S3 cred env vars specified.
      19              : /// See the client creation in [`create_s3_client`] for details on the required env vars.
      20              : /// If real S3 tests are disabled, the test passes, skipping any real test run: currently, there's no way to mark the test ignored in runtime with the
      21              : /// deafult test framework, see https://github.com/rust-lang/rust/issues/68007 for details.
      22              : ///
      23              : /// First, the test creates a set of S3 objects with keys `/${random_prefix_part}/${base_prefix_str}/sub_prefix_${i}/blob_${i}` in [`upload_remote_data`]
      24              : /// where
      25              : /// * `random_prefix_part` is set for the entire S3 client during the S3 client creation in [`create_s3_client`], to avoid multiple test runs interference
      26              : /// * `base_prefix_str` is a common prefix to use in the client requests: we would want to ensure that the client is able to list nested prefixes inside the bucket
      27              : ///
      28              : /// Then, verifies that the client does return correct prefixes when queried:
      29              : /// * with no prefix, it lists everything after its `${random_prefix_part}/` — that should be `${base_prefix_str}` value only
      30              : /// * with `${base_prefix_str}/` prefix, it lists every `sub_prefix_${i}`
      31              : ///
      32              : /// With the real S3 enabled and `#[cfg(test)]` Rust configuration used, the S3 client test adds a `max-keys` param to limit the response keys.
      33              : /// This way, we are able to test the pagination implicitly, by ensuring all results are returned from the remote storage and avoid uploading too many blobs to S3,
      34              : /// since current default AWS S3 pagination limit is 1000.
      35              : /// (see https://docs.aws.amazon.com/AmazonS3/latest/API/API_ListObjectsV2.html#API_ListObjectsV2_RequestSyntax)
      36              : ///
      37              : /// Lastly, the test attempts to clean up and remove all uploaded S3 files.
      38              : /// If any errors appear during the clean up, they get logged, but the test is not failed or stopped until clean up is finished.
      39            4 : #[test_context(MaybeEnabledStorageWithTestBlobs)]
      40              : #[tokio::test]
      41            4 : async fn pagination_should_work(ctx: &mut MaybeEnabledStorageWithTestBlobs) -> anyhow::Result<()> {
      42            4 :     let ctx = match ctx {
      43            0 :         MaybeEnabledStorageWithTestBlobs::Enabled(ctx) => ctx,
      44            4 :         MaybeEnabledStorageWithTestBlobs::Disabled => return Ok(()),
      45            0 :         MaybeEnabledStorageWithTestBlobs::UploadsFailed(e, _) => {
      46            0 :             anyhow::bail!("S3 init failed: {e:?}")
      47              :         }
      48              :     };
      49              : 
      50            0 :     let cancel = CancellationToken::new();
      51            0 : 
      52            0 :     let test_client = Arc::clone(&ctx.enabled.client);
      53            0 :     let expected_remote_prefixes = ctx.remote_prefixes.clone();
      54              : 
      55            0 :     let base_prefix = RemotePath::new(Utf8Path::new(ctx.enabled.base_prefix))
      56            0 :         .context("common_prefix construction")?;
      57            0 :     let root_remote_prefixes = test_client
      58            0 :         .list(None, ListingMode::WithDelimiter, None, &cancel)
      59            0 :         .await?
      60              :         .prefixes
      61            0 :         .into_iter()
      62            0 :         .collect::<HashSet<_>>();
      63            0 :     assert_eq!(
      64            0 :         root_remote_prefixes, HashSet::from([base_prefix.clone()]),
      65            0 :         "remote storage root prefixes list mismatches with the uploads. Returned prefixes: {root_remote_prefixes:?}"
      66              :     );
      67              : 
      68            0 :     let nested_remote_prefixes = test_client
      69            0 :         .list(
      70            0 :             Some(&base_prefix.add_trailing_slash()),
      71            0 :             ListingMode::WithDelimiter,
      72            0 :             None,
      73            0 :             &cancel,
      74            0 :         )
      75            0 :         .await?
      76              :         .prefixes
      77            0 :         .into_iter()
      78            0 :         .collect::<HashSet<_>>();
      79            0 :     let remote_only_prefixes = nested_remote_prefixes
      80            0 :         .difference(&expected_remote_prefixes)
      81            0 :         .collect::<HashSet<_>>();
      82            0 :     let missing_uploaded_prefixes = expected_remote_prefixes
      83            0 :         .difference(&nested_remote_prefixes)
      84            0 :         .collect::<HashSet<_>>();
      85            0 :     assert_eq!(
      86            0 :         remote_only_prefixes.len() + missing_uploaded_prefixes.len(), 0,
      87            0 :         "remote storage nested prefixes list mismatches with the uploads. Remote only prefixes: {remote_only_prefixes:?}, missing uploaded prefixes: {missing_uploaded_prefixes:?}",
      88              :     );
      89              : 
      90            0 :     Ok(())
      91            4 : }
      92              : 
      93              : /// Tests that S3 client can list all files in a folder, even if the response comes paginated and requirees multiple S3 queries.
      94              : /// Uses real S3 and requires [`ENABLE_REAL_S3_REMOTE_STORAGE_ENV_VAR_NAME`] and related S3 cred env vars specified. Test will skip real code and pass if env vars not set.
      95              : /// See `s3_pagination_should_work` for more information.
      96              : ///
      97              : /// First, create a set of S3 objects with keys `random_prefix/folder{j}/blob_{i}.txt` in [`upload_remote_data`]
      98              : /// Then performs the following queries:
      99              : ///    1. `list(None)`. This should return all files `random_prefix/folder{j}/blob_{i}.txt`
     100              : ///    2. `list("folder1")`.  This  should return all files `random_prefix/folder1/blob_{i}.txt`
     101            4 : #[test_context(MaybeEnabledStorageWithSimpleTestBlobs)]
     102              : #[tokio::test]
     103              : async fn list_no_delimiter_works(
     104              :     ctx: &mut MaybeEnabledStorageWithSimpleTestBlobs,
     105            4 : ) -> anyhow::Result<()> {
     106            4 :     let ctx = match ctx {
     107            0 :         MaybeEnabledStorageWithSimpleTestBlobs::Enabled(ctx) => ctx,
     108            4 :         MaybeEnabledStorageWithSimpleTestBlobs::Disabled => return Ok(()),
     109            0 :         MaybeEnabledStorageWithSimpleTestBlobs::UploadsFailed(e, _) => {
     110            0 :             anyhow::bail!("S3 init failed: {e:?}")
     111              :         }
     112              :     };
     113            0 :     let cancel = CancellationToken::new();
     114            0 :     let test_client = Arc::clone(&ctx.enabled.client);
     115            0 :     let base_prefix =
     116            0 :         RemotePath::new(Utf8Path::new("folder1")).context("common_prefix construction")?;
     117            0 :     let root_files = test_client
     118            0 :         .list(None, ListingMode::NoDelimiter, None, &cancel)
     119            0 :         .await
     120            0 :         .context("client list root files failure")?
     121              :         .keys
     122            0 :         .into_iter()
     123            0 :         .collect::<HashSet<_>>();
     124            0 :     assert_eq!(
     125            0 :         root_files,
     126            0 :         ctx.remote_blobs.clone(),
     127            0 :         "remote storage list on root mismatches with the uploads."
     128              :     );
     129              : 
     130              :     // Test that max_keys limit works. In total there are about 21 files (see
     131              :     // upload_simple_remote_data call in test_real_s3.rs).
     132            0 :     let limited_root_files = test_client
     133            0 :         .list(
     134            0 :             None,
     135            0 :             ListingMode::NoDelimiter,
     136            0 :             Some(NonZeroU32::new(2).unwrap()),
     137            0 :             &cancel,
     138            0 :         )
     139            0 :         .await
     140            0 :         .context("client list root files failure")?;
     141            0 :     assert_eq!(limited_root_files.keys.len(), 2);
     142              : 
     143            0 :     let nested_remote_files = test_client
     144            0 :         .list(Some(&base_prefix), ListingMode::NoDelimiter, None, &cancel)
     145            0 :         .await
     146            0 :         .context("client list nested files failure")?
     147              :         .keys
     148            0 :         .into_iter()
     149            0 :         .collect::<HashSet<_>>();
     150            0 :     let trim_remote_blobs: HashSet<_> = ctx
     151            0 :         .remote_blobs
     152            0 :         .iter()
     153            0 :         .map(|x| x.get_path())
     154            0 :         .filter(|x| x.starts_with("folder1"))
     155            0 :         .map(|x| RemotePath::new(x).expect("must be valid path"))
     156            0 :         .collect();
     157            0 :     assert_eq!(
     158              :         nested_remote_files, trim_remote_blobs,
     159            0 :         "remote storage list on subdirrectory mismatches with the uploads."
     160              :     );
     161            0 :     Ok(())
     162            4 : }
     163              : 
     164            4 : #[test_context(MaybeEnabledStorage)]
     165              : #[tokio::test]
     166            4 : async fn delete_non_exising_works(ctx: &mut MaybeEnabledStorage) -> anyhow::Result<()> {
     167            4 :     let ctx = match ctx {
     168            0 :         MaybeEnabledStorage::Enabled(ctx) => ctx,
     169            4 :         MaybeEnabledStorage::Disabled => return Ok(()),
     170              :     };
     171              : 
     172            0 :     let cancel = CancellationToken::new();
     173              : 
     174            0 :     let path = RemotePath::new(Utf8Path::new(
     175            0 :         format!("{}/for_sure_there_is_nothing_there_really", ctx.base_prefix).as_str(),
     176            0 :     ))
     177            0 :     .with_context(|| "RemotePath conversion")?;
     178              : 
     179            0 :     ctx.client
     180            0 :         .delete(&path, &cancel)
     181            0 :         .await
     182            0 :         .expect("should succeed");
     183            0 : 
     184            0 :     Ok(())
     185            4 : }
     186              : 
     187            4 : #[test_context(MaybeEnabledStorage)]
     188              : #[tokio::test]
     189            4 : async fn delete_objects_works(ctx: &mut MaybeEnabledStorage) -> anyhow::Result<()> {
     190            4 :     let ctx = match ctx {
     191            0 :         MaybeEnabledStorage::Enabled(ctx) => ctx,
     192            4 :         MaybeEnabledStorage::Disabled => return Ok(()),
     193              :     };
     194              : 
     195            0 :     let cancel = CancellationToken::new();
     196              : 
     197            0 :     let path1 = RemotePath::new(Utf8Path::new(format!("{}/path1", ctx.base_prefix).as_str()))
     198            0 :         .with_context(|| "RemotePath conversion")?;
     199              : 
     200            0 :     let path2 = RemotePath::new(Utf8Path::new(format!("{}/path2", ctx.base_prefix).as_str()))
     201            0 :         .with_context(|| "RemotePath conversion")?;
     202              : 
     203            0 :     let path3 = RemotePath::new(Utf8Path::new(format!("{}/path3", ctx.base_prefix).as_str()))
     204            0 :         .with_context(|| "RemotePath conversion")?;
     205              : 
     206            0 :     let (data, len) = upload_stream("remote blob data1".as_bytes().into());
     207            0 :     ctx.client.upload(data, len, &path1, None, &cancel).await?;
     208              : 
     209            0 :     let (data, len) = upload_stream("remote blob data2".as_bytes().into());
     210            0 :     ctx.client.upload(data, len, &path2, None, &cancel).await?;
     211              : 
     212            0 :     let (data, len) = upload_stream("remote blob data3".as_bytes().into());
     213            0 :     ctx.client.upload(data, len, &path3, None, &cancel).await?;
     214              : 
     215            0 :     ctx.client.delete_objects(&[path1, path2], &cancel).await?;
     216              : 
     217            0 :     let prefixes = ctx
     218            0 :         .client
     219            0 :         .list(None, ListingMode::WithDelimiter, None, &cancel)
     220            0 :         .await?
     221              :         .prefixes;
     222              : 
     223            0 :     assert_eq!(prefixes.len(), 1);
     224              : 
     225            0 :     ctx.client.delete_objects(&[path3], &cancel).await?;
     226              : 
     227            0 :     Ok(())
     228            4 : }
     229              : 
     230            4 : #[test_context(MaybeEnabledStorage)]
     231              : #[tokio::test]
     232            4 : async fn upload_download_works(ctx: &mut MaybeEnabledStorage) -> anyhow::Result<()> {
     233            4 :     let MaybeEnabledStorage::Enabled(ctx) = ctx else {
     234            4 :         return Ok(());
     235              :     };
     236              : 
     237            0 :     let cancel = CancellationToken::new();
     238              : 
     239            0 :     let path = RemotePath::new(Utf8Path::new(format!("{}/file", ctx.base_prefix).as_str()))
     240            0 :         .with_context(|| "RemotePath conversion")?;
     241              : 
     242            0 :     let orig = bytes::Bytes::from_static("remote blob data here".as_bytes());
     243            0 : 
     244            0 :     let (data, len) = wrap_stream(orig.clone());
     245            0 : 
     246            0 :     ctx.client.upload(data, len, &path, None, &cancel).await?;
     247              : 
     248              :     // Normal download request
     249            0 :     let dl = ctx.client.download(&path, &cancel).await?;
     250            0 :     let buf = download_to_vec(dl).await?;
     251            0 :     assert_eq!(&buf, &orig);
     252              : 
     253              :     // Full range (end specified)
     254            0 :     let dl = ctx
     255            0 :         .client
     256            0 :         .download_byte_range(&path, 0, Some(len as u64), &cancel)
     257            0 :         .await?;
     258            0 :     let buf = download_to_vec(dl).await?;
     259            0 :     assert_eq!(&buf, &orig);
     260              : 
     261              :     // partial range (end specified)
     262            0 :     let dl = ctx
     263            0 :         .client
     264            0 :         .download_byte_range(&path, 4, Some(10), &cancel)
     265            0 :         .await?;
     266            0 :     let buf = download_to_vec(dl).await?;
     267            0 :     assert_eq!(&buf, &orig[4..10]);
     268              : 
     269              :     // partial range (end beyond real end)
     270            0 :     let dl = ctx
     271            0 :         .client
     272            0 :         .download_byte_range(&path, 8, Some(len as u64 * 100), &cancel)
     273            0 :         .await?;
     274            0 :     let buf = download_to_vec(dl).await?;
     275            0 :     assert_eq!(&buf, &orig[8..]);
     276              : 
     277              :     // Partial range (end unspecified)
     278            0 :     let dl = ctx
     279            0 :         .client
     280            0 :         .download_byte_range(&path, 4, None, &cancel)
     281            0 :         .await?;
     282            0 :     let buf = download_to_vec(dl).await?;
     283            0 :     assert_eq!(&buf, &orig[4..]);
     284              : 
     285              :     // Full range (end unspecified)
     286            0 :     let dl = ctx
     287            0 :         .client
     288            0 :         .download_byte_range(&path, 0, None, &cancel)
     289            0 :         .await?;
     290            0 :     let buf = download_to_vec(dl).await?;
     291            0 :     assert_eq!(&buf, &orig);
     292              : 
     293            0 :     debug!("Cleanup: deleting file at path {path:?}");
     294            0 :     ctx.client
     295            0 :         .delete(&path, &cancel)
     296            0 :         .await
     297            0 :         .with_context(|| format!("{path:?} removal"))?;
     298              : 
     299            0 :     Ok(())
     300            4 : }
     301              : 
     302            4 : #[test_context(MaybeEnabledStorage)]
     303              : #[tokio::test]
     304            4 : async fn copy_works(ctx: &mut MaybeEnabledStorage) -> anyhow::Result<()> {
     305            4 :     let MaybeEnabledStorage::Enabled(ctx) = ctx else {
     306            4 :         return Ok(());
     307              :     };
     308              : 
     309            0 :     let cancel = CancellationToken::new();
     310              : 
     311            0 :     let path = RemotePath::new(Utf8Path::new(
     312            0 :         format!("{}/file_to_copy", ctx.base_prefix).as_str(),
     313            0 :     ))
     314            0 :     .with_context(|| "RemotePath conversion")?;
     315            0 :     let path_dest = RemotePath::new(Utf8Path::new(
     316            0 :         format!("{}/file_dest", ctx.base_prefix).as_str(),
     317            0 :     ))
     318            0 :     .with_context(|| "RemotePath conversion")?;
     319              : 
     320            0 :     let orig = bytes::Bytes::from_static("remote blob data content".as_bytes());
     321            0 : 
     322            0 :     let (data, len) = wrap_stream(orig.clone());
     323            0 : 
     324            0 :     ctx.client.upload(data, len, &path, None, &cancel).await?;
     325              : 
     326              :     // Normal download request
     327            0 :     ctx.client.copy_object(&path, &path_dest, &cancel).await?;
     328              : 
     329            0 :     let dl = ctx.client.download(&path_dest, &cancel).await?;
     330            0 :     let buf = download_to_vec(dl).await?;
     331            0 :     assert_eq!(&buf, &orig);
     332              : 
     333            0 :     debug!("Cleanup: deleting file at path {path:?}");
     334            0 :     ctx.client
     335            0 :         .delete_objects(&[path.clone(), path_dest.clone()], &cancel)
     336            0 :         .await
     337            0 :         .with_context(|| format!("{path:?} removal"))?;
     338              : 
     339            0 :     Ok(())
     340            4 : }
        

Generated by: LCOV version 2.1-beta