#![ cfg( feature = "cli_help_template" ) ]
use cli_fmt::help::*;
fn two_group_data() -> CliHelpData
{
CliHelpData
{
binary : "myapp".into(),
tagline : "example tool".into(),
groups : vec!
[
CommandGroup
{
name : "Group A".into(),
entries : vec!
[
CommandEntry { name : "cmd-one".into(), desc : "first command".into() },
CommandEntry { name : "cmd-two".into(), desc : "second command".into() },
],
},
CommandGroup
{
name : "Group B".into(),
entries : vec!
[
CommandEntry { name : "cmd-three".into(), desc : "third command".into() },
CommandEntry { name : "cmd-four".into(), desc : "fourth command".into() },
],
},
],
options : vec!
[
OptionEntry { name : "format::text|json".into(), desc : "Output format".into() },
OptionEntry { name : "dry::bool".into(), desc : "Dry-run".into() },
],
examples : vec!
[
ExampleEntry { invocation : "myapp cmd-one".into(), desc : Some( "run one".into() ) },
ExampleEntry { invocation : "myapp cmd-two".into(), desc : None },
],
}
}
fn no_tty_style() -> CliHelpStyle
{
CliHelpStyle { tty_detect : false, ..CliHelpStyle::default() }
}
#[ test ]
fn test_column_alignment()
{
let tmpl = CliHelpTemplate::new( no_tty_style(), two_group_data() );
let out = tmpl.render();
assert!(
out.contains( "cmd-one " ),
"cmd-one must be padded to 20 chars (cmd_name_width), got:\n{out}",
);
assert!(
out.contains( "dry::bool " ),
"dry::bool must be padded to 18 chars (opt_name_width), got:\n{out}",
);
assert!(
!out.contains( "\x1b[" ),
"no ANSI codes in no-TTY mode, got:\n{out}",
);
}
#[ test ]
fn test_no_ansi_codes()
{
let tmpl = CliHelpTemplate::new( no_tty_style(), two_group_data() );
let out = tmpl.render();
assert!(
!out.contains( "\x1b[" ),
"ANSI escape sequences must be absent with tty_detect=false, got:\n{out}",
);
}
#[ test ]
fn test_explicit_tty_detect_false()
{
let style = CliHelpStyle { tty_detect : false, ..CliHelpStyle::default() };
let tmpl = CliHelpTemplate::new( style, two_group_data() );
let out = tmpl.render();
assert!(
!out.contains( "\x1b[" ),
"explicit tty_detect=false must suppress all ANSI codes, got:\n{out}",
);
assert!( out.contains( "Group A" ), "group header must appear in output, got:\n{out}" );
assert!( out.contains( "cmd-one" ), "command name must appear in output, got:\n{out}" );
}
#[ test ]
fn test_name_not_truncated()
{
let style = CliHelpStyle { cmd_name_width : 10, tty_detect : false, ..CliHelpStyle::default() };
let data = CliHelpData
{
binary : "app".into(),
tagline : "test".into(),
groups : vec![ CommandGroup
{
name : "G".into(),
entries : vec![ CommandEntry { name : "eleven-char".into(), desc : "desc".into() } ],
}],
options : vec![],
examples : vec![],
};
let out = CliHelpTemplate::new( style, data ).render();
assert!(
out.contains( "eleven-char" ),
"11-char name must not be truncated when cmd_name_width=10, got:\n{out}",
);
}
#[ test ]
fn test_no_options_section()
{
let mut data = two_group_data();
data.options = vec![];
let out = CliHelpTemplate::new( no_tty_style(), data ).render();
assert!(
!out.contains( "Options:" ),
"Options section must be absent when options vec is empty, got:\n{out}",
);
}
#[ test ]
fn test_no_examples_section()
{
let mut data = two_group_data();
data.examples = vec![];
let out = CliHelpTemplate::new( no_tty_style(), data ).render();
assert!(
!out.contains( "Examples:" ),
"Examples section must be absent when examples vec is empty, got:\n{out}",
);
}
#[ test ]
fn test_single_group_binary_name()
{
let data = CliHelpData
{
binary : "myapp".into(),
tagline : "a single-group tool".into(),
groups : vec![ CommandGroup
{
name : "Cmds".into(),
entries : vec![ CommandEntry { name : "run".into(), desc : "run it".into() } ],
}],
options : vec![],
examples : vec![],
};
let out = CliHelpTemplate::new( no_tty_style(), data ).render();
assert!( out.contains( "Usage: myapp" ), "Usage: header with binary must appear, got:\n{out}" );
assert!( out.contains( "Commands:" ), "Commands: header must appear, got:\n{out}" );
assert!( out.contains( "Cmds" ), "group header must appear in output, got:\n{out}" );
assert!( out.contains( "run" ), "command name must appear in output, got:\n{out}" );
assert!(
!out.contains( "\x1b[" ),
"no ANSI codes with tty_detect=false, got:\n{out}",
);
}
#[ test ]
fn test_style_default_fields()
{
let s = CliHelpStyle::default();
assert_eq!( s.cmd_indent, 4 );
assert_eq!( s.cmd_name_width, 20 );
assert_eq!( s.grp_indent, 2 );
assert_eq!( s.opt_indent, 2 );
assert_eq!( s.opt_name_width, 18 );
assert_eq!( s.col_gap, 2 );
assert_eq!( s.example_indent, 2 );
}
#[ test ]
fn test_example_desc_rendered()
{
let out = CliHelpTemplate::new( no_tty_style(), two_group_data() ).render();
let first_line = out.lines()
.find( |l| l.contains( "myapp cmd-one" ) )
.unwrap_or_default();
assert!(
first_line.contains( "# run one" ),
"ExampleEntry with desc=Some must render '# run one' on the invocation line, got:\n{first_line:?}",
);
let second_line = out.lines()
.find( |l| l.contains( "myapp cmd-two" ) )
.unwrap_or_default();
assert!(
!second_line.contains( '#' ),
"ExampleEntry with desc=None must not render '#' on the invocation line, got:\n{second_line:?}",
);
}
#[ test ]
fn test_style_color_defaults()
{
let s = CliHelpStyle::default();
assert_eq!( s.color_tagline, "\x1b[1m", "color_tagline must be bold ANSI code" );
assert_eq!( s.color_group, "\x1b[33m\x1b[1m", "color_group must be yellow+bold ANSI codes" );
assert_eq!( s.color_option, "\x1b[1;36m", "color_option must be bold cyan ANSI code" );
assert_eq!( s.color_example, "\x1b[2m", "color_example must be dim ANSI code" );
assert_eq!( s.color_reset, "\x1b[0m", "color_reset must be ANSI reset sequence" );
assert!( s.tty_detect, "tty_detect must be true by default" );
}
#[ test ]
fn test_empty_groups()
{
let data = CliHelpData
{
binary : "app".into(),
tagline : "test tool".into(),
groups : vec![],
options : vec![ OptionEntry { name : "verbose::bool".into(), desc : "Enable verbose".into() } ],
examples : vec![],
};
let out = CliHelpTemplate::new( no_tty_style(), data ).render();
assert!( !out.is_empty(), "render with empty groups must return non-empty string" );
assert!( out.contains( "app" ), "binary name must appear in output, got:\n{out}" );
assert!( out.contains( "test tool" ), "tagline must appear in output, got:\n{out}" );
}
#[ test ]
fn test_opt_name_not_truncated()
{
let style = CliHelpStyle { opt_name_width : 10, tty_detect : false, ..CliHelpStyle::default() };
let data = CliHelpData
{
binary : "app".into(),
tagline : "test".into(),
groups : vec![],
options : vec![ OptionEntry { name : "format::json".into(), desc : "format specifier".into() } ],
examples : vec![],
};
let out = CliHelpTemplate::new( style, data ).render();
assert!(
out.contains( "format::json" ),
"12-char option name must not be truncated when opt_name_width=10, got:\n{out}",
);
}
#[ test ]
fn test_tty_detect_true_suppresses_ansi_in_non_tty()
{
let out = CliHelpTemplate::new( CliHelpStyle::default(), two_group_data() ).render();
assert!(
!out.contains( "\x1b[" ),
"tty_detect=true in non-TTY test environment must suppress ANSI codes, got:\n{out}"
);
}
#[ test ]
fn test_no_data_fmt_dependency()
{
let cargo = include_str!( "../Cargo.toml" );
assert!(
!cargo.contains( "data_fmt" ),
"cli_fmt must not depend on data_fmt — uses strs_tools primitives only"
);
}