use std::{
collections::{BTreeMap, HashSet},
time::{Duration, Instant}
};
use pretty_assertions::assert_eq;
use super::{
ast::{DEPTH, LEAF, Nesting, nest_function, nest_parsable},
recovery::{FAMILIES, SCALE}
};
use crate::{
Parser, SourceSpan,
diagnostics::{self, DiagnoseResult, DiagnosticKind, Edit},
support::{on_small_stack, read_error_test_cases}
};
pub(super) fn kind_name(kind: &DiagnosticKind) -> &'static str
{
match kind
{
DiagnosticKind::UnclosedDelimiter { .. } => "UnclosedDelimiter",
DiagnosticKind::MissingExpression { .. } => "MissingExpression",
DiagnosticKind::UnopenedDelimiter { .. } => "UnopenedDelimiter",
DiagnosticKind::MissingRightOperand { .. } => "MissingRightOperand",
DiagnosticKind::MissingLeftOperand { .. } => "MissingLeftOperand",
DiagnosticKind::BareIdentifier => "BareIdentifier",
DiagnosticKind::MissingDiceFaces => "MissingDiceFaces",
DiagnosticKind::IncompleteDropClause => "IncompleteDropClause",
DiagnosticKind::MisplacedDropClause => "MisplacedDropClause",
DiagnosticKind::IncompleteParameterDefinition =>
{
"IncompleteParameterDefinition"
},
DiagnosticKind::MissingParameter => "MissingParameter",
DiagnosticKind::TrailingInput => "TrailingInput",
DiagnosticKind::EmptyExpression => "EmptyExpression",
DiagnosticKind::UnexpectedToken => "UnexpectedToken",
DiagnosticKind::UnexpectedEof => "UnexpectedEof",
DiagnosticKind::DuplicateParameter { .. } => "DuplicateParameter",
DiagnosticKind::BindingCollidesWithParameter { .. } =>
{
"BindingCollidesWithParameter"
},
DiagnosticKind::DuplicateBinding { .. } => "DuplicateBinding",
DiagnosticKind::UseBeforeBind { .. } => "UseBeforeBind"
}
}
#[derive(Debug)]
pub(super) struct Replayed
{
pub corrected_source: String,
pub placeholders: Vec<(SourceSpan, &'static str, &'static [&'static str])>
}
pub(super) fn replay(
source: &str,
result: &DiagnoseResult
) -> Vec<Vec<Replayed>>
{
let mut fixes: Vec<&Edit> = Vec::new();
result
.diagnostics
.iter()
.map(|diagnostic| {
let replayed = diagnostic
.suggestions
.iter()
.map(|suggestion| compose(source, &fixes, &suggestion.edits))
.collect();
if let Some(first) = diagnostic.suggestions.first()
{
fixes.extend(&first.edits);
}
replayed
})
.collect()
}
fn compose(source: &str, fixes: &[&Edit], own: &[Edit]) -> Replayed
{
let all = fixes
.iter()
.map(|&edit| (edit, false))
.chain(own.iter().map(|edit| (edit, true)))
.collect::<Vec<_>>();
let covers = |later: &Edit, earlier: &Edit| {
if earlier.span.start == earlier.span.end
{
later.span.start < earlier.span.start
&& earlier.span.start < later.span.end
}
else
{
later.span.start <= earlier.span.start
&& earlier.span.end <= later.span.end
}
};
let mut edits = all
.iter()
.enumerate()
.filter(|&(i, (edit, _))| {
!all[i + 1..].iter().any(|(later, _)| covers(later, edit))
})
.map(|(_, &pair)| pair)
.collect::<Vec<_>>();
edits.sort_by_key(|(edit, _)| (edit.span.start, edit.span.end));
let mut corrected_source = String::new();
let mut placeholders = Vec::new();
let mut copied = 0;
for (edit, is_own) in edits.iter().copied()
{
assert!(
edit.span.start >= copied,
"overlapping edits in {:?}: {:?}",
source,
edits
);
corrected_source.push_str(&source[copied..edit.span.start]);
let base = corrected_source.len();
corrected_source.push_str(&edit.replacement);
if is_own
{
placeholders.extend(edit.placeholders.iter().map(|p| {
(
SourceSpan {
start: base + p.span.start,
end: base + p.span.end
},
p.description,
p.valid_kinds
)
}));
}
copied = edit.span.end;
}
corrected_source.push_str(&source[copied..]);
Replayed {
corrected_source,
placeholders
}
}
#[test]
fn test_error_diagnostics()
{
let test_cases = read_error_test_cases(include_str!(
"../../tests/test_parser_errors.txt"
));
assert!(
!test_cases.is_empty(),
"no test cases found in test_parser_errors.txt"
);
let mut seen = HashSet::new();
for (index, case) in test_cases.iter().enumerate()
{
assert!(
seen.insert(case.source),
"duplicate test case: {:?}",
case.source
);
let result = diagnostics::diagnose(case.source);
let replayed = replay(case.source, &result);
assert_eq!(
result.diagnostics.len(),
case.expected_diagnostics.len(),
"case {}: {:?} — expected {} diagnostics, got {}: {:?}",
index + 1,
case.source,
case.expected_diagnostics.len(),
result.diagnostics.len(),
result
.diagnostics
.iter()
.map(|d| format!("{}", d))
.collect::<Vec<_>>()
);
for (di, (actual, expected)) in result
.diagnostics
.iter()
.zip(case.expected_diagnostics.iter())
.enumerate()
{
assert_eq!(
kind_name(&actual.kind),
expected.kind,
"case {}: {:?} — diagnostic {} kind mismatch",
index + 1,
case.source,
di + 1
);
assert_eq!(
(actual.span.start, actual.span.end),
expected.span,
"case {}: {:?} — diagnostic {} span mismatch",
index + 1,
case.source,
di + 1
);
assert_eq!(
actual.message,
expected.message,
"case {}: {:?} — diagnostic {} message mismatch",
index + 1,
case.source,
di + 1
);
assert_eq!(
format!("{}", actual),
expected.rendered,
"case {}: {:?} — diagnostic {} rendered output mismatch",
index + 1,
case.source,
di + 1
);
assert_eq!(
actual.related.len(),
expected.related.len(),
"case {}: {:?} — diagnostic {} related count mismatch: \
expected {}, got {}",
index + 1,
case.source,
di + 1,
expected.related.len(),
actual.related.len()
);
for (ri, (actual_rel, expected_rel)) in actual
.related
.iter()
.zip(expected.related.iter())
.enumerate()
{
assert_eq!(
(actual_rel.span.start, actual_rel.span.end),
expected_rel.span,
"case {}: {:?} — diagnostic {} related {} span mismatch",
index + 1,
case.source,
di + 1,
ri + 1
);
assert_eq!(
actual_rel.message,
expected_rel.message,
"case {}: {:?} — diagnostic {} related {} message mismatch",
index + 1,
case.source,
di + 1,
ri + 1
);
}
assert_eq!(
actual.suggestions.len(),
expected.suggestions.len(),
"case {}: {:?} — diagnostic {} suggestion count \
mismatch: expected {}, got {}",
index + 1,
case.source,
di + 1,
expected.suggestions.len(),
actual.suggestions.len()
);
for (si, expected_suggestion) in
expected.suggestions.iter().enumerate()
{
let suggestion = &replayed[di][si];
assert_eq!(
suggestion.corrected_source,
expected_suggestion.corrected_source,
"case {}: {:?} — diagnostic {} suggestion {} \
corrected_source mismatch",
index + 1,
case.source,
di + 1,
si + 1
);
if case.expected_diagnostics.len() == 1
{
assert!(
Parser::parse(&suggestion.corrected_source).is_ok(),
"case {}: {:?} — suggestion {} {:?} does \
not parse cleanly",
index + 1,
case.source,
si + 1,
suggestion.corrected_source
);
}
assert_eq!(
suggestion.placeholders.len(),
expected_suggestion.placeholders.len(),
"case {}: {:?} — diagnostic {} suggestion {} \
placeholder count mismatch",
index + 1,
case.source,
di + 1,
si + 1
);
for (pi, (actual_ph, expected_ph)) in suggestion
.placeholders
.iter()
.zip(expected_suggestion.placeholders.iter())
.enumerate()
{
assert_eq!(
(actual_ph.0.start, actual_ph.0.end),
expected_ph.span,
"case {}: {:?} — diagnostic {} suggestion \
{} placeholder {} span mismatch",
index + 1,
case.source,
di + 1,
si + 1,
pi + 1
);
assert_eq!(
actual_ph.1,
expected_ph.description,
"case {}: {:?} — diagnostic {} suggestion \
{} placeholder {} description mismatch",
index + 1,
case.source,
di + 1,
si + 1,
pi + 1
);
assert_eq!(
actual_ph.2.to_vec(),
expected_ph.valid_kinds,
"case {}: {:?} — diagnostic {} suggestion \
{} placeholder {} valid_kinds mismatch",
index + 1,
case.source,
di + 1,
si + 1,
pi + 1
);
}
}
}
}
}
#[test]
fn test_corrected_sources_parse()
{
let test_cases = read_error_test_cases(include_str!(
"../../tests/test_parser_errors.txt"
));
for case in &test_cases
{
let result = diagnostics::diagnose(case.source);
if let Some(corrected) = &result.corrected_source
{
assert!(
Parser::parse(corrected).is_ok(),
"corrected source for {:?} does not parse: {:?}",
case.source,
corrected
);
}
}
}
#[test]
fn test_valid_programs_produce_no_diagnostics()
{
let valid = vec![
"0",
"42",
"-1",
"3D6",
"3d6",
"1D20 + 5",
"2D8 - 1D4",
"3 * 4 + 2",
"2 ^ 10",
"10 % 3",
"(1D6 + 2) * 3",
"((1 + 2))",
"{x}",
"{x}D6",
"{x}D{y}",
"{x}: {x}D6",
"{x}, {y}: {x} + {y}",
"{a}, {b}, {c}: ({a} + {b}) * {c}",
"[1:20]",
"[1:6]",
"2D[1,2,3]",
"4D6 drop lowest",
"4D6 drop highest",
"4D6 drop lowest 1",
"8D6 drop lowest 3 drop highest 1",
"1D6 + 1D8 + 1D10",
];
for source in &valid
{
let result = diagnostics::diagnose(source);
assert!(
result.diagnostics.is_empty(),
"valid program {:?} produced {} diagnostics: {:?}",
source,
result.diagnostics.len(),
result
.diagnostics
.iter()
.map(|d| format!("{}", d))
.collect::<Vec<_>>()
);
assert_eq!(
result.corrected_source.as_deref(),
Some(*source),
"valid program {:?} corrected_source mismatch",
source
);
}
}
#[test]
fn test_diagnose_duplicate_parameter_rendered_output()
{
let result = diagnostics::diagnose("{x}, {x}: {x}");
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert!(matches!(
diag.kind,
DiagnosticKind::DuplicateParameter { ref name } if name == "x"
));
assert_eq!(
format!("{}", diag),
"duplicate parameter `x` (6..7): parameter `x` is declared more than \
once; review references to `x` in the body — one may have meant a \
different parameter or an external variable"
);
assert_eq!(diag.related.len(), 1);
assert_eq!(diag.related[0].span.start, 1);
assert_eq!(diag.related[0].span.end, 2);
assert_eq!(diag.related[0].message, "first declared here");
}
#[test]
fn test_diagnose_duplicate_canonical_parameter()
{
let source = "{a b}, {a\n b}: 1";
let result = diagnostics::diagnose(source);
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert!(matches!(
diag.kind,
DiagnosticKind::DuplicateParameter { ref name } if name == "a b"
));
assert_eq!(diag.span, SourceSpan { start: 8, end: 14 });
let corrected = diag.suggestions[0].apply(source);
assert_eq!(corrected, "{a b}, {new0}: 1");
assert!(Parser::parse(&corrected).is_ok());
}
#[test]
fn test_validator_skipped_after_parse_fix()
{
let result = diagnostics::diagnose("{x}, {x}: {x");
assert_eq!(
result.diagnostics.len(),
1,
"expected only the parser diagnostic, got {:?}",
result
.diagnostics
.iter()
.map(|d| format!("{}", d))
.collect::<Vec<_>>()
);
assert!(
!matches!(
result.diagnostics[0].kind,
DiagnosticKind::DuplicateParameter { .. }
),
"validator should not run after the recovering parse; got {:?}",
result.diagnostics[0].kind
);
}
#[test]
fn test_diagnostic_kind_unopened_delimiter_display()
{
let kind = DiagnosticKind::UnopenedDelimiter { closer: ')' };
assert_eq!(format!("{}", kind), "unexpected `)`");
let kind = DiagnosticKind::UnopenedDelimiter { closer: ']' };
assert_eq!(format!("{}", kind), "unexpected `]`");
let kind = DiagnosticKind::UnopenedDelimiter { closer: '}' };
assert_eq!(format!("{}", kind), "unexpected `}`");
}
#[test]
fn test_diagnostic_kind_unexpected_eof_display()
{
let kind = DiagnosticKind::UnexpectedEof;
assert_eq!(format!("{}", kind), "unexpected end of input");
}
#[test]
fn test_diagnose_unexpected_token_stops_at_whitespace()
{
let result = diagnostics::diagnose("@\t");
assert_eq!(result.diagnostics.len(), 1);
let diag = &result.diagnostics[0];
assert!(matches!(diag.kind, DiagnosticKind::UnexpectedToken));
assert_eq!((diag.span.start, diag.span.end), (0, 1));
assert_eq!(diag.message, "unexpected `@`");
assert!(diag.suggestions.is_empty());
}
#[test]
fn test_diagnose_stray_whitespace()
{
for (source, expected, spans) in [
("\u{a0}1", " 1", vec![(0, 2)]),
("1\u{a0}", "1 ", vec![(1, 3)]),
("1 +\u{a0}2", "1 + 2", vec![(3, 5)]),
("(1\u{a0})", "(1 )", vec![(2, 4)]),
(
"3d\u{a0}6 drop\u{2003}lowest",
"3d 6 drop lowest",
vec![(2, 4), (10, 13)]
),
("{x},\u{b}{y}: {x}", "{x}, {y}: {x}", vec![(4, 5)]),
("{x}@\u{a0}(1)", "{x}@ (1)", vec![(4, 6)])
]
{
let result = diagnostics::diagnose(source);
assert_eq!(result.corrected_source.as_deref(), Some(expected));
assert!(Parser::parse(expected).is_ok());
assert_eq!(
result
.diagnostics
.iter()
.map(|diag| (diag.span.start, diag.span.end))
.collect::<Vec<_>>(),
spans,
"{:?}",
source
);
for diag in &result.diagnostics
{
assert!(matches!(diag.kind, DiagnosticKind::UnexpectedToken));
assert_eq!(diag.suggestions.len(), 1);
}
}
let result = diagnostics::diagnose("\u{a0}1");
let diag = &result.diagnostics[0];
assert_eq!(
diag.message,
"unexpected `U+00A0`; only spaces, tabs, and line breaks may separate \
tokens"
);
assert_eq!(
diag.suggestions[0].description,
"replace `U+00A0` with a space"
);
}
#[test]
fn test_diagnose_stray_whitespace_outside_braces()
{
let result = diagnostics::diagnose("{hit\u{a0}points} +\u{a0}1");
assert_eq!(
result.corrected_source.as_deref(),
Some("{hit\u{a0}points} + 1")
);
assert_eq!(result.diagnostics.len(), 1);
assert_eq!(
result.diagnostics[0].span,
SourceSpan { start: 15, end: 17 }
);
let result = diagnostics::diagnose("{x + 2\u{a0}* 3");
assert_eq!(result.corrected_source.as_deref(), Some("{x} + 2 * 3"));
assert_eq!(result.diagnostics.len(), 2);
assert!(matches!(
result.diagnostics[0].kind,
DiagnosticKind::UnclosedDelimiter { opener: '{', .. }
));
assert_eq!(result.diagnostics[1].span, SourceSpan { start: 6, end: 8 });
}
#[test]
fn test_diagnose_stray_whitespace_before_bare_parameter()
{
let result = diagnostics::diagnose("{x}, \u{a0}hit points: 0");
assert_eq!(
result.corrected_source.as_deref(),
Some("{x}, {hit points}: 0")
);
assert_eq!(result.diagnostics.len(), 2);
assert!(matches!(
result.diagnostics[0].kind,
DiagnosticKind::UnexpectedToken
));
assert_eq!(result.diagnostics[0].span, SourceSpan { start: 5, end: 7 });
assert!(matches!(
result.diagnostics[1].kind,
DiagnosticKind::BareIdentifier
));
assert_eq!(result.diagnostics[1].span, SourceSpan { start: 7, end: 17 });
}
#[test]
fn test_diagnose_stray_whitespace_in_trailing_input()
{
let result = diagnostics::diagnose("1 2\u{a0}3");
assert_eq!(result.corrected_source.as_deref(), Some("1"));
assert_eq!(result.diagnostics.len(), 1);
assert!(matches!(
result.diagnostics[0].kind,
DiagnosticKind::TrailingInput
));
}
#[test]
fn test_diagnose_bare_name_before_stray_whitespace()
{
for (source, expected, what) in [
("hit points\u{a0}: 0", "{hit points} : 0", "parameter"),
(
"{x}, hit points\u{a0}: 0",
"{x}, {hit points} : 0",
"parameter"
),
("1 + hit points\u{a0}", "1 + {hit points} ", "variable")
]
{
let result = diagnostics::diagnose(source);
assert_eq!(result.corrected_source.as_deref(), Some(expected));
assert!(Parser::parse(expected).is_ok());
assert_eq!(result.diagnostics.len(), 2);
let diag = &result.diagnostics[0];
assert!(matches!(diag.kind, DiagnosticKind::BareIdentifier));
assert_eq!(
diag.message,
format!(
"bare identifier `hit points` is not valid here; {}s must be \
wrapped in `{{}}`",
what
)
);
assert_eq!(
diag.suggestions[0].description,
format!("use `hit points` as a {} name", what)
);
assert!(matches!(
result.diagnostics[1].kind,
DiagnosticKind::UnexpectedToken
));
}
}
#[test]
fn test_diagnose_unclosed_name_braces_foreign_whitespace()
{
let result = diagnostics::diagnose("{x\u{a0}+ 2");
assert_eq!(result.corrected_source.as_deref(), Some("{x\u{a0}}+ 2"));
assert!(Parser::parse("{x\u{a0}}+ 2").is_ok());
}
#[test]
fn test_diagnostics_performance()
{
let expressions = vec![
"3D6 + 2",
"1D20",
"4D6 drop lowest",
"2D8 + 1D4 - 3",
"{x}D{y}",
"{x}: {x}D6 + {x}",
"(1D6 + 2) * 3",
"[1:20]",
"1D6 + 1D8 + 1D10",
"{a}, {b}: {a}D{b} + 5",
"3D6 +",
"(3D6",
"xD6",
"3D",
"4D6 drop",
"+ 1D6",
"",
"3D6)",
"1 + * 2",
"3D6 + + 1D3 -",
];
let start = std::time::Instant::now();
for _ in 0..100
{
for expr in &expressions
{
let _ = diagnostics::diagnose(expr);
}
}
let elapsed = start.elapsed();
assert!(
elapsed.as_millis() < 1000,
"diagnostics too slow: {}ms for 2000 calls",
elapsed.as_millis()
);
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_collect_in_use_names_deep()
{
on_small_stack(|| {
for nesting in Nesting::ROTATION.into_iter().chain([Nesting::Mixed])
{
let function = nest_function(nesting, DEPTH);
let names = diagnostics::collect_in_use_names(&function);
let expected = if nesting.binds()
{
HashSet::from(["a", "x"])
}
else
{
HashSet::from(["x"])
};
assert_eq!(names, expected, "{:?}", nesting);
}
});
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_group() { diagnose_deep(Nesting::Group) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_range_start() { diagnose_deep(Nesting::RangeStart) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_range_end() { diagnose_deep(Nesting::RangeEnd) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_negation() { diagnose_deep(Nesting::Negation) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_exponent() { diagnose_deep(Nesting::Exponent) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_addition() { diagnose_deep(Nesting::Addition) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_subtraction() { diagnose_deep(Nesting::Subtraction) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_multiplication()
{
diagnose_deep(Nesting::Multiplication)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_division() { diagnose_deep(Nesting::Division) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_modulo() { diagnose_deep(Nesting::Modulo) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_dice_count() { diagnose_deep(Nesting::DiceCount) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_dice_faces() { diagnose_deep(Nesting::DiceFaces) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_custom_count() { diagnose_deep(Nesting::CustomCount) }
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_drop_expression()
{
diagnose_deep(Nesting::DropExpression)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_drop_clauses() { diagnose_deep(Nesting::DropClauses) }
fn diagnose_deep(nesting: Nesting)
{
assert!(!nesting.binds(), "{:?} introduces bindings", nesting);
on_small_stack(|| {
let nest = nest_parsable(nesting, DEPTH, 1).to_string();
let cases = [
(
format!("{{b}}: {{b}}@(1) + {} + {{x}}", nest),
format!("{{b}}: {{y}}@(1) + {} + {{x}}", nest),
"BindingCollidesWithParameter"
),
(
format!("{{b}}@(1) + {{b}}@(2) + {} + {{x}}", nest),
format!("{{b}}@(1) + {{y}}@(2) + {} + {{x}}", nest),
"DuplicateBinding"
),
(
format!("{{b}} + {{b}}@(1) + {} + {{x}}", nest),
format!("{{b}} + {{y}}@(1) + {} + {{x}}", nest),
"UseBeforeBind"
)
];
for (source, corrected, kind) in cases
{
let result = diagnostics::diagnose(&source);
assert_eq!(result.diagnostics.len(), 1, "{:?}: {}", nesting, kind);
let diagnostic = &result.diagnostics[0];
assert_eq!(kind_name(&diagnostic.kind), kind, "{:?}", nesting);
assert!(
diagnostic.suggestions[0].apply(&source) == corrected,
"{:?}: {} did not suggest renaming `b` to `y`",
nesting,
kind
);
}
});
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_group()
{
diagnose_deep_failing(Nesting::Group, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_binding()
{
diagnose_deep_failing(Nesting::Binding, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_range_start()
{
diagnose_deep_failing(Nesting::RangeStart, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_range_end()
{
diagnose_deep_failing(Nesting::RangeEnd, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_negation()
{
diagnose_deep_failing(Nesting::Negation, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_exponent()
{
diagnose_deep_failing(Nesting::Exponent, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_addition()
{
diagnose_deep_failing(Nesting::Addition, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_subtraction()
{
diagnose_deep_failing(Nesting::Subtraction, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_multiplication()
{
diagnose_deep_failing(Nesting::Multiplication, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_division()
{
diagnose_deep_failing(Nesting::Division, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_modulo()
{
diagnose_deep_failing(Nesting::Modulo, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_dice_count()
{
diagnose_deep_failing(Nesting::DiceCount, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_dice_faces()
{
diagnose_deep_failing(Nesting::DiceFaces, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_custom_count()
{
diagnose_deep_failing(Nesting::CustomCount, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_drop_expression()
{
diagnose_deep_failing(Nesting::DropExpression, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_drop_clauses()
{
diagnose_deep_failing(Nesting::DropClauses, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_hole_mixed()
{
diagnose_deep_failing(Nesting::Mixed, Breakage::Hole)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_group()
{
diagnose_deep_failing(Nesting::Group, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_binding()
{
diagnose_deep_failing(Nesting::Binding, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_range_start()
{
diagnose_deep_failing(Nesting::RangeStart, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_range_end()
{
diagnose_deep_failing(Nesting::RangeEnd, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_negation()
{
diagnose_deep_failing(Nesting::Negation, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_exponent()
{
diagnose_deep_failing(Nesting::Exponent, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_addition()
{
diagnose_deep_failing(Nesting::Addition, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_subtraction()
{
diagnose_deep_failing(Nesting::Subtraction, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_multiplication()
{
diagnose_deep_failing(Nesting::Multiplication, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_division()
{
diagnose_deep_failing(Nesting::Division, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_modulo()
{
diagnose_deep_failing(Nesting::Modulo, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_dice_count()
{
diagnose_deep_failing(Nesting::DiceCount, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_dice_faces()
{
diagnose_deep_failing(Nesting::DiceFaces, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_custom_count()
{
diagnose_deep_failing(Nesting::CustomCount, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_drop_expression()
{
diagnose_deep_failing(Nesting::DropExpression, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_drop_clauses()
{
diagnose_deep_failing(Nesting::DropClauses, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_truncated_mixed()
{
diagnose_deep_failing(Nesting::Mixed, Breakage::Truncated)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_group()
{
diagnose_deep_failing(Nesting::Group, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_binding()
{
diagnose_deep_failing(Nesting::Binding, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_range_start()
{
diagnose_deep_failing(Nesting::RangeStart, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_range_end()
{
diagnose_deep_failing(Nesting::RangeEnd, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_negation()
{
diagnose_deep_failing(Nesting::Negation, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_exponent()
{
diagnose_deep_failing(Nesting::Exponent, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_addition()
{
diagnose_deep_failing(Nesting::Addition, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_subtraction()
{
diagnose_deep_failing(Nesting::Subtraction, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_multiplication()
{
diagnose_deep_failing(Nesting::Multiplication, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_division()
{
diagnose_deep_failing(Nesting::Division, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_modulo()
{
diagnose_deep_failing(Nesting::Modulo, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_dice_count()
{
diagnose_deep_failing(Nesting::DiceCount, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_dice_faces()
{
diagnose_deep_failing(Nesting::DiceFaces, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_custom_count()
{
diagnose_deep_failing(Nesting::CustomCount, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_drop_expression()
{
diagnose_deep_failing(Nesting::DropExpression, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_drop_clauses()
{
diagnose_deep_failing(Nesting::DropClauses, Breakage::Stray)
}
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_deep_stray_mixed()
{
diagnose_deep_failing(Nesting::Mixed, Breakage::Stray)
}
#[derive(Clone, Copy, Debug)]
enum Breakage
{
Hole,
Truncated,
Stray
}
impl Breakage
{
fn source(self, nesting: Nesting, depth: usize) -> String
{
let source = nest_parsable(nesting, depth, LEAF).to_string();
let leaf = LEAF.to_string();
assert_eq!(source.matches(&leaf).count(), 1, "{:?}", nesting);
let start = source.find(&leaf).unwrap();
let end = start + leaf.len();
match self
{
Breakage::Hole => format!("{}{}", &source[..start], &source[end..]),
Breakage::Truncated => source[..start].to_string(),
Breakage::Stray =>
{
format!("{}@{}", &source[..start], &source[end..])
}
}
}
}
type Census = BTreeMap<&'static str, usize>;
fn census(result: &DiagnoseResult) -> Census
{
let mut census = Census::new();
for diagnostic in &result.diagnostics
{
*census.entry(kind_name(&diagnostic.kind)).or_default() += 1;
}
census
}
fn predict_census(nesting: Nesting, breakage: Breakage) -> (Census, bool)
{
let period = Nesting::ROTATION.len();
let base = period + DEPTH % period;
let results = [base, base + period, base + 2 * period, base + 3 * period]
.map(|depth| diagnostics::diagnose(&breakage.source(nesting, depth)));
let fixed = results[0].corrected_source.is_some();
assert!(
results
.iter()
.all(|r| r.corrected_source.is_some() == fixed),
"{:?}, {:?}: fixability varies with depth",
nesting,
breakage
);
let censuses = results.each_ref().map(census);
let kinds = censuses
.iter()
.flat_map(|census| census.keys().copied())
.collect::<HashSet<_>>();
let periods = (DEPTH - base) / period;
let mut predicted = Census::new();
for kind in kinds
{
let counts = censuses
.each_ref()
.map(|census| census.get(kind).copied().unwrap_or(0) as isize);
let step = counts[1] - counts[0];
assert!(
counts[2] - counts[1] == step && counts[3] - counts[2] == step,
"{:?}, {:?}: {} diagnostics do not grow linearly: {:?}",
nesting,
breakage,
kind,
counts
);
let count = counts[0] + periods as isize * step;
if count > 0
{
predicted.insert(kind, count as usize);
}
}
(predicted, fixed)
}
fn diagnose_deep_failing(nesting: Nesting, breakage: Breakage)
{
on_small_stack(|| {
let (predicted, fixed) = predict_census(nesting, breakage);
let source = breakage.source(nesting, DEPTH);
let result = diagnostics::diagnose(&source);
assert_eq!(census(&result), predicted, "{:?}, {:?}", nesting, breakage);
assert_eq!(
result.corrected_source.is_some(),
fixed,
"{:?}, {:?}",
nesting,
breakage
);
if let Some(corrected) = &result.corrected_source
{
assert!(
Parser::parse(corrected).is_ok(),
"{:?}, {:?}: corrected source fails to parse",
nesting,
breakage
);
}
});
}
const WIDTH: usize = 2_000;
const RUNS: usize = 3;
const TIME_SCALE: u32 = 3 * SCALE as u32;
const TIME_FLOOR: Duration = Duration::from_micros(100);
type Member = fn(usize) -> String;
const DIAGNOSE_FAMILIES: &[(&str, Member)] = &[
("leading closers", |n| format!("{}1", ")".repeat(n))),
("bare dice count", |n| format!("{}D6", "x".repeat(n))),
("parameter list", |n| vec!["{x}"; n].join(", ")),
("bare identifier sum", |n| vec!["x"; n].join(" + ")),
("long identifier", |n| "xd".repeat(n)),
("trailing input", |n| format!("1{}", " 2".repeat(n))),
("stray whitespace", |n| vec!["1"; n].join(" +\u{a0}")),
("strays after unclosed names", |n| "{x + 2\u{a0}".repeat(n)),
("misplaced drops", |n| vec!["3 drop lowest"; n].join(" + ")),
("operandless drops", |n| {
format!("1{}", " + drop lowest".repeat(n))
})
];
#[test]
#[ignore = "stress: run with just stress"]
fn test_diagnose_is_linear()
{
let families = FAMILIES
.iter()
.map(|family| (family.name, family.source))
.chain(DIAGNOSE_FAMILIES.iter().copied());
for (name, source) in families
{
let sources = [WIDTH, SCALE * WIDTH].map(source);
let mut times = [Duration::MAX; 2];
let mut sizes = [0; 2];
for _ in 0..RUNS
{
for (i, source) in sources.iter().enumerate()
{
let start = Instant::now();
let result = diagnostics::diagnose(source);
times[i] = times[i].min(start.elapsed());
assert!(!result.diagnostics.is_empty(), "{}", name);
sizes[i] = text_size(&result);
}
}
assert!(
sizes[1] <= (SCALE + 1) * sizes[0],
"{} produced {} bytes, then {} for {} times the input",
name,
sizes[0],
sizes[1],
SCALE
);
assert!(
times[1] <= TIME_SCALE * times[0].max(TIME_FLOOR),
"{} took {:?}, then {:?} for {} times the input",
name,
times[0],
times[1],
SCALE
);
}
}
fn text_size(result: &DiagnoseResult) -> usize
{
result
.diagnostics
.iter()
.map(|diagnostic| {
diagnostic.message.len()
+ diagnostic
.related
.iter()
.map(|label| label.message.len())
.sum::<usize>()
+ diagnostic
.suggestions
.iter()
.map(|suggestion| {
suggestion.description.len()
+ suggestion
.edits
.iter()
.map(|edit| edit.replacement.len())
.sum::<usize>()
})
.sum::<usize>()
})
.sum()
}