Skip to content

Commit

Permalink
feat(s3s-test): impl custom test suite
Browse files Browse the repository at this point in the history
  • Loading branch information
Nugine committed Oct 13, 2024
1 parent 9fb8e90 commit 7859e4d
Show file tree
Hide file tree
Showing 12 changed files with 777 additions and 0 deletions.
19 changes: 19 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -94,3 +94,22 @@ jobs:
with:
name: e2e-mint-logs
path: ./target/s3s-proxy.log

e2e-fs:
needs: skip-check
if: needs.skip-check.outputs.should_skip != 'true'
name: e2e (s3s-fs, s3s-e2e)
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- uses: dtolnay/rust-toolchain@master
with:
toolchain: stable
- uses: Swatinem/rust-cache@v2
- run: ./scripts/e2e-fs.sh
- uses: actions/upload-artifact@v4
with:
name: e2e-fs-logs
path: |
./target/s3s-fs.log
./target/s3s-e2e.log
1 change: 1 addition & 0 deletions .gitignore
Original file line number Diff line number Diff line change
Expand Up @@ -3,3 +3,4 @@
.vscode

/codegen/s3.json
.env
32 changes: 32 additions & 0 deletions crates/s3s-test/Cargo.toml
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
[package]
name = "s3s-test"
version = "0.0.0"
description = "s3s test suite"
readme = "../../README.md"
keywords = ["s3"]
categories = ["web-programming", "web-programming::http-server"]
edition.workspace = true
repository.workspace = true
license.workspace = true

[[bin]]
name = "s3s-e2e"
path = "e2e/main.rs"

[dependencies]
serde = { version = "1.0.210", features = ["derive"] }
tokio = { version = "1.40.0", features = ["full"] }
tracing = "0.1.40"
tracing-subscriber = { version = "0.3.18", features = ["env-filter", "time"] }
aws-credential-types = "1.2.1"
aws-sdk-s3 = "1.51.0"
clap = { version = "4.5.17", features = ["derive"] }
dotenvy = "0.15.7"
serde_json = "1.0.128"
indexmap = "2.6.0"
colored = "2.1.0"

[dependencies.aws-config]
version = "1.5.6"
default-features = false
features = ["behavior-version-latest"]
54 changes: 54 additions & 0 deletions crates/s3s-test/e2e/main.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
use s3s_test::Result;
use s3s_test::TestFixture;
use s3s_test::TestSuite;

use tracing::debug;

struct Basic {
client: aws_sdk_s3::Client,
}

impl TestSuite for Basic {
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);
Ok(Self { client })
}
}

struct BasicOps {
client: aws_sdk_s3::Client,
}

impl TestFixture<Basic> for BasicOps {
async fn setup(suite: &Basic) -> 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);
Ok(())
}
}

fn main() {
s3s_test::cli::main(|tcx| {
macro_rules! case {
($s:ident, $x:ident, $c:ident) => {{
let mut suite = tcx.suite::<$s>(stringify!($s));
let mut fixture = suite.fixture::<$x>(stringify!($x));
fixture.case(stringify!($c), $x::$c);
}};
}

case!(Basic, BasicOps, list_buckets);
})
}
102 changes: 102 additions & 0 deletions crates/s3s-test/src/cli.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,102 @@
use std::path::PathBuf;

use crate::report::FnResult;
use crate::tcx::TestContext;

use clap::Parser;
use colored::ColoredString;
use colored::Colorize;

type StdError = Box<dyn std::error::Error + Send + Sync + 'static>;

fn setup_tracing() {
use std::io::IsTerminal;
use tracing_subscriber::EnvFilter;

let env_filter = EnvFilter::from_default_env();
let enable_color = std::io::stdout().is_terminal();

tracing_subscriber::fmt()
.pretty()
.with_env_filter(env_filter)
.with_ansi(enable_color)
.init();
}

#[derive(Debug, Parser)]
struct Opt {
#[clap(long)]
json: Option<PathBuf>,
}

fn status(passed: bool) -> ColoredString {
if passed {
"PASSED".green()
} else {
"FAILED".red()
}
}

