feat: add --report switch to generate json outputs
for machine processing of results, maybe checking test coverage? also huge refactor of printing part
This commit is contained in:
parent
b9271ad47d
commit
7d56ea3565
2 changed files with 214 additions and 128 deletions
99
src/fmt.rs
Normal file
99
src/fmt.rs
Normal file
|
@ -0,0 +1,99 @@
|
||||||
|
use indexmap::IndexMap;
|
||||||
|
|
||||||
|
use crate::{PostWomanCollection, PostWomanError};
|
||||||
|
|
||||||
|
pub const TIMESTAMP_FMT: &str = "%H:%M:%S%.6f";
|
||||||
|
|
||||||
|
pub trait PrintableResult {
|
||||||
|
fn print(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
pub trait ReportableResult {
|
||||||
|
fn report(self);
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO this is not really nice, maybe a struct? Maybe pass them in some other way??
|
||||||
|
pub type RunResult = (Result<String, PostWomanError>, String, String, i64);
|
||||||
|
|
||||||
|
impl PrintableResult for RunResult {
|
||||||
|
fn print(self) {
|
||||||
|
let (result, _namespace, _name, _elapsed) = self;
|
||||||
|
match result {
|
||||||
|
Ok(x) => print!("{x}"),
|
||||||
|
Err(e) => eprintln!(" ! {e}"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReportableResult for RunResult {
|
||||||
|
fn report(self) {
|
||||||
|
let (res, namespace, name, elapsed) = self;
|
||||||
|
let success = res.is_ok();
|
||||||
|
let result = match res {
|
||||||
|
Ok(x) => x,
|
||||||
|
Err(e) => e.to_string(),
|
||||||
|
};
|
||||||
|
|
||||||
|
println!(
|
||||||
|
"{}",
|
||||||
|
serde_json::to_string(
|
||||||
|
&serde_json::json!({
|
||||||
|
"namespace": namespace,
|
||||||
|
"route": name,
|
||||||
|
"success": success,
|
||||||
|
"result": result,
|
||||||
|
"elapsed": elapsed,
|
||||||
|
})
|
||||||
|
)
|
||||||
|
.expect("failed serializing literal json")
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO the last tuple element is "compact"... this really needs a better way, maybe a struct!!
|
||||||
|
pub type ListResult = (IndexMap<String, PostWomanCollection>, bool);
|
||||||
|
|
||||||
|
impl PrintableResult for ListResult {
|
||||||
|
fn print(self) {
|
||||||
|
let (collections, compact) = self;
|
||||||
|
for (namespace, collection) in collections {
|
||||||
|
println!("-> {namespace}");
|
||||||
|
|
||||||
|
for (key, value) in collection.env.unwrap_or_default() {
|
||||||
|
println!(" + {key}={}", crate::ext::stringify_toml(&value));
|
||||||
|
}
|
||||||
|
|
||||||
|
for (name, mut endpoint) in collection.route {
|
||||||
|
println!(" - {name} \t{} \t{}", endpoint.method.as_deref().unwrap_or("GET"), endpoint.url);
|
||||||
|
if ! compact {
|
||||||
|
if let Some(ref query) = endpoint.query {
|
||||||
|
for query in query {
|
||||||
|
println!(" |? {query}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ref headers) = endpoint.headers {
|
||||||
|
for header in headers {
|
||||||
|
println!(" |: {header}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if let Some(ref _x) = endpoint.body {
|
||||||
|
if let Ok(body) = endpoint.body() {
|
||||||
|
println!(" |> {}", body.replace("\n", "\n |> "));
|
||||||
|
} else {
|
||||||
|
println!(" |> [!] invalid body");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
println!();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl ReportableResult for ListResult {
|
||||||
|
fn report(self) {
|
||||||
|
let (collections, _compact) = self;
|
||||||
|
println!("{}", serde_json::to_string(&collections).expect("collections are not valid json"));
|
||||||
|
}
|
||||||
|
}
|
243
src/main.rs
243
src/main.rs
|
@ -1,11 +1,13 @@
|
||||||
mod model;
|
mod model;
|
||||||
mod errors;
|
mod errors;
|
||||||
mod ext;
|
mod ext;
|
||||||
|
mod fmt;
|
||||||
|
|
||||||
use std::str::FromStr;
|
use std::str::FromStr;
|
||||||
|
|
||||||
use clap::{Parser, Subcommand};
|
use clap::{Parser, Subcommand};
|
||||||
|
|
||||||
|
use fmt::{PrintableResult, ReportableResult};
|
||||||
use indexmap::IndexMap;
|
use indexmap::IndexMap;
|
||||||
pub use model::PostWomanCollection;
|
pub use model::PostWomanCollection;
|
||||||
pub use errors::PostWomanError;
|
pub use errors::PostWomanError;
|
||||||
|
@ -26,7 +28,11 @@ struct PostWomanArgs {
|
||||||
|
|
||||||
/// start a multi-thread runtime, with multiple worker threads
|
/// start a multi-thread runtime, with multiple worker threads
|
||||||
#[arg(short = 'M', long, default_value_t = false)]
|
#[arg(short = 'M', long, default_value_t = false)]
|
||||||
multi_threaded: bool,
|
multi_thread: bool,
|
||||||
|
|
||||||
|
/// emit json report document instead of pretty printing
|
||||||
|
#[arg(short = 'R', long, default_value_t = false)]
|
||||||
|
report: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Subcommand, Debug)]
|
#[derive(Subcommand, Debug)]
|
||||||
|
@ -57,11 +63,11 @@ pub enum PostWomanActions {
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
const TIMESTAMP_FMT: &str = "%H:%M:%S%.6f";
|
const DEFAULT_ACTION: PostWomanActions = PostWomanActions::List { compact: true };
|
||||||
|
|
||||||
fn main() {
|
fn main() {
|
||||||
let args = PostWomanArgs::parse();
|
let args = PostWomanArgs::parse();
|
||||||
let multi_thread = args.multi_threaded;
|
let multi_thread = args.multi_thread;
|
||||||
|
|
||||||
// if we got a regex, test it early to avoid wasting work when invalid
|
// if we got a regex, test it early to avoid wasting work when invalid
|
||||||
if let Some(PostWomanActions::Run { ref query, .. }) = args.action {
|
if let Some(PostWomanActions::Run { ref query, .. }) = args.action {
|
||||||
|
@ -77,35 +83,114 @@ fn main() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
let task = async move {
|
match args.action.as_ref().unwrap_or(&DEFAULT_ACTION) {
|
||||||
|
PostWomanActions::List { compact } => {
|
||||||
let mut pool = tokio::task::JoinSet::new();
|
if args.report {
|
||||||
|
(collections, *compact).report();
|
||||||
for (collection_name, collection) in collections {
|
} else {
|
||||||
run_postwoman(&args, collection_name, collection, &mut pool).await;
|
(collections, *compact).print();
|
||||||
}
|
|
||||||
|
|
||||||
while let Some(j) = pool.join_next().await {
|
|
||||||
match j {
|
|
||||||
Err(e) => eprintln!("! error joining task: {e}"),
|
|
||||||
Ok(res) => res.print(),
|
|
||||||
}
|
}
|
||||||
}
|
},
|
||||||
};
|
|
||||||
|
|
||||||
eprintln!("~@ {APP_USER_AGENT}");
|
PostWomanActions::Run { query, parallel, debug, dry_run } => {
|
||||||
if multi_thread {
|
let task = async move {
|
||||||
tokio::runtime::Builder::new_multi_thread()
|
let mut pool = tokio::task::JoinSet::new();
|
||||||
.enable_all()
|
|
||||||
.build()
|
for (collection_name, collection) in collections {
|
||||||
.expect("failed creating tokio multi-thread runtime")
|
run_collection_endpoints(
|
||||||
.block_on(task)
|
collection_name,
|
||||||
} else {
|
collection,
|
||||||
tokio::runtime::Builder::new_current_thread()
|
query.clone(),
|
||||||
.enable_all()
|
*parallel,
|
||||||
.build()
|
*debug,
|
||||||
.expect("failed creating tokio current-thread runtime")
|
*dry_run,
|
||||||
.block_on(task)
|
args.report,
|
||||||
|
&mut pool
|
||||||
|
).await;
|
||||||
|
}
|
||||||
|
|
||||||
|
while let Some(j) = pool.join_next().await {
|
||||||
|
if let Err(e) = j {
|
||||||
|
eprintln!("! error joining task: {e}");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
eprintln!("~@ {APP_USER_AGENT}");
|
||||||
|
if multi_thread {
|
||||||
|
tokio::runtime::Builder::new_multi_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("failed creating tokio multi-thread runtime")
|
||||||
|
.block_on(task)
|
||||||
|
} else {
|
||||||
|
tokio::runtime::Builder::new_current_thread()
|
||||||
|
.enable_all()
|
||||||
|
.build()
|
||||||
|
.expect("failed creating tokio current-thread runtime")
|
||||||
|
.block_on(task)
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// TODO too many arguments
|
||||||
|
async fn run_collection_endpoints(
|
||||||
|
namespace: String,
|
||||||
|
collection: PostWomanCollection,
|
||||||
|
query: String,
|
||||||
|
parallel: bool,
|
||||||
|
debug: bool,
|
||||||
|
dry_run: bool,
|
||||||
|
report: bool,
|
||||||
|
pool: &mut tokio::task::JoinSet<()>
|
||||||
|
) {
|
||||||
|
// this is always safe to compile because we tested it beforehand
|
||||||
|
let pattern = regex::Regex::new(&query).expect("tested it before and still failed here???");
|
||||||
|
let client = std::sync::Arc::new(collection.client.unwrap_or_default());
|
||||||
|
let env = std::sync::Arc::new(collection.env.unwrap_or_default());
|
||||||
|
|
||||||
|
for (name, mut endpoint) in collection.route {
|
||||||
|
if pattern.find(&name).is_none() { continue };
|
||||||
|
|
||||||
|
if debug { endpoint.extract = Some(ext::StringOr::T(model::ExtractorConfig::Debug)) };
|
||||||
|
let _client = client.clone();
|
||||||
|
let _env = env.clone();
|
||||||
|
let _namespace = namespace.clone();
|
||||||
|
|
||||||
|
let task = async move {
|
||||||
|
let before = chrono::Local::now();
|
||||||
|
eprintln!(" : [{}] {_namespace}::{name} \tsending request...", before.format(fmt::TIMESTAMP_FMT));
|
||||||
|
|
||||||
|
let res = if dry_run {
|
||||||
|
Ok("".to_string())
|
||||||
|
} else {
|
||||||
|
endpoint
|
||||||
|
.fill(&_env)
|
||||||
|
.execute(&_client)
|
||||||
|
.await
|
||||||
|
};
|
||||||
|
|
||||||
|
let after = chrono::Local::now();
|
||||||
|
let elapsed = (after - before).num_milliseconds();
|
||||||
|
|
||||||
|
let timestamp = after.format(fmt::TIMESTAMP_FMT);
|
||||||
|
let symbol = if res.is_ok() { " + " } else { "<!>" };
|
||||||
|
let verb = if res.is_ok() { "done in" } else { "failed after" };
|
||||||
|
eprintln!("{symbol}[{timestamp}] {_namespace}::{name} \t{verb} {elapsed}ms", );
|
||||||
|
|
||||||
|
if report {
|
||||||
|
(res, _namespace, name, elapsed).report();
|
||||||
|
} else {
|
||||||
|
(res, _namespace, name, elapsed).print();
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
if parallel {
|
||||||
|
pool.spawn(task);
|
||||||
|
} else {
|
||||||
|
task.await;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -149,101 +234,3 @@ fn load_collections(store: &mut IndexMap<String, PostWomanCollection>, mut path:
|
||||||
|
|
||||||
true
|
true
|
||||||
}
|
}
|
||||||
|
|
||||||
const DEFAULT_ACTION: PostWomanActions = PostWomanActions::List { compact: true };
|
|
||||||
type RunResult = (Result<String, PostWomanError>, String, String, chrono::DateTime<chrono::Local>);
|
|
||||||
|
|
||||||
async fn run_postwoman(args: &PostWomanArgs, namespace: String, collection: PostWomanCollection, pool: &mut tokio::task::JoinSet<RunResult>) {
|
|
||||||
let action = args.action.as_ref().unwrap_or(&DEFAULT_ACTION);
|
|
||||||
|
|
||||||
match action {
|
|
||||||
PostWomanActions::List { compact } => {
|
|
||||||
println!("-> {namespace}");
|
|
||||||
|
|
||||||
for (key, value) in collection.env.unwrap_or_default() {
|
|
||||||
println!(" + {key}={}", ext::stringify_toml(&value));
|
|
||||||
}
|
|
||||||
|
|
||||||
for (name, mut endpoint) in collection.route {
|
|
||||||
println!(" - {name} \t{} \t{}", endpoint.method.as_deref().unwrap_or("GET"), endpoint.url);
|
|
||||||
if ! *compact {
|
|
||||||
if let Some(ref query) = endpoint.query {
|
|
||||||
for query in query {
|
|
||||||
println!(" |? {query}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(ref headers) = endpoint.headers {
|
|
||||||
for header in headers {
|
|
||||||
println!(" |: {header}");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if let Some(ref _x) = endpoint.body {
|
|
||||||
if let Ok(body) = endpoint.body() {
|
|
||||||
println!(" |> {}", body.replace("\n", "\n |> "));
|
|
||||||
} else {
|
|
||||||
println!(" |> [!] invalid body");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
println!();
|
|
||||||
},
|
|
||||||
PostWomanActions::Run { query, parallel, debug, dry_run } => {
|
|
||||||
// this is always safe to compile because we tested it beforehand
|
|
||||||
let pattern = regex::Regex::new(query).expect("tested it before and still failed here???");
|
|
||||||
let client = std::sync::Arc::new(collection.client.unwrap_or_default());
|
|
||||||
let env = std::sync::Arc::new(collection.env.unwrap_or_default());
|
|
||||||
for (name, mut endpoint) in collection.route {
|
|
||||||
if pattern.find(&name).is_some() {
|
|
||||||
if *debug { endpoint.extract = Some(ext::StringOr::T(model::ExtractorConfig::Debug)) };
|
|
||||||
let _client = client.clone();
|
|
||||||
let _env = env.clone();
|
|
||||||
let _endpoint = endpoint.clone();
|
|
||||||
let _dry_run = *dry_run;
|
|
||||||
let _name = name.clone();
|
|
||||||
let _namespace = namespace.clone();
|
|
||||||
let task = async move {
|
|
||||||
let before = chrono::Local::now();
|
|
||||||
eprintln!(" : [{}] {_namespace}::{_name} \tsending request...", before.format(TIMESTAMP_FMT));
|
|
||||||
if _dry_run {
|
|
||||||
(Ok("".to_string()), _namespace, _name, before)
|
|
||||||
} else {
|
|
||||||
let res = _endpoint
|
|
||||||
.fill(&_env)
|
|
||||||
.execute(&_client)
|
|
||||||
.await;
|
|
||||||
(res, _namespace, _name, before)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
if *parallel {
|
|
||||||
pool.spawn(task);
|
|
||||||
} else {
|
|
||||||
task.await.print();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
trait PrintableResult {
|
|
||||||
fn print(self);
|
|
||||||
}
|
|
||||||
|
|
||||||
impl PrintableResult for RunResult {
|
|
||||||
fn print(self) {
|
|
||||||
let (result, namespace, name, before) = self;
|
|
||||||
let success = result.is_ok();
|
|
||||||
let after = chrono::Local::now();
|
|
||||||
let elapsed = (after - before).num_milliseconds();
|
|
||||||
let timestamp = after.format(TIMESTAMP_FMT);
|
|
||||||
let symbol = if success { " + " } else { "<!>" };
|
|
||||||
let verb = if success { "done in" } else { "failed after" };
|
|
||||||
eprintln!("{symbol}[{timestamp}] {namespace}::{name} \t{verb} {elapsed}ms", );
|
|
||||||
match result {
|
|
||||||
Ok(x) => print!("{x}"),
|
|
||||||
Err(e) => eprintln!(" ! {e}"),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue