#![ cfg( feature = "output" ) ]
use cli_fmt::output::*;
use strs_tools::string::lines::*;
#[ test ]
fn output_config_default_is_no_processing()
{
let config = OutputConfig::default();
assert!( config.is_default() );
assert!( !config.has_processing() );
}
#[ test ]
fn output_config_with_head_has_processing()
{
let config = OutputConfig::default().with_head( 5 );
assert!( !config.is_default() );
assert!( config.has_processing() );
}
#[ test ]
fn output_config_with_tail_has_processing()
{
let config = OutputConfig::default().with_tail( 5 );
assert!( config.has_processing() );
}
#[ test ]
fn output_config_with_width_has_processing()
{
let config = OutputConfig::default().with_width( 80 );
assert!( config.has_processing() );
}
#[ test ]
fn output_config_with_width_zero_has_processing()
{
let config = OutputConfig::default().with_width( 0 );
assert!(
config.has_processing(),
"AP-12: with_width(0) must set has_processing() == true — Some(0) is not None"
);
assert!(
!config.is_default(),
"AP-12: with_width(0) must set is_default() == false — width deviates from None default"
);
let result = process_output( "this is a very long line indeed", "", &config );
assert!(
!result.width_truncated,
"AP-12: width=0 short-circuits truncation at runtime — width_truncated must be false even with Some(0) stored"
);
}
#[ test ]
fn select_streams_both()
{
let config = OutputConfig::default();
let result = process_output( "stdout", "stderr", &config );
assert_eq!( result.content, "stderr\nstdout" );
}
#[ test ]
fn select_streams_stdout_only()
{
let config = OutputConfig::default()
.with_stream_filter( StreamFilter::Stdout );
let result = process_output( "stdout", "stderr", &config );
assert_eq!( result.content, "stdout" );
}
#[ test ]
fn select_streams_stderr_only()
{
let config = OutputConfig::default()
.with_stream_filter( StreamFilter::Stderr );
let result = process_output( "stdout", "stderr", &config );
assert_eq!( result.content, "stderr" );
}
#[ test ]
fn select_streams_empty_stdout()
{
let config = OutputConfig::default();
let result = process_output( "", "stderr", &config );
assert_eq!( result.content, "stderr" );
}
#[ test ]
fn select_streams_empty_stderr()
{
let config = OutputConfig::default();
let result = process_output( "stdout", "", &config );
assert_eq!( result.content, "stdout" );
}
#[ test ]
fn select_streams_both_empty()
{
let config = OutputConfig::default();
let result = process_output( "", "", &config );
assert_eq!( result.content, "" );
assert_eq!( result.lines_omitted, 0 );
assert!( !result.width_truncated );
}
#[ test ]
fn head_basic()
{
let text = "line1\nline2\nline3\nline4\nline5";
let result = head( text, 2 );
assert_eq!( result, "line1\nline2" );
}
#[ test ]
fn head_exceeds_total()
{
let text = "line1\nline2\nline3";
let result = head( text, 10 );
assert_eq!( result, "line1\nline2\nline3" );
}
#[ test ]
fn head_exact()
{
let text = "line1\nline2\nline3";
let result = head( text, 3 );
assert_eq!( result, "line1\nline2\nline3" );
}
#[ test ]
fn head_empty()
{
let text = "";
let result = head( text, 5 );
assert_eq!( result, "" );
}
#[ test ]
fn tail_basic()
{
let text = "line1\nline2\nline3\nline4\nline5";
let result = tail( text, 2 );
assert_eq!( result, "line4\nline5" );
}
#[ test ]
fn tail_exceeds_total()
{
let text = "line1\nline2\nline3";
let result = tail( text, 10 );
assert_eq!( result, "line1\nline2\nline3" );
}
#[ test ]
fn tail_exact()
{
let text = "line1\nline2\nline3";
let result = tail( text, 3 );
assert_eq!( result, "line1\nline2\nline3" );
}
#[ test ]
fn tail_empty()
{
let text = "";
let result = tail( text, 5 );
assert_eq!( result, "" );
}
#[ test ]
fn head_tail_combined_no_overlap()
{
let config = OutputConfig::default()
.with_head( 2 )
.with_tail( 2 );
let result = process_output( "line1\nline2\nline3\nline4\nline5", "", &config );
assert_eq!( result.content, "line1\nline2\nline4\nline5" );
assert_eq!( result.lines_omitted, 1 );
}
#[ test ]
fn head_tail_overlap_shows_all()
{
let config = OutputConfig::default()
.with_head( 3 )
.with_tail( 3 );
let result = process_output( "line1\nline2\nline3\nline4\nline5", "", &config );
assert_eq!( result.content, "line1\nline2\nline3\nline4\nline5" );
assert_eq!( result.lines_omitted, 0 );
}
#[ test ]
fn head_tail_exact_fit()
{
let config = OutputConfig::default()
.with_head( 2 )
.with_tail( 1 );
let result = process_output( "line1\nline2\nline3", "", &config );
assert_eq!( result.content, "line1\nline2\nline3" );
assert_eq!( result.lines_omitted, 0 );
}
#[ test ]
fn width_no_truncation_needed()
{
let config = OutputConfig::default().with_width( 50 );
let result = process_output( "short line", "", &config );
assert!( result.content.starts_with( "short line" ) );
assert!( !result.width_truncated );
}
#[ test ]
fn width_truncation_with_arrow()
{
let config = OutputConfig::default().with_width( 10 );
let result = process_output( "this is a very long line that needs truncation", "", &config );
assert!( result.width_truncated );
assert!( result.content.contains( "→" ) );
assert!( result.content.len() < 50 );
}
#[ test ]
fn width_zero_disables()
{
let config = OutputConfig::default().with_width( 0 );
let result = process_output( "this is a very long line", "", &config );
assert_eq!( result.content, "this is a very long line" );
assert!( !result.width_truncated );
}
#[ test ]
fn width_custom_suffix()
{
let config = OutputConfig::default()
.with_width( 10 )
.with_suffix( "..." );
let result = process_output( "this is a very long line", "", &config );
assert!( result.width_truncated );
assert!( result.content.contains( "..." ) );
}
#[ test ]
fn width_exact_boundary()
{
let config = OutputConfig::default().with_width( 10 );
let result = process_output( "0123456789", "", &config );
assert!( result.content.starts_with( "0123456789" ) );
assert!( !result.width_truncated );
}
#[ test ]
fn width_one_over_boundary()
{
let config = OutputConfig::default().with_width( 10 );
let result = process_output( "01234567890", "", &config );
assert!(
result.width_truncated,
"FT-44: 11 chars at width=10 exceeds limit by 1 — truncation fires at > max_width (contrast FT-11: == max_width does not)"
);
}
#[ test ]
fn ansi_preserved_when_no_truncation()
{
let config = OutputConfig::default().with_width( 50 );
let input = "\x1b[31mred text\x1b[0m";
let result = process_output( input, "", &config );
assert!( result.content.starts_with( input ) );
assert!( result.content.contains( "\x1b[31m" ) );
}
#[ test ]
fn ansi_preserved_with_truncation()
{
let config = OutputConfig::default().with_width( 8 );
let input = "\x1b[31mred text that is very long\x1b[0m";
let result = process_output( input, "", &config );
assert!( result.content.contains( "\x1b[31m" ) );
assert!( result.width_truncated );
}
#[ test ]
fn combined_head_and_width()
{
let config = OutputConfig::default()
.with_head( 2 )
.with_width( 10 );
let input = "line1 is very long\nline2 is also long\nline3\nline4";
let result = process_output( input, "", &config );
assert_eq!( result.lines_omitted, 2 );
assert!( result.width_truncated );
let lines : Vec< &str > = result.content.lines().collect();
assert_eq!( lines.len(), 2 );
}
#[ test ]
fn combined_tail_and_width()
{
let config = OutputConfig::default()
.with_tail( 2 )
.with_width( 10 );
let input = "line1\nline2\nline3 is very long\nline4 is also long";
let result = process_output( input, "", &config );
assert_eq!( result.lines_omitted, 2 );
let lines : Vec< &str > = result.content.lines().collect();
assert_eq!( lines.len(), 2 );
assert!(
result.content.contains( "line3" ),
"tail(2) must retain line3 (3rd line), got:\n{}", result.content
);
assert!(
result.content.contains( "line4" ),
"tail(2) must retain line4 (4th line), got:\n{}", result.content
);
assert!(
!result.content.contains( "line1" ),
"tail(2) must drop line1 (1st line), got:\n{}", result.content
);
assert!(
!result.content.contains( "line2" ),
"tail(2) must drop line2 (2nd line), got:\n{}", result.content
);
}
#[ test ]
fn combined_streams_head_width()
{
let config = OutputConfig::default()
.with_head( 3 )
.with_width( 15 );
let result = process_output( "out1\nout2 is long\nout3", "err1\nerr2 is also long", &config );
assert!( result.content.starts_with( "err1" ) );
let lines : Vec< &str > = result.content.lines().collect();
assert_eq!( lines.len(), 3 ); assert!(
result.width_truncated,
"FT-33: retained line 'err2 is also long' (18 chars) exceeds width=15 — width_truncated must be true"
);
}
#[ test ]
fn merge_streams_ordering()
{
let result = merge_streams( "stdout", "stderr", &StreamFilter::Both );
assert_eq!( result, "stderr\nstdout" );
}
#[ test ]
fn merge_streams_stdout_only()
{
let result = merge_streams( "hello", "world", &StreamFilter::Stdout );
assert_eq!( result, "hello", "AP-14: Stdout filter returns only stdout; stderr discarded" );
}
#[ test ]
fn merge_streams_stderr_only()
{
let result = merge_streams( "hello", "world", &StreamFilter::Stderr );
assert_eq!( result, "world", "AP-15: Stderr filter returns only stderr; stdout discarded" );
}
#[ test ]
fn process_output_head_lines_omitted()
{
let config = OutputConfig::default().with_head( 2 );
let result = process_output( "line1\nline2\nline3\nline4\nline5", "", &config );
assert_eq!( result.lines_omitted, 3 );
assert_eq!( result.content, "line1\nline2" );
}
#[ test ]
fn is_default_stream_filter()
{
let config = OutputConfig::default().with_stream_filter( StreamFilter::Stdout );
assert!(
!config.is_default(),
"config with stream_filter=Stdout must not satisfy is_default()"
);
}
#[ test ]
fn is_default_width_suffix()
{
let config = OutputConfig::default().with_suffix( "..." );
assert!(
!config.is_default(),
"config with width_suffix='...' must not satisfy is_default()"
);
}
#[ test ]
fn is_default_unicode_aware()
{
let config = OutputConfig::default().with_unicode_aware( true );
assert!(
!config.is_default(),
"config with unicode_aware=true must not satisfy is_default()"
);
}
#[ test ]
fn is_default_tail()
{
let config = OutputConfig::default().with_tail( 2 );
assert!(
!config.is_default(),
"config with tail=2 must not satisfy is_default()"
);
}
#[ test ]
fn is_default_width()
{
let config = OutputConfig::default().with_width( 5 );
assert!(
!config.is_default(),
"config with width=5 must not satisfy is_default()"
);
}
#[ test ]
fn head_zero_produces_empty()
{
let config = OutputConfig::default().with_head( 0 );
let result = process_output( "a\nb\nc", "", &config );
assert!(
result.content.is_empty(),
"head(0) must produce empty content, got:\n{:?}", result.content
);
assert_eq!(
result.lines_omitted, 3,
"head(0) on 3-line input must report 3 lines omitted"
);
}
#[ test ]
fn tail_zero_produces_empty()
{
let config = OutputConfig::default().with_tail( 0 );
let result = process_output( "a\nb\nc", "", &config );
assert!(
result.content.is_empty(),
"tail(0) must produce empty content, got:\n{:?}", result.content
);
assert_eq!(
result.lines_omitted, 3,
"tail(0) on 3-line input must report 3 lines omitted"
);
}
#[ test ]
fn width_one_truncates()
{
let config = OutputConfig::default().with_width( 1 );
let result = process_output( "hello", "", &config );
assert!(
result.width_truncated,
"width=1 must trigger truncation for 'hello' (5 visible chars), got:\n{:?}", result.content
);
assert!(
!result.content.contains( '→' ),
"FT-17: at width=1 the '→' suffix (1 char) exceeds the 1-char budget and must be omitted, got:\n{:?}", result.content
);
}
#[ test ]
fn unicode_aware_truncation()
{
let config = OutputConfig::default()
.with_unicode_aware( true )
.with_width( 3 );
let result = process_output( "café", "", &config );
assert!(
result.width_truncated,
"unicode_aware=true must trigger truncation when visual width (4) > max_width (3), got:\n{:?}", result.content
);
}
#[ test ]
fn unicode_aware_false_char_not_byte()
{
let config = OutputConfig::default().with_width( 1 );
let result = process_output( "é", "", &config );
assert!(
!result.width_truncated,
"FT-43: unicode_aware=false counts chars (1), not bytes (2); width=1 matches char count — no truncation"
);
}
#[ test ]
fn merge_streams_stderr_trailing_newline()
{
let result = merge_streams( "out", "err\n", &StreamFilter::Both );
assert_eq!(
result, "err\nout",
"stderr with trailing newline must not produce double-newline separator, got:\n{result:?}"
);
assert!(
!result.contains( "\n\n" ),
"result must not contain double-newline sequence, got:\n{result:?}"
);
}
#[ test ]
fn merge_streams_both_trailing_newlines_no_double_newline()
{
let result = merge_streams( "out\n", "err\n", &StreamFilter::Both );
assert_eq!(
result, "err\nout\n",
"both streams with trailing newlines must not produce double-newline, got:\n{result:?}"
);
assert!(
!result.contains( "\n\n" ),
"result must not contain double-newline sequence, got:\n{result:?}"
);
}
#[ test ]
fn merge_streams_stdout_trailing_newline_separator()
{
let result = merge_streams( "out\n", "err", &StreamFilter::Both );
assert_eq!(
result, "err\nout\n",
"stderr without trailing newline must get exactly one newline separator, got:\n{result:?}"
);
}
#[ test ]
fn output_config_new_matches_default()
{
let from_new = OutputConfig::new();
let from_default = OutputConfig::default();
assert_eq!( from_new.head, from_default.head, "head field must match" );
assert_eq!( from_new.tail, from_default.tail, "tail field must match" );
assert_eq!( from_new.width, from_default.width, "width field must match" );
assert_eq!( from_new.width_suffix, from_default.width_suffix, "width_suffix field must match" );
assert_eq!( from_new.stream_filter, from_default.stream_filter, "stream_filter field must match" );
assert_eq!( from_new.unicode_aware, from_default.unicode_aware, "unicode_aware field must match" );
assert!(
from_new.is_default(),
"OutputConfig::new() must satisfy is_default()"
);
}
#[ test ]
fn stdout_filter_with_head()
{
let config = OutputConfig::default()
.with_stream_filter( StreamFilter::Stdout )
.with_head( 2 );
let result = process_output( "a\nb\nc", "err", &config );
assert!(
result.content.contains( "a" ) && result.content.contains( "b" ),
"first 2 stdout lines must be retained, got:\n{:?}", result.content
);
assert!(
!result.content.contains( "c" ),
"3rd stdout line must be omitted by head(2), got:\n{:?}", result.content
);
assert!(
!result.content.contains( "err" ),
"stderr must be discarded entirely by StreamFilter::Stdout, got:\n{:?}", result.content
);
assert_eq!(
result.lines_omitted, 1,
"head(2) on 3-line stdout-only stream must omit 1 line"
);
}
#[ test ]
fn stderr_filter_with_head()
{
let config = OutputConfig::default()
.with_stream_filter( StreamFilter::Stderr )
.with_head( 2 );
let result = process_output( "x", "err1\nerr2\nerr3", &config );
assert!( !result.content.contains( "x" ), "FT-42: stdout discarded by Stderr filter" );
assert!( result.content.contains( "err1" ), "FT-42: err1 retained by head(2)" );
assert!( result.content.contains( "err2" ), "FT-42: err2 retained by head(2)" );
assert!( !result.content.contains( "err3" ), "FT-42: err3 dropped by head(2)" );
assert_eq!(
result.lines_omitted, 1,
"FT-42: one line omitted by head(2) on 3-line stderr-only stream"
);
}
#[ test ]
fn head_tail_width_triple_combination()
{
let input = "abcdefghij\nklmnopqrst\nuvwxyzabcd\nefghijklmn\nopqrstuvwx\nyzabcdefgh";
let config = OutputConfig::default()
.with_head( 2 )
.with_tail( 2 )
.with_width( 8 );
let result = process_output( input, "", &config );
assert_eq!(
result.lines_omitted, 2,
"head(2)+tail(2) on 6-line input must omit 2 middle lines"
);
assert!(
result.width_truncated,
"all retained lines exceed 8 chars — width_truncated must be true"
);
let lines : Vec< &str > = result.content.lines().collect();
assert_eq!(
lines.len(), 4,
"head(2)+tail(2) must retain exactly 4 lines, got {} lines:\n{:?}", lines.len(), result.content
);
}
#[ test ]
fn width_empty_suffix_no_marker()
{
let config = OutputConfig::default()
.with_width( 10 )
.with_suffix( "" );
let result = process_output( "01234567890123456789", "", &config );
assert!(
result.width_truncated,
"20-char line with width=10 must trigger truncation"
);
assert!(
result.content.starts_with( "0123456789" ),
"truncated content must start with first 10 visible chars, got:\n{:?}", result.content
);
assert!(
!result.content.contains( '→' ),
"empty suffix must produce no truncation marker — '→' must not appear, got:\n{:?}", result.content
);
}
#[ test ]
fn empty_stdout_stderr_with_head()
{
let config = OutputConfig::default().with_head( 2 );
let result = process_output( "", "err1\nerr2\nerr3", &config );
assert!(
result.content.contains( "err1" ) && result.content.contains( "err2" ),
"first 2 stderr lines must be retained, got:\n{:?}", result.content
);
assert!(
!result.content.contains( "err3" ),
"3rd stderr line must be omitted by head(2), got:\n{:?}", result.content
);
assert_eq!(
result.lines_omitted, 1,
"head(2) on 3-line stderr-only merged stream must omit 1 line"
);
}
#[ test ]
fn width_zero_with_head()
{
let config = OutputConfig::default()
.with_width( 0 )
.with_head( 2 );
let result = process_output( "longline1\nlongline2\nlongline3", "", &config );
let lines : Vec< &str > = result.content.lines().collect();
assert_eq!(
lines.len(), 2,
"head(2) must retain first 2 lines, got {} lines:\n{:?}", lines.len(), result.content
);
assert_eq!(
lines[ 0 ], "longline1",
"first line must be intact (no truncation), got:\n{:?}", lines[ 0 ]
);
assert_eq!(
lines[ 1 ], "longline2",
"second line must be intact (no truncation), got:\n{:?}", lines[ 1 ]
);
assert!(
!result.width_truncated,
"width=0 must disable truncation even when head is active"
);
assert_eq!(
result.lines_omitted, 1,
"head(2) on 3-line input must omit 1 line"
);
}
#[ test ]
fn merge_streams_both_empty_infallible()
{
let result = merge_streams( "", "", &StreamFilter::Both );
assert_eq!(
result, "",
"merge_streams with two empty inputs must return empty string, got:\n{:?}", result
);
}
#[ test ]
fn test_strs_tools_sole_runtime_dependency()
{
let cargo = include_str!( "../Cargo.toml" );
assert!(
cargo.contains( "strs_tools" ),
"cli_fmt Cargo.toml must declare strs_tools as a dependency"
);
let deps_section = cargo
.split( "[dependencies]" )
.nth( 1 )
.unwrap_or( "" )
.split( "\n[" )
.next()
.unwrap_or( "" );
let dep_lines : Vec< &str > = deps_section
.lines()
.filter( | l | !l.trim().is_empty() && !l.trim_start().starts_with( '#' ) )
.collect();
assert_eq!(
dep_lines.len(), 1,
"cli_fmt must have exactly one runtime dependency (strs_tools), found {} dep lines:\n{:?}",
dep_lines.len(),
dep_lines
);
assert!(
dep_lines[ 0 ].starts_with( "strs_tools" ),
"the sole runtime dependency must be strs_tools, got:\n{:?}", dep_lines[ 0 ]
);
}