tease 0.5.2

Build interactive desktop demos for Rust functions and machine-learning models
Documentation
use std::sync::{
    atomic::{AtomicUsize, Ordering},
    Arc,
};
use tease::{Choice, Error, Input, Output, Teaser, Value};

fn bounded() -> Input {
    Input::Number {
        label: Some("Temperature".into()),
        initial_value: 20.0,
        min: Some(0.0),
        max: Some(100.0),
    }
}
fn slider(min: f64, max: f64, step: f64, initial_value: f64) -> Input {
    Input::Slider {
        label: Some("Gain".into()),
        min,
        max,
        step,
        initial_value,
    }
}
fn dropdown() -> Input {
    Input::Dropdown {
        label: Some("Material".into()),
        initial_value: 0,
        options: vec![
            Choice::text("Steel", "steel"),
            Choice::text("Aluminum", "aluminum"),
        ],
    }
}

#[test]
fn default_and_both_numeric_callback_styles_work() {
    assert_eq!(
        Teaser::default()
            .prepare()
            .unwrap()
            .predict(&[Value::Number(7.0)])
            .unwrap(),
        [Value::Number(7.0)]
    );
    fn sum(values: Vec<f64>) -> f64 {
        values.iter().sum()
    }
    for demo in [
        Teaser::default()
            .with_inputs(vec![Input::default(); 2])
            .with_function(sum),
        Teaser::default()
            .with_inputs(vec![Input::default(); 2])
            .with_function(|values| values.iter().sum()),
    ] {
        let demo = demo.prepare().unwrap();
        assert_eq!(
            demo.predict(&[Value::Number(2.0), Value::Number(3.0)])
                .unwrap(),
            [Value::Number(5.0)]
        );
    }
}

#[test]
fn typed_predictor_accepts_categories_and_multiple_named_results() {
    let demo = Teaser::default()
        .with_inputs(vec![bounded(), dropdown()])
        .with_outputs(vec![
            Output::text("Material"),
            Output::number("Temperature", 2),
        ])
        .with_predictor(|values| {
            Ok(vec![
                Value::Text(values[1].as_text()?.into()),
                Value::Number(values[0].as_number()?),
            ])
        });
    let demo = demo.prepare().unwrap();
    let expected = [Value::Text("steel".into()), Value::Number(20.0)];
    assert_eq!(
        demo.predict(&[Value::Number(20.0), Value::Text("steel".into())])
            .unwrap(),
        expected
    );
}

#[test]
fn every_callback_setter_replaces_the_previous_one() {
    let demo = Teaser::default()
        .with_function(|_| 1.0)
        .with_predictor(|_| Ok(vec![Value::Number(2.0)]))
        .prepare()
        .unwrap();
    assert_eq!(
        demo.predict(&[Value::Number(0.0)]).unwrap(),
        [Value::Number(2.0)]
    );
    let demo = Teaser::default()
        .with_predictor(|_| Ok(vec![Value::Number(2.0)]))
        .with_function(|_| 3.0)
        .prepare()
        .unwrap();
    assert_eq!(
        demo.predict(&[Value::Number(0.0)]).unwrap(),
        [Value::Number(3.0)]
    );
}

#[test]
fn rejected_inputs_do_not_reach_the_model() {
    let calls = Arc::new(AtomicUsize::new(0));
    let seen = calls.clone();
    let demo = Teaser::default()
        .with_inputs(vec![bounded()])
        .with_function(move |values| {
            seen.fetch_add(1, Ordering::Relaxed);
            values[0]
        });
    let demo = demo.prepare().unwrap();
    for values in [
        vec![],
        vec![Value::Text("20".into())],
        vec![Value::Number(-1.0)],
        vec![Value::Number(101.0)],
        vec![Value::Number(f64::NAN)],
        vec![Value::Number(f64::INFINITY)],
        vec![Value::Number(f64::NEG_INFINITY)],
    ] {
        assert!(matches!(demo.predict(&values), Err(Error::Input { .. })));
    }
    assert_eq!(calls.load(Ordering::Relaxed), 0);
    for boundary in [0.0, 100.0] {
        assert!(demo.predict(&[Value::Number(boundary)]).is_ok());
    }
    assert_eq!(calls.load(Ordering::Relaxed), 2);
}

#[test]
fn prediction_error_can_be_corrected_and_resubmitted() {
    let demo = Teaser::default().with_predictor(|values| {
        let divisor = values[0].as_number()?;
        if divisor == 0.0 {
            Err("Divisor must not be zero.".into())
        } else {
            Ok(vec![Value::Number(10.0 / divisor)])
        }
    });
    let demo = demo.prepare().unwrap();
    assert_eq!(
        demo.predict(&[Value::Number(0.0)]),
        Err(Error::Prediction("Divisor must not be zero.".into()))
    );
    assert_eq!(
        demo.predict(&[Value::Number(2.0)]).unwrap(),
        [Value::Number(5.0)]
    );
}

