Unverified Commit e5db7a0d authored by Nugine's avatar Nugine Committed by GitHub
Browse files

feat(s3s-test): use arc (#196)

parent 0caf7982
Loading
Loading
Loading
Loading
+91 −15
Original line number Diff line number Diff line
#![forbid(unsafe_code)]
#![deny(
    clippy::all, //
    clippy::cargo, //
    clippy::pedantic, //
    clippy::self_named_module_files, //
)]
#![warn(
    clippy::dbg_macro, //
)]
#![allow(
    clippy::module_name_repetitions, //
    clippy::missing_errors_doc, // TODO
    clippy::missing_panics_doc, // TODO
    clippy::multiple_crate_versions, // TODO: check later
)]

use s3s_test::Result;
use s3s_test::TestFixture;
use s3s_test::TestSuite;

use tracing::debug;
use std::fmt;
use std::sync::Arc;

struct Basic {
    client: aws_sdk_s3::Client,
use aws_sdk_s3::error::ProvideErrorMetadata;
use aws_sdk_s3::error::SdkError;
use aws_sdk_s3::Client;
use tracing::error;

fn check<T, E>(result: Result<T, SdkError<E>>, allowed_codes: &[&str]) -> Result<Option<T>, SdkError<E>>
where
    E: fmt::Debug + ProvideErrorMetadata,
{
    if let Err(SdkError::ServiceError(ref err)) = result {
        if let Some(code) = err.err().code() {
            if allowed_codes.contains(&code) {
                return Ok(None);
            }
        }
    }
    if let Err(ref err) = result {
        error!(?err);
    }
    match result {
        Ok(val) => Ok(Some(val)),
        Err(err) => Err(err),
    }
}

#[tracing::instrument(skip(c))]
async fn delete_bucket(c: &Client, bucket: &str) -> Result<()> {
    let result = c.delete_bucket().bucket(bucket).send().await;
    check(result, &["NoSuchBucket"])?;
    Ok(())
}

#[tracing::instrument(skip(c))]
async fn create_bucket(c: &Client, bucket: &str) -> Result<()> {
    c.create_bucket().bucket(bucket).send().await?;
    Ok(())
}

impl TestSuite for Basic {
struct E2E {
    client: Client,
}

impl TestSuite for E2E {
    async fn setup() -> Result<Self> {
        let sdk_conf = aws_config::from_env().load().await;
        let s3_conf = aws_sdk_s3::config::Builder::from(&sdk_conf)
            .force_path_style(true) // FIXME: remove force_path_style
            .build();
        let client = aws_sdk_s3::Client::from_conf(s3_conf);
        let client = Client::from_conf(s3_conf);
        Ok(Self { client })
    }
}

struct BasicOps {
    client: aws_sdk_s3::Client,
struct Basic {
    client: Client,
}

impl TestFixture<Basic> for BasicOps {
    async fn setup(suite: &Basic) -> Result<Self> {
impl TestFixture<E2E> for Basic {
    async fn setup(suite: Arc<E2E>) -> Result<Self> {
        Ok(Self {
            client: suite.client.clone(),
        })
    }
}

impl BasicOps {
    async fn list_buckets(&self) -> Result<()> {
        let resp = self.client.list_buckets().send().await?;
        debug!(?resp);
impl Basic {
    async fn test_list_buckets(self: Arc<Self>) -> Result<()> {
        let c = &self.client;

        let buckets = ["test-list-buckets-1", "test-list-buckets-2"];
        for &bucket in &buckets {
            delete_bucket(c, bucket).await?;
        }

        for &bucket in &buckets {
            create_bucket(c, bucket).await?;
        }

        let resp = c.list_buckets().send().await?;
        let bucket_list: Vec<_> = resp.buckets.as_deref().unwrap().iter().filter_map(|b| b.name()).collect();

        for &bucket in &buckets {
            assert!(bucket_list.contains(&bucket));
        }

        for &bucket in &buckets {
            delete_bucket(c, bucket).await?;
        }

        Ok(())
    }
}
@@ -49,6 +125,6 @@ fn main() {
            }};
        }

        case!(Basic, BasicOps, list_buckets);
    })
        case!(E2E, Basic, test_list_buckets);
    });
}
+8 −0
Original line number Diff line number Diff line
@@ -27,3 +27,11 @@ impl fmt::Display for Failed {
        }
    }
}

