use clap::Command;
use console::Style;
use proptest::prelude::*;
use serde_json::{json, Value};
use serde_yaml;
use standout::cli::{App, Output};
use standout::{OutputMode, Theme};
fn output_mode_strategy() -> impl Strategy<Value = OutputMode> {
prop_oneof![
Just(OutputMode::Auto),
Just(OutputMode::Term),
Just(OutputMode::Text),
Just(OutputMode::TermDebug),
Just(OutputMode::Json),
Just(OutputMode::Yaml),
Just(OutputMode::Xml),
Just(OutputMode::Csv),
]
}
fn theme_strategy() -> impl Strategy<Value = Option<Theme>> {
prop_oneof![
Just(None),
Just(Some(Theme::new())),
Just(Some(
Theme::new()
.add("title", Style::new().bold())
.add("highlight", Style::new().cyan())
.add("error", Style::new().red().bold())
)),
]
}
fn template_strategy() -> impl Strategy<Value = &'static str> {
prop_oneof![
Just("{{ . }}"),
Just("[title]{{ . }}[/title]"),
Just("[highlight]Output: [title]{{ . }}[/title][/highlight]"),
]
}
fn json_data_strategy() -> impl Strategy<Value = Value> {
let leaf = prop_oneof![
Just(Value::Null),
any::<bool>().prop_map(Value::Bool),
any::<f64>().prop_map(|f| json!(f)),
"[a-zA-Z0-9]*".prop_map(Value::String),
];
leaf.prop_recursive(
4, 64, 10, |inner| {
prop_oneof![
prop::collection::vec(inner.clone(), 0..10).prop_map(Value::Array),
prop::collection::hash_map("[a-zA-Z0-9]*", inner, 0..10)
.prop_map(|m| { Value::Object(m.into_iter().collect()) })
]
},
)
}
fn validate_structured_output(output: &str, mode: OutputMode) {
match mode {
OutputMode::Json => {
let parsed: Result<Value, _> = serde_json::from_str(output);
assert!(
parsed.is_ok(),
"JSON output should be parseable: {}",
output
);
}
OutputMode::Yaml => {
let parsed: Result<Value, _> = serde_yaml::from_str(output);
assert!(
parsed.is_ok(),
"YAML output should be parseable: {}",
output
);
}
OutputMode::Xml => {
assert!(!output.is_empty(), "XML output should not be empty");
assert!(
output.contains('<') && output.contains('>'),
"XML output should contain tags: {}",
output
);
}
OutputMode::Csv => {
assert!(!output.is_empty(), "CSV output should not be empty");
}
_ => {}
}
}
proptest! {
#[test]
fn test_rendering_invariants(
mode in output_mode_strategy(),
theme in theme_strategy(),
template in template_strategy(),
data in json_data_strategy()
) {
let builder = App::builder()
.command(
"test",
move |_m, _ctx| Ok(Output::Render(data.clone())),
template,
).unwrap();
let builder = if let Some(t) = theme {
builder.theme(t)
} else {
builder
};
let app = builder.build().expect("Failed to build app");
let cmd = Command::new("app").subcommand(Command::new("test"));
let matches = cmd.try_get_matches_from(["app", "test"]).unwrap();
let result = app.dispatch(matches, mode);
assert!(result.is_handled());
if let Some(output) = result.output() {
validate_structured_output(output, mode);
}
}
}