#[test]
fn wrong_count_type_and_nonfinite_results_are_rejected() {
    for result in [
        vec![],
        vec![Value::Number(1.0); 2],
        vec![Value::Text("1".into())],
        vec![Value::Number(f64::NAN)],
        vec![Value::Number(f64::INFINITY)],
    ] {
        let demo = Teaser::default()
            .with_predictor(move |_| Ok(result.clone()))
            .prepare()
            .unwrap();
        assert!(matches!(
            demo.predict(&[Value::Number(0.0)]),
            Err(Error::Output(_))
        ));
    }
    let demo = Teaser::default()
        .with_output(Output::text("Class"))
        .with_predictor(|_| Ok(vec![Value::Number(1.0)]))
        .prepare()
        .unwrap();
    assert!(matches!(
        demo.predict(&[Value::Number(0.0)]),
        Err(Error::Output(_))
    ));
}

#[test]
fn invalid_component_definitions_are_rejected_before_execution() {
    let cases = vec![
        Input::number("NaN", f64::NAN),
        Input::Number {
            label: None,
            initial_value: 0.0,
            min: Some(2.0),
            max: Some(1.0),
        },
        Input::Number {
            label: None,
            initial_value: 0.0,
            min: None,
            max: Some(f64::INFINITY),
        },
        Input::Number {
            label: None,
            initial_value: 3.0,
            min: Some(0.0),
            max: Some(1.0),
        },
        slider(0.0, 0.0, 1.0, 0.0),
        slider(2.0, 1.0, 1.0, 1.0),
        slider(0.0, 1.0, 0.0, 0.0),
        slider(0.0, 1.0, -1.0, 0.0),
        slider(0.0, 1.0, f64::NAN, 0.0),
        slider(0.0, 1.0, 0.1, 0.05),
        slider(0.0, 1.0, 0.1, 1.1),
        slider(-f64::MAX, f64::MAX, 1.0, 0.0),
        Input::Dropdown {
            label: None,
            options: vec![],
            initial_value: 0,
        },
        Input::Dropdown {
            label: None,
            options: vec![Choice::number("One", 1.0)],
            initial_value: 1,
        },
        Input::Dropdown {
            label: None,
            options: vec![Choice::number("Infinite", f64::INFINITY)],
            initial_value: 0,
        },
    ];
    for input in cases {
        let demo = Teaser::default().with_inputs(vec![input]);
        assert!(matches!(demo.validate(), Err(Error::Configuration(_))));
    }
    assert!(Teaser::default().with_inputs(vec![]).validate().is_err());
    assert!(Teaser::default().with_outputs(vec![]).validate().is_err());
    assert!(Teaser::default()
        .with_output(Output::number("Result", 16))
        .validate()
        .is_err());
}

#[test]
fn configuration_errors_are_returned_before_opening_native_window() {
    assert!(matches!(
        Teaser::default().with_inputs(vec![]).run(),
        Err(Error::Configuration(_))
    ));
}

#[test]
fn numeric_adapter_rejects_text_inputs_and_multiple_outputs() {
    assert!(Teaser::default()
        .with_inputs(vec![dropdown()])
        .validate()
        .is_err());
    assert!(Teaser::default()
        .with_output(Output::text("Class"))
        .validate()
        .is_err());
    assert!(Teaser::default()
        .with_outputs(vec![Output::default(); 2])
        .validate()
        .is_err());
    let demo = Teaser::default()
        .with_inputs(vec![Input::Dropdown {
            label: None,
            initial_value: 0,
            options: vec![Choice::number("Double", 2.0)],
        }])
        .with_function(|values| values[0] * 2.0)
        .prepare()
        .unwrap();
    assert_eq!(
        demo.predict(&[Value::Number(2.0)]).unwrap(),
        [Value::Number(4.0)]
    );
}

#[test]
fn decimal_slider_steps_are_checked_without_rejecting_roundoff() {
    let demo = Teaser::default()
        .with_inputs(vec![slider(0.1, 1.0, 0.1, 0.3)])
        .prepare()
        .unwrap();
    for value in [0.1, 0.3, 0.1 + 0.2, 1.0] {
        assert!(demo.predict(&[Value::Number(value)]).is_ok(), "{value}");
    }
    for value in [0.0, 0.25, 1.1] {
        assert!(matches!(
            demo.predict(&[Value::Number(value)]),
            Err(Error::Input { field: Some(0), .. })
        ));
    }
}

#[test]
fn arbitrary_dropdown_values_are_rejected() {
    let demo = Teaser::default()
        .with_inputs(vec![dropdown()])
        .with_output(Output::text("Material"))
        .with_predictor(|values| Ok(values.to_vec()))
        .prepare()
        .unwrap();
    assert!(matches!(
        demo.predict(&[Value::Text("plastic".into())]),
        Err(Error::Input { field: Some(0), .. })
    ));
}