#![cfg(feature = "arbitrary")]
use std::collections::BTreeMap;
use std::ffi::OsString;
use arbitrary::Arbitrary;
use facet_core::Facet;
use heck::ToKebabCase;
use rand::{RngCore, SeedableRng};
use rand_chacha::ChaCha8Rng;
use crate::config_value_parser::from_config_value;
use crate::layers::cli::{CliConfigBuilder, parse_cli};
use crate::schema::{ArgKind, ArgLevelSchema, Schema};
use crate::ToArgs;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ArbitraryCheckError {
pub successful_samples: usize,
pub attempts: usize,
pub message: String,
}
impl core::fmt::Display for ArbitraryCheckError {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
write!(
f,
"{message} (successful_samples={successful}, attempts={attempts})",
message = self.message,
successful = self.successful_samples,
attempts = self.attempts
)
}
}
impl std::error::Error for ArbitraryCheckError {}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TestToArgsConsistencyConfig {
pub success_count: usize,
pub max_attempts: usize,
pub random_data_len: usize,
pub prefill_sample_count: usize,
pub root_seed: Option<u64>,
}
impl Default for TestToArgsConsistencyConfig {
fn default() -> Self {
Self {
success_count: 500,
max_attempts: 10_000,
random_data_len: 1024,
prefill_sample_count: 256,
root_seed: None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct TestToArgsRoundTrip {
pub success_count_per_leaf: usize,
pub success_count_global: usize,
pub max_attempts_per_leaf: usize,
pub max_attempts_global: usize,
pub random_data_len: usize,
pub prefill_sample_count: usize,
pub root_seed: Option<u64>,
}
impl Default for TestToArgsRoundTrip {
fn default() -> Self {
Self {
success_count_per_leaf: 4,
success_count_global: 4,
max_attempts_per_leaf: 4 * 4_000,
max_attempts_global: 4 * 80,
random_data_len: 1024,
prefill_sample_count: 256,
root_seed: None,
}
}
}
#[derive(Debug)]
struct EntropyPool {
sample_len: usize,
prefill_sample_count: usize,
root_seed: u64,
next_offset: usize,
bytes: Vec<u8>,
rng: ChaCha8Rng,
}
impl EntropyPool {
fn new(sample_len: usize, prefill_sample_count: usize, root_seed: Option<u64>) -> Self {
let sample_len = sample_len.max(1);
let prefill_sample_count = prefill_sample_count.max(1);
let root_seed = root_seed.unwrap_or_else(rand::random::<u64>);
let byte_len = sample_len.saturating_mul(prefill_sample_count);
let mut pool = Self {
sample_len,
prefill_sample_count,
root_seed,
next_offset: 0,
bytes: vec![0u8; byte_len],
rng: ChaCha8Rng::seed_from_u64(root_seed),
};
pool.refill();
pool
}
fn next_sample(&mut self) -> &[u8] {
if self.next_offset + self.sample_len > self.bytes.len() {
self.refill();
}
let start = self.next_offset;
let end = start + self.sample_len;
self.next_offset = end;
&self.bytes[start..end]
}
fn context_suffix(&self) -> String {
format!(
"root_seed={} random_data_len={} prefill_sample_count={}",
self.root_seed, self.sample_len, self.prefill_sample_count
)
}
fn refill(&mut self) {
self.rng.fill_bytes(&mut self.bytes);
self.next_offset = 0;
}
}
pub fn assert_to_args_consistency<T>(
config: TestToArgsConsistencyConfig,
) -> Result<(), ArbitraryCheckError>
where
T: Facet<'static> + for<'a> Arbitrary<'a> + core::fmt::Debug,
{
let mut entropy_pool = EntropyPool::new(
config.random_data_len,
config.prefill_sample_count,
config.root_seed,
);
let mut successful_samples = 0usize;
let mut attempts = 0usize;
while successful_samples < config.success_count && attempts < config.max_attempts {
attempts += 1;
let mut rng = arbitrary::Unstructured::new(entropy_pool.next_sample());
let Ok(instance) = T::arbitrary(&mut rng) else {
continue;
};
let args1 = instance.to_args().map_err(|error| ArbitraryCheckError {
successful_samples,
attempts,
message: format!("first to_args() call failed: {error}"),
})?;
let args2 = instance.to_args().map_err(|error| ArbitraryCheckError {
successful_samples,
attempts,
message: format!("second to_args() call failed: {error}"),
})?;
if args1 != args2 {
return Err(ArbitraryCheckError {
successful_samples,
attempts,
message: format!(
"to_args() is non-deterministic for generated value: {instance:?}\nfirst={args1:?}\nsecond={args2:?}\n{}",
entropy_pool.context_suffix()
),
});
}
successful_samples += 1;
}
if successful_samples < config.success_count {
return Err(ArbitraryCheckError {
successful_samples,
attempts,
message: format!(
"insufficient arbitrary coverage for consistency test\n{}",
entropy_pool.context_suffix()
),
});
}
Ok(())
}
pub fn assert_to_args_roundtrip<T>(config: TestToArgsRoundTrip) -> Result<(), ArbitraryCheckError>
where
T: Facet<'static> + for<'a> Arbitrary<'a> + PartialEq + core::fmt::Debug,
{
let schema = Schema::from_shape(T::SHAPE).map_err(|error| ArbitraryCheckError {
successful_samples: 0,
attempts: 0,
message: format!("failed to build schema: {error}"),
})?;
let mut command_tree = command_node_from_arg_level(schema.args());
let command_paths = collect_command_paths(&mut command_tree);
if command_paths.is_empty() {
return assert_to_args_roundtrip_global::<T>(config);
}
let mut entropy_pool = EntropyPool::new(
config.random_data_len,
config.prefill_sample_count,
config.root_seed,
);
let mut matched_samples_by_path = vec![0usize; command_paths.len()];
let mut remaining_paths = matched_samples_by_path.len();
let max_attempts_total = remaining_paths.saturating_mul(config.max_attempts_per_leaf);
let mut total_successful_samples = 0usize;
let mut total_attempts = 0usize;
while remaining_paths > 0 && total_attempts < max_attempts_total {
total_attempts += 1;
let mut rng = arbitrary::Unstructured::new(entropy_pool.next_sample());
let Ok(instance) = T::arbitrary(&mut rng) else {
continue;
};
let args = instance.to_args().map_err(|error| ArbitraryCheckError {
successful_samples: total_successful_samples,
attempts: total_attempts,
message: format!("to_args() failed: {error}"),
})?;
let Some(leaf_id) = extract_subcommand_leaf_id_from_args(&args, &command_tree) else {
continue;
};
let matched_samples = &mut matched_samples_by_path[leaf_id];
if *matched_samples >= config.success_count_per_leaf {
continue;
}
let parsed = parse_from_os_args_with_schema::<T>(&schema, &args).map_err(|message| {
ArbitraryCheckError {
successful_samples: total_successful_samples,
attempts: total_attempts,
message: format!(
"failed to parse generated args for path {:?}\nargs={args:?}\nvalue={instance:?}\nerror={message}\n{}",
command_paths[leaf_id],
entropy_pool.context_suffix()
),
}
})?;
if instance != parsed {
return Err(ArbitraryCheckError {
successful_samples: total_successful_samples,
attempts: total_attempts,
message: format!(
"roundtrip mismatch for path {:?}\noriginal={instance:?}\nparsed={parsed:?}\nargs={args:?}\n{}",
command_paths[leaf_id],
entropy_pool.context_suffix()
),
});
}
*matched_samples += 1;
total_successful_samples += 1;
if *matched_samples == config.success_count_per_leaf {
remaining_paths -= 1;
}
}
if remaining_paths > 0 {
let missing_paths = command_paths
.iter()
.zip(&matched_samples_by_path)
.filter(|(_, matched_samples)| **matched_samples < config.success_count_per_leaf)
.map(|(path, matched_samples)| {
format!(
"{path:?}: matched {matched_samples} samples after {total_attempts} total attempts"
)
})
.collect::<Vec<_>>()
.join(", ");
return Err(ArbitraryCheckError {
successful_samples: total_successful_samples,
attempts: total_attempts,
message: format!(
"insufficient coverage for command paths: {missing_paths}\n{}",
entropy_pool.context_suffix()
),
});
}
Ok(())
}
fn assert_to_args_roundtrip_global<T>(
config: TestToArgsRoundTrip,
) -> Result<(), ArbitraryCheckError>
where
T: Facet<'static> + for<'a> Arbitrary<'a> + PartialEq + core::fmt::Debug,
{
let schema = Schema::from_shape(T::SHAPE).map_err(|error| ArbitraryCheckError {
successful_samples: 0,
attempts: 0,
message: format!("failed to build schema: {error}"),
})?;
let mut entropy_pool = EntropyPool::new(
config.random_data_len,
config.prefill_sample_count,
config.root_seed,
);
let mut successful_samples = 0usize;
let mut attempts = 0usize;
while successful_samples < config.success_count_global && attempts < config.max_attempts_global
{
attempts += 1;
let mut rng = arbitrary::Unstructured::new(entropy_pool.next_sample());
let Ok(instance) = T::arbitrary(&mut rng) else {
continue;
};
let args = instance.to_args().map_err(|error| ArbitraryCheckError {
successful_samples,
attempts,
message: format!("to_args() failed: {error}"),
})?;
let parsed = parse_from_os_args_with_schema::<T>(&schema, &args).map_err(|message| {
ArbitraryCheckError {
successful_samples,
attempts,
message: format!(
"failed to parse generated args\nargs={args:?}\nvalue={instance:?}\nerror={message}\n{}",
entropy_pool.context_suffix()
),
}
})?;
if instance != parsed {
return Err(ArbitraryCheckError {
successful_samples,
attempts,
message: format!(
"roundtrip mismatch\noriginal={instance:?}\nparsed={parsed:?}\nargs={args:?}\n{}",
entropy_pool.context_suffix()
),
});
}
successful_samples += 1;
}
if successful_samples < config.success_count_global {
return Err(ArbitraryCheckError {
successful_samples,
attempts,
message: format!(
"insufficient arbitrary coverage for roundtrip test\n{}",
entropy_pool.context_suffix()
),
});
}
Ok(())
}
#[derive(Clone, Debug)]
struct CommandBranch {
cli_name: String,
effective_name: String,
node: CommandNode,
}
#[derive(Clone, Debug, Default)]
struct CommandNode {
positional_count: usize,
leaf_id: Option<usize>,
named_flag_consumes_value: BTreeMap<String, bool>,
subcommands: Vec<CommandBranch>,
}
fn command_node_from_arg_level(level: &ArgLevelSchema) -> CommandNode {
let mut node = CommandNode::default();
for (name, schema) in level.args() {
match schema.kind() {
ArgKind::Positional => {
node.positional_count += 1;
}
ArgKind::Named { counted, .. } => {
let consumes_value = !counted && !schema.value().inner_if_option().is_bool();
node.named_flag_consumes_value
.insert(name.to_kebab_case(), consumes_value);
}
}
}
for subcommand in level.subcommands().values() {
node.subcommands.push(CommandBranch {
cli_name: subcommand.cli_name().to_string(),
effective_name: subcommand.effective_name().to_string(),
node: command_node_from_arg_level(subcommand.args()),
});
}
node
}
fn collect_command_paths(root: &mut CommandNode) -> Vec<Vec<String>> {
fn visit(node: &mut CommandNode, current: &mut Vec<String>, output: &mut Vec<Vec<String>>) {
if node.subcommands.is_empty() {
if !current.is_empty() {
node.leaf_id = Some(output.len());
output.push(current.clone());
}
return;
}
for branch in &mut node.subcommands {
current.push(branch.effective_name.clone());
visit(&mut branch.node, current, output);
let _ = current.pop();
}
}
let mut output = Vec::new();
let mut current = Vec::new();
visit(root, &mut current, &mut output);
output
}
fn extract_subcommand_leaf_id_from_args(args: &[OsString], root: &CommandNode) -> Option<usize> {
fn walk(node: &CommandNode, tokens: &[OsString], index: &mut usize) -> Option<usize> {
let mut positionals_seen = 0usize;
while *index < tokens.len() {
let token = tokens[*index].to_string_lossy();
let token = token.as_ref();
if token.starts_with("--") {
let flag_name = token.trim_start_matches("--");
if let Some(consumes_value) = node.named_flag_consumes_value.get(flag_name) {
if *consumes_value {
*index = (*index + 2).min(tokens.len());
} else {
*index += 1;
}
} else {
*index += 1;
if *index < tokens.len()
&& !tokens[*index].to_string_lossy().starts_with('-')
{
*index += 1;
}
}
continue;
}
if token.starts_with('-') {
*index += 1;
continue;
}
if positionals_seen < node.positional_count {
positionals_seen += 1;
*index += 1;
continue;
}
if let Some(branch) = node
.subcommands
.iter()
.find(|branch| command_token_matches_cli_name(token, &branch.cli_name))
{
*index += 1;
return walk(&branch.node, tokens, index);
}
return node.leaf_id;
}
node.leaf_id
}
let mut index = 0usize;
walk(root, args, &mut index)
}
fn command_token_matches_cli_name(token: &str, cli_name: &str) -> bool {
let mut cli_name_chars = cli_name.chars();
for token_char in token.chars() {
let normalized = if token_char == '_' {
'-'
} else {
token_char.to_ascii_lowercase()
};
if Some(normalized) != cli_name_chars.next() {
return false;
}
}
cli_name_chars.next().is_none()
}
fn parse_from_os_args_with_schema<T>(schema: &Schema, args: &[OsString]) -> Result<T, String>
where
T: Facet<'static>,
{
let cli_config = CliConfigBuilder::new().args_os(args).strict().build();
let layer_output = parse_cli(schema, &cli_config);
if !layer_output.diagnostics.is_empty() {
let diagnostics = layer_output
.diagnostics
.iter()
.map(|diagnostic| diagnostic.message.as_str())
.collect::<Vec<_>>()
.join("; ");
return Err(format!("cli parse failed: {diagnostics}"));
}
let Some(value) = layer_output.value else {
return Err("cli parse returned no value".to_string());
};
from_config_value(&value).map_err(|error| format!("config value parse failed: {error}"))
}
#[cfg(test)]
mod tests {
use super::*;
use crate as args;
use crate::FigueBuiltins;
use facet::Facet;
use std::time::Duration;
use std::time::Instant;
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum Command {
Build {
#[facet(args::named)]
release: bool,
},
Clean,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum NestedAction {
Set {
#[facet(args::positional)]
file: String,
},
Get,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum NestedCommand {
Output {
#[facet(args::subcommand)]
path: NestedAction,
},
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct NestedCli {
#[facet(args::subcommand)]
command: NestedCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct Cli {
#[facet(args::named)]
verbose: bool,
#[facet(args::subcommand)]
command: Command,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, Default, PartialEq)]
#[facet(rename_all = "kebab-case")]
struct VendoredGlobalArgs {
#[facet(args::named, default)]
debug: bool,
#[facet(args::named)]
log_filter: Option<String>,
#[facet(args::named)]
log_file: Option<String>,
}
#[derive(Facet, arbitrary::Arbitrary, Debug)]
struct VendoredDiscordArchiveCli {
#[facet(flatten)]
global_args: VendoredGlobalArgs,
#[facet(flatten)]
#[arbitrary(default)]
builtins: FigueBuiltins,
#[facet(args::subcommand)]
command: VendoredCommand,
}
impl PartialEq for VendoredDiscordArchiveCli {
fn eq(&self, other: &Self) -> bool {
self.global_args == other.global_args && self.command == other.command
}
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredCommand {
BotToken(VendoredBotTokenArgs),
Cache(VendoredCacheArgs),
Home(VendoredHomeArgs),
Invite(VendoredInviteArgs),
Live(VendoredLiveArgs),
OutputDir(VendoredOutputDirArgs),
Sync(VendoredSyncArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredBotTokenArgs {
#[facet(args::subcommand)]
command: VendoredBotTokenCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredBotTokenCommand {
Clear(VendoredBotTokenClearArgs),
Set(VendoredBotTokenSetArgs),
ShowSource(VendoredBotTokenShowSourceArgs),
Validate(VendoredBotTokenValidateArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredBotTokenClearArgs;
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredBotTokenSetArgs {
#[facet(args::positional)]
token: Option<String>,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredBotTokenShowSourceArgs {
#[facet(args::named)]
token: Option<String>,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredBotTokenValidateArgs {
#[facet(args::named)]
token: Option<String>,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredCacheArgs {
#[facet(args::subcommand)]
command: VendoredCacheCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredCacheCommand {
Clean(VendoredCacheCleanArgs),
Open(VendoredCacheOpenArgs),
Show(VendoredCacheShowArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredCacheCleanArgs;
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredCacheOpenArgs;
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredCacheShowArgs;
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredHomeArgs {
#[facet(args::subcommand)]
command: VendoredHomeCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredHomeCommand {
Open(VendoredHomeOpenArgs),
Show(VendoredHomeShowArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredHomeOpenArgs;
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredHomeShowArgs;
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[facet(rename_all = "kebab-case")]
struct VendoredInviteArgs {
#[facet(args::named)]
token: Option<String>,
#[facet(args::named, default)]
no_open: bool,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[facet(rename_all = "kebab-case")]
struct VendoredLiveArgs {
#[facet(args::named)]
token: Option<String>,
#[facet(args::subcommand)]
command: VendoredLiveCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredLiveCommand {
Attachment(VendoredLiveAttachmentArgs),
Channel(VendoredLiveChannelArgs),
Guild(VendoredLiveGuildArgs),
Message(VendoredLiveMessageArgs),
Thread(VendoredLiveThreadArgs),
User(VendoredLiveUserArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredLiveAttachmentArgs {
#[facet(args::subcommand)]
command: VendoredLiveAttachmentCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredLiveAttachmentCommand {
List(VendoredLiveAttachmentListArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[facet(rename_all = "kebab-case")]
struct VendoredLiveAttachmentListArgs {
#[facet(args::named)]
channel_id: Option<u64>,
#[facet(args::named)]
thread_id: Option<u64>,
#[facet(args::named)]
before: Option<String>,
#[facet(args::named)]
limit: Option<u8>,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredLiveChannelArgs {
#[facet(args::subcommand)]
command: VendoredLiveChannelCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredLiveChannelCommand {
List(VendoredLiveChannelListArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[facet(rename_all = "kebab-case")]
struct VendoredLiveChannelListArgs {
#[facet(args::named)]
guild_id: u64,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredLiveGuildArgs {
#[facet(args::subcommand)]
command: VendoredLiveGuildCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredLiveGuildCommand {
List(VendoredLiveGuildListArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredLiveGuildListArgs;
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredLiveMessageArgs {
#[facet(args::subcommand)]
command: VendoredLiveMessageCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredLiveMessageCommand {
List(VendoredLiveMessageListArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[facet(rename_all = "kebab-case")]
struct VendoredLiveMessageListArgs {
#[facet(args::named)]
channel_id: Option<u64>,
#[facet(args::named)]
thread_id: Option<u64>,
#[facet(args::named)]
before: Option<String>,
#[facet(args::named)]
limit: Option<u8>,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredLiveThreadArgs {
#[facet(args::subcommand)]
command: VendoredLiveThreadCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredLiveThreadCommand {
List(VendoredLiveThreadListArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[facet(rename_all = "kebab-case")]
struct VendoredLiveThreadListArgs {
#[facet(args::named)]
guild_id: u64,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredLiveUserArgs {
#[facet(args::subcommand)]
command: VendoredLiveUserCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredLiveUserCommand {
List(VendoredLiveUserListArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[facet(rename_all = "kebab-case")]
struct VendoredLiveUserListArgs {
#[facet(args::named)]
guild_id: u64,
#[facet(args::named)]
after_user_id: Option<u64>,
#[facet(args::named)]
limit: Option<u64>,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredOutputDirArgs {
#[facet(args::subcommand)]
command: VendoredOutputDirCommand,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[repr(u8)]
enum VendoredOutputDirCommand {
Open(VendoredOutputDirOpenArgs),
Set(VendoredOutputDirSetArgs),
Show(VendoredOutputDirShowArgs),
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredOutputDirOpenArgs;
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredOutputDirSetArgs {
#[facet(args::positional)]
path: String,
}
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
struct VendoredOutputDirShowArgs;
#[derive(Facet, arbitrary::Arbitrary, Debug, PartialEq)]
#[facet(rename_all = "kebab-case")]
struct VendoredSyncArgs {
#[facet(args::named)]
output_dir: Option<String>,
}
#[test]
fn arbitrary_consistency_smoke_test() {
assert_to_args_consistency::<Cli>(TestToArgsConsistencyConfig::default())
.expect("consistency check should pass");
}
#[test]
fn arbitrary_roundtrip_smoke_test() {
assert_to_args_roundtrip::<Cli>(TestToArgsRoundTrip::default())
.expect("roundtrip check should pass");
}
#[test]
fn configurable_roundtrip_stress_test_completes_quickly() {
let start = Instant::now();
assert_to_args_roundtrip::<Cli>(TestToArgsRoundTrip::default())
.expect("roundtrip check should pass");
assert!(
start.elapsed() < Duration::from_secs(3),
"roundtrip stress test took {:?}",
start.elapsed()
);
}
#[test]
fn collects_nested_command_paths() {
let schema = Schema::from_shape(NestedCli::SHAPE).expect("schema should be valid");
let mut tree = command_node_from_arg_level(schema.args());
let paths = collect_command_paths(&mut tree);
assert!(
paths.contains(&vec!["Output".to_string(), "Set".to_string()]),
"expected Output -> Set path"
);
assert!(
paths.contains(&vec!["Output".to_string(), "Get".to_string()]),
"expected Output -> Get path"
);
}
#[test]
fn vendored_discord_archive_leaf_count_matches_fixture() {
let schema = Schema::from_shape(VendoredDiscordArchiveCli::SHAPE)
.expect("vendored schema should be valid");
let mut tree = command_node_from_arg_level(schema.args());
let paths = collect_command_paths(&mut tree);
assert_eq!(paths.len(), 20, "vendored fixture should expose 20 leaves");
}
#[test]
fn vendored_discord_archive_roundtrip_stress_test_completes_quickly() {
let start = Instant::now();
assert_to_args_roundtrip::<VendoredDiscordArchiveCli>(TestToArgsRoundTrip::default())
.expect("vendored discord archive roundtrip should pass");
assert!(
start.elapsed() < Duration::from_secs(2),
"vendored discord archive roundtrip took {:?}",
start.elapsed()
);
}
}