use std::sync::Arc;
use noyalib::{
from_str, BudgetBreach, CroppedRegion, Error, Location, RenderOptions, Result, Value,
};
#[test]
fn error_source_io_some() {
use std::error::Error as StdError;
let ioe = std::io::Error::other("nope");
let e = Error::Io(ioe);
assert!(e.source().is_some());
}
#[test]
fn error_source_shared_some() {
use std::error::Error as StdError;
let inner = Error::EndOfStream;
let e = Error::Shared(Arc::new(inner));
assert!(e.source().is_some());
}
#[test]
fn error_source_other_none() {
use std::error::Error as StdError;
let e = Error::EndOfStream;
assert!(e.source().is_none());
}
#[test]
fn error_from_io_error() {
let ioe = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "x");
let e: Error = ioe.into();
assert!(matches!(e, Error::Io(_)));
}
#[test]
fn format_with_source_line_out_of_range_falls_back() {
let e = Error::ParseWithLocation {
message: "boom".into(),
location: Location::new(999, 1, 0),
};
let s = e.format_with_source("only one line");
assert!(s.contains("boom"));
}
#[test]
fn format_with_source_line_zero_uses_first_line() {
let e = Error::ParseWithLocation {
message: "early".into(),
location: Location::default(), };
let s = e.format_with_source("first line\nsecond line");
assert!(s.contains("early"));
}
#[test]
fn format_with_source_with_valid_location() {
let source = "line 1\nline 2 — bad\nline 3";
let e = Error::ParseWithLocation {
message: "bad token".into(),
location: Location::new(2, 5, 12),
};
let s = e.format_with_source(source);
assert!(s.contains("error"));
assert!(s.contains("line 2"));
assert!(s.contains("^"));
}
#[test]
fn format_with_source_no_location_falls_back_to_display() {
let e = Error::EndOfStream;
let s = e.format_with_source("anything");
assert_eq!(s, format!("{e}"));
}
#[test]
fn format_with_source_radius_empty_source() {
let e = Error::ParseWithLocation {
message: "x".into(),
location: Location::new(1, 1, 0),
};
let s = e.format_with_source_radius("", 2);
assert!(s.contains("x"));
}
#[test]
fn format_with_source_radius_out_of_range_line() {
let e = Error::ParseWithLocation {
message: "x".into(),
location: Location::new(50, 1, 0),
};
let s = e.format_with_source_radius("only line", 2);
assert!(s.contains("x"));
}
#[test]
fn format_with_source_radius_line_zero_uses_first() {
let e = Error::ParseWithLocation {
message: "boot".into(),
location: Location::default(),
};
let s = e.format_with_source_radius("a\nb\nc", 1);
assert!(s.contains("boot"));
}
#[test]
fn format_with_source_radius_no_location_falls_back() {
let e = Error::EndOfStream;
let s = e.format_with_source_radius("anything", 2);
assert_eq!(s, format!("{e}"));
}
#[test]
fn format_with_source_radius_with_valid_location_renders_window() {
let source = "l1\nl2\nl3 BAD\nl4\nl5";
let e = Error::ParseWithLocation {
message: "bad".into(),
location: Location::new(3, 4, 7),
};
let s = e.format_with_source_radius(source, 1);
assert!(s.contains("l2"));
assert!(s.contains("l3"));
assert!(s.contains("l4"));
assert!(s.contains("^"));
}
#[test]
fn format_with_source_truncated_short_input() {
let e = Error::EndOfStream;
let full = e.format_with_source("anything");
let bounded = e.format_with_source_truncated("anything", full.len() + 100);
assert_eq!(bounded, full);
}
#[test]
fn format_with_source_truncated_exceeds_budget() {
let source = "abcdefghijklmnopqrstuvwxyz";
let e = Error::ParseWithLocation {
message: "this is a long message that should be cropped".into(),
location: Location::new(1, 5, 4),
};
let s = e.format_with_source_truncated(source, 30);
assert!(s.len() <= 30);
assert!(s.ends_with("..."));
}
#[test]
fn format_with_source_radius_truncated_works() {
let source = "l1\nl2\nl3\nl4\nl5";
let e = Error::ParseWithLocation {
message: "x".into(),
location: Location::new(3, 1, 6),
};
let s = e.format_with_source_radius_truncated(source, 1, 50);
assert!(s.len() <= 50);
}
#[test]
fn render_options_new_equals_default() {
let a = RenderOptions::new();
let b = RenderOptions::default();
assert_eq!(a, b);
}
#[test]
fn render_options_crop_radius_setter() {
let opts = RenderOptions::new().crop_radius(7);
assert_eq!(opts.crop_radius, 7);
assert!(!opts.color);
}
#[test]
fn render_options_color_setter() {
let opts = RenderOptions::new().color(true);
assert!(opts.color);
assert_eq!(opts.crop_radius, 2);
}
#[test]
fn render_options_chained_builder() {
let opts = RenderOptions::new().crop_radius(0).color(true);
assert_eq!(opts.crop_radius, 0);
assert!(opts.color);
}
#[test]
fn render_default_engages_radius() {
let source = "a:\n b: 1\n c: 2\n";
let r: Result<Value> = from_str(source);
if let Err(e) = r {
let s = e.render(source);
assert!(s.contains("error"));
}
}
#[test]
fn render_with_radius_zero_uses_single_line() {
let source = "a: [unclosed";
let r: Result<Value> = from_str(source);
if let Err(e) = r {
let opts = RenderOptions::new().crop_radius(0);
let s = e.render_with_options(source, &opts);
assert!(s.contains("error"));
}
}
#[test]
fn render_with_color_engages_colorize_render() {
let source = "a: [unclosed";
let r: Result<Value> = from_str(source);
if let Err(e) = r {
let opts = RenderOptions::new().color(true).crop_radius(2);
let s = e.render_with_options(source, &opts);
assert!(s.contains("\x1b["));
}
}
#[test]
fn render_with_color_radius_zero() {
let source = "a: [unclosed";
let r: Result<Value> = from_str(source);
if let Err(e) = r {
let opts = RenderOptions::new().color(true).crop_radius(0);
let _s = e.render_with_options(source, &opts);
}
}
#[test]
fn cropped_region_extract_happy_path() {
let src = "a\nb\nc\nd\ne";
let r = CroppedRegion::extract(src, 3, 1);
assert_eq!(r.lines, vec!["b", "c", "d"]);
assert_eq!(r.focus_index, 1);
assert_eq!(r.focus_line, 3);
assert_eq!(r.low_line, 2);
}
#[test]
fn cropped_region_extract_empty_source() {
let r = CroppedRegion::extract("", 1, 1);
assert!(r.lines.is_empty());
assert_eq!(r.focus_line, 0);
}
#[test]
fn cropped_region_extract_target_out_of_range_clamps() {
let src = "a\nb\nc";
let r = CroppedRegion::extract(src, 99, 1);
assert_eq!(r.focus_line, 3);
}
#[test]
fn cropped_region_extract_zero_radius() {
let src = "a\nb\nc\nd\ne";
let r = CroppedRegion::extract(src, 3, 0);
assert_eq!(r.lines, vec!["c"]);
assert_eq!(r.focus_index, 0);
}
#[test]
fn cropped_region_extract_radius_overflows_top() {
let src = "a\nb\nc";
let r = CroppedRegion::extract(src, 1, 5);
assert_eq!(r.low_line, 1);
}
#[test]
fn into_shared_already_shared_reuses_arc() {
let inner = Arc::new(Error::EndOfStream);
let e = Error::Shared(Arc::clone(&inner));
let arc = e.into_shared();
assert!(Arc::ptr_eq(&arc, &inner));
}
#[test]
fn into_shared_wraps_other_in_arc() {
let arc = Error::EndOfStream.into_shared();
assert!(matches!(&*arc, Error::EndOfStream));
}
#[test]
fn is_shared_and_as_inner() {
let e = Error::Shared(Arc::new(Error::EndOfStream));
assert!(e.is_shared());
assert!(e.as_inner().is_some());
let plain = Error::EndOfStream;
assert!(!plain.is_shared());
assert!(plain.as_inner().is_none());
}
#[test]
fn parse_at_carries_location() {
let e = Error::parse_at("bad", "a: x", 3);
assert!(matches!(e, Error::ParseWithLocation { .. }));
assert_eq!(e.location().unwrap().index(), 3);
}
#[test]
fn deserialize_at_carries_location() {
let e = Error::deserialize_at("bad", "a: x", 1);
assert!(matches!(e, Error::DeserializeWithLocation { .. }));
}
#[test]
fn from_shared_constructor() {
let e = Error::from_shared(Arc::new(Error::EndOfStream));
assert!(e.is_shared());
}
#[test]
fn display_every_variant() {
let cases: Vec<Error> = vec![
Error::Parse("p".into()),
Error::ParseWithLocation {
message: "p".into(),
location: Location::default(),
},
Error::Serialize("s".into()),
Error::Deserialize("d".into()),
Error::DeserializeWithLocation {
message: "d".into(),
location: Location::default(),
},
Error::Custom("c".into()),
Error::RecursionLimitExceeded { depth: 4 },
Error::DuplicateKey("k".into()),
Error::RepetitionLimitExceeded,
Error::Budget(BudgetBreach::MaxNodes {
limit: 1,
observed: 2,
}),
Error::UnknownAnchor("a".into()),
Error::UnknownAnchorAt {
name: "a".into(),
location: Location::default(),
suggestion: None,
},
Error::MissingField("f".into()),
Error::UnknownField("f".into()),
Error::ScalarInMergeElement,
Error::SequenceInMergeElement,
Error::TaggedInMerge,
Error::Invalid("v".into()),
Error::TypeMismatch {
expected: "int",
found: "str".into(),
},
Error::Shared(Arc::new(Error::EndOfStream)),
Error::EndOfStream,
Error::MoreThanOneDocument,
Error::ScalarInMerge,
Error::EmptyTag,
Error::FailedToParseNumber("nan".into()),
Error::Message("m".into(), Some(7)),
Error::Message("m".into(), None),
Error::Io(std::io::Error::other("i")),
];
for e in cases {
let s = format!("{e}");
assert!(!s.is_empty());
}
}
#[test]
fn budget_breach_display_all_variants() {
let cases = vec![
BudgetBreach::MaxEvents {
limit: 1,
observed: 2,
},
BudgetBreach::MaxNodes {
limit: 1,
observed: 2,
},
BudgetBreach::MaxTotalScalarBytes {
limit: 1,
observed: 2,
},
BudgetBreach::MaxDocuments {
limit: 1,
observed: 2,
},
BudgetBreach::MaxMergeKeys {
limit: 1,
observed: 2,
},
BudgetBreach::AliasAnchorRatio {
ratio: 1.5,
anchors: 2,
aliases: 4,
},
];
for c in cases {
let s = format!("{c}");
assert!(!s.is_empty());
}
}
#[test]
fn serde_ser_error_custom() {
let e: Error = <Error as serde::ser::Error>::custom("ser-msg");
assert!(matches!(e, Error::Custom(_)));
}
#[test]
fn serde_de_error_custom() {
let e: Error = <Error as serde::de::Error>::custom("de-msg");
assert!(matches!(e, Error::Custom(_)));
}
#[test]
fn serde_de_error_missing_field() {
let e: Error = <Error as serde::de::Error>::missing_field("foo");
assert!(matches!(e, Error::MissingField(_)));
}
#[test]
fn serde_de_error_unknown_field() {
let e: Error = <Error as serde::de::Error>::unknown_field("bad", &["good"]);
assert!(matches!(e, Error::UnknownField(_)));
}
#[test]
fn location_for_parse_with_location() {
let e = Error::ParseWithLocation {
message: "x".into(),
location: Location::new(2, 3, 5),
};
assert!(e.location().is_some());
}
#[test]
fn location_for_deserialize_with_location() {
let e = Error::DeserializeWithLocation {
message: "x".into(),
location: Location::new(1, 1, 0),
};
assert!(e.location().is_some());
}
#[test]
fn location_for_unknown_anchor_at() {
let e = Error::UnknownAnchorAt {
name: "x".into(),
location: Location::new(1, 1, 0),
suggestion: None,
};
assert!(e.location().is_some());
}
#[test]
fn location_for_shared_delegates() {
let inner = Error::ParseWithLocation {
message: "x".into(),
location: Location::new(1, 1, 0),
};
let e = Error::Shared(Arc::new(inner));
assert!(e.location().is_some());
}
#[test]
fn location_for_other_returns_none() {
let e = Error::EndOfStream;
assert!(e.location().is_none());
}