#![allow(clippy::panic)]
use oxigdal_gpu::{BandExpression, BandMathError, band_expression_to_wgsl, parse_band_expression};
#[test]
fn test_parse_simple_band_diff() {
let expr = parse_band_expression("B2-B1").expect("parse");
match expr {
BandExpression::Sub(a, b) => {
assert!(matches!(*a, BandExpression::Band(1)));
assert!(matches!(*b, BandExpression::Band(0)));
}
other => panic!("expected Sub, got {:?}", other),
}
}
#[test]
fn test_parse_ndvi_formula() {
let expr = parse_band_expression("(B4-B3)/(B4+B3)").expect("parse");
let b3 = 0.2_f32;
let b4 = 0.6_f32;
let bands = vec![0.0_f32, 0.0, b3, b4];
let got = expr.evaluate(&bands).expect("eval");
let want = (b4 - b3) / (b4 + b3);
assert!((got - want).abs() < 1e-6, "got {}, want {}", got, want);
}
#[test]
fn test_parse_nested_parens() {
let expr = parse_band_expression("((B1+B2)*B3)").expect("parse");
let bands = vec![1.0_f32, 2.0, 4.0];
let got = expr.evaluate(&bands).expect("eval");
assert!((got - 12.0).abs() < 1e-6);
}
#[test]
fn test_parse_unary_minus() {
let e1 = parse_band_expression("-B1").expect("parse");
assert!((e1.evaluate(&[3.5]).expect("eval") + 3.5).abs() < 1e-6);
let e2 = parse_band_expression("-(B1+B2)").expect("parse");
assert!((e2.evaluate(&[2.0, 3.0]).expect("eval") + 5.0).abs() < 1e-6);
}
#[test]
fn test_parse_function_call_log() {
let expr = parse_band_expression("log(B5)").expect("parse");
let bands = vec![0.0_f32, 0.0, 0.0, 0.0, std::f32::consts::E];
let got = expr.evaluate(&bands).expect("eval");
assert!((got - 1.0).abs() < 1e-6, "got {}", got);
}
#[test]
fn test_parse_function_call_clamp_three_args() {
let expr = parse_band_expression("clamp(B1, 0.0, 1.0)").expect("parse");
assert!((expr.evaluate(&[-0.5]).expect("eval") - 0.0).abs() < 1e-6);
assert!((expr.evaluate(&[0.5]).expect("eval") - 0.5).abs() < 1e-6);
assert!((expr.evaluate(&[1.5]).expect("eval") - 1.0).abs() < 1e-6);
}
#[test]
fn test_parse_invalid_band_index_errors() {
match parse_band_expression("B0") {
Err(BandMathError::InvalidBandIndex(0)) => {}
other => panic!("expected InvalidBandIndex(0), got {:?}", other),
}
match parse_band_expression("B1000") {
Err(BandMathError::InvalidBandIndex(1000)) => {}
other => panic!("expected InvalidBandIndex(1000), got {:?}", other),
}
}
#[test]
fn test_parse_unknown_function_errors() {
match parse_band_expression("foobar(B1)") {
Err(BandMathError::UnknownFunction(name)) => assert_eq!(name, "foobar"),
other => panic!("expected UnknownFunction, got {:?}", other),
}
}
#[test]
fn test_parse_trailing_tokens_errors() {
match parse_band_expression("B1 + B2 B3") {
Err(BandMathError::TrailingTokens(_)) => {}
other => panic!("expected TrailingTokens, got {:?}", other),
}
}
#[test]
fn test_codegen_emits_band_binding() {
let expr = parse_band_expression("B1 + B2").expect("parse");
let shader = band_expression_to_wgsl(&expr, &[0, 1, 2]);
assert!(
shader.contains("band_1: array<f32>"),
"shader missing band_1: {}",
shader
);
assert!(
shader.contains("band_2: array<f32>"),
"shader missing band_2: {}",
shader
);
assert!(shader.contains("output: array<f32>"));
assert!(shader.contains("@compute"));
assert!(shader.contains("@workgroup_size(64)"));
}
#[test]
fn test_codegen_constant_folds() {
let expr = parse_band_expression("2.0 + 3.0").expect("parse");
let shader = band_expression_to_wgsl(&expr, &[]);
assert!(
shader.contains("output[idx] = 5.0;") || shader.contains("output[idx] = 5;"),
"shader did not constant-fold: {}",
shader
);
assert!(
!shader.contains("(2.0 + 3.0)") && !shader.contains("(2 + 3)"),
"shader still has unfolded literals: {}",
shader
);
}
#[test]
fn test_codegen_round_trip_evaluates_same_as_ast() {
let parsed = parse_band_expression("(B1+B2)*B3 - sqrt(B4)").expect("parse");
let hand = BandExpression::Sub(
Box::new(BandExpression::Mul(
Box::new(BandExpression::Add(
Box::new(BandExpression::Band(0)),
Box::new(BandExpression::Band(1)),
)),
Box::new(BandExpression::Band(2)),
)),
Box::new(BandExpression::Sqrt(Box::new(BandExpression::Band(3)))),
);
for sample in &[
[1.0_f32, 2.0, 3.0, 4.0],
[0.5_f32, -1.5, 2.0, 9.0],
[10.0_f32, 0.0, 0.5, 16.0],
] {
let want = hand.evaluate(sample).expect("eval hand");
let got = parsed.evaluate(sample).expect("eval parsed");
assert!(
(got - want).abs() < 1e-5,
"parsed != hand: got {}, want {} (sample {:?})",
got,
want,
sample
);
}
}
#[test]
fn test_pow_operator_and_function_equivalent() {
let by_op = parse_band_expression("B1 ^ 3.0").expect("parse op");
let by_fn = parse_band_expression("pow(B1, 3.0)").expect("parse fn");
let got_op = by_op.evaluate(&[2.0]).expect("eval");
let got_fn = by_fn.evaluate(&[2.0]).expect("eval");
assert!((got_op - 8.0).abs() < 1e-6);
assert!((got_fn - 8.0).abs() < 1e-6);
let shader = band_expression_to_wgsl(&by_op, &[0]);
assert!(
shader.contains("pow("),
"shader must use pow(), got: {}",
shader
);
assert!(!shader.contains(" ^ "), "shader must not contain '^'");
}
#[test]
fn test_div_by_literal_zero_errors() {
match parse_band_expression("B1 / 0.0") {
Err(BandMathError::DivByConstantZero) => {}
other => panic!("expected DivByConstantZero, got {:?}", other),
}
}
#[test]
fn test_min_max_parse_and_eval() {
let expr = parse_band_expression("max(B1, B2) - min(B1, B2)").expect("parse");
let got = expr.evaluate(&[3.0, 7.0]).expect("eval");
assert!((got - 4.0).abs() < 1e-6);
}
#[test]
fn test_empty_input_errors() {
assert!(matches!(
parse_band_expression(""),
Err(BandMathError::EmptyExpression)
));
assert!(matches!(
parse_band_expression(" "),
Err(BandMathError::EmptyExpression)
));
}