#[tokio::main]
async fn async_main(opt: &Opt, register: impl FnOnce(&mut TestContext)) -> Result<(), StdError> {
let mut tcx = TestContext::new();
register(&mut tcx);

let report = crate::runner::run(&mut tcx).await;

if let Some(ref json_path) = opt.json {
let report_json = serde_json::to_string_pretty(&report)?;
std::fs::write(json_path, report_json)?;
}

let w = format!("{:.3}", report.duration_ms).len();

for suite in &report.suites {
let suite_name = suite.name.as_str().magenta();
for fixture in &suite.fixtures {
let fixture_name = fixture.name.as_str().blue();
for case in &fixture.cases {
let case_name = case.name.as_str().cyan();
let status = status(case.passed);
let duration = case.duration_ms;
println!("{status} {duration:>w$.3}ms [{suite_name}/{fixture_name}/{case_name}]");
if !case.passed {
if let Some(ref run) = case.run {
let hint = match run.result {
FnResult::Ok => "".normal(),
FnResult::Err(_) => "ERROR".red(),
FnResult::Panicked => "PANICKED".red().bold(),
};
let msg = if let FnResult::Err(ref e) = run.result {
e.as_str()
} else {
""
};
println!(" {hint} {msg}");
}
}
}
let status = status(fixture.case_count.all_passed());
let duration = fixture.duration_ms;
println!("{status} {duration:>w$.3}ms [{suite_name}/{fixture_name}]");
}
let status = status(suite.fixture_count.all_passed());
let duration = suite.duration_ms;
println!("{status} {duration:>w$.3}ms [{suite_name}]");
}
let status = status(report.suite_count.all_passed());
let duration = report.duration_ms;
println!("{status} {duration:>w$.3}ms");

Ok(())
}

pub fn main(register: impl FnOnce(&mut TestContext)) {
dotenvy::dotenv().ok();
setup_tracing();
let opt = Opt::parse();
if let Err(err) = async_main(&opt, register) {
eprintln!("{err}");
std::process::exit(1);
}
}
29 changes: 29 additions & 0 deletions crates/s3s-test/src/error.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,29 @@
use std::fmt;

pub type Result<T = (), E = Failed> = std::result::Result<T, E>;

#[derive(Debug)]
pub struct Failed {
source: Option<Box<dyn std::error::Error + Send + Sync + 'static>>,
}

impl<E> From<E> for Failed
where
E: std::error::Error + Send + Sync + 'static,
{
fn from(source: E) -> Self {
Self {
source: Some(Box::new(source)),
}
}
}

impl fmt::Display for Failed {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
if let Some(source) = &self.source {
write!(f, "Failed: {source}")
} else {
write!(f, "Failed")
}
}
}
27 changes: 27 additions & 0 deletions crates/s3s-test/src/lib.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
#![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
)]

mod error;
mod runner;
mod traits;

pub mod cli;
pub mod report;
pub mod tcx;

pub use self::error::{Failed, Result};
pub use self::traits::*;
82 changes: 82 additions & 0 deletions crates/s3s-test/src/report.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,82 @@
use serde::{Deserialize, Serialize};

#[derive(Serialize, Deserialize)]
pub struct Report {
pub suite_count: CountSummary,
pub duration_ns: u64,
pub duration_ms: f64,

pub suites: Vec<SuiteReport>,
}

#[derive(Serialize, Deserialize)]
pub struct SuiteReport {
pub name: String,

pub fixture_count: CountSummary,
pub duration_ns: u64,
pub duration_ms: f64,

pub setup: Option<FnSummary>,
pub teardown: Option<FnSummary>,
pub fixtures: Vec<FixtureReport>,
}

#[derive(Serialize, Deserialize)]
pub struct FixtureReport {
pub name: String,

pub case_count: CountSummary,
pub duration_ns: u64,
pub duration_ms: f64,

pub setup: Option<FnSummary>,
pub teardown: Option<FnSummary>,
pub cases: Vec<CaseReport>,
}

#[derive(Serialize, Deserialize)]
pub struct CaseReport {
pub name: String,

pub passed: bool,
pub duration_ns: u64,
pub duration_ms: f64,

pub run: Option<FnSummary>,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct FnSummary {
pub result: FnResult,
pub duration_ns: u64,
pub duration_ms: f64,
}

#[derive(Debug, Serialize, Deserialize)]
pub struct CountSummary {
pub total: u64,
pub passed: u64,
pub failed: u64,
}

impl CountSummary {
#[must_use]
pub fn all_passed(&self) -> bool {
self.passed == self.total
}
}

#[derive(Debug, Serialize, Deserialize)]
pub enum FnResult {
Ok,
Err(String),
Panicked,
}

impl FnResult {
#[must_use]
pub fn is_ok(&self) -> bool {
matches!(self, FnResult::Ok)
}
}
Loading

0 comments on commit 7859e4d

Please sign in to comment.