diff --git a/.github/workflows/ci-release.yml b/.github/workflows/ci-release.yml index 95126ecb6f6f..615354b40d55 100644 --- a/.github/workflows/ci-release.yml +++ b/.github/workflows/ci-release.yml @@ -1124,7 +1124,7 @@ jobs: env: TESTS_VERSION: ${{ env.TESTS_VERSION }} run: | - uv sync --frozen --group tests --extra all + uv sync --frozen --group tests --extra all-dotnet - name: Remove Ansys processes (if any) shell: powershell diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index 8ba1330e7ce2..85ff6cb309fb 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -1341,7 +1341,7 @@ jobs: env: TESTS_VERSION: ${{ env.TESTS_VERSION }} run: | - uv sync --frozen --group tests --extra all + uv sync --frozen --group tests --extra all-dotnet - name: Wait for master cache update uses: ./.github/actions/check-cache diff --git a/.github/workflows/manual_draft.yml b/.github/workflows/manual_draft.yml index 6f45ce8dcd49..8f1d5be2ca4c 100644 --- a/.github/workflows/manual_draft.yml +++ b/.github/workflows/manual_draft.yml @@ -740,7 +740,7 @@ jobs: - name: Install pyaedt and tests dependencies run: | .venv\Scripts\Activate.ps1 - pip install . --group tests + pip install ".[all-dotnet]" --group tests - name: Remove Ansys processes (if any) shell: powershell diff --git a/.github/workflows/nightly-tests.yml b/.github/workflows/nightly-tests.yml index a93b984950a6..3d25b5c467c8 100644 --- a/.github/workflows/nightly-tests.yml +++ b/.github/workflows/nightly-tests.yml @@ -988,7 +988,7 @@ jobs: env: TESTS_VERSION: ${{ env.TESTS_VERSION }} run: | - uv sync --frozen --group tests --extra all + uv sync --frozen --group tests --extra all-dotnet - name: Remove Ansys processes (if any) shell: powershell diff --git a/.github/workflows/sync-release.yml b/.github/workflows/sync-release.yml index 95f0d270383e..8b3ee48d150b 100644 --- a/.github/workflows/sync-release.yml +++ b/.github/workflows/sync-release.yml @@ -1228,7 +1228,7 @@ jobs: - name: Install pyaedt and tests dependencies run: | - uv sync --frozen --group tests --extra all + uv sync --frozen --group tests --extra all-dotnet - name: Remove Ansys processes (if any) shell: powershell diff --git a/.github/workflows/update-testmondata-cache.yml b/.github/workflows/update-testmondata-cache.yml index 215783779e1c..9da179266edb 100644 --- a/.github/workflows/update-testmondata-cache.yml +++ b/.github/workflows/update-testmondata-cache.yml @@ -933,7 +933,7 @@ jobs: env: TESTS_VERSION: ${{ env.TESTS_VERSION }} run: | - uv sync --frozen --group tests --extra all + uv sync --frozen --group tests --extra all-dotnet - name: Restore testmondata cache uses: ./.github/actions/testmon-cache diff --git a/.github/workflows/weekly-tests.yml b/.github/workflows/weekly-tests.yml index f23336b6aa30..07c409d11242 100644 --- a/.github/workflows/weekly-tests.yml +++ b/.github/workflows/weekly-tests.yml @@ -982,7 +982,7 @@ jobs: env: TESTS_VERSION: ${{ env.TESTS_VERSION }} run: | - uv sync --frozen --group tests --extra all + uv sync --frozen --group tests --extra all-dotnet - name: Remove Ansys processes (if any) diff --git a/doc/changelog.d/7861.fixed.md b/doc/changelog.d/7861.fixed.md new file mode 100644 index 000000000000..1933e3f8225c --- /dev/null +++ b/doc/changelog.d/7861.fixed.md @@ -0,0 +1 @@ +Test_export_to_aedt_test_FAIL_New diff --git a/pyproject.toml b/pyproject.toml index 744697d3623a..83fa6463b26a 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -449,4 +449,3 @@ showcontent = true directory = "test" name = "Test" showcontent = true - diff --git a/src/ansys/aedt/core/application/design.py b/src/ansys/aedt/core/application/design.py index a4b6f14050e3..e372b1e27139 100644 --- a/src/ansys/aedt/core/application/design.py +++ b/src/ansys/aedt/core/application/design.py @@ -1467,7 +1467,7 @@ def odesign(self) -> object: >>> app.odesign """ - if settings.use_multi_desktop: # pragma: no cover + if settings.use_multi_desktop and self._desktop_class.is_grpc_api: # pragma: no cover self._desktop_class.grpc_plugin.recreate_application(True) if self._design_name: self._odesign = self.oproject.SetActiveDesign(self._design_name) @@ -1521,7 +1521,7 @@ def oproject(self) -> _OProject: >>> app.oproject """ - if settings.use_multi_desktop: # pragma: no cover + if settings.use_multi_desktop and self._desktop_class.is_grpc_api: # pragma: no cover self._desktop_class.grpc_plugin.recreate_application(True) return cast(_OProject, self._oproject) diff --git a/src/ansys/aedt/core/desktop.py b/src/ansys/aedt/core/desktop.py index 54455ae4ac0a..ea87f4b37aaa 100644 --- a/src/ansys/aedt/core/desktop.py +++ b/src/ansys/aedt/core/desktop.py @@ -1435,7 +1435,7 @@ def odesktop(self) -> object: >>> d.odesktop """ - if settings.use_grpc_api: + if self.is_grpc_api: tries = 0 while tries < 5: try: diff --git a/src/ansys/aedt/core/filtersolutions.py b/src/ansys/aedt/core/filtersolutions.py index 262910b65828..a20943496891 100644 --- a/src/ansys/aedt/core/filtersolutions.py +++ b/src/ansys/aedt/core/filtersolutions.py @@ -71,10 +71,6 @@ def __init__(self, version: str | None = None) -> None: ) FilterDesignBase._active_design.close() - if version is not None: - # Clear global state before initialization - settings.aedt_version = None - FilterDesignBase._active_design = self self.version = version if version else settings.aedt_version @@ -137,16 +133,27 @@ def _create_design(self, desktop_version, desktop_process_id): ------- :class:``AEDT`` design object """ - settings.use_grpc_api = None - if isinstance(FilterDesignBase._active_design, LumpedDesign): - return Circuit(version=desktop_version, aedt_process_id=desktop_process_id) - elif isinstance(FilterDesignBase._active_design, DistributedDesign): - if getattr(self, "insert_hfss_3dl_design", True): - return Hfss3dLayout(version=desktop_version, aedt_process_id=desktop_process_id) - elif getattr(self, "insert_hfss_design", True): - return Hfss(version=desktop_version, aedt_process_id=desktop_process_id) - elif getattr(self, "insert_circuit_design", True): + # Preserve the current version to ensure proper session reconnection. + original_version = settings.aedt_version + original_use_grpc = settings.use_grpc_api + try: + settings.aedt_version = desktop_version + # FilterSolutions starts AEDT in COM mode; force COM when reconnecting. + settings.use_grpc_api = False + if isinstance(FilterDesignBase._active_design, LumpedDesign): return Circuit(version=desktop_version, aedt_process_id=desktop_process_id) + elif isinstance(FilterDesignBase._active_design, DistributedDesign): + if getattr(self, "insert_hfss_3dl_design", True): + return Hfss3dLayout(version=desktop_version, aedt_process_id=desktop_process_id) + elif getattr(self, "insert_hfss_design", True): + return Hfss(version=desktop_version, aedt_process_id=desktop_process_id) + elif getattr(self, "insert_circuit_design", True): + return Circuit(version=desktop_version, aedt_process_id=desktop_process_id) + return None + finally: + # Restore the original version in case of early exit or exception. + settings.aedt_version = original_version + settings.use_grpc_api = original_use_grpc class LumpedDesign(FilterDesignBase): diff --git a/src/ansys/aedt/core/filtersolutions_core/dll_interface.py b/src/ansys/aedt/core/filtersolutions_core/dll_interface.py index 5b0d0fdbc4ac..da12910a62bb 100644 --- a/src/ansys/aedt/core/filtersolutions_core/dll_interface.py +++ b/src/ansys/aedt/core/filtersolutions_core/dll_interface.py @@ -81,13 +81,17 @@ def _init_dll_path(self, version): ) self.dll_path = os.path.join(aedt_versions.installed_versions[version], "nuhertz", "FilterSolutionsAPI.dll") print("DLL Path:", self.dll_path) + # Ensure the DLL directory is in PATH so Borland BPL dependencies are found + dll_dir = os.path.dirname(self.dll_path) + if dll_dir not in os.environ.get("PATH", ""): + os.environ["PATH"] = dll_dir + os.pathsep + os.environ.get("PATH", "") if not os.path.isfile(self.dll_path): raise RuntimeError(f"The 'FilterSolutions' API DLL was not found at {self.dll_path}.") # pragma: no cover self._version = version def _init_dll(self, show_gui) -> None: """Load DLL and initialize application parameters to default values.""" - self._dll = ctypes.cdll.LoadLibrary(self.dll_path) + self._dll = ctypes.CDLL(self.dll_path, winmode=0) self._define_dll_functions() self.show_gui = show_gui if show_gui: # pragma: no cover diff --git a/src/ansys/aedt/core/filtersolutions_core/export_to_aedt.py b/src/ansys/aedt/core/filtersolutions_core/export_to_aedt.py index 19cfac016030..fb50ed0deb04 100644 --- a/src/ansys/aedt/core/filtersolutions_core/export_to_aedt.py +++ b/src/ansys/aedt/core/filtersolutions_core/export_to_aedt.py @@ -1758,15 +1758,15 @@ def substrate_er(self) -> SubstrateType | str: substrate_er_value_str = create_string_buffer(100) status = self._dll.getEr(substrate_er_value_str, byref(substrate_er_index), 100) self._dll_interface.raise_error(status) - if substrate_er_index.value in [e.value for e in SubstrateEr]: + if substrate_er_index.value in SubstrateEr._value2member_map_: return SubstrateEr(substrate_er_index.value) else: return substrate_er_value_str.value.decode("ascii") @substrate_er.setter def substrate_er(self, substrate_input: SubstrateEr | str) -> None: - if substrate_input in list(SubstrateEr): - substrate_er_index = SubstrateEr(substrate_input).value + if isinstance(substrate_input, SubstrateEr): + substrate_er_index = substrate_input.value substrate_er_value = "" elif isinstance(substrate_input, str): substrate_er_value = substrate_input @@ -1801,15 +1801,15 @@ def substrate_resistivity(self) -> SubstrateResistivity | str: substrate_resistivity_value_str = create_string_buffer(100) status = self._dll.getResistivity(substrate_resistivity_value_str, byref(substrate_resistivity_index), 100) self._dll_interface.raise_error(status) - if substrate_resistivity_index.value in [e.value for e in SubstrateResistivity]: + if substrate_resistivity_index.value in SubstrateResistivity._value2member_map_: return SubstrateResistivity(substrate_resistivity_index.value) else: return substrate_resistivity_value_str.value.decode("ascii") @substrate_resistivity.setter def substrate_resistivity(self, substrate_input: SubstrateResistivity | str) -> None: - if substrate_input in list(SubstrateResistivity): - substrate_resistivity_index = SubstrateResistivity(substrate_input).value + if isinstance(substrate_input, SubstrateResistivity): + substrate_resistivity_index = substrate_input.value substrate_resistivity_value = "" elif isinstance(substrate_input, str): substrate_resistivity_value = substrate_input @@ -1844,15 +1844,15 @@ def substrate_loss_tangent(self) -> SubstrateEr | str: substrate_loss_tangent_value_str = create_string_buffer(100) status = self._dll.getLossTangent(substrate_loss_tangent_value_str, byref(substrate_loss_tangent_index), 100) self._dll_interface.raise_error(status) - if substrate_loss_tangent_index.value in [e.value for e in SubstrateEr]: + if substrate_loss_tangent_index.value in SubstrateEr._value2member_map_: return SubstrateEr(substrate_loss_tangent_index.value) else: return substrate_loss_tangent_value_str.value.decode("ascii") @substrate_loss_tangent.setter def substrate_loss_tangent(self, substrate_input: SubstrateEr | str) -> None: - if substrate_input in list(SubstrateEr): - substrate_loss_tangent_index = SubstrateEr(substrate_input).value + if isinstance(substrate_input, SubstrateEr): + substrate_loss_tangent_index = substrate_input.value substrate_loss_tangent_value = "" elif isinstance(substrate_input, str): substrate_loss_tangent_value = substrate_input diff --git a/tests/system/filter_solutions/conftest.py b/tests/system/filter_solutions/conftest.py index 120064a4ba85..43642b78ee73 100644 --- a/tests/system/filter_solutions/conftest.py +++ b/tests/system/filter_solutions/conftest.py @@ -23,13 +23,22 @@ # SOFTWARE. import locale +import time +import psutil import pytest +from ansys.aedt.core import settings from ansys.aedt.core.filtersolutions import DistributedDesign +from ansys.aedt.core.filtersolutions import FilterDesignBase from ansys.aedt.core.filtersolutions import LumpedDesign +import ansys.aedt.core.filtersolutions_core as filtersolutions_core from tests.conftest import DESKTOP_VERSION +# Filter Solutions export attaches PyAEDT to the AEDT process started by the DLL. +# That session is typically COM-based (no gRPC port), so force COM for reconnection +# even when the global test config has use_grpc enabled. +settings.use_grpc_api = False # Ensure US English locale (with cross-platform fallbacks) for _loc in ("en_US.UTF-8", "English_United States.1252", "en_US"): try: @@ -42,13 +51,87 @@ locale.setlocale(locale.LC_ALL, "C") +def _wait_for_process_exit(process_id: int, timeout: float = 30.0) -> None: + """Wait until an AEDT process has fully exited.""" + try: + psutil.Process(process_id).wait(timeout=timeout) + except (psutil.NoSuchProcess, psutil.TimeoutExpired): + pass + + +def _terminate_aedt_processes(*process_ids: int) -> None: + """Terminate AEDT processes started by FilterSolutions export.""" + for process_id in process_ids: + if not process_id: + continue + try: + psutil.Process(process_id).kill() + except psutil.NoSuchProcess: + pass + _wait_for_process_exit(process_id) + + +def _reinitialize_filtersolutions_dll() -> None: + """Reinitialize the in-process FilterSolutions DLL after an AEDT export session.""" + dll = filtersolutions_core._internal_dll_interface + if dll is not None: + dll.restore_defaults() + FilterDesignBase._active_design = None + + +def _running_aedt_process_ids() -> list[int]: + process_ids = [] + for proc in psutil.process_iter(["pid", "exe", "name"]): + if _is_aedt_process(proc): + process_ids.append(proc.info["pid"]) + return process_ids + + +def prepare_filtersolutions_for_export() -> None: + """Ensure no stale AEDT export sessions interfere with the next export test.""" + _terminate_aedt_processes(*_running_aedt_process_ids()) + _reinitialize_filtersolutions_dll() + time.sleep(1) + + +def _is_aedt_process(process: psutil.Process) -> bool: + try: + exe = (process.info.get("exe") or "").lower() + except (psutil.NoSuchProcess, psutil.AccessDenied): + return False + return "ansys inc" in exe or "ansysem" in exe + + +def release_exported_design(design_app) -> None: + """Detach PyAEDT from an exported design without corrupting the FilterSolutions DLL. + + ``close_desktop()`` shuts down AEDT while the DLL still holds references to that + session, which leads to access violations and AEDT communications failures in + subsequent tests. This helper releases the PyAEDT connection, terminates the + exported AEDT process, and reinitializes the DLL through ``restore_defaults()``. + """ + process_id = design_app.desktop_class.aedt_process_id + try: + design_app.desktop_class.release_desktop(close_projects=False, close_on_exit=False) + except Exception: + pass + _terminate_aedt_processes(process_id) + _reinitialize_filtersolutions_dll() + + @pytest.fixture def lumped_design(): """Fixture for creating a LumpedDesign object.""" - return LumpedDesign(DESKTOP_VERSION) + settings.use_grpc_api = False + design = LumpedDesign(DESKTOP_VERSION) + yield design + design.close() @pytest.fixture def distributed_design(): """Fixture for creating a DistributedDesign object.""" - return DistributedDesign(DESKTOP_VERSION) + settings.use_grpc_api = False + design = DistributedDesign(DESKTOP_VERSION) + yield design + design.close() diff --git a/tests/system/filter_solutions/test_desktop_types.py b/tests/system/filter_solutions/test_desktop_types.py index ab6baa4c6211..e5c3ddcf779a 100644 --- a/tests/system/filter_solutions/test_desktop_types.py +++ b/tests/system/filter_solutions/test_desktop_types.py @@ -22,22 +22,29 @@ # OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE # SOFTWARE. -import os import pytest from ansys.aedt.core import Circuit from ansys.aedt.core import Hfss from ansys.aedt.core import Hfss3dLayout +from ansys.aedt.core.filtersolutions_core.export_to_aedt import PartLibraries from ansys.aedt.core.generic.settings import is_linux from tests.conftest import DESKTOP_VERSION +from tests.system.filter_solutions.conftest import prepare_filtersolutions_for_export +from tests.system.filter_solutions.conftest import release_exported_design +from tests.system.filter_solutions.resources import read_resource_file -ON_CI = os.getenv("ON_CI", "false").lower() == "true" + +@pytest.fixture(autouse=True) +def _prepare_filtersolutions_export_tests(): + prepare_filtersolutions_for_export() + yield + prepare_filtersolutions_for_export() @pytest.mark.skipif(is_linux, reason="FilterSolutions API is not supported on Linux.") @pytest.mark.skipif(DESKTOP_VERSION < "2026.1", reason="Skipped on versions earlier than 2026.1") -@pytest.mark.skipif(ON_CI, reason="Lead to access violation issues on CI runners") class TestClass: def test_lumped_exported_desktop(self, lumped_design): schem_name = lumped_design.export_to_aedt.schematic_name @@ -51,7 +58,17 @@ def test_lumped_exported_desktop(self, lumped_design): assert variables["C1"].si_value == pytest.approx(1.967e-12) assert variables["L2"].si_value == pytest.approx(1.288e-8) assert variables["C3"].si_value == pytest.approx(6.366e-12) - circuit.desktop_class.close_desktop() + release_exported_design(circuit) + + def test_import_tuned_variables(self, lumped_design): + lumped_design.export_to_aedt.simulate_after_export_enabled = True + lumped_design.export_to_aedt.optimize_after_export_enabled = True + lumped_design.export_to_aedt.part_libraries = PartLibraries.LUMPED + circuit = lumped_design.export_to_aedt.export_design() + assert lumped_design.export_to_aedt.import_tuned_variables().splitlines() == read_resource_file( + "imported_netlist.ckt", "Lumped" + ) + release_exported_design(circuit) def test_distributed_circuit_exported_desktop(self, distributed_design): schem_name = distributed_design.export_to_aedt.schematic_name @@ -69,7 +86,7 @@ def test_distributed_circuit_exported_desktop(self, distributed_design): assert variables["S1"].si_value == pytest.approx(3.362e-3) assert variables["S2"].si_value == pytest.approx(2.172e-2) assert variables["S3"].si_value == pytest.approx(1.008e-2) - circuit.desktop_class.close_desktop() + release_exported_design(circuit) def test_distributed_hfss3dl_exported_desktop(self, distributed_design): schem_name = distributed_design.export_to_aedt.schematic_name @@ -88,7 +105,7 @@ def test_distributed_hfss3dl_exported_desktop(self, distributed_design): assert variables["S1"].si_value == pytest.approx(3.36225452227e-3) assert variables["S2"].si_value == pytest.approx(2.17231965814e-2) assert variables["S3"].si_value == pytest.approx(1.00773795179e-2) - hfss3dl.desktop_class.close_desktop() + release_exported_design(hfss3dl) def test_distributed_hfss_exported_desktop(self, distributed_design): schem_name = distributed_design.export_to_aedt.schematic_name @@ -107,4 +124,4 @@ def test_distributed_hfss_exported_desktop(self, distributed_design): assert variables["S1"].si_value == pytest.approx(3.36225452227e-3) assert variables["S2"].si_value == pytest.approx(2.17231965814e-2) assert variables["S3"].si_value == pytest.approx(1.00773795179e-2) - hfss.desktop_class.close_desktop() + release_exported_design(hfss) diff --git a/tests/system/filter_solutions/test_export_to_aedt/test_export_to_aedt.py b/tests/system/filter_solutions/test_export_to_aedt/test_export_to_aedt.py index c326a8f2fdb3..075535170ec5 100644 --- a/tests/system/filter_solutions/test_export_to_aedt/test_export_to_aedt.py +++ b/tests/system/filter_solutions/test_export_to_aedt/test_export_to_aedt.py @@ -35,7 +35,6 @@ from ansys.aedt.core.generic.settings import is_linux from tests.conftest import DESKTOP_VERSION from tests.conftest import SKIP_MODELITHICS -from tests.system.filter_solutions.resources import read_resource_file from tests.system.filter_solutions.resources import resource_path ON_CI = os.getenv("ON_CI", "false").lower() == "true" @@ -474,17 +473,6 @@ def test_save_library_parts_config(self, lumped_design): assert lumped_design.export_to_aedt.substrate_dielectric_height == "3 mm" assert lumped_design.export_to_aedt.substrate_loss_tangent == "0.065 " - @pytest.mark.skipif(DESKTOP_VERSION < "2026.1", reason="Skipped on versions earlier than 2026.1") - def test_import_tuned_variables(self, lumped_design): - lumped_design.export_to_aedt.simulate_after_export_enabled = True - lumped_design.export_to_aedt.optimize_after_export_enabled = True - lumped_design.export_to_aedt.part_libraries = PartLibraries.LUMPED - app = lumped_design.export_to_aedt.export_design() - assert lumped_design.export_to_aedt.import_tuned_variables().splitlines() == read_resource_file( - "imported_netlist.ckt", "Lumped" - ) - app.desktop_class.close_desktop() - def test_part_libraries(self, lumped_design): assert lumped_design.export_to_aedt.part_libraries == PartLibraries.LUMPED assert len(PartLibraries) == 3 diff --git a/tests/unit/test_desktop.py b/tests/unit/test_desktop.py index f588327aa890..65972811161e 100644 --- a/tests/unit/test_desktop.py +++ b/tests/unit/test_desktop.py @@ -102,6 +102,7 @@ def test_get_available_toolkits() -> None: def test_desktop_odesktop_retries(mock_settings, mock_sleep, mock_desktop) -> None: """Test Desktop.odesktop property retries to get the odesktop object.""" desktop = Desktop() + desktop.is_grpc_api = True desktop.grpc_plugin = MagicMock() aedt_app = MagicMock() mock_odesktop = PropertyMock(name="oui", side_effect=[Exception("Failure"), aedt_app]) @@ -116,6 +117,7 @@ def test_desktop_odesktop_retries(mock_settings, mock_sleep, mock_desktop) -> No def test_desktop_odesktop_setter(mock_desktop) -> None: """Test Desktop.odesktop property retries to get the odesktop object.""" desktop = Desktop() + desktop.is_grpc_api = False aedt_app = MagicMock() desktop.grpc_plugin = MagicMock()