diff --git a/.github/workflows/ci_cd.yml b/.github/workflows/ci_cd.yml index c8ce84d91..da8667ef6 100644 --- a/.github/workflows/ci_cd.yml +++ b/.github/workflows/ci_cd.yml @@ -697,7 +697,7 @@ jobs: coverage: name: Merging coverage - needs: [remote-connect, embedding-tests, embedding-scripts-tests, launch-tests] + needs: [remote-connect, embedding-tests, embedding-scripts-tests, embedding-rpc-tests, launch-tests] runs-on: ubuntu-latest steps: - name: Checkout repository diff --git a/doc/changelog.d/1017.added.md b/doc/changelog.d/1017.added.md new file mode 100644 index 000000000..9d3ea6e8c --- /dev/null +++ b/doc/changelog.d/1017.added.md @@ -0,0 +1 @@ +add `globals` kwarg to app and adjust `ideconfig` behavior \ No newline at end of file diff --git a/examples/embedding_n_remote/embedding_remote.py b/examples/embedding_n_remote/embedding_remote.py index a33b73161..d5c16a0b8 100644 --- a/examples/embedding_n_remote/embedding_remote.py +++ b/examples/embedding_n_remote/embedding_remote.py @@ -172,8 +172,7 @@ def write_file_contents_to_console(path, number_lines=-1): # Find the mechanical installation path & version. # Open an embedded instance of Mechanical and set global variables. -app = mech.App() -app.update_globals(globals()) +app = mech.App(globals=globals()) print(app) diff --git a/src/ansys/mechanical/core/embedding/app.py b/src/ansys/mechanical/core/embedding/app.py index 6ce99e50b..863f86f86 100644 --- a/src/ansys/mechanical/core/embedding/app.py +++ b/src/ansys/mechanical/core/embedding/app.py @@ -89,6 +89,11 @@ def _start_application(configuration: AddinConfiguration, version, db_file) -> " return Ansys.Mechanical.Embedding.Application(db_file) +def is_initialized(): + """Check if the app has been initialized.""" + return True if len(INSTANCES) != 0 else False + + class GetterWrapper(object): """Wrapper class around an attribute of an object.""" @@ -124,6 +129,9 @@ class App: private_appdata : bool, optional Setting for a temporary AppData directory. Default is False. Enables running parallel instances of Mechanical. + globals : dict, optional + Global variables to be updated. For example, globals(). + Replaces "app.update_globals(globals())". config : AddinConfiguration, optional Configuration for addins. By default "Mechanical" is used and ACT Addins are disabled. copy_profile : bool, optional @@ -140,6 +148,10 @@ class App: >>> app = App(private_appdata=True, copy_profile=False) + Update the global variables with globals + + >>> app = App(globals=globals()) + Create App with "Mechanical" configuration and no ACT Addins >>> from ansys.mechanical.core.embedding import AddinConfiguration @@ -163,6 +175,7 @@ def __init__(self, db_file=None, private_appdata=False, **kwargs): return if len(INSTANCES) > 0: raise Exception("Cannot have more than one embedded mechanical instance!") + version = kwargs.get("version") if version is not None: try: @@ -192,6 +205,10 @@ def __init__(self, db_file=None, private_appdata=False, **kwargs): self._updated_scopes: typing.List[typing.Dict[str, typing.Any]] = [] self._subscribe() + globals = kwargs.get("globals") + if globals is not None and is_initialized(): + self.update_globals(globals) + def __repr__(self): """Get the product info.""" import clr @@ -501,8 +518,7 @@ def update_globals( Examples -------- >>> from ansys.mechanical.core import App - >>> app = App() - >>> app.update_globals(globals()) + >>> app = App(globals=globals()) """ self._updated_scopes.append(globals_dict) globals_dict.update(global_variables(self, enums)) @@ -580,8 +596,7 @@ def print_tree(self, node=None, max_lines=80, lines_count=0, indentation=""): Examples -------- >>> from ansys.mechanical.core import App - >>> app = App() - >>> app.update_globals(globals()) + >>> app = App(globals=globals()) >>> app.print_tree() ... ├── Project ... | ├── Model diff --git a/src/ansys/mechanical/core/embedding/enum_importer.py b/src/ansys/mechanical/core/embedding/enum_importer.py index 61eca5605..ea98be95a 100644 --- a/src/ansys/mechanical/core/embedding/enum_importer.py +++ b/src/ansys/mechanical/core/embedding/enum_importer.py @@ -25,7 +25,6 @@ A useful subset of what is imported by Ansys Inc/v{NNN}/ACT/apis/Mechanical.py """ - import clr clr.AddReference("Ansys.Mechanical.DataModel") diff --git a/src/ansys/mechanical/core/embedding/global_importer.py b/src/ansys/mechanical/core/embedding/global_importer.py new file mode 100644 index 000000000..f7953ec44 --- /dev/null +++ b/src/ansys/mechanical/core/embedding/global_importer.py @@ -0,0 +1,50 @@ +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""Import Mechanical globals.""" + +from ansys.mechanical.core.embedding.app import is_initialized + +if not is_initialized(): + raise Exception("Globals cannot be imported until the embedded app is initialized.") + +import clr + +clr.AddReference("Ansys.Mechanical.DataModel") +clr.AddReference("Ansys.ACT.Interfaces") + + +clr.AddReference("System.Collections") +clr.AddReference("Ansys.ACT.WB1") +clr.AddReference("Ansys.Mechanical.DataModel") + +# from Ansys.ACT.Mechanical import Transaction +# When ansys-pythonnet issue #14 is fixed, uncomment above +from Ansys.ACT.Core.Math import Point2D, Point3D # noqa isort: skip +from Ansys.ACT.Math import Vector3D # noqa isort: skip +from Ansys.Core.Units import Quantity # noqa isort: skip +from Ansys.Mechanical.DataModel import MechanicalEnums # noqa isort: skip +from Ansys.Mechanical.Graphics import Point, SectionPlane # noqa isort: skip + +from ansys.mechanical.core.embedding.transaction import Transaction # noqa isort: skip + +import System # noqa isort: skip +import Ansys # noqa isort: skip diff --git a/src/ansys/mechanical/core/embedding/imports.py b/src/ansys/mechanical/core/embedding/imports.py index 60ec3351f..179e9743c 100644 --- a/src/ansys/mechanical/core/embedding/imports.py +++ b/src/ansys/mechanical/core/embedding/imports.py @@ -46,36 +46,37 @@ def global_variables(app: "ansys.mechanical.core.App", enums: bool = False) -> t To also import all the enums, set the parameter enums to true. """ vars = global_entry_points(app) - import clr # isort: skip - - clr.AddReference("System.Collections") - clr.AddReference("Ansys.ACT.WB1") - clr.AddReference("Ansys.Mechanical.DataModel") - # from Ansys.ACT.Mechanical import Transaction - # When ansys-pythonnet issue #14 is fixed, uncomment above - from Ansys.ACT.Core.Math import Point2D, Point3D - from Ansys.ACT.Math import Vector3D - from Ansys.ACT.Mechanical.Fields import VariableDefinitionType - from Ansys.Core.Units import Quantity - from Ansys.Mechanical.DataModel import MechanicalEnums - from Ansys.Mechanical.Graphics import Point, SectionPlane - - import System # isort: skip - import Ansys # isort: skip - - vars["Quantity"] = Quantity - vars["System"] = System - vars["Ansys"] = Ansys + + from ansys.mechanical.core.embedding.app import is_initialized + from ansys.mechanical.core.embedding.transaction import Transaction + vars["Transaction"] = Transaction - vars["MechanicalEnums"] = MechanicalEnums - # Graphics - vars["Point"] = Point - vars["SectionPlane"] = SectionPlane - # Math - vars["Point2D"] = Point2D - vars["Point3D"] = Point3D - vars["Vector3D"] = Vector3D - vars["VariableDefinitionType"] = VariableDefinitionType + + # Import modules if the app is initialized + if is_initialized(): + from ansys.mechanical.core.embedding.global_importer import ( + Ansys, + MechanicalEnums, + Point, + Point2D, + Point3D, + Quantity, + SectionPlane, + System, + Vector3D, + ) + + vars["Quantity"] = Quantity + vars["System"] = System + vars["Ansys"] = Ansys + vars["MechanicalEnums"] = MechanicalEnums + # Graphics + vars["Point"] = Point + vars["SectionPlane"] = SectionPlane + # Math + vars["Point2D"] = Point2D + vars["Point3D"] = Point3D + vars["Vector3D"] = Vector3D if enums: vars.update(get_all_enums()) @@ -95,32 +96,3 @@ def get_all_enums() -> typing.Dict[str, typing.Any]: if type(the_enum).__name__ == "CLRMetatype": enums[attr] = the_enum return enums - - -class Transaction: # When ansys-pythonnet issue #14 is fixed, this class will be removed - """ - A class to speed up bulk user interactions using Ansys ACT Mechanical Transaction. - - Example - ------- - >>> with Transaction() as transaction: - ... pass # Perform bulk user interactions here - ... - """ - - def __init__(self): - """Initialize the Transaction class.""" - import clr - - clr.AddReference("Ansys.ACT.WB1") - import Ansys - - self._transaction = Ansys.ACT.Mechanical.Transaction() - - def __enter__(self): - """Enter the context of the transaction.""" - return self - - def __exit__(self, exc_type, exc_val, exc_tb): - """Exit the context of the transaction and disposes of resources.""" - self._transaction.Dispose() diff --git a/src/ansys/mechanical/core/embedding/transaction.py b/src/ansys/mechanical/core/embedding/transaction.py new file mode 100644 index 000000000..d2fb1b69d --- /dev/null +++ b/src/ansys/mechanical/core/embedding/transaction.py @@ -0,0 +1,51 @@ +# Copyright (C) 2022 - 2025 ANSYS, Inc. and/or its affiliates. +# SPDX-License-Identifier: MIT +# +# +# Permission is hereby granted, free of charge, to any person obtaining a copy +# of this software and associated documentation files (the "Software"), to deal +# in the Software without restriction, including without limitation the rights +# to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +# copies of the Software, and to permit persons to whom the Software is +# furnished to do so, subject to the following conditions: +# +# The above copyright notice and this permission notice shall be included in all +# copies or substantial portions of the Software. +# +# THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +# AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +# LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +# OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE +# SOFTWARE. +"""A class to speed up bulk user interactions using Ansys ACT Mechanical Transaction.""" + + +class Transaction: # When ansys-pythonnet issue #14 is fixed, this class will be removed + """ + A class to speed up bulk user interactions using Ansys ACT Mechanical Transaction. + + Example + ------- + >>> with Transaction() as transaction: + ... pass # Perform bulk user interactions here + ... + """ + + def __init__(self): + """Initialize the Transaction class.""" + import clr + + clr.AddReference("Ansys.ACT.WB1") + import Ansys + + self._transaction = Ansys.ACT.Mechanical.Transaction() + + def __enter__(self): + """Enter the context of the transaction.""" + return self + + def __exit__(self, exc_type, exc_val, exc_tb): + """Exit the context of the transaction and disposes of resources.""" + self._transaction.Dispose() diff --git a/src/ansys/mechanical/core/ide_config.py b/src/ansys/mechanical/core/ide_config.py index ff8b87b21..3dd0b7a6e 100644 --- a/src/ansys/mechanical/core/ide_config.py +++ b/src/ansys/mechanical/core/ide_config.py @@ -28,6 +28,7 @@ import re import site import sys +import warnings import ansys.tools.path as atp import click @@ -151,13 +152,23 @@ def _cli_impl( stubs_location = get_stubs_location() # Get all revision numbers available in ansys-mechanical-stubs revns = get_stubs_versions(stubs_location) - # Check the IDE and raise an exception if it's not VS Code - if revision < min(revns) or revision > max(revns): + + # Check if the user revision number is less or greater than the min and max revisions + # in the ansys-mechanical-stubs package location + if revision < min(revns): raise Exception(f"PyMechanical Stubs are not available for {revision}") - elif ide != "vscode": + elif revision > max(revns): + warnings.warn( + f"PyMechanical Stubs are not available for {revision}. Using {max(revns)} instead.", + stacklevel=2, + ) + revision = max(revns) + + # Check the IDE and raise an exception if it's not VS Code + if ide != "vscode": raise Exception(f"{ide} is not supported at the moment.") - else: - return _vscode_impl(target, revision) + + return _vscode_impl(target, revision) @click.command() @@ -202,8 +213,11 @@ def cli(ide: str, target: str, revision: int) -> None: $ ansys-mechanical-ideconfig --ide vscode --target user --revision 251 """ - exe = atp.get_mechanical_path(allow_input=False, version=revision) - version = atp.version_from_path("mechanical", exe) + if not revision: + exe = atp.get_mechanical_path(allow_input=False, version=revision) + version = atp.version_from_path("mechanical", exe) + else: + version = revision return _cli_impl( ide, diff --git a/tests/embedding/test_app.py b/tests/embedding/test_app.py index 5a5e76b02..c8742ea09 100644 --- a/tests/embedding/test_app.py +++ b/tests/embedding/test_app.py @@ -31,6 +31,7 @@ import pytest +from ansys.mechanical.core.embedding.app import is_initialized from ansys.mechanical.core.embedding.cleanup_gui import cleanup_gui from ansys.mechanical.core.embedding.ui import _launch_ui import ansys.mechanical.core.embedding.utils as utils @@ -465,7 +466,7 @@ def test_tempfile_cleanup(tmp_path: pytest.TempPathFactory, run_subprocess): @pytest.mark.embedding_scripts -def test_attribute_error(tmp_path: pytest.TempPathFactory, pytestconfig, rootdir, run_subprocess): +def test_attribute_error(tmp_path: pytest.TempPathFactory, pytestconfig, rootdir): """Test cleanup function to remove the temporary mechdb file and folder.""" # Change directory to tmp_path os.chdir(tmp_path) @@ -485,7 +486,9 @@ def test_attribute_error(tmp_path: pytest.TempPathFactory, pytestconfig, rootdir # Run the script and assert the AttributeError is raised stdout, stderr = subprocess.Popen( - [sys.executable, tmp_file_script, version], stdout=subprocess.PIPE, stderr=subprocess.PIPE + [sys.executable, tmp_file_script, "--version", version], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, ).communicate() # Assert the AttributeError is raised @@ -530,3 +533,29 @@ def test_app_lock_file_open(embedded_app, tmp_path: pytest.TempPathFactory): # Assert a warning is emitted if the lock file is going to be removed with pytest.warns(UserWarning): embedded_app.open(project_file, remove_lock=True) + + +@pytest.mark.embedding +def test_app_initialized(embedded_app): + """Test the app is initialized.""" + assert is_initialized() + + +@pytest.mark.embedding +def test_app_not_initialized(run_subprocess, pytestconfig, rootdir): + """Test the app is not initialized.""" + version = pytestconfig.getoption("ansys_version") + embedded_py = os.path.join(rootdir, "tests", "scripts", "run_embedded_app.py") + + process, stdout, stderr = run_subprocess( + [ + sys.executable, + embedded_py, + "--version", + version, + "--test_not_initialized", + ] + ) + stdout = stdout.decode() + + assert "The app is not initialized" in stdout diff --git a/tests/embedding/test_appdata.py b/tests/embedding/test_appdata.py index 0d08710a1..a18a79daf 100644 --- a/tests/embedding/test_appdata.py +++ b/tests/embedding/test_appdata.py @@ -36,12 +36,33 @@ @pytest.mark.python_env def test_private_appdata(pytestconfig, run_subprocess, rootdir): """Test embedded instance does not save ShowTriad using a test-scoped Python environment.""" - version = pytestconfig.getoption("ansys_version") embedded_py = os.path.join(rootdir, "tests", "scripts", "run_embedded_app.py") - run_subprocess([sys.executable, embedded_py, version, "True", "Set"]) - process, stdout, stderr = run_subprocess([sys.executable, embedded_py, version, "True", "Run"]) + run_subprocess( + [ + sys.executable, + embedded_py, + "--version", + version, + "--private_appdata", + "True", + "--action", + "Set", + ] + ) + process, stdout, stderr = run_subprocess( + [ + sys.executable, + embedded_py, + "--version", + version, + "--private_appdata", + "True", + "--action", + "Run", + ] + ) stdout = stdout.decode() assert "ShowTriad value is True" in stdout @@ -54,9 +75,42 @@ def test_normal_appdata(pytestconfig, run_subprocess, rootdir): embedded_py = os.path.join(rootdir, "tests", "scripts", "run_embedded_app.py") - run_subprocess([sys.executable, embedded_py, version, "False", "Set"]) - process, stdout, stderr = run_subprocess([sys.executable, embedded_py, version, "False", "Run"]) - run_subprocess([sys.executable, embedded_py, version, "False", "Reset"]) + run_subprocess( + [ + sys.executable, + embedded_py, + "--version", + version, + "--private_appdata", + "False", + "--action", + "Set", + ] + ) + process, stdout, stderr = run_subprocess( + [ + sys.executable, + embedded_py, + "--version", + version, + "--private_appdata", + "False", + "--action", + "Run", + ] + ) + run_subprocess( + [ + sys.executable, + embedded_py, + "--version", + version, + "--private_appdata", + "False", + "--action", + "Reset", + ] + ) stdout = stdout.decode() # Assert ShowTriad was set to False for regular embedded session diff --git a/tests/embedding/test_globals.py b/tests/embedding/test_globals.py index aa0f55ff9..67eb220ec 100644 --- a/tests/embedding/test_globals.py +++ b/tests/embedding/test_globals.py @@ -21,10 +21,13 @@ # SOFTWARE. """Embedding tests for global variables associated with Mechanical""" +import subprocess +import sys + import pytest from ansys.mechanical.core import global_variables -from ansys.mechanical.core.embedding.imports import Transaction +from ansys.mechanical.core.embedding.transaction import Transaction @pytest.mark.embedding @@ -62,3 +65,20 @@ def test_global_variable_transaction(embedded_app): DataModel.Project.Name = "New Project" project_name = DataModel.Project.Name assert project_name == "New Project" + + +@pytest.mark.embedding_scripts +def test_global_importer_exception(rootdir): + """Test an exception is raised in global_importer when the embedded app is not initialized.""" + # Path to global_importer.py + global_importer = ( + rootdir / "src" / "ansys" / "mechanical" / "core" / "embedding" / "global_importer.py" + ) + + # Run the global_importer.py script without the app being initialized + stdout, stderr = subprocess.Popen( + [sys.executable, global_importer], stdout=subprocess.PIPE, stderr=subprocess.PIPE + ).communicate() + + # Assert the exception is raised + assert "Globals cannot be imported until the embedded app is initialized." in stderr.decode() diff --git a/tests/scripts/run_embedded_app.py b/tests/scripts/run_embedded_app.py index cd67ac8ff..e95d3308d 100644 --- a/tests/scripts/run_embedded_app.py +++ b/tests/scripts/run_embedded_app.py @@ -21,48 +21,71 @@ # SOFTWARE. """Launch embedded instance.""" -# import logging -import sys +import argparse import ansys.mechanical.core as pymechanical +from ansys.mechanical.core.embedding.app import is_initialized -# from ansys.mechanical.core.embedding.logger import Configuration - -def launch_app(version, private_appdata): +def launch_app(args): """Launch embedded instance of app.""" - # Configuration.configure(level=logging.DEBUG, to_stdout=True, base_directory=None) - app = pymechanical.App(version=version, private_appdata=private_appdata, copy_profile=True) + if args.debug: + import logging + + from ansys.mechanical.core.embedding.logger import Configuration + + Configuration.configure(level=logging.DEBUG, to_stdout=True, base_directory=None) + + if args.test_not_initialized: + init_msg = "The app is initialized" if is_initialized() else "The app is not initialized" + print(init_msg) + + app = pymechanical.App( + version=int(args.version), + private_appdata=args.private_appdata, + copy_profile=True, + globals=globals(), + ) + return app -def set_showtriad(version, appdata_option, value): +def set_showtriad(args, value): """Launch embedded instance of app & set ShowTriad to False.""" - app = launch_app(version, appdata_option) - app.ExtAPI.Graphics.ViewOptions.ShowTriad = value + app = launch_app(args) + ExtAPI.Graphics.ViewOptions.ShowTriad = value app.close() -def print_showtriad(version, appdata_option): +def print_showtriad(args): """Return ShowTriad value.""" - app = launch_app(version, appdata_option) + app = launch_app(args) print("ShowTriad value is " + str(app.ExtAPI.Graphics.ViewOptions.ShowTriad)) app.close() if __name__ == "__main__": - version = int(sys.argv[1]) - if len(sys.argv) == 2: - launch_app(version, False) - sys.exit(0) + # Set up argparse for command line arguments from subprocess. + parser = argparse.ArgumentParser(description="Launch embedded instance of app.") + parser.add_argument("--version", type=str, help="Mechanical version") + parser.add_argument("--private_appdata", type=str, help="Private appdata") + parser.add_argument("--action", type=str, help="Action to perform") + parser.add_argument("--debug", action="store_true") # 'store_true' implies default=False + parser.add_argument( + "--test_not_initialized", action="store_true" + ) # 'store_true' implies default=False + + # Get and set args from subprocess + args = parser.parse_args() + action = args.action - appdata_option = sys.argv[2] - action = sys.argv[3] + args.private_appdata = args.private_appdata == "True" - private_appdata = appdata_option == "True" if action == "Set": - set_showtriad(version, private_appdata, False) + set_showtriad(args, False) elif action == "Run": - print_showtriad(version, private_appdata) + print_showtriad(args) elif action == "Reset": - set_showtriad(version, private_appdata, True) + set_showtriad(args, True) + else: + launch_app(args) diff --git a/tests/test_cli.py b/tests/test_cli.py index f96ceeb89..de0e76183 100644 --- a/tests/test_cli.py +++ b/tests/test_cli.py @@ -25,9 +25,11 @@ import subprocess import sys +import ansys.tools.path as atp +from click.testing import CliRunner import pytest -from ansys.mechanical.core.ide_config import _cli_impl as ideconfig_cli_impl +from ansys.mechanical.core.ide_config import cli as ideconfig_cli from ansys.mechanical.core.ide_config import get_stubs_location, get_stubs_versions from ansys.mechanical.core.run import _cli_impl @@ -285,32 +287,19 @@ def get_settings_location() -> str: @pytest.mark.cli -def test_ideconfig_cli_ide_exception(capfd, pytestconfig): +def test_ideconfig_cli_ide_exception(pytestconfig): """Test IDE configuration raises an exception for anything but vscode.""" revision = int(pytestconfig.getoption("ansys_version")) - with pytest.raises(Exception): - ideconfig_cli_impl( - ide="pycharm", - target="user", - revision=revision, - ) - + unsupported_ide = "pycharm" -def test_ideconfig_cli_version_exception(pytestconfig): - """Test the IDE configuration raises an exception when the version is out of bounds.""" - revision = int(pytestconfig.getoption("ansys_version")) - stubs_location = get_stubs_location() - stubs_revns = get_stubs_versions(stubs_location) + # Set up the runner for click + runner = CliRunner() + result = runner.invoke( + ideconfig_cli, ["--ide", unsupported_ide, "--target", "user", "--revision", str(revision)] + ) - # If revision number is greater than the maximum stubs revision number - # assert an exception is raised - if revision > max(stubs_revns): - with pytest.raises(Exception): - ideconfig_cli_impl( - ide="vscode", - target="user", - revision=revision, - ) + assert result.exception is not None + assert f"{unsupported_ide} is not supported at the moment" in result.exception.args[0] @pytest.mark.cli @@ -320,51 +309,41 @@ def test_ideconfig_cli_user_settings(capfd, pytestconfig): # Get the revision number revision = int(pytestconfig.getoption("ansys_version")) stubs_location = get_stubs_location() - - # Run the IDE configuration command for the user settings type - ideconfig_cli_impl( - ide="vscode", - target="user", - revision=revision, - ) - - # Get output of the IDE configuration command - out, err = capfd.readouterr() - out = out.replace("\\\\", "\\") - # Get the path to the settings.json file based on operating system env vars settings_json = get_settings_location() - assert f"Update {settings_json} with the following information" in out - assert str(stubs_location) in out + runner = CliRunner() + result = runner.invoke( + ideconfig_cli, ["--ide", "vscode", "--target", "user", "--revision", str(revision)] + ) + stdout = result.output.replace("\\\\", "\\") + + assert result.exit_code == 0 + assert f"Update {settings_json} with the following information" in stdout + assert str(stubs_location) in stdout @pytest.mark.cli @pytest.mark.version_range(MIN_STUBS_REVN, MAX_STUBS_REVN) -def test_ideconfig_cli_workspace_settings(capfd, pytestconfig): +def test_ideconfig_cli_workspace_settings(pytestconfig): """Test the IDE configuration prints correct information for workplace settings.""" # Set the revision number revision = int(pytestconfig.getoption("ansys_version")) stubs_location = get_stubs_location() - - # Run the IDE configuration command - ideconfig_cli_impl( - ide="vscode", - target="workspace", - revision=revision, - ) - - # Get output of the IDE configuration command - out, err = capfd.readouterr() - out = out.replace("\\\\", "\\") - # Get the path to the settings.json file based on the current directory & .vscode folder settings_json = Path.cwd() / ".vscode" / "settings.json" + runner = CliRunner() + result = runner.invoke( + ideconfig_cli, ["--ide", "vscode", "--target", "workspace", "--revision", str(revision)] + ) + stdout = result.output.replace("\\\\", "\\") + # Assert the correct settings.json file and stubs location is in the output - assert f"Update {settings_json} with the following information" in out - assert str(stubs_location) in out - assert "Please ensure the .vscode folder is in the root of your project or repository" in out + assert result.exit_code == 0 + assert f"Update {settings_json} with the following information" in stdout + assert str(stubs_location) in stdout + assert "Please ensure the .vscode folder is in the root of your project or repository" in stdout @pytest.mark.cli @@ -443,6 +422,66 @@ def test_ideconfig_cli_default(test_env, run_subprocess, rootdir, pytestconfig): # Decode stdout and fix extra backslashes in paths stdout = subprocess_output_ideconfig[1].decode().replace("\\\\", "\\") - assert revision in stdout + # Get the version from the Mechanical executable + exe = atp.get_mechanical_path(allow_input=False, version=revision) + if exe: + exe_version = atp.version_from_path("mechanical", exe) + assert str(exe_version) in stdout + assert str(settings_json_fragment) in stdout assert venv_loc in stdout + + +@pytest.mark.cli +def test_ideconfig_revision_max_min(): + """Test the IDE configuration location when no arguments are supplied.""" + # Set the revision number to be greater than the maximum stubs revision number + stubs_location = get_stubs_location() + # Get the path to the settings.json file based on operating system env vars + settings_json = get_settings_location() + + # Set the revision number to be greater than the maximum stubs revision number + gt_max_revision = MAX_STUBS_REVN + 10 + # Set the revision number to be less than the minimum stubs revision number + lt_min_revision = MIN_STUBS_REVN - 10 + + # Set up the runner for click + runner = CliRunner() + + # Assert a warning is raised when the revision is greater than the maximum version in the stubs + with pytest.warns(UserWarning): + result = runner.invoke( + ideconfig_cli, + ["--ide", "vscode", "--target", "user", "--revision", str(gt_max_revision)], + ) + stdout = result.output.replace("\\\\", "\\") + + assert result.exit_code == 0 + assert f"Update {settings_json} with the following information" in stdout + assert str(stubs_location) in stdout + assert str(MAX_STUBS_REVN) in stdout + + # Assert an exception is raised when the revision is less than the minimum version in the stubs + result = runner.invoke( + ideconfig_cli, ["--ide", "vscode", "--target", "user", "--revision", str(lt_min_revision)] + ) + assert result.exception is not None + assert f"PyMechanical Stubs are not available for {lt_min_revision}" in result.exception.args[0] + + +@pytest.mark.cli +def test_ideconfig_no_revision(): + """Test the IDE configuration when no revision is supplied.""" + # Set the revision number to be greater than the maximum stubs revision number + stubs_location = get_stubs_location() + # Get the path to the settings.json file based on operating system env vars + settings_json = get_settings_location() + + # Set the runner for click + runner = CliRunner() + result = runner.invoke(ideconfig_cli, ["--ide", "vscode", "--target", "user"]) + stdout = result.output.replace("\\\\", "\\") + + assert result.exit_code == 0 + assert f"Update {settings_json} with the following information" in stdout + assert str(stubs_location) in stdout