Files
ResInsight/GrpcInterface/Python/rips/tests/test_pdmobject_validation.py
T
Kristian Bendiksen 71fa7d2c26 #12940 Add validation for Python GRPC field updates with atomic rollback
Implements comprehensive type and range validation to prevent invalid data
from being set via Python GRPC interface. Invalid values are rejected with
clear error messages. Uses two-phase commit pattern to ensure atomic updates
where either all fields update or none do.

Validation Features:
- Type validation: Ensures field value types match (parsing errors caught)
- Range validation: Checks values against min/max constraints defined with
  setRange(), setMinValue(), setMaxValue()
- Object validation: Validates cross-field constraints via validate() override
- Atomic updates: Two-phase commit ensures all-or-nothing semantics
- Rollback on error: Failed validation leaves ALL fields at previous values
- Clear error messages: Aggregates and returns all validation failures

Implementation:
- Use std::expected<void, QString> for type-safe error handling (C++23)
- assignFieldValue(): Validates after parsing, rolls back individual field on error
- copyPdmObjectFromRipsToCaf(): Two-phase commit pattern
  - Phase 1: Validate all fields and collect changes (defer UI notifications)
  - Phase 2: If any validation fails, rollback all fields; otherwise commit all
- UpdateExistingPdmObject(): Returns grpc::INVALID_ARGUMENT with validation details

Testing:
- Comprehensive Python tests demonstrating validation behavior
- Test atomic rollback: multiple fields rolled back when one fails
- No regression in existing functionality

Documentation:
- Added validation documentation to CLAUDE.md with examples
- Explains how to set field ranges and handle validation errors in Python

Built on validation infrastructure from commit f342ca77 (PdmFieldHandle and
PdmObjectHandle validation support).
2026-01-21 08:41:09 +01:00

107 lines
3.7 KiB
Python

"""
Test file for issue #12940: Python GRPC interface validates field values
This test verifies that validation prevents invalid values from being set.
"""
import sys
import os
import pytest
sys.path.insert(1, os.path.join(sys.path[0], "../../"))
import rips
def test_range_validation_minimum_enforced(rips_instance, initialize_test):
"""
Verifies that setting value below minimum range is rejected.
The wellBoreFluidPvtTable field has setMinValue(0) in C++, so negative values
should be rejected with a clear error message.
"""
well_path_coll = rips_instance.project.well_path_collection()
# Create a new modeled well path
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.name = "test_validation"
well_path.update()
# Get completion settings
completions_settings = well_path.completion_settings()
# Set wellBoreFluidPvtTable to a negative value (below minimum of 0)
completions_settings.well_bore_fluid_pvt_table = -5
# Should raise grpc._channel._InactiveRpcError with validation message
with pytest.raises(Exception) as exc_info:
completions_settings.update()
error_message = str(exc_info.value)
assert "Validation failed" in error_message
assert (
"WellBoreFluidPvtTable" in error_message
or "well_bore_fluid_pvt_table" in error_message
)
assert "-5" in error_message or "below minimum" in error_message
def test_valid_value_should_succeed(rips_instance, initialize_test):
"""
Control test: Valid values should always work.
This test should pass both before and after implementing validation.
"""
well_path_coll = rips_instance.project.well_path_collection()
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.name = "test_valid"
well_path.update()
completions_settings = well_path.completion_settings()
# Set valid values (within range)
completions_settings.well_bore_fluid_pvt_table = 5 # Valid: >= 0
completions_settings.fluid_in_place_region = 10 # Valid
# This should succeed both before and after validation implementation
completions_settings.update()
# Verify values were set correctly
completions_settings_updated = well_path.completion_settings()
assert completions_settings_updated.well_bore_fluid_pvt_table == 5
assert completions_settings_updated.fluid_in_place_region == 10
def test_value_unchanged_after_validation_error(rips_instance, initialize_test):
"""Verifies field value doesn't change if validation fails (rollback)."""
well_path_coll = rips_instance.project.well_path_collection()
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.name = "test_rollback"
well_path.update()
completions_settings = well_path.completion_settings()
# Set valid initial value
completions_settings.well_bore_fluid_pvt_table = 42
completions_settings.fluid_in_place_region = 99
completions_settings.update()
# Try to set invalid value
completions_settings.well_bore_fluid_pvt_table = -5
# Also set a different version for (should also be rolled back)
completions_settings.fluid_in_place_region = 12
with pytest.raises(Exception):
completions_settings.update()
# Value should remain at the previous valid value (42)
completions_settings_refreshed = well_path.completion_settings()
assert (
completions_settings_refreshed.well_bore_fluid_pvt_table == 42
), "Field value should not change after failed validation (rollback should occur)"
assert (
completions_settings_refreshed.fluid_in_place_region == 99
), "Field value should not change after failed validation (rollback should occur)"