webots-proto-schema 0.2.0

Versioned schemas, typed nodes, and validation for Webots PROTO.
Documentation
use webots_proto_ast::proto::parser::Parser;
use webots_proto_schema::{Severity, validate};

#[test]
fn test_validate_wrong_field_type() {
    let input = r#"#VRML_SIM R2025a utf8
Robot {
  name 123
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    assert!(diagnostics.has_errors());
    let errors: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Error)
        .collect();
    for err in &errors {
        println!("Error: {}", err.message);
    }
    assert!(!errors.is_empty());
    assert!(errors.iter().any(|e| e.message.contains("Type mismatch")));
}

#[test]
fn test_validate_unknown_field() {
    let input = r#"#VRML_SIM R2025a utf8
Robot {
    unknownField "value"
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    let warnings: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Warning)
        .collect();
    assert!(!warnings.is_empty());
    assert!(warnings[0].message.contains("Unknown field"));
}

#[test]
fn test_validate_unknown_node() {
    let input = r#"#VRML_SIM R2025a utf8
UnknownNode {
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    let warnings: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Warning)
        .collect();
    assert!(!warnings.is_empty());
    assert!(warnings[0].message.contains("Unknown node type"));
}

#[test]
fn test_validate_undefined_is_binding() {
    let input = r#"#VRML_SIM R2025a utf8
PROTO MyProto [
] {
  Robot {
    name IS myName
  }
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    assert!(diagnostics.has_errors());
    let errors: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Error)
        .collect();
    assert!(
        errors
            .iter()
            .any(|e| e.message.contains("Undefined IS reference"))
    );
}

#[test]
fn test_validate_valid_proto() {
    let input = r#"#VRML_SIM R2025a utf8
PROTO MyProto [
  field SFString name "my_robot"
] {
  Robot {
    name IS name
  }
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    assert!(!diagnostics.has_errors());
}

#[test]
fn test_validate_vec3f_sequence() {
    let input = r#"#VRML_SIM R2025a utf8
Robot {
    translation 1 0 0
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    assert!(!diagnostics.has_errors());
}

#[test]
fn test_validate_invalid_vec3f_sequence() {
    let input = r#"#VRML_SIM R2025a utf8
Robot {
    translation 1 0
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    assert!(diagnostics.has_errors());
    let errors: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Error)
        .collect();
    assert!(errors[0].message.contains("Invalid number sequence length"));
}

#[test]
fn test_validate_invalid_use() {
    let input = r#"#VRML_SIM R2025a utf8
Group {
    children [
        USE MY_NODE
    ]
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    assert!(diagnostics.has_errors());
    let errors: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Error)
        .collect();
    assert!(
        errors
            .iter()
            .any(|e| e.message.contains("Undefined USE reference"))
    );
}

#[test]
fn test_validate_valid_def_use() {
    let input = r#"#VRML_SIM R2025a utf8
Group {
    children [
        DEF MY_NODE Shape {
        }
        USE MY_NODE
    ]
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    let errors: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Error)
        .collect();
    if !errors.is_empty() {
        let msg = errors
            .iter()
            .map(|e| format!("{} at {:?}", e.message, e.span))
            .collect::<Vec<_>>()
            .join("\n");
        panic!("Validation failed:\n{}", msg);
    }
}

#[test]
fn test_validate_restrictions_enforced() {
    let input = r#"#VRML_SIM R2025a utf8
PROTO Sample [
  field SFString { "adult", "kid" } size "baby"
] {
  Robot {
    name IS size
  }
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    assert!(diagnostics.has_errors());
    let errors: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Error)
        .collect();
    assert!(
        errors
            .iter()
            .any(|e| e.message.contains("restriction") || e.message.contains("restrictions"))
    );
}

#[test]
fn test_validate_camel_case_fields() {
    let input = r#"#VRML_SIM R2025a utf8
Robot {
    controllerArgs ["arg1" "arg2"]
    customData "payload"
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    let warnings: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Warning)
        .collect();
    assert!(warnings.is_empty());
}

#[test]
fn test_warns_when_solid_physics_has_no_inertia_source() {
    let input = r#"#VRML_SIM R2025a utf8
Solid {
  physics Physics {
    mass 1
  }
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    let warnings: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Warning)
        .collect();
    assert!(
        warnings
            .iter()
            .any(|w| w.message.contains("Undefined inertia matrix"))
    );
}

#[test]
fn test_does_not_warn_when_solid_has_bounding_object() {
    let input = r#"#VRML_SIM R2025a utf8
Solid {
  boundingObject Box {
    size 1 1 1
  }
  physics Physics {
    mass 1
  }
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    let warnings: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Warning)
        .collect();
    assert!(
        !warnings
            .iter()
            .any(|w| w.message.contains("Undefined inertia matrix"))
    );
}

#[test]
fn test_warns_on_duplicate_default_sibling_solid_names() {
    let input = r#"#VRML_SIM R2025a utf8
Solid {
  children [
    HingeJoint {
      endPoint Solid {
      }
    }
    HingeJoint {
      endPoint Solid {
      }
    }
  ]
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    let warnings: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Warning)
        .collect();
    assert!(
        warnings
            .iter()
            .any(|w| w.message.contains("unique among sibling Solid nodes"))
    );
}

#[test]
fn test_warns_when_shape_geometry_contains_cadshape() {
    let input = r#"#VRML_SIM R2025a utf8
Shape {
  geometry CadShape {
    url [ "meshes/robot.obj" ]
  }
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);
    let warnings: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Warning)
        .collect();

    assert_eq!(
        warnings.len(),
        1,
        "expected one warning, got: {:?}",
        warnings.iter().map(|d| &d.message).collect::<Vec<_>>()
    );
    assert!(
        warnings.iter().any(|w| w
            .message
            .contains("Cannot insert CadShape node in 'geometry' field of Shape node")),
        "expected CadShape geometry warning, got: {:?}",
        warnings.iter().map(|d| &d.message).collect::<Vec<_>>()
    );
}

#[test]
fn test_warns_when_robot_physics_has_no_inertia_source() {
    let input = r#"#VRML_SIM R2025a utf8
Robot {
  physics Physics {
    mass 1
  }
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    let warnings: Vec<_> = diagnostics
        .iter()
        .filter(|d| d.severity == Severity::Warning)
        .collect();
    assert!(
        warnings
            .iter()
            .any(|w| w.message.contains("Undefined inertia matrix")),
        "expected inertia warning, got: {:?}",
        warnings.iter().map(|d| &d.message).collect::<Vec<_>>()
    );
}

#[test]
fn test_validate_flat_mfvec3f_array_values() {
    let input = r#"#VRML_SIM R2025a utf8
Accelerometer {
  lookupTable [ -100 0.01 0, 100 0.01 0 ]
}
"#;
    let mut parser = Parser::new(input);
    let doc = parser.parse_document().unwrap();
    let diagnostics = validate(&doc);

    assert!(
        !diagnostics.has_errors(),
        "expected no validation errors, got: {:?}",
        diagnostics.iter().collect::<Vec<_>>()
    );
}