Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
66 changes: 56 additions & 10 deletions flow360/component/simulation/services.py
Original file line number Diff line number Diff line change
Expand Up @@ -602,7 +602,12 @@ def dict_preprocessing(params_as_dict: dict) -> dict:
except pd.ValidationError as err:
validation_errors = err.errors()
except Exception as err: # pylint: disable=broad-exception-caught
validation_errors = handle_generic_exception(err, validation_errors)
import traceback # pylint: disable=import-outside-toplevel

stack = traceback.format_exc()
validation_errors = handle_generic_exception(
err, validation_errors, loc_prefix=None, error_stack=stack
)
finally:
if validation_context is not None:
validation_warnings = list(validation_context.validation_warnings)
Expand Down Expand Up @@ -645,8 +650,44 @@ def clean_unrelated_setting_from_params_dict(params: dict, root_item_type: str)
return params


def _sanitize_stack_trace(stack: str) -> str:
"""
Sanitize file paths in stack trace to only show paths starting from 'flow360/'.

Gracefully returns the original stack if sanitization fails.

Parameters
----------
stack : str
The original stack trace string.

Returns
-------
str
The sanitized stack trace with shortened file paths, or the original
stack if sanitization fails.
"""
# pylint: disable=import-outside-toplevel
import re

try:
# Remove the "Traceback (most recent call last):\n" prefix
stack = re.sub(r"^Traceback \(most recent call last\):\n\s*", "", stack)

# Pattern to match file paths containing 'flow360/'
# Captures everything before 'flow360/' and replaces with just 'flow360/'
pattern = r'File "[^"]*[/\\](flow360[/\\][^"]*)"'
replacement = r'File "\1"'
return re.sub(pattern, replacement, stack)
except Exception: # pylint: disable=broad-exception-caught
return stack


def handle_generic_exception(
err: Exception, validation_errors: Optional[list], loc_prefix: Optional[list[str]] = None
err: Exception,
validation_errors: Optional[list],
loc_prefix: Optional[list[str]] = None,
error_stack: Optional[str] = None,
) -> list:
"""
Handles generic exceptions during validation, adding to validation errors.
Expand All @@ -659,6 +700,8 @@ def handle_generic_exception(
Current list of validation errors, may be None.
loc_prefix : list or None
Prefix of the location of the generic error to help locate the issue
error_stack : str or None
The error stack trace, if available.

Returns
-------
Expand All @@ -668,14 +711,17 @@ def handle_generic_exception(
if validation_errors is None:
validation_errors = []

validation_errors.append(
{
"type": err.__class__.__name__.lower().replace("error", "_error"),
"loc": ["unknown"] if loc_prefix is None else loc_prefix,
"msg": str(err),
"ctx": {},
}
)
error_entry = {
"type": err.__class__.__name__.lower().replace("error", "_error"),
"loc": ["unknown"] if loc_prefix is None else loc_prefix,
"msg": str(err),
"ctx": {},
}

if error_stack is not None:
error_entry["debug"] = _sanitize_stack_trace(error_stack)

validation_errors.append(error_entry)
return validation_errors


Expand Down
69 changes: 69 additions & 0 deletions tests/simulation/service/test_services_v2.py
Original file line number Diff line number Diff line change
Expand Up @@ -1533,6 +1533,75 @@ def check_setting_preserved(
)


def test_sanitize_stack_trace():
"""Test that _sanitize_stack_trace properly sanitizes file paths and removes traceback prefix."""
from flow360.component.simulation.services import _sanitize_stack_trace

# Test case 1: Full stack trace with traceback prefix and absolute paths
input_stack = """Traceback (most recent call last):
File "/disk2/ben/Flow360-R2/flow360/component/simulation/services.py", line 553, in validate_model
validation_info = ParamsValidationInfo(
File "/disk2/ben/Flow360-R2/flow360/component/simulation/validation/validation_context.py", line 437, in __init__
self.farfield_method = self._get_farfield_method_(param_as_dict=param_as_dict)
File "/disk2/ben/Flow360-R2/flow360/component/simulation/validation/validation_context.py", line 162, in _get_farfield_method_
if meshing["type_name"] == "MeshingParams":
KeyError: 'type_name'"""

expected_output = """File "flow360/component/simulation/services.py", line 553, in validate_model
validation_info = ParamsValidationInfo(
File "flow360/component/simulation/validation/validation_context.py", line 437, in __init__
self.farfield_method = self._get_farfield_method_(param_as_dict=param_as_dict)
File "flow360/component/simulation/validation/validation_context.py", line 162, in _get_farfield_method_
if meshing["type_name"] == "MeshingParams":
KeyError: 'type_name'"""

result = _sanitize_stack_trace(input_stack)
assert result == expected_output

# Test case 2: Stack trace without traceback prefix (already sanitized prefix)
input_stack_no_prefix = """File "/home/user/projects/flow360/component/simulation/services.py", line 100, in some_function
some_code()"""

expected_no_prefix = """File "flow360/component/simulation/services.py", line 100, in some_function
some_code()"""

result_no_prefix = _sanitize_stack_trace(input_stack_no_prefix)
assert result_no_prefix == expected_no_prefix

# Test case 3: Stack trace with non-flow360 paths should remain unchanged for those paths
input_mixed = """Traceback (most recent call last):
File "/usr/lib/python3.10/site-packages/pydantic/main.py", line 100, in validate
return cls.model_validate(obj)
File "/disk2/ben/Flow360-R2/flow360/component/simulation/services.py", line 50, in my_func
do_something()"""

expected_mixed = """File "/usr/lib/python3.10/site-packages/pydantic/main.py", line 100, in validate
return cls.model_validate(obj)
File "flow360/component/simulation/services.py", line 50, in my_func
do_something()"""

result_mixed = _sanitize_stack_trace(input_mixed)
assert result_mixed == expected_mixed

# Test case 4: Empty string should return empty string
assert _sanitize_stack_trace("") == ""

# Test case 5: String with no file paths should remain unchanged (except traceback prefix)
input_no_paths = "Some error message without file paths"
assert _sanitize_stack_trace(input_no_paths) == input_no_paths

# Test case 6: Windows-style paths
input_windows = """Traceback (most recent call last):
File "C:\\Users\\dev\\Flow360-R2\\flow360\\component\\simulation\\services.py", line 100, in func
code()"""

expected_windows = """File "flow360\\component\\simulation\\services.py", line 100, in func
code()"""

result_windows = _sanitize_stack_trace(input_windows)
assert result_windows == expected_windows


def test_validate_error_location_with_selector():
"""
Test that validation error locations are correctly preserved when errors occur
Expand Down
Loading