mirror of
https://github.com/storytold/cadcraft.git
synced 2026-10-26 03:46:53 -06:00
Squash of PR #557 by @vangel76. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
182 lines
6.5 KiB
Rust
182 lines
6.5 KiB
Rust
//! Reading the user's own `.lin` and `.pat` files (#552). Every file here is a tiny synthetic one
|
|
//! written for these tests, with our own names.
|
|
|
|
use cadcraft_doc::library::files::{MAX_LINETYPE_ELEMENTS, MAX_PATTERN_DASHES, MAX_PATTERN_FAMILIES, parse_lin, parse_pat};
|
|
|
|
const LIN: &str = "\
|
|
;; my own linetypes
|
|
*MYDASH,My dash __ __ __
|
|
A,.5,-.25
|
|
|
|
*MYDOTS
|
|
A,0,-.1,0,-.2
|
|
*MYTEXT,Text line --MY--MY--
|
|
A,.5,-.2,[\"MY\",STANDARD,S=.1,R=0.0,X=-0.1,Y=-.05],-.2
|
|
*MYSHAPE,Shape line
|
|
A,.25,-.1,[MYBOX,myshapes.shx,S=.1],-.1,1
|
|
";
|
|
|
|
#[test]
|
|
fn simple_and_text_linetypes_are_read() {
|
|
let r = parse_lin(LIN);
|
|
let names: Vec<&str> = r.items.iter().map(|l| l.name.as_str()).collect();
|
|
assert_eq!(names, ["MYDASH", "MYDOTS", "MYTEXT", "MYSHAPE"]);
|
|
let dash = &r.items[0];
|
|
assert_eq!(dash.description, "My dash __ __ __");
|
|
assert_eq!(dash.pattern.iter().map(|e| e.length).collect::<Vec<_>>(), [0.5, -0.25]);
|
|
assert_eq!(r.items[1].description, "");
|
|
assert_eq!(r.items[1].pattern.len(), 4);
|
|
|
|
// The text belongs to the gap before it: drawn at that gap's end.
|
|
let text = &r.items[2].pattern;
|
|
assert_eq!(text.len(), 3);
|
|
assert_eq!(text[1].length, -0.2);
|
|
assert_eq!(text[1].text.as_deref(), Some("MY"));
|
|
assert_eq!(text[1].style.as_deref(), Some("STANDARD"));
|
|
assert!((text[1].scale - 0.1).abs() < 1e-12);
|
|
assert!((text[1].offset.x + 0.1).abs() < 1e-12 && (text[1].offset.y + 0.05).abs() < 1e-12);
|
|
assert!(!text[1].absolute);
|
|
assert!(text[2].text.is_none());
|
|
|
|
// Shapes need SHX files: left out with a note, the dashes kept.
|
|
let shape = &r.items[3].pattern;
|
|
assert_eq!(shape.iter().map(|e| e.length).collect::<Vec<_>>(), [0.25, -0.1, -0.1, 1.0]);
|
|
assert!(shape.iter().all(|e| e.text.is_none() && e.shape.is_none()));
|
|
assert!(r.warnings.iter().any(|w| w.contains("MYBOX")), "{:?}", r.warnings);
|
|
}
|
|
|
|
#[test]
|
|
fn text_rotation_units_and_absolute_angles() {
|
|
let r = parse_lin("*R1\nA,1,[\"x\",S1,A=90,S=1],-1\n*R2\nA,1,[\"x\",S1,R=1.5r],-1\n*R3\nA,1,[\"a,b]\",S1,U=100g],-1\n");
|
|
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
|
let rot = |i: usize| r.items[i].pattern[0].rotation;
|
|
assert!((rot(0) - std::f64::consts::FRAC_PI_2).abs() < 1e-12 && r.items[0].pattern[0].absolute);
|
|
assert!((rot(1) - 1.5).abs() < 1e-12);
|
|
assert!((rot(2) - std::f64::consts::FRAC_PI_2).abs() < 1e-12);
|
|
// Commas and brackets inside the quoted text are text.
|
|
assert_eq!(r.items[2].pattern[0].text.as_deref(), Some("a,b]"));
|
|
}
|
|
|
|
#[test]
|
|
fn broken_linetypes_are_skipped_with_warnings() {
|
|
let text = "\
|
|
*NOPATTERN
|
|
*BADNUM
|
|
A,.5,abc
|
|
*BADALIGN
|
|
B,.5,-.5
|
|
*UNCLOSED
|
|
A,.5,[\"x\",S1
|
|
*
|
|
A,1,-1
|
|
A,1,-1
|
|
*GOOD
|
|
A,1,-1
|
|
*GOOD
|
|
A,2,-2
|
|
*ZERO
|
|
A,0,0
|
|
*NOSTYLE
|
|
A,1,[\"x\"],-1
|
|
";
|
|
let r = parse_lin(text);
|
|
assert_eq!(r.items.iter().map(|l| l.name.as_str()).collect::<Vec<_>>(), ["GOOD"]);
|
|
assert_eq!(r.items[0].pattern[0].length, 1.0, "the first definition is kept");
|
|
for what in ["NOPATTERN", "BADNUM", "BADALIGN", "UNCLOSED", "without a name", "without a *NAME", "defined twice", "ZERO", "NOSTYLE"] {
|
|
assert!(r.warnings.iter().any(|w| w.contains(what)), "no warning about {what}: {:?}", r.warnings);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_linetype_files_never_panic_and_stay_capped() {
|
|
let long_line = format!("*LONG\nA,{}", vec!["1"; 5000].join(","));
|
|
let many = format!("*MANY\nA,{}", vec!["1,-1"; MAX_LINETYPE_ELEMENTS].join(","));
|
|
let defs: String = (0..5000).map(|i| format!("*L{i}\nA,1,-1\n")).collect();
|
|
let huge = "*HUGE\nA,1e300,-1\n*NAN\nA,NaN,-1\n*INF\nA,inf,-1\n";
|
|
for text in [long_line.as_str(), many.as_str(), defs.as_str(), huge, "", "\u{feff}", "]]][[[\"", "*\u{0}\nA,1", "A,1,-1\n*X,", "*X\nA,[[[[\"]]]"]
|
|
{
|
|
let r = parse_lin(text);
|
|
assert!(r.items.len() <= 4096);
|
|
assert!(r.items.iter().all(|l| l.pattern.len() <= MAX_LINETYPE_ELEMENTS && l.pattern.iter().all(|e| e.length.is_finite())));
|
|
assert!(r.warnings.len() <= 101);
|
|
}
|
|
assert!(parse_lin(huge).items.is_empty());
|
|
assert_eq!(parse_lin(&defs).items.len(), 4096);
|
|
}
|
|
|
|
const PAT: &str = "\
|
|
; my own patterns
|
|
*MYGRID, My grid
|
|
0, 0,0, 0,.5
|
|
90, 0,0, 0,.5
|
|
*MYBRICK,My brick ; trailing comment
|
|
0, 0,0, 0,.25
|
|
90, 0,0, .25,.25, .25,-.25
|
|
";
|
|
|
|
#[test]
|
|
fn hatch_patterns_are_read() {
|
|
let r = parse_pat(PAT);
|
|
assert!(r.warnings.is_empty(), "{:?}", r.warnings);
|
|
assert_eq!(r.items.len(), 2);
|
|
let grid = &r.items[0];
|
|
assert_eq!((grid.name.as_str(), grid.description.as_str()), ("MYGRID", "My grid"));
|
|
assert_eq!(grid.lines.len(), 2);
|
|
assert_eq!(grid.lines[1].angle, 90.0);
|
|
assert_eq!(grid.lines[1].delta, (0.0, 0.5));
|
|
let brick = &r.items[1];
|
|
assert_eq!(brick.description, "My brick");
|
|
assert_eq!(brick.lines[1].origin, (0.0, 0.0));
|
|
assert_eq!(brick.lines[1].dashes, [0.25, -0.25]);
|
|
}
|
|
|
|
#[test]
|
|
fn lowercase_pattern_names_are_upper_cased() {
|
|
let r = parse_pat("*mypat\n45,0,0,0,.125\n");
|
|
assert_eq!(r.items[0].name, "MYPAT");
|
|
}
|
|
|
|
#[test]
|
|
fn broken_patterns_are_skipped_with_warnings() {
|
|
let text = "\
|
|
0,0,0,0,1
|
|
*EMPTY
|
|
*SHORT
|
|
0,0,0
|
|
*ZEROGAP
|
|
0,0,0,1,0
|
|
*BADNUM
|
|
0,0,0,0,x
|
|
*OK
|
|
0,0,0,0,1
|
|
*OK
|
|
45,0,0,0,1
|
|
";
|
|
let r = parse_pat(text);
|
|
assert_eq!(r.items.iter().map(|p| p.name.as_str()).collect::<Vec<_>>(), ["OK"]);
|
|
assert_eq!(r.items[0].lines[0].angle, 0.0, "the first definition is kept");
|
|
for what in ["without a *NAME", "EMPTY", "SHORT", "ZEROGAP", "BADNUM", "defined twice"] {
|
|
assert!(r.warnings.iter().any(|w| w.contains(what)), "no warning about {what}: {:?}", r.warnings);
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn hostile_pattern_files_never_panic_and_stay_capped() {
|
|
let families = format!("*FAM\n{}", "0,0,0,0,1\n".repeat(MAX_PATTERN_FAMILIES + 5));
|
|
let dashes = format!("*DASH\n0,0,0,0,1,{}\n", vec!["1"; MAX_PATTERN_DASHES + 1].join(","));
|
|
let ok_dashes = format!("*DASHOK\n0,0,0,0,1,{}\n", vec!["1"; MAX_PATTERN_DASHES].join(","));
|
|
let defs: String = (0..5000).map(|i| format!("*P{i}\n0,0,0,0,1\n")).collect();
|
|
let big = "*BIG\n1e300,0,0,0,1\n*NAN\nNaN,0,0,0,1\n";
|
|
for text in [families.as_str(), dashes.as_str(), ok_dashes.as_str(), defs.as_str(), big, "", "*", "***\n,,,,,", "*X\n,,,,,,,,,,"] {
|
|
let r = parse_pat(text);
|
|
assert!(r.items.len() <= 4096);
|
|
assert!(r.items.iter().all(|p| p.lines.len() <= MAX_PATTERN_FAMILIES && p.lines.iter().all(|l| l.dashes.len() <= MAX_PATTERN_DASHES)));
|
|
assert!(r.warnings.len() <= 101);
|
|
}
|
|
assert!(parse_pat(&families).items.is_empty());
|
|
assert!(parse_pat(&dashes).items.is_empty());
|
|
assert_eq!(parse_pat(&ok_dashes).items.len(), 1);
|
|
assert!(parse_pat(big).items.is_empty());
|
|
assert_eq!(parse_pat(&defs).items.len(), 4096);
|
|
}
|