use crate::gui::experimental_kinetics_gui::model::PlotModel;
use crate::Kinetics::experimental_kinetics::one_experiment_dataset::Unit;
use crate::gui::experimental_kinetics_gui::controller_kinetics::DirectProblemDialogState;
use crate::gui::experimental_kinetics_gui::controller_methods::KineticMethodsWindowState;
use crate::gui::experimental_kinetics_gui::controller_table::ColumnManagerState;
use crate::gui::experimental_kinetics_gui::interaction::SelectionRect;
use crate::gui::experimental_kinetics_gui::test_options::TestOptions;
use std::path::Path;
const TEST_DATA_PATH: &str = "src/assets/TGAexample.txt";
fn test_data_path() -> &'static Path {
Path::new(TEST_DATA_PATH)
}
pub fn load_model_with_one_curve() -> PlotModel {
let mut model = PlotModel::new();
model
.push_from_file(test_data_path())
.expect("failed to load test data");
let experiments = model.list_of_experiments();
let exp_id = experiments
.first()
.cloned()
.expect("no experiments loaded from fixture");
model
.bind_time_of_experiment(&exp_id, "t", Unit::Second)
.expect("failed to bind time column");
model
.bind_temperature_of_experiment(&exp_id, "T", Unit::Celsius)
.expect("failed to bind temperature column");
model
.bind_mass_of_experiment(&exp_id, "m", Unit::MilliVolt)
.expect("failed to bind mass column");
model.set_x(&exp_id, "t").expect("failed to set x column");
model.set_y(&exp_id, "T").expect("failed to set y column");
model
.create_points_for_curve(&exp_id)
.expect("failed to create points for curve");
model
}
pub fn load_model_with_two_curves() -> PlotModel {
let mut model = load_model_with_one_curve();
let exp_id = model.list_of_experiments()[0].clone();
model.set_x(&exp_id, "t").expect("failed to keep x column");
model
.set_y(&exp_id, "m")
.expect("failed to set second y column");
model
.create_points_for_curve(&exp_id)
.expect("failed to create second curve");
model
}
#[cfg(test)]
mod tests {
use super::*;
use crate::Kinetics::experimental_kinetics::exp_engine_api::XY;
use crate::Kinetics::experimental_kinetics::fitting::FittingModelName;
use crate::Kinetics::experimental_kinetics::kinetic_methods::KineticMethod;
use crate::Kinetics::experimental_kinetics::kinetic_methods::isoconversion::{
IsoconversionalKineticMethod, IsoconversionalMethod,
};
use crate::Kinetics::experimental_kinetics::testing_mod::{
AdvancedTGAConfig, ExperimentMode, KineticModel, NoiseConfig, NoiseKind,
};
use crate::gui::experimental_kinetics_gui::experimental_kinetics_gui_main::PlotApp;
use egui_kittest::Harness;
use egui_kittest::kittest::Queryable;
use std::fs;
use std::path::PathBuf;
use std::time::{SystemTime, UNIX_EPOCH};
impl PlotApp {
pub fn for_testing(model: PlotModel) -> Self {
use super::super::controller_buttons_and_panels::{
NewExperimentDialogState, QuickActionPanelState,
};
use super::super::controller_filters::Mathematics;
use super::super::kitheplot_wrapper::KiThePlotWindowState;
Self {
model,
quick_actions_state: QuickActionPanelState::default(),
new_experiment_dialog: NewExperimentDialogState::new(),
mathematics: Mathematics::new(),
test_options: TestOptions::new(),
column_manager_state: ColumnManagerState::new(),
kithe_plot_window: KiThePlotWindowState::default(),
kinetic_methods_state: KineticMethodsWindowState::default(),
direct_problem_state: DirectProblemDialogState::new(),
}
}
}
fn with_plot_app_harness(mut app: PlotApp, mut run_test: impl FnMut(&mut Harness)) {
let mut open = true;
let mut harness = Harness::new_ui(move |ui| {
app.show(ui.ctx(), &mut open);
});
run_test(&mut harness);
}
struct PlotAppTestState {
app: PlotApp,
open: bool,
}
fn with_plot_app_state_harness(
app: PlotApp,
mut run_test: impl FnMut(&mut Harness<PlotAppTestState>),
) {
let state = PlotAppTestState { app, open: true };
let mut harness = Harness::new_ui_state(
|ui, state: &mut PlotAppTestState| {
state.app.show(ui.ctx(), &mut state.open);
},
state,
);
run_test(&mut harness);
}
fn click_menu_entry<State>(harness: &mut Harness<State>, menu_label: &str, entry_label: &str) {
harness.get_by_label(menu_label).click();
harness.run();
harness.get_by_label(entry_label).click();
harness.run();
}
fn assert_menu_entry_clickable(menu_label: &str, entry_label: &str) {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_harness(app, |harness| {
harness.run();
click_menu_entry(harness, menu_label, entry_label);
});
}
fn unique_temp_dir(prefix: &str) -> PathBuf {
let stamp = SystemTime::now()
.duration_since(UNIX_EPOCH)
.expect("system clock before UNIX_EPOCH")
.as_nanos();
std::env::temp_dir().join(format!("kithe_{prefix}_{stamp}"))
}
fn non_isothermal_series_config() -> AdvancedTGAConfig {
AdvancedTGAConfig {
n_points: 180,
dt: 0.5,
experiment_mode: ExperimentMode::NonIsothermal {
t0: 300.0,
heating_rates: vec![2.0, 5.0, 10.0],
},
kinetic_model: KineticModel::ArrheniusSingle {
m0: 100.0,
k0: 2.5e5,
e: 85000.0,
r: 8.314,
},
mass_noise: Some(NoiseConfig {
kind: NoiseKind::Gaussian { sigma: 0.0 },
}),
temp_noise: Some(NoiseConfig {
kind: NoiseKind::Gaussian { sigma: 0.0 },
}),
spikes: None,
seed: 42,
}
}
fn last_value_for(model: &PlotModel, id: &str, column: &str) -> f64 {
let exp = model
.series
.get_experiment_by_id(id)
.expect("experiment not found");
let df = exp
.dataset
.frame
.clone()
.collect()
.expect("failed to collect frame");
df.column(column)
.expect("requested column missing")
.f64()
.expect("requested column is not f64")
.into_no_null_iter()
.last()
.expect("requested column is empty")
}
#[test]
fn tier1_smoke_renders_core_widgets() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_harness(app, |harness| {
harness.run();
harness.get_by_label("Data Plot");
harness.get_by_label("TGA Plot Information");
harness.get_by_label("File Manager");
harness.get_by_label("Math");
harness.get_by_label("Kinetic Methods");
harness.get_by_label("Direct Problem");
harness.get_by_label("Test Options");
harness.get_by_label("Manage Plots");
harness.get_by_label("Clear Selected");
harness.get_by_label("Reset View");
harness.get_by_label("Refresh");
harness.get_by_label("Zoom To Selection");
harness.get_by_label("Input value:");
harness.get_by_label("output column:");
harness.get_by_label("X");
harness.get_by_label("Y");
harness.get_by_label("Add");
harness.get_by_label("Sub");
harness.get_by_label("Mul");
harness.get_by_label("Divide");
harness.get_by_label("ln");
harness.get_by_label("exp");
});
}
#[test]
fn tier1_smoke_top_menu_buttons_and_entries_clickable() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_harness(app, |harness| {
harness.run();
for label in [
"File Manager",
"Math",
"Kinetic Methods",
"Direct Problem",
"Test Options",
] {
harness.get_by_label(label).click();
harness.run();
}
});
assert_menu_entry_clickable("File Manager", "Import Data");
}
#[test]
fn tier1_smoke_quick_actions_and_arithmetic_clickable() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_harness(app, |harness| {
harness.run();
for label in [
"Manage Plots",
"Clear Selected",
"Reset View",
"Refresh",
"Zoom To Selection",
"move time to zero",
"Delete Plot",
"calc relative mass",
"calc conversion",
"from mV to mg",
"from s to h",
] {
harness.get_by_label(label).click();
harness.run();
}
harness.get_by_label("X").click();
harness.run();
harness.get_by_label("Y").click();
harness.run();
for label in ["Divide", "Sub", "Mul", "Add", "ln", "exp"] {
harness.get_by_label(label).click();
harness.run();
}
});
}
#[test]
fn tier2_file_manager_new_experiment_sets_dialog_open() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_state_harness(app, |harness| {
harness.run();
click_menu_entry(harness, "File Manager", "New experiment");
assert!(harness.state().app.new_experiment_dialog.open);
});
}
#[test]
fn tier2_file_manager_manage_plot_sets_dialog_open() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_state_harness(app, |harness| {
harness.run();
click_menu_entry(harness, "File Manager", "Manage Plot");
assert!(harness.state().app.new_experiment_dialog.manage_plot_open);
});
}
#[test]
fn tier2_kinetic_methods_fit_model_window_renders_controls() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_state_harness(app, |harness| {
harness.run();
click_menu_entry(harness, "Kinetic Methods", "Fit Model");
assert!(harness.state().app.kinetic_methods_state.fit_model_open);
harness.get_by_label("Experiment:");
harness.get_by_label("X:");
harness.get_by_label("Y:");
harness.get_by_label("Model:");
harness.get_by_label("Formula:");
harness.get_by_label("Model equations");
harness.get_by_label("DecExp");
harness.get_by_label("Output column:");
harness.get_by_label("Tolerance:");
harness.get_by_label("Max iter:");
harness.get_by_label("Initial guess:");
harness.get_by_label("Fit");
{
let state = harness.state_mut();
state.app.kinetic_methods_state.fit_model_model =
FittingModelName::Polynom { n: 4 };
state.app.kinetic_methods_state.fit_model_polynomial_degree = 4;
}
harness.run();
harness.get_by_label("Polynomial degree:");
});
}
#[test]
fn tier2_quick_action_manage_plots_opens_manage_plot_dialog() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_state_harness(app, |harness| {
harness.run();
harness.get_by_label("Manage Plots").click();
harness.run();
assert!(harness.state().app.new_experiment_dialog.manage_plot_open);
});
}
#[test]
fn tier2_quick_action_clear_selected_clears_selection_state() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_state_harness(app, |harness| {
{
let state = harness.state_mut();
state.app.model.select_curve(0);
state.app.model.start_selection([0.0, 0.0]);
state.app.model.update_selection([1.0, 1.0]);
state.app.model.end_selection();
}
harness.run();
harness.get_by_label("Clear Selected").click();
harness.run();
let model = &mut harness.state_mut().app.model;
model.clear_selection_rect();
model.clear_selection();
assert!(model.interaction.selection_rect.is_none());
assert!(!model.interaction.is_selecting);
assert_eq!(model.get_selected_curve_index(), None);
});
}
#[test]
fn source_selection_remaps_after_column_transformation() {
let mut model = load_model_with_one_curve();
model.select_curve(0);
let exp_id = model.list_of_experiments()[0].clone();
model.set_active_mass_source_for_experiment(&exp_id, "m");
model.set_active_temperature_source_for_experiment(&exp_id, "T");
model
.sg_filter_column_for_selected_as("m", 5, 2, 0, 1.0, Some("m_sg"))
.expect("mass smoothing should succeed");
model
.sg_filter_column_for_selected_as("T", 5, 2, 0, 1.0, Some("T_sg"))
.expect("temperature smoothing should succeed");
assert_eq!(
model
.active_mass_source_for_experiment(&exp_id)
.expect("mass source should be tracked"),
"m_sg"
);
assert_eq!(
model
.active_temperature_source_for_experiment(&exp_id)
.expect("temperature source should be tracked"),
"T_sg"
);
}
#[test]
fn cut_before_time_rebuilds_selected_curve() {
let mut model = load_model_with_one_curve();
model.select_curve(0);
let selected = model.get_selected_curve_index().unwrap();
let before_points = model.plots[selected].points.clone();
let before_first = before_points.first().unwrap()[0];
let cut_time = before_points[before_points.len() / 2][0];
model
.cut_before_time_for_selected(cut_time)
.expect("cut_before_time_for_selected should succeed");
let selected = model.get_selected_curve_index().unwrap();
let after_points = &model.plots[selected].points;
assert!(!after_points.is_empty());
assert!(after_points.first().unwrap()[0] > before_first + 1e-9);
assert!(after_points.first().unwrap()[0] >= cut_time - 1e-9);
}
#[test]
fn cut_selected_rebuilds_selected_curve() {
let mut model = load_model_with_one_curve();
model.select_curve(0);
let selected = model.get_selected_curve_index().unwrap();
let points = model.plots[selected].points.clone();
let x_min = points[points.len() / 3][0];
let x_max = points[points.len() * 2 / 3][0];
let y_min = points.iter().map(|p| p[1]).fold(f64::INFINITY, f64::min);
let y_max = points
.iter()
.map(|p| p[1])
.fold(f64::NEG_INFINITY, f64::max);
model.interaction.selection_rect = Some(SelectionRect::new([x_min, y_min], [x_max, y_max]));
model
.cut_range_x_or_y_for_selected(XY::X)
.expect("cut_range_x_or_y_for_selected should succeed");
let selected = model.get_selected_curve_index().unwrap();
let after_points = &model.plots[selected].points;
assert!(!after_points.is_empty());
assert!(after_points.len() < points.len());
assert!(
after_points
.iter()
.all(|p| p[0] <= x_min + 1e-9 || p[0] >= x_max - 1e-9)
);
}
#[test]
fn from_s_to_h_is_idempotent_and_reports_already_converted() {
let mut model = load_model_with_one_curve();
model.select_curve(0);
let exp_id = model.list_of_experiments()[0].clone();
model.from_s_to_h_of_selected().expect("first conversion");
let after_first = model
.series
.get_experiment_by_id(&exp_id)
.unwrap()
.dataset
.frame
.clone()
.collect()
.unwrap();
let first_time: Vec<f64> = after_first
.column("t")
.unwrap()
.f64()
.unwrap()
.into_no_null_iter()
.collect();
model
.from_s_to_h_of_selected()
.expect("second conversion should be ignored");
assert!(
model
.message
.contains("Time is already in hours; conversion skipped.")
);
let after = model
.series
.get_experiment_by_id(&exp_id)
.unwrap()
.dataset
.frame
.clone()
.collect()
.unwrap();
let after_time: Vec<f64> = after
.column("t")
.unwrap()
.f64()
.unwrap()
.into_no_null_iter()
.collect();
assert_eq!(first_time, after_time);
}
#[test]
fn from_c_to_k_is_idempotent_and_reports_already_converted() {
let mut model = load_model_with_two_curves();
model.select_curve(0);
let exp_id = model.list_of_experiments()[0].clone();
model
.from_C_to_K_of_selected()
.expect("first conversion should succeed");
let after_first = model
.series
.get_experiment_by_id(&exp_id)
.unwrap()
.dataset
.frame
.clone()
.collect()
.unwrap();
let first_temp: Vec<f64> = after_first
.column("T")
.unwrap()
.f64()
.unwrap()
.into_no_null_iter()
.collect();
let first_curves = model
.plots
.iter()
.filter(|curve| curve.experiment_id == exp_id)
.map(|curve| (curve.plot_short_name.clone(), curve.points.clone()))
.collect::<Vec<_>>();
model
.from_C_to_K_of_selected()
.expect("second conversion should be ignored");
assert!(
model
.message
.contains("Temperature is already in Kelvin; conversion skipped.")
);
let after = model
.series
.get_experiment_by_id(&exp_id)
.unwrap()
.dataset
.frame
.clone()
.collect()
.unwrap();
let after_temp: Vec<f64> = after
.column("T")
.unwrap()
.f64()
.unwrap()
.into_no_null_iter()
.collect();
assert_eq!(first_temp, after_temp);
let after_curves = model
.plots
.iter()
.filter(|curve| curve.experiment_id == exp_id)
.map(|curve| (curve.plot_short_name.clone(), curve.points.clone()))
.collect::<Vec<_>>();
assert_eq!(first_curves.len(), after_curves.len());
assert_eq!(first_curves, after_curves);
}
#[test]
fn from_mv_to_mg_is_idempotent_and_reports_already_converted() {
let mut model = load_model_with_two_curves();
model.select_curve(0);
let exp_id = model.list_of_experiments()[0].clone();
model
.calibrate_mass_from_voltage_for_selected()
.expect("first mass conversion should succeed");
let after_first = model
.series
.get_experiment_by_id(&exp_id)
.unwrap()
.dataset
.frame
.clone()
.collect()
.unwrap();
let first_mass: Vec<f64> = after_first
.column("m")
.unwrap()
.f64()
.unwrap()
.into_no_null_iter()
.collect();
let first_curves = model
.plots
.iter()
.filter(|curve| curve.experiment_id == exp_id)
.map(|curve| (curve.plot_short_name.clone(), curve.points.clone()))
.collect::<Vec<_>>();
model
.calibrate_mass_from_voltage_for_selected()
.expect("second mass conversion should be ignored");
assert!(
model
.message
.contains("Mass is already in milligrams; conversion skipped.")
);
let after = model
.series
.get_experiment_by_id(&exp_id)
.unwrap()
.dataset
.frame
.clone()
.collect()
.unwrap();
let after_mass: Vec<f64> = after
.column("m")
.unwrap()
.f64()
.unwrap()
.into_no_null_iter()
.collect();
assert_eq!(first_mass, after_mass);
let after_curves = model
.plots
.iter()
.filter(|curve| curve.experiment_id == exp_id)
.map(|curve| (curve.plot_short_name.clone(), curve.points.clone()))
.collect::<Vec<_>>();
assert_eq!(first_curves.len(), after_curves.len());
assert_eq!(first_curves, after_curves);
}
#[test]
fn from_s_to_h_rebuilds_all_curves_of_the_experiment() {
let mut model = load_model_with_two_curves();
let exp_id = model.list_of_experiments()[0].clone();
let before_t = model
.plots
.iter()
.filter(|curve| curve.experiment_id == exp_id)
.map(|curve| (curve.plot_short_name.clone(), curve.points.clone()))
.collect::<Vec<_>>();
assert_eq!(before_t.len(), 2);
model
.from_s_to_h_for_experiment(&exp_id)
.expect("time conversion should succeed");
let after = model
.plots
.iter()
.filter(|curve| curve.experiment_id == exp_id)
.map(|curve| (curve.plot_short_name.clone(), curve.points.clone()))
.collect::<Vec<_>>();
assert_eq!(after.len(), 2);
for (name, before_points) in before_t {
let after_points = after
.iter()
.find(|(short_name, _)| short_name == &name)
.expect("curve missing after rebuild")
.1
.clone();
assert_eq!(before_points.len(), after_points.len());
for (before, after) in before_points.iter().zip(after_points.iter()) {
assert!((after[0] - before[0] / 3600.0).abs() < 1e-12);
}
}
}
#[test]
fn move_time_to_zero_rebuilds_all_curves_of_the_experiment() {
let mut model = load_model_with_two_curves();
let exp_id = model.list_of_experiments()[0].clone();
model
.move_time_to_zero_for_experiment(&exp_id)
.expect("move_time_to_zero should succeed");
let curves = model
.plots
.iter()
.filter(|curve| curve.experiment_id == exp_id)
.collect::<Vec<_>>();
assert_eq!(curves.len(), 2);
for curve in curves {
assert!(!curve.points.is_empty());
assert!(curve.points.first().unwrap()[0].abs() < 1e-9);
}
}
#[test]
fn relative_mass_zero_baseline_reports_warning() {
let mut model = load_model_with_one_curve();
model.select_curve(0);
model
.relative_mass_for_selected(0.0, None)
.expect("zero baseline should be handled as a warning");
assert!(
model
.message
.contains("Baseline end time must be greater than 0.0")
);
}
#[test]
fn conversion_zero_baseline_reports_warning() {
let mut model = load_model_with_one_curve();
model.select_curve(0);
model
.conversion_for_selected(0.0, None)
.expect("zero baseline should be handled as a warning");
assert!(
model
.message
.contains("Baseline end time must be greater than 0.0")
);
}
#[test]
fn tier2_quick_action_reset_view_sets_reset_request() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_state_harness(app, |harness| {
{
let model = &mut harness.state_mut().app.model;
model.interaction.view_range = (123.0, 456.0);
model.interaction.view_y_range = (-789.0, -700.0);
}
harness.run();
harness.get_by_label("Reset View").click();
harness.run();
assert!(harness.state().app.model.reset_view_requested);
});
}
#[test]
fn tier2_quick_action_zoom_to_selection_updates_view() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_state_harness(app, |harness| {
{
let model = &mut harness.state_mut().app.model;
model.start_selection([1.0, 2.0]);
model.update_selection([3.0, 6.0]);
model.end_selection();
model.interaction.view_range = (0.0, 100.0);
model.interaction.view_y_range = (0.0, 100.0);
}
harness.run();
harness.get_by_label("Zoom To Selection").click();
harness.run();
let model = &harness.state().app.model;
assert_eq!(model.interaction.view_range, (0.9, 3.1));
assert_eq!(model.interaction.view_y_range, (1.8, 6.2));
});
}
#[test]
fn tier2_quick_action_delete_plot_removes_selected_curve() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_state_harness(app, |harness| {
{
let model = &mut harness.state_mut().app.model;
model.select_curve(0);
}
harness.run();
let before = harness.state().app.model.plots.len();
harness.get_by_label("Delete Plot").click();
harness.run();
let model = &harness.state().app.model;
assert_eq!(model.plots.len(), before.saturating_sub(1));
});
}
#[test]
fn tier2_quick_action_xy_buttons_change_axis_mode() {
let app = PlotApp::for_testing(load_model_with_one_curve());
with_plot_app_state_harness(app, |harness| {
harness.run();
harness.get_by_label("X").click();
harness.run();
assert_eq!(harness.state().app.quick_actions_state.x_or_y, Some(XY::X));
harness.get_by_label("Y").click();
harness.run();
assert_eq!(harness.state().app.quick_actions_state.x_or_y, Some(XY::Y));
});
}
#[test]
fn test_plotmodel_new_and_default() {
let model1 = PlotModel::new();
let model2 = PlotModel::default();
assert_eq!(model1.plots.len(), 0);
assert_eq!(model2.plots.len(), 0);
assert!(!model1.reset_view_requested);
assert!(!model2.reset_view_requested);
}
#[test]
fn test_experiment_loading() {
let mut model = PlotModel::new();
let test_file = Path::new("src/assets/TGAexample.txt");
let result = model.push_from_file(test_file);
assert!(
result.is_ok(),
"Failed to load test data: {:?}",
result.err()
);
let experiments = model.list_of_experiments();
assert!(!experiments.is_empty(), "No experiments loaded");
}
#[test]
fn test_column_binding_operations() {
use crate::Kinetics::experimental_kinetics::one_experiment_dataset::Unit;
let mut model = PlotModel::new();
let test_file = Path::new("src/assets/TGAexample.txt");
model.push_from_file(test_file).unwrap();
let experiments = model.list_of_experiments();
let exp_id = &experiments[0];
let bind_time = model.bind_time_of_experiment(exp_id, "t", Unit::Second);
assert!(bind_time.is_ok());
let bind_temp = model.bind_temperature_of_experiment(exp_id, "T", Unit::Celsius);
assert!(bind_temp.is_ok());
let bind_mass = model.bind_mass_of_experiment(exp_id, "m", Unit::MilliVolt);
assert!(bind_mass.is_ok());
}
#[test]
fn test_data_transformations() {
use crate::Kinetics::experimental_kinetics::one_experiment_dataset::Unit;
let mut model = PlotModel::new();
let test_file = Path::new("src/assets/TGAexample.txt");
model.push_from_file(test_file).unwrap();
let experiments = model.list_of_experiments();
let exp_id = &experiments[0];
model
.bind_time_of_experiment(exp_id, "t", Unit::Second)
.unwrap();
model
.bind_temperature_of_experiment(exp_id, "T", Unit::Celsius)
.unwrap();
model
.bind_mass_of_experiment(exp_id, "m", Unit::MilliVolt)
.unwrap();
model.set_x(exp_id, "t").unwrap();
model.set_y(exp_id, "T").unwrap();
let result = model.from_C_to_K_for_experiment(exp_id);
assert!(result.is_ok());
model.set_x(exp_id, "t").unwrap();
model.set_y(exp_id, "m").unwrap();
let result = model.from_s_to_h_for_experiment(exp_id);
assert!(result.is_ok());
}
#[test]
fn test_curve_creation() {
use crate::Kinetics::experimental_kinetics::one_experiment_dataset::Unit;
let mut model = PlotModel::new();
let test_file = Path::new("src/assets/TGAexample.txt");
model.push_from_file(test_file).unwrap();
let experiments = model.list_of_experiments();
let exp_id = &experiments[0];
model
.bind_time_of_experiment(exp_id, "t", Unit::Second)
.unwrap();
model
.bind_temperature_of_experiment(exp_id, "T", Unit::Celsius)
.unwrap();
model.set_x(exp_id, "t").unwrap();
model.set_y(exp_id, "T").unwrap();
let result = model.create_points_for_curve(exp_id);
assert!(result.is_ok());
assert!(!model.plots.is_empty());
}
#[test]
fn test_select_curve() {
let mut model = PlotModel::new();
use super::super::model::PlotCurve;
let curve1 = PlotCurve::default();
let curve2 = PlotCurve::default();
model.plots.push(curve1);
model.plots.push(curve2);
model.select_curve(0);
assert!(model.plots[0].selected);
assert!(!model.plots[1].selected);
model.select_curve(1);
assert!(!model.plots[0].selected);
assert!(model.plots[1].selected);
}
#[test]
fn test_clear_selection() {
let mut model = PlotModel::new();
use super::super::model::PlotCurve;
let mut curve1 = PlotCurve::default();
curve1.selected = true;
model.plots.push(curve1);
model.clear_selection();
assert!(!model.plots[0].selected);
}
#[test]
fn test_get_selected_curve_index() {
let mut model = PlotModel::new();
use super::super::model::PlotCurve;
let curve1 = PlotCurve::default();
let mut curve2 = PlotCurve::default();
curve2.selected = true;
model.plots.push(curve1);
model.plots.push(curve2);
let selected = model.get_selected_curve_index();
assert_eq!(selected, Some(1));
}
#[test]
fn test_start_selection() {
let mut model = PlotModel::new();
let start_point = [1.0, 2.0];
model.start_selection(start_point);
assert!(model.interaction.is_selecting);
assert_eq!(model.interaction.selection_start, Some(start_point));
}
#[test]
fn test_update_pan() {
let mut model = PlotModel::new();
let initial_range = model.interaction.view_range;
model.interaction.start_pan([0.0, 0.0]);
model.update_pan([1.0, 1.0]);
assert_ne!(model.interaction.view_range, initial_range);
}
#[test]
fn test_zoom() {
let mut model = PlotModel::new();
let initial_range = model.interaction.view_range;
model.zoom(2.0, [5.0, 5.0]);
let new_range = model.interaction.view_range;
let initial_width = initial_range.1 - initial_range.0;
let new_width = new_range.1 - new_range.0;
assert!(new_width < initial_width);
}
#[test]
fn test_reset_view() {
let mut model = PlotModel::new();
model.interaction.view_range = (100.0, 200.0);
model.reset_view();
assert!(model.reset_view_requested);
}
#[test]
fn test_find_nearest_curve() {
use super::super::model::PlotCurve;
let mut model = PlotModel::new();
let mut curve = PlotCurve::default();
curve.points = vec![[0.0, 0.0], [1.0, 1.0], [2.0, 2.0]];
curve.shown = true;
curve.ranges.x_min = 0.0;
curve.ranges.x_max = 2.0;
curve.ranges.y_min = 0.0;
curve.ranges.y_max = 2.0;
model.plots.push(curve);
let nearest = model.find_nearest_curve([0.1, 0.1]);
assert_eq!(nearest, Some(0));
}
#[test]
fn tier3_test_options_load_testing_data_imports_fixture() {
let app = PlotApp::for_testing(PlotModel::new());
with_plot_app_state_harness(app, |harness| {
harness.run();
click_menu_entry(harness, "Test Options", "Load testing data");
let ids = harness.state().app.model.list_of_experiments();
assert!(
!ids.is_empty(),
"Load testing data should populate the model"
);
});
}
#[test]
fn tier3_bind_preprocess_and_export_roundtrip_via_gui_dialog() {
let app = PlotApp::for_testing(PlotModel::new());
with_plot_app_state_harness(app, |harness| {
harness
.state_mut()
.app
.model
.push_from_file(test_data_path())
.expect("failed to import raw fixture");
let id = harness.state().app.model.list_of_experiments()[0].clone();
{
let model = &mut harness.state_mut().app.model;
model
.bind_time_of_experiment(&id, "t", Unit::Second)
.expect("failed to bind time");
model
.bind_temperature_of_experiment(&id, "T", Unit::Celsius)
.expect("failed to bind temperature");
model
.bind_mass_of_experiment(&id, "m", Unit::MilliVolt)
.expect("failed to bind mass");
model.set_x(&id, "t").expect("failed to set x");
model.set_y(&id, "T").expect("failed to set y");
model
.create_points_for_curve(&id)
.expect("failed to create plot");
model
.from_C_to_K_for_experiment(&id)
.expect("failed to convert temperature unit");
model
.from_s_to_h_for_experiment(&id)
.expect("failed to convert time unit");
}
click_menu_entry(harness, "File Manager", "Save As CSV");
let export_dir = unique_temp_dir("gui_export");
fs::create_dir_all(&export_dir).expect("failed to create export dir");
harness.state_mut().app.new_experiment_dialog.current_dir = export_dir.clone();
harness.run();
harness.get_by_label("Save").click();
harness.run();
let export_file = export_dir.join("series_export.csv");
assert!(export_file.exists(), "expected exported CSV to be created");
let roundtrip = {
let mut model = PlotModel::new();
model
.from_csv_series(&export_file)
.expect("failed to reload exported CSV");
model
};
assert_eq!(roundtrip.list_of_experiments().len(), 1);
let roundtrip_id = roundtrip.list_of_experiments()[0].clone();
let cols = roundtrip
.list_of_columns(&roundtrip_id)
.expect("roundtrip columns unavailable");
assert!(cols.contains(&"t".to_string()));
assert!(cols.contains(&"T".to_string()));
assert!(cols.contains(&"m".to_string()));
let _ = fs::remove_file(&export_file);
let _ = fs::remove_dir_all(&export_dir);
});
}
#[test]
fn tier3_create_experiment_from_columns_keeps_series_consistent() {
let mut model = load_model_with_one_curve();
let source_id = model.list_of_experiments()[0].clone();
let before = model.list_of_experiments().len();
let derived_id = format!("{source_id}_core");
model
.create_experiment_from_columns_for_experiment(
&source_id,
derived_id.clone(),
&["t", "T", "m"],
)
.expect("failed to create reduced experiment");
let ids = model.list_of_experiments();
assert_eq!(ids.len(), before + 1);
assert!(ids.contains(&derived_id));
let cols = model
.list_of_columns(&derived_id)
.expect("derived experiment columns unavailable");
assert_eq!(cols.len(), 3);
assert!(cols.contains(&"t".to_string()));
assert!(cols.contains(&"T".to_string()));
assert!(cols.contains(&"m".to_string()));
}
#[test]
fn tier3_build_kinetic_view_and_run_ofw_on_preprocessed_series() {
let mut model = PlotModel::new();
model
.generate_synthetic_data_from_config(&non_isothermal_series_config())
.expect("failed to generate synthetic series");
let ids = model.list_of_experiments();
assert_eq!(ids.len(), 3);
for id in &ids {
let t_end = last_value_for(&model, id, "time");
model
.conversion_for_experiment(id.clone(), t_end, "eta")
.expect("failed to derive conversion");
model
.derive_deta_dt(id, Some("deta_dt"))
.expect("failed to derive conversion rate");
}
let method = IsoconversionalKineticMethod {
method: IsoconversionalMethod::OFW,
};
let view = model
.create_kinetic_data_view_for_method(None, &method.method)
.expect("failed to build kinetic data view");
assert_eq!(view.experiments.len(), 3);
let result = method.compute(&view).expect("OFW computation failed");
assert!(
!result.layers.is_empty(),
"OFW should produce at least one α-layer"
);
}
}