diff options
Diffstat (limited to 'sources/pyside6/tests/tools')
17 files changed, 1014 insertions, 31 deletions
diff --git a/sources/pyside6/tests/tools/__init__.py b/sources/pyside6/tests/tools/__init__.py new file mode 100644 index 000000000..31f792369 --- /dev/null +++ b/sources/pyside6/tests/tools/__init__.py @@ -0,0 +1 @@ +from init_paths import init_test_paths diff --git a/sources/pyside6/tests/tools/list-class-hierarchy.py b/sources/pyside6/tests/tools/list-class-hierarchy.py index 924706457..b773b7c58 100644 --- a/sources/pyside6/tests/tools/list-class-hierarchy.py +++ b/sources/pyside6/tests/tools/list-class-hierarchy.py @@ -1,32 +1,6 @@ #!/usr/bin/python - -############################################################################# -## -## Copyright (C) 2016 The Qt Company Ltd. -## Contact: https://www.qt.io/licensing/ -## -## This file is part of the test suite of Qt for Python. -## -## $QT_BEGIN_LICENSE:GPL-EXCEPT$ -## Commercial License Usage -## Licensees holding valid commercial Qt licenses may use this file in -## accordance with the commercial license agreement provided with the -## Software or, alternatively, in accordance with the terms contained in -## a written agreement between you and The Qt Company. For licensing terms -## and conditions see https://www.qt.io/terms-conditions. For further -## information use the contact form at https://www.qt.io/contact-us. -## -## GNU General Public License Usage -## Alternatively, this file may be used under the terms of the GNU -## General Public License version 3 as published by the Free Software -## Foundation with exceptions as appearing in the file LICENSE.GPL3-EXCEPT -## included in the packaging of this file. Please review the following -## information to ensure the GNU General Public License requirements will -## be met: https://www.gnu.org/licenses/gpl-3.0.html. -## -## $QT_END_LICENSE$ -## -############################################################################# +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 # This is a small script printing out Qt binding class hierarchies # for comparison purposes. @@ -45,8 +19,7 @@ from inspect import isclass ignore = ["staticMetaObject", "pyqtConfigure", "registerUserData", - "thread", - ] + "thread"] def recurse_into(el, obj): @@ -73,7 +46,7 @@ def recurse_into(el, obj): return symbols -if __name__=='__main__': +if __name__ == '__main__': modules = [ 'QtCore', 'QtGui', 'QtHelp', diff --git a/sources/pyside6/tests/tools/metaobjectdump/CMakeLists.txt b/sources/pyside6/tests/tools/metaobjectdump/CMakeLists.txt new file mode 100644 index 000000000..f1ad6ab16 --- /dev/null +++ b/sources/pyside6/tests/tools/metaobjectdump/CMakeLists.txt @@ -0,0 +1 @@ +PYSIDE_TEST(test_metaobjectdump.py) diff --git a/sources/pyside6/tests/tools/metaobjectdump/baseline_default_birthdayparty.json b/sources/pyside6/tests/tools/metaobjectdump/baseline_default_birthdayparty.json new file mode 100644 index 000000000..6a695dd5c --- /dev/null +++ b/sources/pyside6/tests/tools/metaobjectdump/baseline_default_birthdayparty.json @@ -0,0 +1 @@ +[{"classes": [{"className": "BirthdayParty", "qualifiedClassName": "BirthdayParty", "object": true, "superClasses": [{"access": "public", "name": "QObject"}], "classInfos": [{"name": "QML.Element", "value": "auto"}, {"name": "DefaultProperty", "value": "guests"}], "properties": [{"name": "host", "type": "Person", "index": 0, "read": "host", "notify": "host_changed", "write": "host"}, {"name": "guests", "type": "QQmlListProperty<Person>", "index": 1}], "signals": [{"access": "public", "name": "host_changed", "arguments": [], "returnType": "void"}, {"access": "public", "name": "guests_changed", "arguments": [], "returnType": "void"}]}], "outputRevision": 68, "QML_IMPORT_NAME": "People", "QML_IMPORT_MAJOR_VERSION": 1, "QML_IMPORT_MINOR_VERSION": 0, "QT_MODULES": ["QtCore", "QtQml"]}]
\ No newline at end of file diff --git a/sources/pyside6/tests/tools/metaobjectdump/baseline_default_person.json b/sources/pyside6/tests/tools/metaobjectdump/baseline_default_person.json new file mode 100644 index 000000000..571056c2a --- /dev/null +++ b/sources/pyside6/tests/tools/metaobjectdump/baseline_default_person.json @@ -0,0 +1 @@ +[{"classes": [{"className": "Person", "qualifiedClassName": "Person", "object": true, "superClasses": [{"access": "public", "name": "QObject"}], "classInfos": [{"name": "QML.Element", "value": "anonymous"}], "properties": [{"name": "name", "type": "QString", "index": 0, "read": "name", "notify": "name_changed", "write": "name"}, {"name": "shoe_size", "type": "int", "index": 1, "read": "shoe_size", "notify": "shoe_size_changed", "write": "shoe_size"}], "signals": [{"access": "public", "name": "name_changed", "arguments": [], "returnType": "void"}, {"access": "public", "name": "shoe_size_changed", "arguments": [], "returnType": "void"}]}, {"className": "Boy", "qualifiedClassName": "Boy", "object": true, "superClasses": [{"access": "public", "name": "Person"}], "classInfos": [{"name": "QML.Element", "value": "auto"}]}, {"className": "Girl", "qualifiedClassName": "Girl", "object": true, "superClasses": [{"access": "public", "name": "Person"}], "classInfos": [{"name": "QML.Element", "value": "auto"}]}], "outputRevision": 68, "QML_IMPORT_NAME": "People", "QML_IMPORT_MAJOR_VERSION": 1, "QML_IMPORT_MINOR_VERSION": 0, "QT_MODULES": ["QtCore", "QtQml"]}]
\ No newline at end of file diff --git a/sources/pyside6/tests/tools/metaobjectdump/baseline_inheritance_birthdayparty.json b/sources/pyside6/tests/tools/metaobjectdump/baseline_inheritance_birthdayparty.json new file mode 100644 index 000000000..0491e41cc --- /dev/null +++ b/sources/pyside6/tests/tools/metaobjectdump/baseline_inheritance_birthdayparty.json @@ -0,0 +1 @@ +[{"classes": [{"className": "BirthdayParty", "qualifiedClassName": "BirthdayParty", "object": true, "superClasses": [{"access": "public", "name": "QObject"}], "classInfos": [{"name": "QML.Element", "value": "auto"}], "properties": [{"name": "host", "type": "Person", "index": 0, "read": "host", "notify": "host_changed", "write": "host"}, {"name": "guests", "type": "QQmlListProperty<Person>", "index": 1}], "signals": [{"access": "public", "name": "host_changed", "arguments": [], "returnType": "void"}, {"access": "public", "name": "guests_changed", "arguments": [], "returnType": "void"}]}], "outputRevision": 68, "QML_IMPORT_NAME": "People", "QML_IMPORT_MAJOR_VERSION": 1, "QML_IMPORT_MINOR_VERSION": 0, "QT_MODULES": ["QtCore", "QtQml"]}]
\ No newline at end of file diff --git a/sources/pyside6/tests/tools/metaobjectdump/baseline_inheritance_person.json b/sources/pyside6/tests/tools/metaobjectdump/baseline_inheritance_person.json new file mode 100644 index 000000000..82021ee0a --- /dev/null +++ b/sources/pyside6/tests/tools/metaobjectdump/baseline_inheritance_person.json @@ -0,0 +1 @@ +[{"classes": [{"className": "Person", "qualifiedClassName": "Person", "object": true, "superClasses": [{"access": "public", "name": "QObject"}], "classInfos": [{"name": "QML.Element", "value": "auto"}, {"name": "QML.Creatable", "value": "false"}, {"name": "QML.UncreatableReason", "value": "Person is an abstract base class."}], "properties": [{"name": "name", "type": "QString", "index": 0, "read": "name", "notify": "name_changed", "write": "name"}, {"name": "shoe_size", "type": "int", "index": 1, "read": "shoe_size", "notify": "shoe_size_changed", "write": "shoe_size"}], "signals": [{"access": "public", "name": "name_changed", "arguments": [], "returnType": "void"}, {"access": "public", "name": "shoe_size_changed", "arguments": [], "returnType": "void"}]}, {"className": "Boy", "qualifiedClassName": "Boy", "object": true, "superClasses": [{"access": "public", "name": "Person"}], "classInfos": [{"name": "QML.Element", "value": "auto"}]}, {"className": "Girl", "qualifiedClassName": "Girl", "object": true, "superClasses": [{"access": "public", "name": "Person"}], "classInfos": [{"name": "QML.Element", "value": "auto"}]}], "outputRevision": 68, "QML_IMPORT_NAME": "People", "QML_IMPORT_MAJOR_VERSION": 1, "QML_IMPORT_MINOR_VERSION": 0, "QT_MODULES": ["QtCore", "QtQml"]}]
\ No newline at end of file diff --git a/sources/pyside6/tests/tools/metaobjectdump/baseline_property_happybirthdaysong.json b/sources/pyside6/tests/tools/metaobjectdump/baseline_property_happybirthdaysong.json new file mode 100644 index 000000000..c009111b7 --- /dev/null +++ b/sources/pyside6/tests/tools/metaobjectdump/baseline_property_happybirthdaysong.json @@ -0,0 +1 @@ +[{"classes": [{"className": "HappyBirthdaySong", "qualifiedClassName": "HappyBirthdaySong", "object": false, "superClasses": [{"access": "public", "name": "QPyQmlPropertyValueSource"}], "classInfos": [{"name": "QML.Element", "value": "auto"}], "properties": [{"name": "name", "type": "QString", "index": 0, "read": "name", "notify": "name_changed", "write": "name"}], "signals": [{"access": "public", "name": "name_changed", "arguments": [], "returnType": "void"}], "slots": [{"access": "public", "name": "advance", "arguments": [], "returnType": "void"}]}], "outputRevision": 68, "QML_IMPORT_NAME": "People", "QML_IMPORT_MAJOR_VERSION": 1, "QML_IMPORT_MINOR_VERSION": 0, "QT_MODULES": ["QtCore", "QtQml"]}]
\ No newline at end of file diff --git a/sources/pyside6/tests/tools/metaobjectdump/test_metaobjectdump.py b/sources/pyside6/tests/tools/metaobjectdump/test_metaobjectdump.py new file mode 100644 index 000000000..5e7412bf5 --- /dev/null +++ b/sources/pyside6/tests/tools/metaobjectdump/test_metaobjectdump.py @@ -0,0 +1,73 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +import os +import sys +import subprocess +import unittest + +from pathlib import Path + +"""Test for pyside6-metaobjectdump. + +The test prints commands to regenerate the base line.""" + + +def msg_regenerate(cmd, baseline): + cmd_str = " ".join(cmd) + return (f"# Regenerate {baseline}\n" + f"{cmd_str} > {baseline}") + + +@unittest.skipIf(sys.version_info < (3, 8), "Needs a recent ast module") +class TestMetaObjectDump(unittest.TestCase): + """Test for the metaobjectdump tool. Compares the output of metaobjectdump.py for some + example files in compact format.""" + + def setUp(self): + super().setUp() + self._dir = Path(__file__).parent.resolve() + pyside_root = self._dir.parents[4] + self._metaobjectdump_tool = pyside_root / "sources" / "pyside-tools" / "metaobjectdump.py" + self._examples_dir = (pyside_root / "examples" / + "qml" / "tutorials" / "extending-qml-advanced") + + # Compile a list of examples (tuple [file, base line, command]) + examples = [] + for d in ["advanced2-Inheritance-and-coercion", "advanced3-Default-properties"]: + example_dir = self._examples_dir / d + examples.append(example_dir / "birthdayparty.py") + examples.append(example_dir / "person.py") + # Example with slot + examples.append(self._examples_dir / "advanced6-Property-value-source" + / "happybirthdaysong.py") + + metaobjectdump_cmd_root = [sys.executable, os.fspath(self._metaobjectdump_tool), "-c", "-s"] + self._examples = [] + for example in examples: + name = example.parent.name + # Simplify "advanced2-Inheritance-and-coercion" -> "inheritance" + short_name = name.split("-")[1].lower() + baseline_name = f"baseline_{short_name}_{example.stem}.json" + baseline_path = self._dir / baseline_name + cmd = metaobjectdump_cmd_root + [os.fspath(example)] + self._examples.append((example, baseline_path, cmd)) + print(msg_regenerate(cmd, baseline_path)) + + def testMetaObjectDump(self): + self.assertTrue(self._examples_dir.is_dir()) + self.assertTrue(self._metaobjectdump_tool.is_file()) + + for example, baseline, cmd in self._examples: + self.assertTrue(example.is_file()) + self.assertTrue(baseline.is_file()) + baseline_data = baseline.read_text() + + popen = subprocess.Popen(cmd, stdout=subprocess.PIPE) + actual = popen.communicate()[0].decode("UTF-8") + self.assertEqual(popen.returncode, 0) + self.assertEqual(baseline_data, actual) + + +if __name__ == '__main__': + unittest.main() diff --git a/sources/pyside6/tests/tools/pyside6-android-deploy/CMakeLists.txt b/sources/pyside6/tests/tools/pyside6-android-deploy/CMakeLists.txt new file mode 100644 index 000000000..c32d636ed --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-android-deploy/CMakeLists.txt @@ -0,0 +1,3 @@ +if(CMAKE_HOST_UNIX) + PYSIDE_TEST(test_pyside6_android_deploy.py) +endif() diff --git a/sources/pyside6/tests/tools/pyside6-android-deploy/extensive_android_deploy_test.py b/sources/pyside6/tests/tools/pyside6-android-deploy/extensive_android_deploy_test.py new file mode 100644 index 000000000..271f8eebd --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-android-deploy/extensive_android_deploy_test.py @@ -0,0 +1,88 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +""" + Extensive manual test of pyside6-android-deploy + + Note: Not to be added into the CI +""" + +import logging +import unittest +import tempfile +import shutil +import sys +import os +import importlib +from pathlib import Path + + +class TestPySide6Deploy(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.pyside_root = Path(__file__).parents[5].resolve() + example_root = cls.pyside_root / "examples" + example_application = example_root / "gui" / "analogclock" + cls.temp_dir = tempfile.mkdtemp() + cls.temp_example = Path( + shutil.copytree(example_application, Path(cls.temp_dir) / "analogclock") + ).resolve() + cls.current_dir = Path.cwd() + + sys.path.append(str(cls.pyside_root / "sources" / "pyside-tools")) + cls.deploy_lib = importlib.import_module("deploy_lib") + cls.android_deploy = importlib.import_module("android_deploy") + sys.modules["android_deploy"] = cls.android_deploy + + if os.environ.get("WHEEL_PYSIDE") is not None: + cls.pyside_wheel = Path(os.environ.get("WHEEL_PYSIDE")).resolve() + else: + raise Exception("Environment variable WHEEL_PYSIDE does not exist") + + if os.environ.get("WHEEL_SHIBOKEN") is not None: + cls.shiboken_wheel = Path(os.environ.get("WHEEL_SHIBOKEN")).resolve() + else: + raise Exception("Environment variable WHEEL_SHIBOKEN does not exist") + + def setUp(self): + os.chdir(self.temp_example) + self.config_file = self.temp_example / "pysidedeploy.spec" + + def testDeployment(self): + self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel, + pyside_wheel=self.pyside_wheel, keep_deployment_files=True, + loglevel=logging.INFO, force=True) + + print("Testing with config file") + self.android_deploy.main(name="android_app", config_file=self.config_file, + loglevel=logging.INFO, force=True) + + def testWithNdkSdk(self): + if os.environ.get("ANDROID_SDK_ROOT") is not None: + android_sdk_root = Path(os.environ.get("ANDROID_SDK_ROOT")).resolve() + else: + raise Exception("Environment variable ANDROID_SDK_ROOT does not exist") + + if os.environ.get("ANDROID_NDK_ROOT") is not None: + android_ndk_root = Path(os.environ.get("ANDROID_NDK_ROOT")).resolve() + else: + raise Exception("Environment variable ANDROID_NDK_ROOT does not exist") + + self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel, + pyside_wheel=self.pyside_wheel, + ndk_path=android_ndk_root, + sdk_path=android_sdk_root, + keep_deployment_files=True, + loglevel=logging.INFO, force=True) + + def tearDown(self) -> None: + super().tearDown() + os.chdir(self.current_dir) + + @classmethod + def tearDownClass(cls) -> None: + shutil.rmtree(Path(cls.temp_dir)) + + +if __name__ == "__main__": + unittest.main() diff --git a/sources/pyside6/tests/tools/pyside6-android-deploy/test_pyside6_android_deploy.py b/sources/pyside6/tests/tools/pyside6-android-deploy/test_pyside6_android_deploy.py new file mode 100644 index 000000000..ec575e923 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-android-deploy/test_pyside6_android_deploy.py @@ -0,0 +1,274 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import importlib +import os +import re +import shutil +import sys +import tempfile +import unittest +import subprocess +from pathlib import Path +from unittest import mock +from unittest.mock import patch + +sys.path.append(os.fspath(Path(__file__).resolve().parents[2])) +from init_paths import init_test_paths # noqa: E402 +init_test_paths(False) + + +class DeployTestBase(unittest.TestCase): + @classmethod + def setUpClass(cls): + cls.pyside_root = Path(__file__).parents[5].resolve() + cls.example_root = cls.pyside_root / "examples" + cls.temp_dir = tempfile.mkdtemp() + cls.current_dir = Path.cwd() + cls.pyside_wheel = Path("/tmp/PySide6-6.5.0a1-6.5.0-cp37-abi3-android_x86_64.whl") + cls.shiboken_wheel = Path("/tmp/shiboken6-6.5.0a1-6.5.0-cp37-abi3-android_x86_64.whl") + cls.ndk_path = Path("/tmp/android_sdk/ndk/25.2.9519653") + cls.sdk_path = Path("/tmp/android_sdk") + pyside_tools = cls.pyside_root / "sources" / "pyside-tools" + + # install extra python dependencies + android_requirements_file = pyside_tools / "requirements-android.txt" + with open(android_requirements_file, 'r', encoding='UTF-8') as file: + while line := file.readline(): + dependent_package = line.rstrip() + if not bool(importlib.util.find_spec(dependent_package)): + command = [sys.executable, "-m", "pip", "install", dependent_package] + subprocess.run(command) + + sys.path.append(str(pyside_tools)) + cls.deploy_lib = importlib.import_module("deploy_lib") + cls.android_deploy = importlib.import_module("android_deploy") + sys.modules["android_deploy"] = cls.android_deploy + + # required for comparing long strings + cls.maxDiff = None + + # print no outputs to stdout + sys.stdout = mock.MagicMock() + + def tearDown(self) -> None: + super().tearDown() + os.chdir(self.current_dir) + + @classmethod + def tearDownClass(cls) -> None: + shutil.rmtree(Path(cls.temp_dir)) + + +@patch("deploy_lib.android.android_config.extract_and_copy_jar") +class TestPySide6AndroidDeployWidgets(DeployTestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + example_widget_application = cls.example_root / "gui" / "analogclock" + cls.temp_example = Path( + shutil.copytree(example_widget_application, Path(cls.temp_dir) / "analogclock") + ).resolve() + + def setUp(self): + os.chdir(self.temp_example) + self.config_file = self.temp_example / "pysidedeploy.spec" + self.buildozer_config = self.temp_example / "buildozer.spec" + + @patch("deploy_lib.android.android_config.AndroidConfig._find_local_libs") + @patch("deploy_lib.android.android_config.AndroidConfig._find_dependent_qt_modules") + @patch("deploy_lib.android.android_config.find_qtlibs_in_wheel") + def test_dry_run(self, mock_qtlibs, mock_extraqtmodules, mock_local_libs, mock_extract_jar): + mock_qtlibs.return_value = self.pyside_wheel / "PySide6/Qt/lib" + mock_extraqtmodules.return_value = [] + dependent_plugins = ["platforms_qtforandroid", + "platforminputcontexts_qtvirtualkeyboardplugin", + "iconengines_qsvgicon"] + mock_local_libs.return_value = [], dependent_plugins + self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel, + pyside_wheel=self.pyside_wheel, ndk_path=self.ndk_path, + dry_run=True, force=True) + + self.assertEqual(mock_extract_jar.call_count, 0) + self.assertEqual(mock_qtlibs.call_count, 1) + self.assertEqual(mock_extraqtmodules.call_count, 1) + self.assertEqual(mock_local_libs.call_count, 1) + + @patch("deploy_lib.android.buildozer.BuildozerConfig._BuildozerConfig__find_jars") + @patch("deploy_lib.android.android_config.AndroidConfig.recipes_exist") + @patch("deploy_lib.android.android_config.AndroidConfig._find_dependent_qt_modules") + @patch("deploy_lib.android.android_config.find_qtlibs_in_wheel") + def test_config(self, mock_qtlibs, mock_extraqtmodules, mock_recipes_exist, mock_find_jars, + mock_extract_jar): + jar_dir = "tmp/jar/PySide6/jar" + mock_extract_jar.return_value = Path(jar_dir) + mock_qtlibs.return_value = self.pyside_wheel / "PySide6/Qt/lib" + mock_extraqtmodules.return_value = [] + mock_recipes_exist.return_value = True + jars, init_classes = ["/tmp/jar/PySide6/jar/Qt6Android.jar", + "/tmp/jar/PySide6/jar/Qt6AndroidBindings.jar"], [] + mock_find_jars.return_value = jars, init_classes + + self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel, + pyside_wheel=self.pyside_wheel, ndk_path=self.ndk_path, + init=True, force=True, keep_deployment_files=True) + + self.assertEqual(mock_extract_jar.call_count, 1) + self.assertEqual(mock_qtlibs.call_count, 1) + self.assertEqual(mock_extraqtmodules.call_count, 1) + self.assertEqual(mock_recipes_exist.call_count, 1) + self.assertEqual(mock_find_jars.call_count, 1) + self.assertTrue(self.config_file.exists()) + self.assertTrue(self.buildozer_config.exists()) + + # test config file contents + 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("python", "android_packages"), + "buildozer==1.5.0,cython==0.29.33") + self.assertEqual(config_obj.get_value("android", "wheel_pyside"), + str(self.pyside_wheel.resolve())) + self.assertEqual(config_obj.get_value("android", "wheel_shiboken"), + str(self.shiboken_wheel.resolve())) + self.assertEqual(config_obj.get_value("buildozer", "mode"), "debug") + self.assertEqual(config_obj.get_value("buildozer", "recipe_dir"), + '') + self.assertEqual(config_obj.get_value("buildozer", "jars_dir"), + str(self.temp_example / jar_dir)) + self.assertIn(str(self.ndk_path), config_obj.get_value("buildozer", "ndk_path")) + self.assertEqual(config_obj.get_value("buildozer", "sdk_path"), '') + expected_modules = {"Core", "Gui"} + obtained_modules = set(config_obj.get_value("qt", "modules").split(",")) + self.assertEqual(obtained_modules, expected_modules) + expected_local_libs = "" + self.assertEqual(config_obj.get_value("buildozer", "local_libs"), + expected_local_libs) + self.assertEqual(config_obj.get_value("buildozer", "arch"), "x86_64") + + # test buildozer config file contents + buildozer_config_obj = self.deploy_lib.BaseConfig(config_file=self.buildozer_config) + obtained_jars = set(buildozer_config_obj.get_value("app", "android.add_jars").split(',')) + expected_jars = set(jars) + self.assertEqual(obtained_jars, expected_jars) + obtained_extra_args = buildozer_config_obj.get_value("app", "p4a.extra_args") + extra_args_patrn = re.compile("--qt-libs=(?P<modules>.*) --load-local-libs=" + "(?P<local_libs>.*) --init-classes=(?P<init_classes>.*)") + match = extra_args_patrn.search(obtained_extra_args) + obtained_modules = match.group("modules").split(',') + obtained_local_libs = match.group("local_libs") + obtained_init_classes = match.group("init_classes") + self.assertEqual(set(obtained_modules), expected_modules) + self.assertEqual(obtained_local_libs, expected_local_libs) + self.assertEqual(obtained_init_classes, '') + expected_include_exts = "py,png,jpg,kv,atlas,qml,js" + obtained_include_exts = buildozer_config_obj.get_value("app", "source.include_exts") + self.assertEqual(expected_include_exts, obtained_include_exts) + + self.config_file.unlink() + self.buildozer_config.unlink() + + def test_errors(self, mock_extract_jar): + # test if error raises for non existing NDK + with self.assertRaises(FileNotFoundError) as context: + self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel, + pyside_wheel=self.pyside_wheel, force=True) + self.assertTrue("Unable to find Android NDK" in str(context.exception)) + + # test when cwd() is not project_dir + os.chdir(self.current_dir) + with self.assertRaises(RuntimeError) as context: + self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel, + pyside_wheel=self.pyside_wheel, init=True, force=True) + self.assertTrue("For Android deployment to work" in str(context.exception)) + + +@patch("deploy_lib.config.run_qmlimportscanner") +@patch("deploy_lib.android.android_config.extract_and_copy_jar") +class TestPySide6AndroidDeployQml(DeployTestBase): + @classmethod + def setUpClass(cls): + super().setUpClass() + # setting up example + example_qml_application = cls.example_root / "quick" / "models" / "stringlistmodel" + cls.temp_qml_example = Path( + shutil.copytree(example_qml_application, Path(cls.temp_dir) / "stringlistmodel") + ).resolve() + + def setUp(self): + os.chdir(self.temp_qml_example) + self.config_file = self.temp_qml_example / "pysidedeploy.spec" + self.buildozer_config_file = self.temp_qml_example / "buildozer.spec" + (self.temp_qml_example / "stringlistmodel.py").rename(self.temp_qml_example / "main.py") + (self.temp_qml_example / "stringlistmodel.pyproject").unlink() + + @patch("deploy_lib.android.android_config.AndroidConfig._find_local_libs") + @patch("deploy_lib.android.buildozer.BuildozerConfig._BuildozerConfig__find_jars") + @patch("deploy_lib.android.android_config.AndroidConfig.recipes_exist") + @patch("deploy_lib.android.android_config.AndroidConfig._find_dependent_qt_modules") + @patch("deploy_lib.android.android_config.find_qtlibs_in_wheel") + def test_config_with_Qml(self, mock_qtlibs, mock_extraqtmodules, mock_recipes_exist, + mock_find_jars, mock_local_libs, mock_extract_jar, + mock_qmlimportscanner): + # setting up mocks + jar_dir = "tmp/jar/PySide6/jar" + mock_extract_jar.return_value = Path(jar_dir) + mock_qtlibs.return_value = self.pyside_wheel / "PySide6/Qt/lib" + mock_extraqtmodules.return_value = ['Qml', 'Network', 'QmlModels', 'OpenGL'] + mock_recipes_exist.return_value = True + jars, init_classes = ["/tmp/jar/PySide6/jar/Qt6Android.jar", + "/tmp/jar/PySide6/jar/Qt6AndroidBindings.jar", + "/tmp/jar/PySide6/jar/Qt6AndroidNetworkInformationBackend.jar", + "/tmp/jar/PySide6/jar/Qt6AndroidNetwork.jar"], [] + mock_find_jars.return_value = jars, init_classes + dependent_plugins = ["platforms_qtforandroid", + "platforminputcontexts_qtvirtualkeyboardplugin", + "iconengines_qsvgicon"] + mock_local_libs.return_value = [], dependent_plugins + mock_qmlimportscanner.return_value = ["QtQuick"] + + self.android_deploy.main(name="android_app", shiboken_wheel=self.shiboken_wheel, + pyside_wheel=self.pyside_wheel, ndk_path=self.ndk_path, + init=True, force=True, keep_deployment_files=True) + + self.assertEqual(mock_extract_jar.call_count, 1) + self.assertEqual(mock_qtlibs.call_count, 1) + self.assertEqual(mock_extraqtmodules.call_count, 1) + self.assertEqual(mock_recipes_exist.call_count, 1) + self.assertEqual(mock_find_jars.call_count, 1) + self.assertEqual(mock_qmlimportscanner.call_count, 1) + self.assertTrue(self.config_file.exists()) + self.assertTrue(self.buildozer_config_file.exists()) + + config_obj = self.deploy_lib.BaseConfig(config_file=self.config_file) + expected_modules = {"Quick", "Core", "Gui", "Network", "Qml", "QmlModels", "OpenGL"} + obtained_modules = set(config_obj.get_value("qt", "modules").split(",")) + self.assertEqual(obtained_modules, expected_modules) + expected_local_libs = "" + self.assertEqual(config_obj.get_value("buildozer", "local_libs"), + expected_local_libs) + expected_qt_plugins = set(dependent_plugins) + obtained_qt_plugins = set(config_obj.get_value("android", "plugins").split(",")) + self.assertEqual(expected_qt_plugins, obtained_qt_plugins) + + # test buildozer config file contents + buildozer_config_obj = self.deploy_lib.BaseConfig(config_file=self.buildozer_config_file) + obtained_jars = set(buildozer_config_obj.get_value("app", "android.add_jars").split(',')) + expected_jars = set(jars) + self.assertEqual(obtained_jars, expected_jars) + obtained_extra_args = buildozer_config_obj.get_value("app", "p4a.extra_args") + extra_args_patrn = re.compile("--qt-libs=(?P<modules>.*) --load-local-libs=" + "(?P<local_libs>.*) --init-classes=(?P<init_classes>.*)") + match = extra_args_patrn.search(obtained_extra_args) + obtained_modules = match.group("modules").split(',') + obtained_local_libs = match.group("local_libs") + obtained_init_classes = match.group("init_classes") + self.assertEqual(set(obtained_modules), expected_modules) + self.assertEqual(obtained_local_libs, expected_local_libs) + self.assertEqual(obtained_init_classes, '') + + self.config_file.unlink() + self.buildozer_config_file.unlink() + + +if __name__ == "__main__": + unittest.main() diff --git a/sources/pyside6/tests/tools/pyside6-deploy/CMakeLists.txt b/sources/pyside6/tests/tools/pyside6-deploy/CMakeLists.txt new file mode 100644 index 000000000..7f010fbd6 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-deploy/CMakeLists.txt @@ -0,0 +1 @@ +PYSIDE_TEST(test_pyside6_deploy.py) diff --git a/sources/pyside6/tests/tools/pyside6-deploy/extensive_deploy_test.py b/sources/pyside6/tests/tools/pyside6-deploy/extensive_deploy_test.py new file mode 100644 index 000000000..40afc7f5c --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-deploy/extensive_deploy_test.py @@ -0,0 +1,88 @@ +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +""" + Extensive manual test of pyside6-deploy + + Note: Not to be added into the CI +""" + +import logging +import unittest +import tempfile +import shutil +import sys +import os +import importlib +from pathlib import Path + + +class TestPySide6Deploy(unittest.TestCase): + @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.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")) + cls.deploy_lib = importlib.import_module("deploy_lib") + cls.deploy = importlib.import_module("deploy") + sys.modules["deploy"] = cls.deploy + + def setUpWidgets(self): + os.chdir(self.temp_example_widgets) + self.main_file = self.temp_example_widgets / "tetrix.py" + self.config_file = self.temp_example_widgets / "pysidedeploy.spec" + + def testWidget(self): + self.setUpWidgets() + self.deploy.main(self.main_file, name="widget_app", loglevel=logging.INFO, + keep_deployment_files=True, force=True) + + print("Now testing Widget with config file") + self.deploy.main(self.main_file, config_file=self.config_file, loglevel=logging.INFO, + force=True) + + def setUpQml(self): + os.chdir(self.temp_example_qml) + self.main_file = self.temp_example_qml / "main.py" + self.config_file = self.temp_example_qml / "pysidedeploy.spec" + + def testQml(self): + self.setUpQml() + self.deploy.main(self.main_file, name="qml_app", loglevel=logging.INFO, + keep_deployment_files=True, force=True) + + def testWebEngineQuickDryRun(self): + os.chdir(self.temp_example_webenginequick) + main_file = self.temp_example_webenginequick / "quicknanobrowser.py" + self.deploy.main(main_file, name="qml_app", keep_deployment_files=True, + loglevel=logging.INFO, force=True) + + def tearDown(self) -> None: + super().tearDown() + os.chdir(self.current_dir) + + @classmethod + def tearDownClass(cls) -> None: + shutil.rmtree(Path(cls.temp_dir)) + + +if __name__ == "__main__": + unittest.main() diff --git a/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py b/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py new file mode 100644 index 000000000..db60c8c3f --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-deploy/test_pyside6_deploy.py @@ -0,0 +1,400 @@ +# Copyright (C) 2022 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +import unittest +import tempfile +import shutil +import sys +import os +import importlib +import platform +from pathlib import Path +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) + + +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 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() + cls.example_root = cls.pyside_root / "examples" + cls.temp_dir = tempfile.mkdtemp() + cls.current_dir = Path.cwd() + 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 + + # print no outputs to stdout + sys.stdout = mock.MagicMock() + + @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" + f" --enable-plugin=pyside6 --output-dir={str(self.deployment_files)} --quiet" + 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-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, 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. + original_output = self.deploy.main(self.main_file, dry_run=True, force=True) + self.assertEqual(original_output, self.expected_run_cmd) + + @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 + # init + init_result = self.deploy.main(self.main_file, init=True, force=True) + self.assertEqual(init_result, None) + + # test with config + 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 = 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==2.1") + self.assertEqual(config_obj.get_value("qt", "qml_files"), "") + 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 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)) + + +@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" + f" --enable-plugin=pyside6 --output-dir={str(self.deployment_files)} --quiet" + 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" + ) + + 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-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" + + @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"] + init_result = self.deploy.main(self.main_file, init=True, force=True) + self.assertEqual(init_result, None) + + # test config file contents + 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==2.1") + self.assertEqual(config_obj.get_value("qt", "qml_files"), "main.qml,MovingRectangle.qml") + 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, 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) + + 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) + + +@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", + "BrowserWindow.qml", + "DownloadView.qml", + "FindBar.qml", + "FullScreenNotification.qml", + ] + data_files_cmd = " ".join( + [ + 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" + f" --enable-plugin=pyside6 --output-dir={str(deployment_files)} --quiet" + 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": + expected_run_cmd += ( + " --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-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" + + # create config file + with patch("deploy_lib.config.run_qmlimportscanner") as mock_qmlimportscanner: + mock_qmlimportscanner.return_value = ["QtQuick", "QtWebEngine"] + init_result = self.deploy.main(main_file, init=True, force=True) + self.assertEqual(init_result, None) + + # 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, 2) + + # test config file contents + 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", + ) + 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__": + unittest.main() diff --git a/sources/pyside6/tests/tools/pyside6-qml/CMakeLists.txt b/sources/pyside6/tests/tools/pyside6-qml/CMakeLists.txt new file mode 100644 index 000000000..4d801264a --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-qml/CMakeLists.txt @@ -0,0 +1 @@ +PYSIDE_TEST(test_pyside6_qml.py) diff --git a/sources/pyside6/tests/tools/pyside6-qml/test_pyside6_qml.py b/sources/pyside6/tests/tools/pyside6-qml/test_pyside6_qml.py new file mode 100644 index 000000000..fdaf3d471 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-qml/test_pyside6_qml.py @@ -0,0 +1,75 @@ +# Copyright (C) 2018 The Qt Company Ltd. +# SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +"""Test for pyside6-qml + +The tests does a unittest and some integration tests for pyside6-qml.""" + +from asyncio.subprocess import PIPE +import os +import sys +import unittest +import subprocess +import importlib.util + +from pathlib import Path +sys.path.append(os.fspath(Path(__file__).resolve().parents[2])) +from init_paths import init_test_paths +init_test_paths(False) + + +class TestPySide6QmlUnit(unittest.TestCase): + def setUp(self) -> None: + super().setUp() + self._dir = Path(__file__).parent.resolve() + self.pyside_root = self._dir.parents[4] + + self.pyqml_path = self.pyside_root / "sources" / "pyside-tools" / "qml.py" + self.core_qml_path = (self.pyside_root / "examples" / "qml" / + "tutorials" / "extending-qml-advanced" / "adding") + + self.pyqml_run_cmd = [sys.executable, os.fspath(self.pyqml_path)] + + # self.pyqml_path will not abe able to find pyside and other related binaries, if not added + # to path explicitly. The following lines does that. + self.test_env = os.environ.copy() + self.test_env["PYTHONPATH"] = os.pathsep + os.pathsep.join(sys.path) + + def testImportQmlModules(self): + + # because pyside-tools has a hyphen, a normal 'from pyside-tools import qml' cannot be done + spec = importlib.util.spec_from_file_location("qml", self.pyqml_path) + pyqml = importlib.util.module_from_spec(spec) + spec.loader.exec_module(pyqml) + pyqml.import_qml_modules(self.core_qml_path) + + # path added to sys.path + self.assertIn(str(self.core_qml_path), sys.path) + + # module is imported + self.assertIn("person", sys.modules.keys()) + + # remove the imported modules + sys.path.remove(str(self.core_qml_path)) + del sys.modules["person"] + + # test with module_paths - dir + self.person_path = self.core_qml_path / "person.py" + pyqml.import_qml_modules(self.core_qml_path, module_paths=[self.core_qml_path]) + self.assertIn(str(self.core_qml_path), sys.path) + self.assertIn("person", sys.modules.keys()) + + # test with module_paths - file - in testCoreApplication(self) + + def testCoreApplication(self): + self.pyqml_run_cmd.extend(["--apptype", "core"]) + self.pyqml_run_cmd.append(str(self.core_qml_path / "People" / "Main.qml")) + self.pyqml_run_cmd.extend(["-I", str(self.core_qml_path / "person.py")]) + + result = subprocess.run(self.pyqml_run_cmd, stdout=PIPE, env=self.test_env) + self.assertEqual(result.returncode, 0) + self.assertEqual(result.stdout.rstrip(), b"{'_name': 'Bob Jones', '_shoe_size': 12}") + + +if __name__ == '__main__': + unittest.main() |