use super::*;
fn bk7() -> DispersionModel {
GemMaterial::by_name("Glass (N-BK7)")
.expect("N-BK7 is a built-in")
.dispersion
}
fn texts(items: &[&str]) -> Vec<String> {
items.iter().map(|item| (*item).to_owned()).collect()
}
#[test]
fn every_kind_has_matching_labels_examples_and_an_index() {
for (position, kind) in ModelKind::ALL.into_iter().enumerate() {
assert_eq!(kind.labels().len(), kind.placeholders().len(), "{kind:?}");
assert_eq!(kind.field_count(), kind.labels().len());
assert!(kind.field_count() <= FIELD_COUNT);
assert_eq!(kind.index(), i32::try_from(position).unwrap());
assert_eq!(ModelKind::from_index(kind.index()), Some(kind));
}
assert_eq!(ModelKind::Sellmeier3.field_count(), FIELD_COUNT);
assert_eq!(ModelKind::from_index(-1), None);
assert_eq!(ModelKind::from_index(3), None);
}
#[test]
fn the_examples_are_themselves_a_usable_model() {
for kind in ModelKind::ALL {
let example = texts(kind.placeholders());
assert!(Readout::new(kind, &example).valid, "{kind:?}");
}
}
#[test]
fn units_are_written_with_plain_latin_one_characters() {
for kind in ModelKind::ALL {
for label in kind.labels() {
assert!(
label
.chars()
.all(|c| c.is_ascii() || c == '\u{b5}' || c == '\u{b2}'),
"{label}"
);
}
}
assert_eq!(ModelKind::Sellmeier1.labels()[1], "C1 (\u{b5}m\u{b2})");
}
#[test]
fn a_field_reads_numbers_with_a_point_or_a_decimal_comma() {
assert_eq!(parse_coefficient("1.5"), Ok(1.5));
assert_eq!(parse_coefficient(" 2 "), Ok(2.0));
assert_eq!(parse_coefficient("1,5"), Ok(1.5));
assert_eq!(parse_coefficient("-0.0042"), Ok(-0.0042));
assert_eq!(parse_coefficient("1e-3"), Ok(0.001));
}
#[test]
fn a_field_refuses_what_is_not_a_finite_number() {
assert_eq!(parse_coefficient(""), Err(FieldProblem::Blank));
assert_eq!(parse_coefficient(" "), Err(FieldProblem::Blank));
for bad in [
"abc",
"NaN",
"inf",
"-infinity",
"1e999",
"1,000.5",
"1.2.3",
"--1",
] {
assert_eq!(
parse_coefficient(bad),
Err(FieldProblem::NotANumber),
"{bad}"
);
}
}
#[test]
fn nothing_typed_gives_a_hint_and_no_errors() {
let readout = Readout::new(ModelKind::Sellmeier3, &[]);
assert!(!readout.valid);
assert_eq!(readout.labels.len(), 6);
assert_eq!(readout.placeholders.len(), 6);
assert!(readout.field_errors.iter().all(String::is_empty));
assert!(readout.hint.contains("copy them from a built-in"));
assert!(readout.model_error.is_empty() && readout.model_json.is_empty());
let blanks = vec![String::new(); FIELD_COUNT];
assert_eq!(Readout::new(ModelKind::Sellmeier3, &blanks), readout);
}
#[test]
fn a_half_filled_form_names_each_missing_or_bad_field() {
let readout = Readout::new(ModelKind::Sellmeier1, &texts(&["abc", ""]));
assert!(!readout.valid);
assert_eq!(readout.hint, "");
assert!(readout.field_errors[0].contains("'abc' is not a number"));
assert_eq!(readout.field_errors[1], "Enter a number for C1.");
assert_eq!(readout.model_json, "");
let readout = Readout::new(ModelKind::Cauchy, &texts(&["1.7", "0.006", ""]));
assert!(!readout.valid);
assert!(readout.field_errors[0].is_empty() && readout.field_errors[1].is_empty());
assert_eq!(readout.field_errors[2], "Enter a number for C.");
}
#[test]
fn a_usable_cauchy_fit_fills_the_readout_curve_and_json() {
let readout = Readout::new(ModelKind::Cauchy, &texts(&["1.7", "0.006", "0"]));
let model = DispersionModel::Cauchy {
a: 1.7,
b: 0.006,
c: 0.0,
};
assert!(readout.valid);
assert!(readout.field_errors.iter().all(String::is_empty));
assert_eq!(readout.model_error, "");
assert_eq!(readout.n_d, format!("{:.4}", model.n_d()));
assert_eq!(readout.n_f, format!("{:.4}", model.n_f()));
assert_eq!(readout.n_c, format!("{:.4}", model.n_c()));
assert_eq!(readout.delta, format!("{:.4}", model.delta_f_c()));
assert_eq!(readout.n_d_value.to_bits(), model.n_d().to_bits());
assert_eq!(readout.delta_value.to_bits(), model.delta_f_c().to_bits());
assert!(readout.abbe.parse::<f32>().is_ok(), "{}", readout.abbe);
assert_eq!(readout.model_json, dispersion_model_to_json(&model));
assert_eq!(dispersion_model_from_json(&readout.model_json), Some(model));
}
#[test]
fn the_curve_has_one_point_per_five_nanometers_inside_the_plot() {
let readout = Readout::new(ModelKind::Cauchy, &texts(&["1.7", "0.006", "0"]));
let tokens: Vec<&str> = readout.curve.split_whitespace().collect();
assert_eq!(tokens.len(), 81 * 3);
assert_eq!(&tokens[..3], ["M", "0.00", tokens[2]]);
assert_eq!(tokens[tokens.len() - 2], "100.00");
assert_eq!(tokens.iter().filter(|t| **t == "L").count(), 80);
let first: f32 = tokens[2].parse().unwrap();
let last: f32 = tokens[tokens.len() - 1].parse().unwrap();
assert!(first < last, "{first} {last}");
for y in tokens
.chunks(3)
.map(|point| point[2].parse::<f32>().unwrap())
{
assert!((0.0..=100.0).contains(&y), "{y}");
}
let top: f32 = readout.curve_top.parse().unwrap();
let bottom: f32 = readout.curve_bottom.parse().unwrap();
assert!(top > readout.n_f.parse::<f32>().unwrap());
assert!(bottom < readout.n_c.parse::<f32>().unwrap());
}
#[test]
fn a_flat_curve_is_a_line_not_a_division_by_zero() {
let readout = Readout::new(ModelKind::Cauchy, &texts(&["1.7", "0", "0"]));
assert!(readout.valid);
assert_eq!(readout.abbe, NONE_TEXT);
assert_eq!(readout.warnings.len(), 1);
assert!(readout.warnings[0].contains("no fire"));
for y in readout
.curve
.split_whitespace()
.skip(2)
.step_by(3)
.map(|y| y.parse::<f32>().unwrap())
{
assert!((y - 50.0).abs() < 0.1, "{y}");
}
}
#[test]
fn a_visible_band_resonance_is_a_curve_error_not_a_field_error() {
let readout = Readout::new(ModelKind::Sellmeier1, &texts(&["1.0", "0.36"]));
assert!(!readout.valid);
assert!(readout.field_errors.iter().all(String::is_empty));
assert!(
readout.model_error.contains("600 nm"),
"{}",
readout.model_error
);
assert!(readout.model_json.is_empty() && readout.curve.is_empty());
assert_eq!(readout.n_d, NONE_TEXT);
}
#[test]
fn an_index_below_one_is_refused() {
let readout = Readout::new(ModelKind::Cauchy, &texts(&["0.9", "0", "0"]));
assert!(!readout.valid);
assert!(
readout.model_error.contains("above 1"),
"{}",
readout.model_error
);
}
#[test]
fn warnings_are_listed_for_a_usable_but_odd_curve() {
let anomalous = Readout::new(ModelKind::Cauchy, &texts(&["1.5", "-0.002", "0"]));
assert!(anomalous.valid);
assert_eq!(anomalous.warnings.len(), 1);
assert!(anomalous.warnings[0].contains("rises with wavelength"));
let weak = Readout::new(ModelKind::Cauchy, &texts(&["1.45", "0.0005", "0"]));
assert!(weak.valid);
assert_eq!(weak.warnings.len(), 1);
assert!(weak.warnings[0].contains("Abbe number"));
let glass = Readout::new(ModelKind::Sellmeier3, &coefficient_texts(&bk7()));
assert!(glass.valid);
assert_eq!(glass.warnings, Vec::<String>::new());
}
#[test]
fn a_published_glass_reads_back_its_published_figures() {
let glass = Readout::new(ModelKind::Sellmeier3, &coefficient_texts(&bk7()));
assert!(glass.valid);
let n_d: f32 = glass.n_d.parse().unwrap();
let abbe: f32 = glass.abbe.parse().unwrap();
assert!((n_d - 1.5168).abs() < 0.002, "{n_d}");
assert!((abbe - 64.17).abs() < 1.5, "{abbe}");
}
#[test]
fn copy_options_offer_a_prompt_then_the_built_ins_with_that_model() {
let options = copy_options(ModelKind::Sellmeier3);
assert_eq!(options[0], COPY_PROMPT);
assert!(options.iter().any(|name| name == "Glass (N-BK7)"));
assert!(options.iter().any(|name| name == "Diamond"));
assert!(
copy_options(ModelKind::Sellmeier1).is_empty(),
"no built-in is a one-term Sellmeier, so the combo is hidden"
);
assert!(copy_options(ModelKind::Cauchy).len() > 1);
}
#[test]
fn every_copy_option_fills_every_field_with_a_usable_model() {
for kind in ModelKind::ALL {
for name in copy_options(kind).into_iter().skip(1) {
let copied = copied_texts(kind, &name);
assert_eq!(copied.len(), kind.field_count(), "{name}");
let readout = Readout::new(kind, &copied);
assert!(readout.valid, "{name}: {}", readout.model_error);
}
assert_eq!(copied_texts(kind, COPY_PROMPT), Vec::<String>::new());
assert_eq!(copied_texts(kind, "No Such Stone"), Vec::<String>::new());
}
}
#[test]
fn copying_a_built_in_reproduces_its_curve_bit_for_bit() {
let copied = copied_texts(ModelKind::Sellmeier3, "Glass (N-BK7)");
let readout = Readout::new(ModelKind::Sellmeier3, &copied);
assert_eq!(dispersion_model_from_json(&readout.model_json), Some(bk7()));
}
#[test]
fn the_cauchy_seed_reproduces_the_simple_modes_numbers() {
let seed = cauchy_seed_texts(2.417, 0.0256);
assert_eq!(seed.len(), 3);
let readout = Readout::new(ModelKind::Cauchy, &seed);
assert!(readout.valid);
assert!((readout.n_d_value - 2.417).abs() < 1e-4);
assert!((readout.delta_value - 0.0256).abs() < 1e-4);
}
#[test]
fn a_stored_model_prefills_the_section_and_reads_back_identically() {
let model = bk7();
let prefill = prefill_for(Some(&model));
assert_eq!(prefill.mode, 1);
assert_eq!(prefill.kind, ModelKind::Sellmeier3.index());
assert_eq!(prefill.texts.len(), FIELD_COUNT);
let kind = ModelKind::from_index(prefill.kind).unwrap();
let readout = Readout::new(kind, &prefill.texts);
assert_eq!(readout.model_json, dispersion_model_to_json(&model));
let one = DispersionModel::Sellmeier1 { b1: 1.0, c1: 0.01 };
let padded = prefill_for(Some(&one));
assert_eq!(padded.kind, ModelKind::Sellmeier1.index());
assert_eq!(padded.texts.len(), FIELD_COUNT);
assert!(padded.texts[2..].iter().all(String::is_empty));
let plain = prefill_for(None);
assert_eq!(plain.mode, 0);
assert!(plain.texts.iter().all(String::is_empty));
}
#[test]
fn the_save_json_is_blank_for_the_simple_path_and_a_model_otherwise() {
assert_eq!(model_from_save_json(""), Ok(None));
assert_eq!(model_from_save_json(" "), Ok(None));
let model = bk7();
let json = dispersion_model_to_json(&model);
assert_eq!(model_from_save_json(&json), Ok(Some(model)));
let refusal = model_from_save_json("not a model").expect_err("garbage is refused");
assert!(refusal.contains("cannot be used"));
let resonant = r#"{"kind":"sellmeier1","b1":1.0,"c1":0.36}"#;
assert!(model_from_save_json(resonant).is_err());
}