diff --git a/pyproject.toml b/pyproject.toml index 8e6d567..74bacc6 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -4,7 +4,7 @@ build-backend = "setuptools.build_meta" [project] name = "flexo_syside_lib" -version = "0.4.0" +version = "0.4.1" description = "Using syside sysmlv2 with Flexo" authors = [{name="Robert Karban", email="robert.karban@planetaryutilities.com"}] readme = "README.md" diff --git a/src/flexo_syside_lib/__init__.py b/src/flexo_syside_lib/__init__.py index 521d00f..2e2b3f6 100644 --- a/src/flexo_syside_lib/__init__.py +++ b/src/flexo_syside_lib/__init__.py @@ -27,7 +27,7 @@ ) -__version__ = "0.4.0" +__version__ = "0.4.1" __all__ = [ "build_sysand_command", diff --git a/src/flexo_syside_lib/serde.py b/src/flexo_syside_lib/serde.py index 92d87c1..83c540c 100644 --- a/src/flexo_syside_lib/serde.py +++ b/src/flexo_syside_lib/serde.py @@ -5,6 +5,7 @@ import tempfile import warnings from datetime import datetime, timezone +from importlib.metadata import PackageNotFoundError, version as package_version from pathlib import Path from typing import Any @@ -13,6 +14,11 @@ from .payload import ELEMENT_TYPE_KEY, wrap_elements_as_payload from .utils2 import print_serde_report +try: + _PACKAGE_VERSION = package_version("flexo_syside_lib") +except PackageNotFoundError: + _PACKAGE_VERSION = "unknown" + def make_root_namespace_first_legacy(json_str: str) -> str: obj = json.loads(json_str) @@ -121,10 +127,65 @@ def _build_environment_from_models( with tempfile.TemporaryDirectory(prefix="flexo_syside_env_") as env_dir: _basenames, env_paths = _write_models_to_temp_paths(env_dir, environment_models) model, diagnostics = syside.try_load_model(env_paths) - assert not diagnostics.contains_errors(warnings_as_errors=True) + _assert_no_diagnostic_errors( + diagnostics, + context="environment model load", + input_names=[name for name, _text in environment_models], + staged_paths=env_paths, + ) return model.to_environment() if model is not None and hasattr(model, "to_environment") else None +def _format_diagnostic_entry(diag: Any) -> str: + message = str(getattr(diag, "message", None) or getattr(diag, "text", None) or diag).strip() + severity = getattr(diag, "severity", None) or getattr(diag, "level", None) + severity_name = str(getattr(severity, "name", severity) or "unknown").lower() + + file_path = str( + getattr(diag, "file", None) + or getattr(getattr(diag, "location", None), "file", None) + or getattr(getattr(diag, "location", None), "resource", None) + or "" + ).strip() + line = getattr(diag, "line", None) + col = getattr(diag, "col", None) + + location = file_path + if line is not None: + location = f"{location}:{line}" if location else str(line) + if col is not None: + location = f"{location}:{col}" + if location: + return f"[{severity_name}] {location} {message}".strip() + return f"[{severity_name}] {message}".strip() + + +def _assert_no_diagnostic_errors( + diagnostics: Any, + *, + context: str, + input_names: list[str], + staged_paths: list[str], +) -> None: + if not diagnostics.contains_errors(warnings_as_errors=True): + return + + all_diagnostics = list(getattr(diagnostics, "all", []) or []) + lines = [ + f"flexo_syside_lib { _PACKAGE_VERSION } {context} failed with {len(all_diagnostics)} diagnostic(s)", + f"input_names={input_names}", + f"staged_paths={staged_paths}", + ] + if not all_diagnostics: + lines.append("diagnostics=[]") + else: + lines.append("diagnostics:") + lines.extend(f"- {_format_diagnostic_entry(diag)}" for diag in all_diagnostics[:20]) + if len(all_diagnostics) > 20: + lines.append(f"- ... {len(all_diagnostics) - 20} more diagnostic(s)") + raise AssertionError("\n".join(lines)) + + def model_to_json( model: syside.Model, minimal: bool = False, @@ -171,7 +232,12 @@ def convert_sysml_file_textual_to_json( minimal: bool = False, ) -> tuple[list[dict[str, Any]], str]: model, diagnostics = syside.try_load_model([sysml_file_path]) - assert not diagnostics.contains_errors(warnings_as_errors=True) + _assert_no_diagnostic_errors( + diagnostics, + context="single-file model load", + input_names=[Path(sysml_file_path).name], + staged_paths=[sysml_file_path], + ) json_string = model_to_json( model, @@ -192,7 +258,12 @@ def convert_sysml_files_textual_to_json( minimal: bool = False, ) -> tuple[list[dict[str, Any]], str]: model, diagnostics = syside.try_load_model(sysml_file_paths) - assert not diagnostics.contains_errors(warnings_as_errors=True) + _assert_no_diagnostic_errors( + diagnostics, + context="multi-file model load", + input_names=[Path(sysml_file_path).name for sysml_file_path in sysml_file_paths], + staged_paths=sysml_file_paths, + ) json_string = model_to_json( model, @@ -220,7 +291,12 @@ def convert_sysml_models_textual_to_json( basenames, temp_paths = _write_models_to_temp_paths(tmp_dir, sysml_models) environment = _build_environment_from_models(environment_models) model, diagnostics = syside.try_load_model(temp_paths, environment=environment) - assert not diagnostics.contains_errors(warnings_as_errors=True) + _assert_no_diagnostic_errors( + diagnostics, + context="textual models load", + input_names=[name for name, _text in sysml_models], + staged_paths=temp_paths, + ) json_string = model_to_json( model, @@ -241,7 +317,12 @@ def convert_sysml_string_textual_to_json( minimal: bool = False, ) -> tuple[list[dict[str, Any]], str]: model, diagnostics = syside.load_model(sysml_source=sysml_model_string) - assert not diagnostics.contains_errors(warnings_as_errors=True) + _assert_no_diagnostic_errors( + diagnostics, + context="string model load", + input_names=[""], + staged_paths=[""], + ) json_string = model_to_json(model, minimal) if json_out_path is not None: diff --git a/tests/test_mocked_syside.py b/tests/test_mocked_syside.py index e5e676d..d4c94c8 100644 --- a/tests/test_mocked_syside.py +++ b/tests/test_mocked_syside.py @@ -3,6 +3,8 @@ import tempfile from unittest.mock import Mock, patch +import pytest + from flexo_syside_lib.core import ( _create_json_writer, _create_serialization_options, @@ -254,6 +256,43 @@ def test_convert_sysml_models_to_json_allows_duplicate_basenames_in_environment( assert env_call_paths[1].endswith("LibB\\root\\package.sysml") or env_call_paths[1].endswith("LibB/root/package.sysml") +@patch("flexo_syside_lib.serde.syside") +def test_convert_sysml_models_to_json_surfaces_diagnostics_in_assertion(mock_syside): + env_model = Mock() + env_model.to_environment.return_value = "ENV" + env_diags = Mock() + env_diags.contains_errors.return_value = False + + failure_diag = Mock() + failure_diag.message = "Couldn't resolve import Demo::Library" + failure_diag.severity = "error" + failure_diag.file = "/tmp/0000_workspace/test.sysml" + failure_diag.line = 7 + failure_diag.col = 3 + + main_diags = Mock() + main_diags.contains_errors.return_value = True + main_diags.all = [failure_diag] + + mock_syside.try_load_model.side_effect = [ + (env_model, env_diags), + (Mock(), main_diags), + ] + + with pytest.raises(AssertionError) as excinfo: + convert_sysml_models_textual_to_json( + [("workspace/test.sysml", "package Test;")], + environment_models=[("Demo/root/library.sysml", "library package Demo; end;")], + ) + + message = str(excinfo.value) + assert "textual models load failed" in message + assert "Couldn't resolve import Demo::Library" in message + assert "workspace/test.sysml" in message + assert "0000_workspace" in message + assert "flexo_syside_lib" in message + + @patch("flexo_syside_lib.serde.syside") def test_create_json_writer(mock_syside): mock_writer_class = Mock() diff --git a/tests/test_payload_and_api.py b/tests/test_payload_and_api.py index 60577c1..9ca4a1e 100644 --- a/tests/test_payload_and_api.py +++ b/tests/test_payload_and_api.py @@ -68,7 +68,7 @@ def test_package_root_exports_conversion_helpers(): assert callable(flexo_syside_lib.convert_sysml_string_textual_to_json) assert callable(flexo_syside_lib.convert_json_to_sysml_textual) assert callable(flexo_syside_lib.expand_minimal_json_to_full_json) - assert flexo_syside_lib.__version__ == "0.4.0" + assert flexo_syside_lib.__version__ == "0.4.1" def test_convert_json_to_sysml_textual_invalid_input():