Files
enviPy-bayer/epapi/tests/v1/test_schema_generation.py
Tobias O d06bd0d4fd [Feature] Minimal IUCLID export (#338)
This is an initial implementation that creates a working minimal .i6z document.
It passes schema validation and can be imported into IUCLID.

Caveat:
IUCLID files target individual compounds.
Pathway is not actually covered by the format.

It can be added in either soil or water and soil OECD endpoints.
**I currently only implemented the soil endpoint for all data.**

This sort of works, and I can report all degradation products in a pathway (not a nice view, but we can report many transformation products and add a diagram attachment in the future).

Adding additional information is an absolute pain, as we need to explicitly map each type of information to the relevant OECD field.
I use the XSD scheme for validation, but unfortunately the IUCLID parser is not fully compliant and requires a specific order, etc.

The workflow is: finding the AI structure from the XSD scheme -> make the scheme validation pass -> upload to IUCLID to get obscure error messages -> guess what could be wrong -> repeat 💣

New specifications get released once per year, so we will have to update accordingly.
I believe that this should be a more expensive feature, as it requires significant effort to uphold.

Currently implemented for root compound only in SOIL:

- Soil Texture 2
- Soil Texture 1
- pH value
- Half-life per soil sample / scenario (mapped to disappearance; not sure about that).
- CEC
- Organic Matter (only Carbon)
- Moisture content
- Humidity

<img width="2123" alt="image.png" src="attachments/d29830e1-65ef-4136-8939-1825e0959c62">
<img width="2124" alt="image.png" src="attachments/ac9de2ac-bf68-4ba4-b40b-82f810a9de93">
<img width="2139" alt="image.png" src="attachments/5674c7e6-865e-420e-974a-6b825b331e6c">

Reviewed-on: enviPath/enviPy#338
Co-authored-by: Tobias O <tobias.olenyi@envipath.com>
Co-committed-by: Tobias O <tobias.olenyi@envipath.com>
2026-04-07 19:46:12 +12:00

114 lines
5.1 KiB
Python

"""
Property-based tests for schema generation.
Tests that verify schema generation works correctly for all models,
regardless of their structure.
"""
import pytest
from typing import Type
from pydantic import BaseModel
from envipy_additional_information import registry, EnviPyModel
from epapi.utils.schema_transformers import build_rjsf_output
class TestSchemaGeneration:
"""Test that all models can generate valid RJSF schemas."""
@pytest.mark.parametrize("model_name,model_cls", list(registry.list_models().items()))
def test_all_models_generate_rjsf(self, model_name: str, model_cls: Type[BaseModel]):
"""Every model in the registry should generate valid RJSF format."""
# Skip non-EnviPyModel classes (parsers, etc.)
if not issubclass(model_cls, EnviPyModel):
pytest.skip(f"{model_name} is not an EnviPyModel")
# Should not raise exception
result = build_rjsf_output(model_cls)
# Verify structure
assert isinstance(result, dict), f"{model_name}: Result should be a dict"
assert "schema" in result, f"{model_name}: Missing 'schema' key"
assert "uiSchema" in result, f"{model_name}: Missing 'uiSchema' key"
assert "formData" in result, f"{model_name}: Missing 'formData' key"
assert "groups" in result, f"{model_name}: Missing 'groups' key"
# Verify types
assert isinstance(result["schema"], dict), f"{model_name}: schema should be dict"
assert isinstance(result["uiSchema"], dict), f"{model_name}: uiSchema should be dict"
assert isinstance(result["formData"], dict), f"{model_name}: formData should be dict"
assert isinstance(result["groups"], list), f"{model_name}: groups should be list"
# Verify schema has properties
assert "properties" in result["schema"], f"{model_name}: schema should have 'properties'"
assert isinstance(result["schema"]["properties"], dict), (
f"{model_name}: properties should be dict"
)
@pytest.mark.parametrize("model_name,model_cls", list(registry.list_models().items()))
def test_ui_schema_matches_schema_fields(self, model_name: str, model_cls: Type[BaseModel]):
"""uiSchema keys should match schema properties (or be nested for intervals)."""
if not issubclass(model_cls, EnviPyModel):
pytest.skip(f"{model_name} is not an EnviPyModel")
result = build_rjsf_output(model_cls)
schema_props = set(result["schema"]["properties"].keys())
ui_schema_keys = set(result["uiSchema"].keys())
# uiSchema should have entries for all top-level properties
# (intervals may have nested start/end, but the main field should be present)
assert ui_schema_keys.issubset(schema_props), (
f"{model_name}: uiSchema has keys not in schema: {ui_schema_keys - schema_props}"
)
@pytest.mark.parametrize("model_name,model_cls", list(registry.list_models().items()))
def test_groups_is_list_of_strings(self, model_name: str, model_cls: Type[BaseModel]):
"""Groups should be a list of strings."""
if not issubclass(model_cls, EnviPyModel):
pytest.skip(f"{model_name} is not an EnviPyModel")
result = build_rjsf_output(model_cls)
groups = result["groups"]
assert isinstance(groups, list), f"{model_name}: groups should be list"
assert all(isinstance(g, str) for g in groups), (
f"{model_name}: all groups should be strings, got {groups}"
)
@pytest.mark.parametrize("model_name,model_cls", list(registry.list_models().items()))
def test_form_data_matches_schema(self, model_name: str, model_cls: Type[BaseModel]):
"""formData keys should match schema properties."""
if not issubclass(model_cls, EnviPyModel):
pytest.skip(f"{model_name} is not an EnviPyModel")
result = build_rjsf_output(model_cls)
schema_props = set(result["schema"]["properties"].keys())
form_data_keys = set(result["formData"].keys())
# formData should only contain keys that are in schema
assert form_data_keys.issubset(schema_props), (
f"{model_name}: formData has keys not in schema: {form_data_keys - schema_props}"
)
class TestWidgetTypes:
"""Test that widget types are valid."""
@pytest.mark.parametrize("model_name,model_cls", list(registry.list_models().items()))
def test_widget_types_are_valid(self, model_name: str, model_cls: Type[BaseModel]):
"""All widget types in uiSchema should be valid WidgetType values."""
from envipy_additional_information.ui_config import WidgetType
if not issubclass(model_cls, EnviPyModel):
pytest.skip(f"{model_name} is not an EnviPyModel")
result = build_rjsf_output(model_cls)
valid_widgets = {wt.value for wt in WidgetType}
for field_name, ui_config in result["uiSchema"].items():
widget = ui_config.get("ui:widget")
if widget:
assert widget in valid_widgets, (
f"{model_name}.{field_name}: Invalid widget '{widget}'. Valid: {valid_widgets}"
)