use crate::{parser::*, s_expr::*};
use pretty_assertions::assert_eq;
fn assert_multiline(source: &str, soft_limit: usize, expected: &str)
{
let ast = Parser::parse(source)
.unwrap_or_else(|e| panic!("parse failed for {:?}: {}", source, e));
let opts = SExpressibleOptions::new(0, 4, soft_limit);
let actual = ast.to_s_expr(opts);
assert!(
actual.contains('\n'),
"multi-line branch expected for {:?} at soft_limit {} but got \
single line:\n{}",
source,
soft_limit,
actual
);
assert_eq!(
actual, expected,
"multi-line layout mismatch for {:?} at soft_limit {}",
source, soft_limit
);
}
#[test]
fn test_function_body_wraps_under_tight_soft_limit()
{
assert_multiline("1 + 2", 19, "(function []\n\t(add 1 2))");
}
#[test]
fn test_function_parameters_wrap_each_on_own_line()
{
assert_multiline(
"{alpha}, {beta}, {gamma}: 42",
15,
"(function\n\t[\n\t\t{alpha}\n\t\t{beta}\n\t\t{gamma}\n\t] 42)"
);
}
#[test]
fn test_ternary_form_wraps_each_operand()
{
assert_multiline("1 + 2", 13, "(function []\n\t(add\n\t\t1\n\t\t2))");
}
#[test]
fn test_unary_form_wraps_operand()
{
assert_multiline("-{ab}", 14, "(function []\n\t(neg\n\t\t{ab}))");
}
#[test]
fn test_recursive_wrap_increments_indent_per_level()
{
assert_multiline(
"1 + 2 + 3",
12,
"(function []\n\t(add\n\t\t(add\n\t\t\t1\n\t\t\t2)\n\t\t3))"
);
}
#[test]
fn test_custom_dice_faces_wrap_each_on_own_line()
{
assert_multiline(
"1d[1, 2, 3, 4, 5, 6, 7, 8]",
15,
"(function []\n\t(custom-dice\n\t\t1\n\t\t\
[\n\t\t\t1\n\t\t\t2\n\t\t\t3\n\t\t\t4\n\t\t\t5\n\t\t\t6\n\t\t\t7\n\t\t\
\t8\n\t\t]))"
);
}
#[test]
fn test_multiline_with_spans_emits_prefixes_at_every_level()
{
let ast = Parser::parse("1 + 2").unwrap();
let opts = SExpressibleOptions::new(0, 4, 30).with_spans(true);
let rendered = ast.to_s_expr(opts);
assert_eq!(
rendered,
"^[0 5] (function []\n\t^[0 5] \
(add\n\t\t^[0 1] 1\n\t\t^[4 5] 2))"
);
}
#[test]
fn test_multiline_lossless_roundtrip_under_tight_limits()
{
use crate::support::read_compilation_test_cases;
let cases = read_compilation_test_cases(include_str!(
"../../../tests/test_parse.txt"
));
for soft_limit in [10usize, 30, 80]
{
let opts = SExpressibleOptions::new(0, 4, soft_limit)
.with_spans(true)
.with_groups(true);
for (index, (source, _)) in cases.iter().enumerate()
{
let ast = Parser::parse(source).unwrap_or_else(|e| {
panic!(
"case {}: parse failed for {:?}: {}",
index + 1,
source,
e
)
});
let serialized = ast.to_s_expr(opts);
let reparsed = read_s_expr(&serialized).unwrap_or_else(|e| {
panic!(
"case {} at soft_limit {}: re-read failed for {:?}\n\
serialized: {}\nerror: {}",
index + 1,
soft_limit,
source,
serialized,
e
)
});
assert_eq!(
reparsed,
ast,
"case {} at soft_limit {}: roundtrip mismatch for {:?}\n\
serialized: {}",
index + 1,
soft_limit,
source,
serialized
);
}
}
}
#[test]
fn test_function_with_no_parameters_reserves_two_for_empty_brackets()
{
assert_multiline("[0:5]", 12, "(function []\n\t(range\n\t\t0\n\t\t5))");
}
fn assert_within_limit(source: &str, soft_limit: usize, expected: &str)
{
let ast = Parser::parse(source)
.unwrap_or_else(|e| panic!("parse failed for {:?}: {}", source, e));
let opts = SExpressibleOptions::new(0, 4, soft_limit);
let actual = ast.to_s_expr(opts);
assert_eq!(
actual, expected,
"layout mismatch for {:?} at soft_limit {}",
source, soft_limit
);
for line in actual.lines()
{
assert!(
line_width(line, 4) <= soft_limit,
"line exceeds soft_limit {} for {:?}: {:?}",
soft_limit,
source,
line
);
}
}
fn line_width(line: &str, tab_width: usize) -> usize
{
let text = line.trim_start_matches('\t');
tab_width * (line.len() - text.len()) + text.chars().count()
}
#[test]
fn test_function_fits_exactly_at_soft_limit()
{
assert_within_limit("1 + 2", 23, "(function [] (add 1 2))");
}
#[test]
fn test_function_wraps_one_column_past_soft_limit()
{
assert_within_limit("1 + 2", 22, "(function []\n\t(add 1 2))");
}
#[test]
fn test_body_fits_exactly_after_wrapped_parameters()
{
assert_within_limit(
"{alpha}, {beta}: 42",
24,
"(function\n\t[{alpha} {beta}] 42)"
);
}
#[test]
fn test_body_wraps_after_wrapped_parameters()
{
assert_within_limit(
"{alpha}, {beta}: 420",
24,
"(function\n\t[{alpha} {beta}]\n\t420)"
);
}
#[test]
fn test_body_fits_exactly_after_split_parameters()
{
assert_within_limit(
"{alpha}, {beta}, {gamma}: -{ab}",
17,
"(function\n\t[\n\t\t{alpha}\n\t\t{beta}\n\t\t{gamma}\n\t] (neg {ab}))"
);
}
#[test]
fn test_body_wraps_after_split_parameters()
{
assert_within_limit(
"{alpha}, {beta}, {gamma}: -{abc}",
17,
"(function\n\t[\n\t\t{alpha}\n\t\t{beta}\n\t\t{gamma}\n\t]\n\t(neg {abc}))"
);
}
#[test]
fn test_parameter_list_reserves_enclosing_parentheses()
{
assert_within_limit(
"{alpha}, {beta}, {gam}: 1",
26,
"(function\n\t[\n\t\t{alpha}\n\t\t{beta}\n\t\t{gam}\n\t] 1)"
);
}
#[test]
fn test_binding_fits_exactly_at_soft_limit()
{
assert_within_limit("{x}@(1)", 20, "(function []\n\t(binding {x} 1))");
}
#[test]
fn test_binding_wraps_one_column_past_soft_limit()
{
assert_within_limit(
"{x}@(1)",
19,
"(function []\n\t(binding\n\t\t{x}\n\t\t1))"
);
}
#[test]
fn test_face_list_fits_exactly_at_soft_limit()
{
assert_within_limit(
"1d[1, 2, 3]",
17,
"(function []\n\t(custom-dice\n\t\t1\n\t\t[1 2 3]))"
);
}
#[test]
fn test_face_list_wraps_one_column_past_soft_limit()
{
assert_within_limit(
"1d[1, 2, 3]",
16,
"(function []\n\t(custom-dice\n\t\t1\n\t\t[\n\t\t\t1\n\t\t\t2\n\t\t\t3\n\t\t]))"
);
}
#[test]
fn test_wrapped_unary_operand_fits_exactly_at_soft_limit()
{
assert_within_limit(
"-(1 + 2)",
19,
"(function []\n\t(neg\n\t\t(add 1 2)))"
);
}
#[test]
fn test_wrapped_unary_operand_wraps_one_level_deeper()
{
assert_within_limit(
"-(1 + 2)",
18,
"(function []\n\t(neg\n\t\t(add\n\t\t\t1\n\t\t\t2)))"
);
}
#[test]
fn test_lines_exceed_soft_limit_only_when_unsplittable()
{
use crate::support::read_compilation_test_cases;
let cases = read_compilation_test_cases(include_str!(
"../../../tests/test_parse.txt"
));
for with_groups in [false, true]
{
for soft_limit in 0..=100
{
let opts = SExpressibleOptions::new(0, 4, soft_limit)
.with_groups(with_groups);
for (source, _) in &cases
{
let ast = Parser::parse(source).unwrap();
let rendered = ast.to_s_expr(opts);
for line in rendered.lines()
{
assert!(
line_width(line, 4) <= soft_limit
|| is_single_token(line),
"line exceeds soft_limit {} (with_groups {}) for \
{:?}: {:?}\nrendered:\n{}",
soft_limit,
with_groups,
source,
line,
rendered
);
}
}
}
}
}
fn is_single_token(line: &str) -> bool
{
let mut braced = false;
for c in line.trim_start_matches('\t').chars()
{
match c
{
'{' => braced = true,
'}' => braced = false,
' ' if !braced => return false,
_ =>
{}
}
}
true
}