Files
ResInsight/GrpcInterface/Python/rips/tests/test_pdmobject_validation.py
T
Kristian Bendiksen 4ea32ba563 #12940 Add automatic rollback on validation failure for Python GRPC
Implements automatic state restoration when field validation fails during
update(). The Python object now maintains consistency with the server state
by rolling back to the last known valid values.

- Store protobuf snapshot before update attempt
- Restore both protobuf object and Python attributes on validation failure
- Add test_validation_rollback() to verify rollback behavior
- Update bool validation test to assert automatic rollback
- Provides transaction-like semantics for field updates
2026-01-21 08:41:09 +01:00

306 lines
11 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)"
def test_strict_double_validation_extra_text(rips_instance, initialize_test):
"""Verifies strict validation for double fields."""
well_path_coll = rips_instance.project.well_path_collection()
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.name = "test_double_strict_extra"
well_path.update()
fishbones_collection = well_path.completions().fishbones()
# Successfully set a value
fishbones_collection.main_bore_skin_factor = 50.0
fishbones_collection.update()
# Try a badly formatted value
fishbones_collection.main_bore_skin_factor = "100.5test"
with pytest.raises(Exception) as exc_info:
fishbones_collection.update()
assert "Extra characters found" in str(exc_info.value)
assert "100.5test" in str(exc_info.value)
# Should not overwrite when validation fails
assert well_path.completions().fishbones().main_bore_skin_factor == 50.0
def test_strict_double_validation_text(rips_instance, initialize_test):
"""Verifies strict validation for double fields."""
well_path_coll = rips_instance.project.well_path_collection()
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.name = "test_double_strict"
well_path.update()
fishbones_collection = well_path.completions().fishbones()
fishbones_collection.main_bore_skin_factor = "test"
with pytest.raises(Exception) as exc_info:
fishbones_collection.update()
print("Error", exc_info.value)
assert "unreadable" in str(exc_info.value)
assert "test" in str(exc_info.value)
def test_strict_int_validation(rips_instance, initialize_test):
"""Verifies strict validation for int fields."""
well_path_coll = rips_instance.project.well_path_collection()
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.name = "test_int_strict"
well_path.update()
completions_settings = well_path.completion_settings()
# well_bore_fluid_pvt_table is int
completions_settings.well_bore_fluid_pvt_table = 0.99
with pytest.raises(Exception) as exc_info:
completions_settings.update()
assert "Extra characters found" in str(exc_info.value)
assert "0.99" in str(exc_info.value)
def test_validation_rollback(rips_instance, initialize_test):
"""Verifies that failed updates rollback local state."""
well_path_coll = rips_instance.project.well_path_collection()
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.update()
completions_settings = well_path.completion_settings()
# Set valid value
completions_settings.well_bore_fluid_pvt_table = 5
completions_settings.update()
assert completions_settings.well_bore_fluid_pvt_table == 5
# Try to set invalid value (float instead of int)
completions_settings.well_bore_fluid_pvt_table = 0.99
with pytest.raises(Exception) as exc_info:
completions_settings.update()
assert "Extra characters found" in str(exc_info.value)
# Verify rollback: should be back to 5, not 0.99
assert (
completions_settings.well_bore_fluid_pvt_table == 5
), "Field should rollback to previous valid value after failed update"
def test_enum_validation_message(rips_instance, initialize_test):
"""Verifies enum validation error message lists valid options."""
well_path_coll = rips_instance.project.well_path_collection()
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.update()
completions_settings = well_path.completion_settings()
# gas_inflow_eq is an Enum
# Valid values: STD, R-G, P-P, GPP
completions_settings.gas_inflow_eq = "InvalidValue"
with pytest.raises(Exception) as exc_info:
completions_settings.update()
error_msg = str(exc_info.value)
assert "Failed to set enum text value" in error_msg
assert "Valid options are:" in error_msg
assert "STD" in error_msg
assert "GPP" in error_msg
def test_strict_enum_validation(rips_instance, initialize_test):
"""Verifies strict validation for enum fields (trailing garbage)."""
well_path_coll = rips_instance.project.well_path_collection()
well_path = well_path_coll.add_new_object(rips.ModeledWellPath)
well_path.update()
completions_settings = well_path.completion_settings()
completions_settings.gas_inflow_eq = "R-G"
completions_settings.update()
# STD is valid. "STD.garbage" should fail.
completions_settings.gas_inflow_eq = "STD.garbage"
with pytest.raises(Exception) as exc_info:
completions_settings.update()
error_msg = str(exc_info.value)
# Expect "Extra characters found" because "STD" is parsed successfully, leaving ".garbage"
assert "Extra characters found" in error_msg
def test_strict_bool_validation(rips_instance, initialize_test):
"""Verifies strict validation for bool fields."""
# Get or create a FractureTemplate to test bool validation
fracture_templates = rips_instance.project.descendants(rips.FractureTemplate)
# Skip test if no FractureTemplate available (need a project with templates)
if len(fracture_templates) == 0:
pytest.skip("No FractureTemplate available for testing")
fracture_template = fracture_templates[0]
# Test 1: True and False should work
fracture_template.user_defined_perforation_length = True
fracture_template.update()
assert fracture_template.user_defined_perforation_length is True
fracture_template.user_defined_perforation_length = False
fracture_template.update()
assert fracture_template.user_defined_perforation_length is False
# Test 2: Integer 1 should fail
fracture_template.user_defined_perforation_length = 1
with pytest.raises(Exception) as exc_info:
fracture_template.update()
error_msg = str(exc_info.value)
assert (
"Extra characters found" in error_msg
or "unreadable" in error_msg
or "does not evaluate to either true or false" in error_msg
)
# Verify automatic rollback: should be back to False
assert fracture_template.user_defined_perforation_length is False
# Test 3: Float 0.99 should fail
fracture_template.user_defined_perforation_length = 0.99
with pytest.raises(Exception) as exc_info:
fracture_template.update()
error_msg = str(exc_info.value)
assert (
"Extra characters found" in error_msg
or "unreadable" in error_msg
or "does not evaluate to either true or false" in error_msg
)
# Verify automatic rollback: should be back to False
assert fracture_template.user_defined_perforation_length is False
# Test 4: String "yes" should fail
fracture_template.user_defined_perforation_length = "yes"
with pytest.raises(Exception) as exc_info:
fracture_template.update()
error_msg = str(exc_info.value)
assert (
"Extra characters found" in error_msg
or "unreadable" in error_msg
or "does not evaluate to either true or false" in error_msg
)
# Verify automatic rollback: should be back to False
assert fracture_template.user_defined_perforation_length is False