diff options
Diffstat (limited to 'sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py')
-rw-r--r-- | sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py | 321 |
1 files changed, 236 insertions, 85 deletions
diff --git a/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py b/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py index f730eed40..46b644584 100644 --- a/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py +++ b/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py @@ -7,49 +7,65 @@ import shutil import sys import os import importlib +import platform from pathlib import Path -from configparser import ConfigParser from unittest.mock import patch from unittest import mock +sys.path.append(os.fspath(Path(__file__).resolve().parents[2])) +from init_paths import init_test_paths, _get_qt_lib_dir # noqa: E402 +init_test_paths(False) -class ConfigFile: - def __init__(self, config_file: Path) -> None: - self.config_file = config_file - self.parser = ConfigParser(comment_prefixes="/", allow_no_value=True) - self.parser.read(self.config_file) - def get_value(self, section: str, key: str): - return str(self.parser.get(section, key)) +def is_pyenv_python(): + pyenv_root = os.environ.get("PYENV_ROOT") + if pyenv_root and (resolved_exe := str(Path(sys.executable).resolve())): + return resolved_exe.startswith(pyenv_root) + return False -class TestPySide6Deploy(unittest.TestCase): + +class LongSortedOptionTest(unittest.TestCase): + @staticmethod + def _option_prepare(s): + """ + Take a string and return a list obtained by text.split(). + Options starting with "--" are also sorted." + """ + items = s.split() + for idx in range(len(items)): + if items[idx].startswith("--"): + return items[:idx] + sorted(items[idx:]) + return items + + def assertEqual(self, text_a, text_b): + if (not isinstance(text_a, str) or not isinstance(text_b, str) + or (len(text_a) < 50 and len(text_b) < 50)): + return super().assertEqual(text_a, text_b) + sort_a = self._option_prepare(text_a) + sort_b = self._option_prepare(text_b) + return super().assertEqual(sort_a, sort_b) + + +class DeployTestBase(LongSortedOptionTest): @classmethod def setUpClass(cls): cls.pyside_root = Path(__file__).parents[5].resolve() - example_root = cls.pyside_root / "examples" - example_widgets = example_root / "widgets" / "widgets" / "tetrix" - example_qml = example_root / "qml" / "editingmodel" - example_webenginequick = example_root / "webenginequick" / "nanobrowser" + cls.example_root = cls.pyside_root / "examples" cls.temp_dir = tempfile.mkdtemp() - cls.temp_example_widgets = Path( - shutil.copytree(example_widgets, Path(cls.temp_dir) / "tetrix") - ).resolve() - cls.temp_example_qml = Path( - shutil.copytree(example_qml, Path(cls.temp_dir) / "editingmodel") - ).resolve() - cls.temp_example_webenginequick = Path( - shutil.copytree(example_webenginequick, Path(cls.temp_dir) / "nanobrowser") - ).resolve() cls.current_dir = Path.cwd() - cls.linux_onefile_icon = ( - cls.pyside_root / "sources" / "pyside-tools" / "deploy_lib" / "pyside_icon.jpg" - ) - - sys.path.append(str(cls.pyside_root / "sources" / "pyside-tools")) + tools_path = cls.pyside_root / "sources" / "pyside-tools" + cls.win_icon = tools_path / "deploy_lib" / "pyside_icon.ico" + cls.linux_icon = tools_path / "deploy_lib" / "pyside_icon.jpg" + cls.macos_icon = tools_path / "deploy_lib" / "pyside_icon.icns" + if tools_path not in sys.path: + sys.path.append(str(cls.pyside_root / "sources" / "pyside-tools")) cls.deploy_lib = importlib.import_module("deploy_lib") cls.deploy = importlib.import_module("deploy") sys.modules["deploy"] = cls.deploy + files_to_ignore = [".cpp.o", ".qsb", ".webp"] + cls.dlls_ignore_nuitka = " ".join([f"--noinclude-dlls=*{file}" + for file in files_to_ignore]) # required for comparing long strings cls.maxDiff = None @@ -57,89 +73,188 @@ class TestPySide6Deploy(unittest.TestCase): # print no outputs to stdout sys.stdout = mock.MagicMock() - def setUpWidgets(self): + @classmethod + def tearDownClass(cls) -> None: + shutil.rmtree(Path(cls.temp_dir)) + + def tearDown(self) -> None: + super().tearDown() + os.chdir(self.current_dir) + + +@unittest.skipIf(sys.platform == "darwin" and int(platform.mac_ver()[0].split('.')[0]) <= 11, + "Test only works on macOS version 12+") +@patch("deploy_lib.config.QtDependencyReader.find_plugin_dependencies") +class TestPySide6DeployWidgets(DeployTestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + example_widgets = cls.example_root / "widgets" / "widgets" / "tetrix" + cls.temp_example_widgets = Path( + shutil.copytree(example_widgets, Path(cls.temp_dir) / "tetrix") + ).resolve() + + def setUp(self): os.chdir(self.temp_example_widgets) self.main_file = self.temp_example_widgets / "tetrix.py" self.deployment_files = self.temp_example_widgets / "deployment" + # All the plugins included. This is different from plugins_nuitka, because Nuitka bundles + # some plugins by default + self.all_plugins = ["accessiblebridge", "egldeviceintegrations", "generic", "iconengines", + "imageformats", "platforminputcontexts", "platforms", + "platforms/darwin", "platformthemes", "styles", "xcbglintegrations"] + # Plugins that needs to be passed to Nuitka + plugins_nuitka = ("accessiblebridge,platforminputcontexts,platforms/darwin") self.expected_run_cmd = ( - f"{sys.executable} -m nuitka {str(self.main_file)} --follow-imports --onefile" + f"{sys.executable} -m nuitka {str(self.main_file)} --follow-imports" f" --enable-plugin=pyside6 --output-dir={str(self.deployment_files)} --quiet" - f" --noinclude-qt-translations=True" + f" --noinclude-qt-translations" + f" --include-qt-plugins={plugins_nuitka}" + f" {self.dlls_ignore_nuitka}" ) if sys.platform.startswith("linux"): - self.expected_run_cmd += f" --linux-onefile-icon={str(self.linux_onefile_icon)}" + self.expected_run_cmd += f" --linux-icon={str(self.linux_icon)} --onefile" + elif sys.platform == "darwin": + self.expected_run_cmd += (f" --macos-app-icon={str(self.macos_icon)}" + " --macos-create-app-bundle --standalone") + elif sys.platform == "win32": + self.expected_run_cmd += f" --windows-icon-from-ico={str(self.win_icon)} --onefile" + + if is_pyenv_python(): + self.expected_run_cmd += " --static-libpython=no" self.config_file = self.temp_example_widgets / "pysidedeploy.spec" - def testWidgetDryRun(self): + def testWidgetDryRun(self, mock_plugins): + mock_plugins.return_value = self.all_plugins # Checking for dry run commands is equivalent to mocking the # subprocess.check_call() in commands.py as the the dry run command # is the command being run. - self.setUpWidgets() original_output = self.deploy.main(self.main_file, dry_run=True, force=True) self.assertEqual(original_output, self.expected_run_cmd) - self.config_file.unlink() - def testWidgetConfigFile(self): + @patch("deploy_lib.dependency_util.QtDependencyReader.get_qt_libs_dir") + def testWidgetConfigFile(self, mock_sitepackages, mock_plugins): + mock_sitepackages.return_value = Path(_get_qt_lib_dir()) + mock_plugins.return_value = self.all_plugins # includes both dry run and config_file tests - - self.setUpWidgets() # init init_result = self.deploy.main(self.main_file, init=True, force=True) self.assertEqual(init_result, None) # test with config - config_path = self.temp_example_widgets / "pysidedeploy.spec" - original_output = self.deploy.main(config_file=config_path, dry_run=True, force=True) + original_output = self.deploy.main(config_file=self.config_file, dry_run=True, force=True) self.assertEqual(original_output, self.expected_run_cmd) # # test config file contents - config_obj = ConfigFile(config_file=self.config_file) + config_obj = self.deploy_lib.BaseConfig(config_file=self.config_file) self.assertEqual(config_obj.get_value("app", "input_file"), "tetrix.py") self.assertEqual(config_obj.get_value("app", "project_dir"), ".") self.assertEqual(config_obj.get_value("app", "exec_directory"), ".") - self.assertEqual(config_obj.get_value("python", "packages"), "nuitka,ordered_set,zstandard") + self.assertEqual(config_obj.get_value("python", "packages"), + "Nuitka==2.3.2") self.assertEqual(config_obj.get_value("qt", "qml_files"), "") - self.assertEqual( - config_obj.get_value("nuitka", "extra_args"), "--quiet --noinclude-qt-translations=True" - ) + equ_base = "--quiet --noinclude-qt-translations" + equ_value = equ_base + " --static-libpython=no" if is_pyenv_python() else equ_base + self.assertEqual(config_obj.get_value("nuitka", "extra_args"), equ_value) self.assertEqual(config_obj.get_value("qt", "excluded_qml_plugins"), "") + expected_modules = {"Core", "Gui", "Widgets"} + if sys.platform != "win32": + expected_modules.add("DBus") + obtained_modules = set(config_obj.get_value("qt", "modules").split(",")) + self.assertEqual(obtained_modules, expected_modules) + obtained_qt_plugins = config_obj.get_value("qt", "plugins").split(",") + self.assertEqual(obtained_qt_plugins.sort(), self.all_plugins.sort()) self.config_file.unlink() - def setUpQml(self): + def testErrorReturns(self, mock_plugins): + mock_plugins.return_value = self.all_plugins + # main file and config file does not exists + fake_main_file = self.main_file.parent / "main.py" + with self.assertRaises(RuntimeError) as context: + self.deploy.main(main_file=fake_main_file, config_file=self.config_file) + self.assertTrue("Directory does not contain main.py file." in str(context.exception)) + + def testStandaloneMode(self, mock_plugins): + mock_plugins.return_value = self.all_plugins + # remove --onefile from self.expected_run_cmd and replace it with --standalone + self.expected_run_cmd = self.expected_run_cmd.replace(" --onefile", " --standalone") + # test standalone mode + original_output = self.deploy.main(self.main_file, mode="standalone", dry_run=True, + force=True) + + self.assertEqual(original_output, self.expected_run_cmd) + + +@unittest.skipIf(sys.platform == "darwin" and int(platform.mac_ver()[0].split('.')[0]) <= 11, + "Test only works on macOS version 12+") +@patch("deploy_lib.config.QtDependencyReader.find_plugin_dependencies") +class TestPySide6DeployQml(DeployTestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + example_qml = cls.example_root / "qml" / "editingmodel" + cls.temp_example_qml = Path( + shutil.copytree(example_qml, Path(cls.temp_dir) / "editingmodel") + ).resolve() + + def setUp(self): os.chdir(self.temp_example_qml) self.main_file = self.temp_example_qml / "main.py" self.deployment_files = self.temp_example_qml / "deployment" self.first_qml_file = "main.qml" self.second_qml_file = "MovingRectangle.qml" + + # All the plugins included. This is different from plugins_nuitka, because Nuitka bundles + # some plugins by default + self.all_plugins = ["accessiblebridge", "egldeviceintegrations", "generic", "iconengines", + "imageformats", "networkaccess", "networkinformation", + "platforminputcontexts", "platforms", "platforms/darwin", + "platformthemes", "qmltooling", "scenegraph", "tls", + "xcbglintegrations"] + # Plugins that needs to be passed to Nuitka + plugins_nuitka = ("accessiblebridge,networkaccess,networkinformation,platforminputcontexts," + "platforms/darwin,qml,qmltooling,scenegraph") self.expected_run_cmd = ( - f"{sys.executable} -m nuitka {str(self.main_file)} --follow-imports --onefile" + f"{sys.executable} -m nuitka {str(self.main_file)} --follow-imports" f" --enable-plugin=pyside6 --output-dir={str(self.deployment_files)} --quiet" - f" --noinclude-qt-translations=True --include-qt-plugins=all" + f" --noinclude-qt-translations" + f" {self.dlls_ignore_nuitka}" + " --noinclude-dlls=*/qml/QtQuickEffectMaker/*" + f" --include-qt-plugins={plugins_nuitka}" f" --include-data-files={str(self.temp_example_qml / self.first_qml_file)}=" f"./main.qml --include-data-files=" - f"{str(self.temp_example_qml /self.second_qml_file)}=./MovingRectangle.qml" - ) + f"{str(self.temp_example_qml / self.second_qml_file)}=./MovingRectangle.qml" + ) if sys.platform != "win32": self.expected_run_cmd += ( " --noinclude-dlls=libQt6Charts*" " --noinclude-dlls=libQt6Quick3D* --noinclude-dlls=libQt6Sensors*" " --noinclude-dlls=libQt6Test* --noinclude-dlls=libQt6WebEngine*" - ) + ) else: self.expected_run_cmd += ( " --noinclude-dlls=Qt6Charts*" " --noinclude-dlls=Qt6Quick3D* --noinclude-dlls=Qt6Sensors*" " --noinclude-dlls=Qt6Test* --noinclude-dlls=Qt6WebEngine*" - ) + ) if sys.platform.startswith("linux"): - self.expected_run_cmd += f" --linux-onefile-icon={str(self.linux_onefile_icon)}" + self.expected_run_cmd += f" --linux-icon={str(self.linux_icon)} --onefile" + elif sys.platform == "darwin": + self.expected_run_cmd += (f" --macos-app-icon={str(self.macos_icon)}" + " --macos-create-app-bundle --standalone") + elif sys.platform == "win32": + self.expected_run_cmd += f" --windows-icon-from-ico={str(self.win_icon)} --onefile" + + if is_pyenv_python(): + self.expected_run_cmd += " --static-libpython=no" self.config_file = self.temp_example_qml / "pysidedeploy.spec" - def testQmlConfigFile(self): - self.setUpQml() - + @patch("deploy_lib.dependency_util.QtDependencyReader.get_qt_libs_dir") + def testQmlConfigFile(self, mock_sitepackages, mock_plugins): + mock_sitepackages.return_value = Path(_get_qt_lib_dir()) + mock_plugins.return_value = self.all_plugins # create config file with patch("deploy_lib.config.run_qmlimportscanner") as mock_qmlimportscanner: mock_qmlimportscanner.return_value = ["QtQuick"] @@ -147,45 +262,75 @@ class TestPySide6Deploy(unittest.TestCase): self.assertEqual(init_result, None) # test config file contents - config_obj = ConfigFile(config_file=self.config_file) + config_obj = self.deploy_lib.BaseConfig(config_file=self.config_file) self.assertEqual(config_obj.get_value("app", "input_file"), "main.py") self.assertEqual(config_obj.get_value("app", "project_dir"), ".") self.assertEqual(config_obj.get_value("app", "exec_directory"), ".") - self.assertEqual(config_obj.get_value("python", "packages"), "nuitka,ordered_set,zstandard") + self.assertEqual(config_obj.get_value("python", "packages"), + "Nuitka==2.3.2") self.assertEqual(config_obj.get_value("qt", "qml_files"), "main.qml,MovingRectangle.qml") - self.assertEqual( - config_obj.get_value("nuitka", "extra_args"), "--quiet --noinclude-qt-translations=True" - ) + equ_base = "--quiet --noinclude-qt-translations" + equ_value = equ_base + " --static-libpython=no" if is_pyenv_python() else equ_base + self.assertEqual(config_obj.get_value("nuitka", "extra_args"), equ_value) self.assertEqual( config_obj.get_value("qt", "excluded_qml_plugins"), "QtCharts,QtQuick3D,QtSensors,QtTest,QtWebEngine", ) + expected_modules = {"Core", "Gui", "Qml", "Quick", "Network", "OpenGL", "QmlModels"} + if sys.platform != "win32": + expected_modules.add("DBus") + obtained_modules = set(config_obj.get_value("qt", "modules").split(",")) + self.assertEqual(obtained_modules, expected_modules) + obtained_qt_plugins = config_obj.get_value("qt", "plugins").split(",") + self.assertEqual(obtained_qt_plugins.sort(), self.all_plugins.sort()) self.config_file.unlink() - def testQmlDryRun(self): - self.setUpQml() + def testQmlDryRun(self, mock_plugins): + mock_plugins.return_value = self.all_plugins with patch("deploy_lib.config.run_qmlimportscanner") as mock_qmlimportscanner: mock_qmlimportscanner.return_value = ["QtQuick"] original_output = self.deploy.main(self.main_file, dry_run=True, force=True) self.assertEqual(original_output, self.expected_run_cmd) self.assertEqual(mock_qmlimportscanner.call_count, 1) - self.config_file.unlink() - def testMainFileDryRun(self): - self.setUpQml() + def testMainFileDryRun(self, mock_plugins): + mock_plugins.return_value = self.all_plugins with patch("deploy_lib.config.run_qmlimportscanner") as mock_qmlimportscanner: mock_qmlimportscanner.return_value = ["QtQuick"] original_output = self.deploy.main(Path.cwd() / "main.py", dry_run=True, force=True) self.assertEqual(original_output, self.expected_run_cmd) self.assertEqual(mock_qmlimportscanner.call_count, 1) - self.config_file.unlink() - # this test case retains the QtWebEngine dlls - def testWebEngineQuickDryRun(self): + +@unittest.skipIf(sys.platform == "darwin" and int(platform.mac_ver()[0].split('.')[0]) <= 11, + "Test only works on macOS version 12+") +class TestPySide6DeployWebEngine(DeployTestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + example_webenginequick = cls.example_root / "webenginequick" / "nanobrowser" + cls.temp_example_webenginequick = Path( + shutil.copytree(example_webenginequick, Path(cls.temp_dir) / "nanobrowser") + ).resolve() + + @patch("deploy_lib.config.QtDependencyReader.find_plugin_dependencies") + @patch("deploy_lib.dependency_util.QtDependencyReader.get_qt_libs_dir") + def testWebEngineQuickDryRun(self, mock_sitepackages, mock_plugins): + mock_sitepackages.return_value = Path(_get_qt_lib_dir()) + all_plugins = ["accessiblebridge", "egldeviceintegrations", "generic", "iconengines", + "imageformats", "networkaccess", "networkinformation", + "platforminputcontexts", "platforms", "platforms/darwin", + "platformthemes", "qmltooling", "scenegraph", "tls", + "xcbglintegrations"] + mock_plugins.return_value = all_plugins + # this test case retains the QtWebEngine dlls # setup os.chdir(self.temp_example_webenginequick) main_file = self.temp_example_webenginequick / "quicknanobrowser.py" deployment_files = self.temp_example_webenginequick / "deployment" + # Plugins that needs to be passed to Nuitka + plugins_nuitka = ("accessiblebridge,networkaccess,networkinformation,platforminputcontexts," + "platforms/darwin,qml,qmltooling,scenegraph") qml_files = [ "ApplicationRoot.qml", "BrowserDialog.qml", @@ -196,15 +341,18 @@ class TestPySide6Deploy(unittest.TestCase): ] data_files_cmd = " ".join( [ - f"--include-data-files={str(self.temp_example_webenginequick/file)}=./{file}" + f"--include-data-files={str(self.temp_example_webenginequick / file)}=./{file}" for file in qml_files ] ) expected_run_cmd = ( - f"{sys.executable} -m nuitka {str(main_file)} --follow-imports --onefile" + f"{sys.executable} -m nuitka {str(main_file)} --follow-imports" f" --enable-plugin=pyside6 --output-dir={str(deployment_files)} --quiet" - f" --noinclude-qt-translations=True --include-qt-plugins=all" + f" --noinclude-qt-translations --include-qt-plugins=all" f" {data_files_cmd}" + f" --include-qt-plugins={plugins_nuitka}" + f" {self.dlls_ignore_nuitka}" + " --noinclude-dlls=*/qml/QtQuickEffectMaker/*" ) if sys.platform != "win32": @@ -212,16 +360,21 @@ class TestPySide6Deploy(unittest.TestCase): " --noinclude-dlls=libQt6Charts*" " --noinclude-dlls=libQt6Quick3D* --noinclude-dlls=libQt6Sensors*" " --noinclude-dlls=libQt6Test*" - ) + ) else: expected_run_cmd += ( " --noinclude-dlls=Qt6Charts*" " --noinclude-dlls=Qt6Quick3D* --noinclude-dlls=Qt6Sensors*" " --noinclude-dlls=Qt6Test*" - ) + ) if sys.platform.startswith("linux"): - expected_run_cmd += f" --linux-onefile-icon={str(self.linux_onefile_icon)}" + expected_run_cmd += f" --linux-icon={str(self.linux_icon)} --onefile" + elif sys.platform == "darwin": + expected_run_cmd += (f" --macos-app-icon={str(self.macos_icon)}" + " --macos-create-app-bundle --standalone") + elif sys.platform == "win32": + expected_run_cmd += f" --windows-icon-from-ico={str(self.win_icon)} --onefile" config_file = self.temp_example_webenginequick / "pysidedeploy.spec" @@ -234,25 +387,23 @@ class TestPySide6Deploy(unittest.TestCase): # run dry_run original_output = self.deploy.main(main_file, dry_run=True, force=True) self.assertTrue(original_output, expected_run_cmd) - self.assertEqual(mock_qmlimportscanner.call_count, 1) + self.assertEqual(mock_qmlimportscanner.call_count, 2) # test config file contents - config_obj = ConfigFile(config_file=config_file) + config_obj = self.deploy_lib.BaseConfig(config_file=config_file) self.assertEqual(config_obj.get_value("app", "input_file"), "quicknanobrowser.py") self.assertEqual(config_obj.get_value("qt", "qml_files"), ",".join(qml_files)) self.assertEqual( config_obj.get_value("qt", "excluded_qml_plugins"), "QtCharts,QtQuick3D,QtSensors,QtTest", ) - config_file.unlink() - - def tearDown(self) -> None: - super().tearDown() - os.chdir(self.current_dir) - - @classmethod - def tearDownClass(cls) -> None: - shutil.rmtree(Path(cls.temp_dir)) + expected_modules = {"Core", "Gui", "Quick", "Qml", "WebEngineQuick", "Network", "OpenGL", + "Positioning", "WebEngineCore", "WebChannel", "WebChannelQuick", + "QmlModels"} + if sys.platform != "win32": + expected_modules.add("DBus") + obtained_modules = set(config_obj.get_value("qt", "modules").split(",")) + self.assertEqual(obtained_modules, expected_modules) if __name__ == "__main__": |