impl Failed {
    pub fn from_string(s: impl Into<String>) -> Self {
        Self {
            source: Some(s.into().into()),
        }
    }
}
+26 −13
Original line number Diff line number Diff line
#![allow(clippy::wildcard_imports)]
#![allow(clippy::cast_possible_truncation, clippy::cast_precision_loss)]

use std::time::Instant;

use crate::report::*;
use crate::tcx::*;

use std::sync::Arc;
use std::time::Instant;

use tokio::spawn;
use tracing::info;
use tracing::instrument;
use tracing::Instrument;

macro_rules! run_fn {
    ($call:expr) => {{
        let t0 = std::time::Instant::now();
        let result = $call.await;
        let result = spawn($call.in_current_span()).await;
        let duration_ns = t0.elapsed().as_nanos() as u64;
        let duration_ms = duration_ns as f64 / 1e6;
        let summary = match result {
            Ok(_) => FnSummary {
            Ok(Ok(_)) => FnSummary {
                result: FnResult::Ok,
                duration_ns,
                duration_ms,
            },
            Ok(Err(ref e)) => FnSummary {
                result: FnResult::Err(e.to_string()),
                duration_ns,
                duration_ms,
            },
            Err(ref e) if e.is_panic() => FnSummary {
                result: FnResult::Panicked,
                duration_ns,
                duration_ms,
            },
            Err(ref e) => FnSummary {
                result: FnResult::Err(e.to_string()),
                duration_ns,
@@ -74,7 +87,7 @@ pub async fn run(tcx: &mut TestContext) -> Report {
    }
}

#[instrument(skip(suite), fields(suite_name = suite.name))]
#[instrument(skip(suite), fields(name = suite.name))]
async fn run_suite(suite: &SuiteInfo) -> SuiteReport {
    let total_fixtures = suite.fixtures.len();
    info!(total_fixtures, "Test suite start");
@@ -88,14 +101,14 @@ async fn run_suite(suite: &SuiteInfo) -> SuiteReport {
    'run: {
        let (result, summary) = run_fn!((suite.setup)());
        setup_summary = Some(summary);
        let Ok(mut suite_data) = result else { break 'run };
        let Ok(Ok(suite_data)) = result else { break 'run };

        for fixture in suite.fixtures.values() {
            let report = run_fixture(fixture, &suite_data).await;
            fixtures.push(report);
        }

        let (_, summary) = run_fn!((suite.teardown)(&mut suite_data));
        let (_, summary) = run_fn!((suite.teardown)(suite_data));
        teardown_summary = Some(summary);
    }

@@ -115,7 +128,7 @@ async fn run_suite(suite: &SuiteInfo) -> SuiteReport {
    }
}

#[instrument(skip(fixture, suite_data), fields(fixture_name = fixture.name))]
#[instrument(skip(fixture, suite_data), fields(name = fixture.name))]
async fn run_fixture(fixture: &FixtureInfo, suite_data: &ArcAny) -> FixtureReport {
    let total_cases = fixture.cases.len();
    info!(total_cases, "Test fixture start");
@@ -128,9 +141,9 @@ async fn run_fixture(fixture: &FixtureInfo, suite_data: &ArcAny) -> FixtureRepor

    'run: {
        info!("Test fixture setup");
        let (result, summary) = run_fn!((fixture.setup)(suite_data));
        let (result, summary) = run_fn!((fixture.setup)(Arc::clone(suite_data)));
        setup_summary = Some(summary);
        let Ok(mut fixture_data) = result else { break 'run };
        let Ok(Ok(fixture_data)) = result else { break 'run };

        for case in fixture.cases.values() {
            let report = run_case(case, &fixture_data).await;
@@ -138,7 +151,7 @@ async fn run_fixture(fixture: &FixtureInfo, suite_data: &ArcAny) -> FixtureRepor
        }

        info!("Test fixture teardown");
        let (_, summary) = run_fn!((fixture.teardown)(&mut fixture_data));
        let (_, summary) = run_fn!((fixture.teardown)(fixture_data));
        teardown_summary = Some(summary);
    }

@@ -158,12 +171,12 @@ async fn run_fixture(fixture: &FixtureInfo, suite_data: &ArcAny) -> FixtureRepor
    }
}

#[instrument(skip(case, fixture_data), fields(case_name = case.name))]
#[instrument(skip(case, fixture_data), fields(name = case.name))]
async fn run_case(case: &CaseInfo, fixture_data: &ArcAny) -> CaseReport {
    info!("Test case start");

    let t0 = Instant::now();
    let (_, summary) = run_fn!((case.run)(fixture_data));
    let (_, summary) = run_fn!((case.run)(Arc::clone(fixture_data)));

    info!(?summary, "Test case end");

+25 −15
Original line number Diff line number Diff line
@@ -4,6 +4,7 @@ use crate::traits::TestCase;
use crate::traits::TestFixture;
use crate::traits::TestSuite;

use std::any::type_name;
use std::future::Future;
use std::marker::PhantomData;
use std::pin::Pin;
@@ -15,12 +16,12 @@ pub(crate) type ArcAny = Arc<dyn std::any::Any + Send + Sync + 'static>;
type BoxFuture<'a, T> = Pin<Box<dyn Future<Output = T> + Send + 'a>>;

type SuiteSetupFn = Box<dyn Fn() -> BoxFuture<'static, Result<ArcAny, Failed>>>;
type SuiteTeardownFn = Box<dyn for<'a> Fn(&'a mut ArcAny) -> BoxFuture<'a, Result>>;
type SuiteTeardownFn = Box<dyn Fn(ArcAny) -> BoxFuture<'static, Result>>;

type FixtureSetupFn = Box<dyn for<'a> Fn(&'a ArcAny) -> BoxFuture<'a, Result<ArcAny, Failed>>>;
type FixtureTeardownFn = Box<dyn for<'a> Fn(&'a mut ArcAny) -> BoxFuture<'a, Result>>;
type FixtureSetupFn = Box<dyn Fn(ArcAny) -> BoxFuture<'static, Result<ArcAny, Failed>>>;
type FixtureTeardownFn = Box<dyn Fn(ArcAny) -> BoxFuture<'static, Result>>;

type CaseRunFn = Box<dyn for<'a> Fn(&'a ArcAny) -> BoxFuture<'a, Result>>;
type CaseRunFn = Box<dyn Fn(ArcAny) -> BoxFuture<'static, Result>>;

pub struct TestContext {
    pub(crate) suites: IndexMap<String, SuiteInfo>,
@@ -28,6 +29,7 @@ pub struct TestContext {

pub(crate) struct SuiteInfo {
    pub(crate) name: String,
    // pub(crate) type_id: TypeId,
    pub(crate) setup: SuiteSetupFn,
    pub(crate) teardown: SuiteTeardownFn,
    pub(crate) fixtures: IndexMap<String, FixtureInfo>,
@@ -35,6 +37,7 @@ pub(crate) struct SuiteInfo {

pub(crate) struct FixtureInfo {
    pub(crate) name: String,
    // pub(crate) type_id: TypeId,
    pub(crate) setup: FixtureSetupFn,
    pub(crate) teardown: FixtureTeardownFn,
    pub(crate) cases: IndexMap<String, CaseInfo>,
@@ -52,12 +55,19 @@ pub enum CaseTag {
    ShouldPanic,
}

fn downcast_ref<T: 'static>(any: &ArcAny) -> &T {
    (*any).downcast_ref().unwrap()
fn wrap<T: Send + Sync + 'static>(x: T) -> ArcAny {
    Arc::new(x)
}

fn downcast_mut<T: 'static>(any: &mut ArcAny) -> &mut T {
    Arc::get_mut(any).unwrap().downcast_mut().unwrap()
fn downcast<T: Send + Sync + 'static>(any: ArcAny) -> Arc<T> {
    Arc::downcast(any).unwrap()
}

fn unwrap<T: Send + Sync + 'static>(any: ArcAny) -> Result<T> {
    match Arc::try_unwrap(downcast::<T>(any)) {
        Ok(x) => Ok(x),
        Err(_) => Err(Failed::from_string(format!("Arc<{}> is leaked", type_name::<T>()))),
    }
}

impl TestContext {
@@ -72,8 +82,9 @@ impl TestContext {
                name.clone(),
                SuiteInfo {
                    name: name.clone(),
                    setup: Box::new(|| Box::pin(async { S::setup().await.map(|x| Arc::new(x) as ArcAny) })),
                    teardown: Box::new(|any| Box::pin(S::teardown(downcast_mut(any)))),
                    // type_id: TypeId::of::<S>(),
                    setup: Box::new(|| Box::pin(async { S::setup().await.map(wrap) })),
                    teardown: Box::new(|any| Box::pin(async move { S::teardown(unwrap(any)?).await })),
                    fixtures: IndexMap::new(),
                },
            );
@@ -98,10 +109,9 @@ impl<S: TestSuite> SuiteBuilder<'_, S> {
                name.clone(),
                FixtureInfo {
                    name: name.clone(),
                    setup: Box::new(|any| {
                        Box::pin(async move { X::setup(downcast_ref(any)).await.map(|x| Arc::new(x) as ArcAny) })
                    }),
                    teardown: Box::new(|any| Box::pin(X::teardown(downcast_mut(any)))),
                    // type_id: TypeId::of::<X>(),
                    setup: Box::new(|any| Box::pin(async move { X::setup(downcast(any)).await.map(wrap) })),
                    teardown: Box::new(|any| Box::pin(async move { X::teardown(unwrap(any)?).await })),
                    cases: IndexMap::new(),
                },
            );
@@ -129,7 +139,7 @@ where
            name.clone(),
            CaseInfo {
                name: name.clone(),
                run: Box::new(move |any| Box::pin(case.run(downcast_ref(any)))),
                run: Box::new(move |any| Box::pin(case.run(downcast(any)))),
                tags: Vec::new(),
            },
        );
+7 −6
Original line number Diff line number Diff line
use crate::error::Result;

use std::future::Future;
use std::sync::Arc;

pub trait TestSuite: Sized + Send + Sync + 'static {
    fn setup() -> impl Future<Output = Result<Self>> + Send + 'static;

    fn teardown(&mut self) -> impl Future<Output = Result> + Send + '_ {
    fn teardown(self) -> impl Future<Output = Result> + Send + 'static {
        async { Ok(()) }
    }
}

pub trait TestFixture<S: TestSuite>: Sized + Send + Sync + 'static {
    fn setup(suite: &S) -> impl Future<Output = Result<Self>> + Send + '_;
    fn setup(suite: Arc<S>) -> impl Future<Output = Result<Self>> + Send + 'static;

    fn teardown(&mut self) -> impl Future<Output = Result> + Send + '_ {
    fn teardown(self) -> impl Future<Output = Result> + Send + 'static {
        async { Ok(()) }
    }
}
@@ -24,7 +25,7 @@ where
    X: TestFixture<S>,
    S: TestSuite,
{
    fn run<'a>(&self, fixture: &'a X) -> impl Future<Output = Result> + Send + 'a;
    fn run(&self, fixture: Arc<X>) -> impl Future<Output = Result> + Send + 'static;
}

trait AsyncFn<'a, A> {
@@ -50,12 +51,12 @@ where

impl<C, X, S> TestCase<X, S> for C
where
    C: for<'a> AsyncFn<'a, (&'a X,), Output = Result>,
    C: for<'a> AsyncFn<'a, (Arc<X>,), Output = Result>,
    C: Send + Sync + 'static,
    X: TestFixture<S>,
    S: TestSuite,
{
    fn run<'a>(&self, fixture: &'a X) -> impl Future<Output = Result> + Send + 'a {
    fn run(&self, fixture: Arc<X>) -> impl Future<Output = Result> + Send + 'static {
        AsyncFn::call(self, (fixture,))
    }
}
Loading