use super::*;
use crate::gui::library::local::helpers::test_support::{
VALID_ASC, open_temp_db, temp_db_path_for_test, temp_dir_for_test,
};
use indicatrix::geometry::cuts::StandardGemCuts;
#[test]
fn classify_shape_calls_the_standard_round_brilliant_round() {
let planes = StandardGemCuts::standard_round_brilliant();
assert_eq!(
classify_shape(&planes, 1.0).as_deref(),
Some("Round"),
"16 girdle facets at lw == 1.0 must classify as Round"
);
}
#[test]
fn classify_shape_ignores_fold_count_entirely() {
let planes = StandardGemCuts::standard_round_brilliant();
assert_eq!(classify_shape(&planes, 1.0).as_deref(), Some("Round"));
}
#[test]
fn classify_shape_refuses_to_guess_for_an_elongated_outline() {
let planes = StandardGemCuts::standard_round_brilliant();
assert_eq!(
classify_shape(&planes, 1.6),
None,
"a 1.6 length/width ratio must not be classified Round"
);
}
#[test]
fn classify_shape_returns_none_without_a_usable_girdle() {
assert_eq!(classify_shape(&[], 1.0), None);
}
#[test]
fn catch_file_panic_converts_a_panic_into_an_error_message_instead_of_unwinding() {
let prev_hook = std::panic::take_hook();
std::panic::set_hook(Box::new(|_| {}));
let result = catch_file_panic(std::panic::AssertUnwindSafe(|| -> i32 {
panic!("deliberate test panic");
}));
std::panic::set_hook(prev_hook);
assert_eq!(result, Err("deliberate test panic".to_string()));
}
#[test]
fn catch_file_panic_returns_ok_when_the_closure_does_not_panic() {
assert_eq!(
catch_file_panic(std::panic::AssertUnwindSafe(|| 42)),
Ok(42)
);
}
#[test]
fn import_path_continues_after_one_file_fails_to_parse() {
let dir = temp_dir_for_test("parse_fail");
std::fs::write(dir.join("good.asc"), VALID_ASC).expect("write good.asc");
std::fs::write(dir.join("bad.asc"), "not an asc file").expect("write bad.asc");
let db_path = temp_db_path_for_test("parse_fail");
let db = open_temp_db(&db_path);
let mut progress_calls = 0usize;
let outcome = import_path(&db, &dir, false, |_done, _total| {
progress_calls += 1;
});
let summary = outcome.summary;
assert!(summary.contains("Imported 1"), "summary was: {summary}");
assert!(summary.contains("1 skipped"), "summary was: {summary}");
assert!(summary.contains("bad.asc"), "summary was: {summary}");
assert_eq!(
outcome.imported_ids.len(),
1,
"exactly the one successfully-imported file's id must be reported"
);
assert_eq!(
progress_calls, 2,
"on_progress must still fire once per candidate file, good AND bad"
);
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_file(&db_path);
}
#[test]
fn import_path_recurses_into_subfolders_only_when_requested() {
let dir = temp_dir_for_test("recurse");
std::fs::write(dir.join("top.asc"), VALID_ASC).expect("write top.asc");
let sub = dir.join("sub");
std::fs::create_dir_all(&sub).expect("create subfolder");
std::fs::write(sub.join("nested.asc"), VALID_ASC).expect("write nested.asc");
let flat_db_path = temp_db_path_for_test("recurse_flat");
let flat_db = open_temp_db(&flat_db_path);
let flat_summary = import_path(&flat_db, &dir, false, |_, _| {}).summary;
assert!(
flat_summary.contains("Imported 1"),
"non-recursive import must see only top.asc: {flat_summary}"
);
let deep_db_path = temp_db_path_for_test("recurse_deep");
let deep_db = open_temp_db(&deep_db_path);
let deep_summary = import_path(&deep_db, &dir, true, |_, _| {}).summary;
assert!(
deep_summary.contains("Imported 2"),
"recursive import must see both top.asc and sub/nested.asc: {deep_summary}"
);
let _ = std::fs::remove_dir_all(&dir);
let _ = std::fs::remove_file(&flat_db_path);
let _ = std::fs::remove_file(&deep_db_path);
}
#[test]
fn collect_asc_files_recursive_stops_at_max_depth() {
let root = temp_dir_for_test("depth");
let level1 = root.join("level1");
let level2 = level1.join("level2");
std::fs::create_dir_all(&level2).expect("create nested folders");
std::fs::write(level1.join("shallow.asc"), VALID_ASC).expect("write shallow.asc");
std::fs::write(level2.join("deep.asc"), VALID_ASC).expect("write deep.asc");
let mut visited = HashSet::new();
let mut out = Vec::new();
collect_asc_files_recursive(&level1, true, 1, 1, &mut visited, &mut out);
assert_eq!(
out.len(),
1,
"only the depth-1 file should be found, not the depth-2 one: {out:?}"
);
assert!(out[0].ends_with("shallow.asc"));
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn collect_asc_files_recursive_guards_against_a_symlink_loop() {
let root = temp_dir_for_test("symlink_loop");
std::fs::write(root.join("real.asc"), VALID_ASC).expect("write real.asc");
let link_path = root.join("loop_back");
#[cfg(windows)]
let symlink_result = std::os::windows::fs::symlink_dir(&root, &link_path);
#[cfg(not(windows))]
let symlink_result = std::os::unix::fs::symlink(&root, &link_path);
if let Err(e) = symlink_result {
eprintln!(
"skipping collect_asc_files_recursive_guards_against_a_symlink_loop: \
could not create a directory symlink on this machine ({e}) -- \
likely needs Developer Mode or an elevated process on Windows"
);
let _ = std::fs::remove_dir_all(&root);
return;
}
let mut visited = HashSet::new();
let mut out = Vec::new();
collect_asc_files_recursive(&root, true, 0, MAX_RECURSE_DEPTH, &mut visited, &mut out);
assert_eq!(
out.len(),
1,
"the symlink loop must not cause real.asc to be found more than once: {out:?}"
);
let _ = std::fs::remove_dir_all(&root);
}
#[test]
#[ignore = "manual perf probe, not for CI"]
fn perf_probe_single_file_parse_and_measure_cost() {
const ITERATIONS: u32 = 500;
let t0 = std::time::Instant::now();
for _ in 0..ITERATIONS {
let mut parsed = local::import_asc("trichecker.asc", VALID_ASC).expect("valid .asc");
apply_measured_metadata(&mut parsed.detail);
}
let elapsed = t0.elapsed();
eprintln!(
"parse + apply_measured_metadata: {elapsed:?} total over {ITERATIONS} iterations, \
{:?} average",
elapsed / ITERATIONS
);
}