diff options
author | Jaime Resano <Jaime.Resano-Aisa@qt.io> | 2025-02-24 11:55:30 +0100 |
---|---|---|
committer | Friedemann Kleint <Friedemann.Kleint@qt.io> | 2025-03-12 22:15:41 +0100 |
commit | 58dc331da48ed85bfa8cc431568db4d8705cdffd (patch) | |
tree | 80600eee7e4d8663ff4861ee943716729fde8e32 /sources/pyside6/tests | |
parent | 545ca796dbd93edb66dc3c21c74511fab8e9d0a3 (diff) |
pyproject.toml: 3. Add pyside6-project tests for pyproject.toml changes
This patch adds tests for the pyside6-project CLI tool to validate the
pyproject.toml changes.
The tests ensure that the existing behavior is preserved and that the
new features work as expected.
Task-number: PYSIDE-2714
Change-Id: I096188c1d6d931a3970787f2906b83d2a987f4ed
Reviewed-by: Cristian Maureira-Fredes <cristian.maureira-fredes@qt.io>
Diffstat (limited to 'sources/pyside6/tests')
26 files changed, 525 insertions, 56 deletions
diff --git a/sources/pyside6/tests/tools/pyside6-project/example_drumpad/Python/.pyproject b/sources/pyside6/tests/tools/pyside6-project/example_drumpad/Python/Drumpad.pyproject index a5f654c2b..a5f654c2b 100644 --- a/sources/pyside6/tests/tools/pyside6-project/example_drumpad/Python/.pyproject +++ b/sources/pyside6/tests/tools/pyside6-project/example_drumpad/Python/Drumpad.pyproject diff --git a/sources/pyside6/tests/tools/pyside6-project/example_drumpad/Python/pyproject.toml b/sources/pyside6/tests/tools/pyside6-project/example_drumpad/Python/pyproject.toml new file mode 100644 index 000000000..3eeb1a90d --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/example_drumpad/Python/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "Drumpad" + +[tool.pyside6-project] +files = ["autogen/settings.py", "main.py"] diff --git a/sources/pyside6/tests/tools/pyside6-project/example_project/example_project.pyproject b/sources/pyside6/tests/tools/pyside6-project/example_project/example_project.pyproject new file mode 100644 index 000000000..08fa3eac3 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/example_project/example_project.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["mainwindow.py", "my_widget.py", "folder/file_in_folder.py", "main.py", "subproject/subproject.pyproject"] +} diff --git a/sources/pyside6/tests/tools/pyside6-project/example_project/folder/label_in_folder.py b/sources/pyside6/tests/tools/pyside6-project/example_project/folder/label_in_folder.py new file mode 100644 index 000000000..ea3a6f5a1 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/example_project/folder/label_in_folder.py @@ -0,0 +1,9 @@ +# Copyright (C) 2025 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 +from PySide6.QtWidgets import QLabel + + +class LabelInFolder(QLabel): + def __init__(self): + super().__init__() + self.setText("Label in folder") diff --git a/sources/pyside6/tests/tools/pyside6-project/example_project/main.py b/sources/pyside6/tests/tools/pyside6-project/example_project/main.py new file mode 100644 index 000000000..0f7f96747 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/example_project/main.py @@ -0,0 +1,20 @@ +# Copyright (C) 2025 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 os + +from mainwindow import MainWindow +from PySide6.QtWidgets import QApplication +import sys + + +def main(): + app = QApplication(sys.argv) + window = MainWindow() + if os.getenv("PYSIDE_TESTING"): + return 0 + window.show() + return app.exec() + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/sources/pyside6/tests/tools/pyside6-project/example_project/mainwindow.py b/sources/pyside6/tests/tools/pyside6-project/example_project/mainwindow.py new file mode 100644 index 000000000..fc259c6be --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/example_project/mainwindow.py @@ -0,0 +1,22 @@ +# Copyright (C) 2025 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 +from PySide6.QtWidgets import QMainWindow, QWidget, QVBoxLayout +from folder.label_in_folder import LabelInFolder +from subproject.subproject_button import SubprojectButton + + +class MainWindow(QMainWindow): + def __init__(self, parent=None): + super().__init__(parent) + self.setWindowTitle("Main Window") + + self.central_layout = QVBoxLayout() + self.central_widget = QWidget() + self.setCentralWidget(self.central_widget) + self.central_widget.setLayout(self.central_layout) + + self.label_in_folder = LabelInFolder() + self.central_layout.addWidget(self.label_in_folder) + + self.subproject_button = SubprojectButton() + self.central_layout.addWidget(self.subproject_button) diff --git a/sources/pyside6/tests/tools/pyside6-project/example_project/pyproject.toml b/sources/pyside6/tests/tools/pyside6-project/example_project/pyproject.toml new file mode 100644 index 000000000..59cc29263 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/example_project/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "example_project" + +[tool.pyside6-project] +files = ["folder/file_in_folder.py", "main.py", "mainwindow.py", "my_widget.py", "subproject/subproject.pyproject"] diff --git a/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/pyproject.toml b/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/pyproject.toml new file mode 100644 index 000000000..1ceb0ac0b --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "subproject" + +[tool.pyside6-project] +files = ["subproject_button.py"] diff --git a/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject.pyproject b/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject.pyproject new file mode 100644 index 000000000..abfa1f461 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["subproject_button.py"] +} diff --git a/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject_button.py b/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject_button.py new file mode 100644 index 000000000..40813ff86 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/example_project/subproject/subproject_button.py @@ -0,0 +1,17 @@ +# Copyright (C) 2025 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 +from PySide6.QtWidgets import QPushButton, QApplication +import sys + + +class SubprojectButton(QPushButton): + def __init__(self): + super().__init__() + self.setText("Subproject button") + + +if __name__ == "__main__": + app = QApplication(sys.argv) + button = SubprojectButton() + button.show() + sys.exit(app.exec()) diff --git a/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/existing_pyproject_toml.pyproject b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/existing_pyproject_toml.pyproject new file mode 100644 index 000000000..7ddbd86fd --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/existing_pyproject_toml.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["zzz.py", "main.py"] +} diff --git a/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/expected_pyproject.toml b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/expected_pyproject.toml new file mode 100644 index 000000000..be8aa949f --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/expected_pyproject.toml @@ -0,0 +1,22 @@ +[project] +name = "my_project" +version = "0.1.0" +description = "A sample Python project" +authors = [ + { name = "John Doe", email = "john.doe@example.com" }, +] +optional-dependencies = { dev = ["pytest", "black"], docs = ["sphinx"] } + +# Comment + +[tool.black] +line-length = 88 +target-version = ["py38"] + +# Another comment + +[tool.pyside6-project] +files = ["main.py", "zzz.py"] +[build-system] +requires = ["setuptools >=42"] +build-backend = "setuptools.build_meta" diff --git a/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/main.py b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/main.py new file mode 100644 index 000000000..8d4c96674 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/main.py @@ -0,0 +1,2 @@ +# Copyright (C) 2025 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 diff --git a/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/pyproject.toml b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/pyproject.toml new file mode 100644 index 000000000..ce2656502 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/pyproject.toml @@ -0,0 +1,20 @@ +[project] +name = "my_project" +version = "0.1.0" +description = "A sample Python project" +authors = [ + { name = "John Doe", email = "john.doe@example.com" }, +] +optional-dependencies = { dev = ["pytest", "black"], docs = ["sphinx"] } + +# Comment + +[tool.black] +line-length = 88 +target-version = ["py38"] + +# Another comment + +[build-system] +requires = ["setuptools >=42"] +build-backend = "setuptools.build_meta" diff --git a/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/zzz.py b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/zzz.py new file mode 100644 index 000000000..8d4c96674 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/existing_pyproject_toml/zzz.py @@ -0,0 +1,2 @@ +# Copyright (C) 2025 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 diff --git a/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/invalid_pyproject.pyproject b/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/invalid_pyproject.pyproject new file mode 100644 index 000000000..6d6e84a36 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/invalid_pyproject.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py", 33] +} diff --git a/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/main.py b/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/main.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/main.py diff --git a/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/pyproject.toml b/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/pyproject.toml new file mode 100644 index 000000000..37d23c948 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/pyproject.toml @@ -0,0 +1,2 @@ +this is not a valid pyproject.toml file +because it does not have a valid toml structure diff --git a/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/valid_pyproject.pyproject b/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/valid_pyproject.pyproject new file mode 100644 index 000000000..cc7a74a34 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/invalid_pyproject/valid_pyproject.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["main.py"] +} diff --git a/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/common_file.py b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/common_file.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/common_file.py diff --git a/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/expected_pyproject.toml b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/expected_pyproject.toml new file mode 100644 index 000000000..fda48f5ba --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/expected_pyproject.toml @@ -0,0 +1,5 @@ +[project] +name = "multiple_pyproject" + +[tool.pyside6-project] +files = ["common_file.py", "file1.py", "file2.py"] diff --git a/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/file1.py b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/file1.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/file1.py diff --git a/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/file2.py b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/file2.py new file mode 100644 index 000000000..e69de29bb --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/file2.py diff --git a/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/project1.pyproject b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/project1.pyproject new file mode 100644 index 000000000..105f6f919 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/project1.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["file1.py", "common_file.py"] +} diff --git a/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/project2.pyproject b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/project2.pyproject new file mode 100644 index 000000000..623b08794 --- /dev/null +++ b/sources/pyside6/tests/tools/pyside6-project/multiple_pyproject/project2.pyproject @@ -0,0 +1,3 @@ +{ + "files": ["common_file.py", "file2.py"] +} diff --git a/sources/pyside6/tests/tools/pyside6-project/test_pyside6_project.py b/sources/pyside6/tests/tools/pyside6-project/test_pyside6_project.py index 0e7982a53..d66395251 100644 --- a/sources/pyside6/tests/tools/pyside6-project/test_pyside6_project.py +++ b/sources/pyside6/tests/tools/pyside6-project/test_pyside6_project.py @@ -1,16 +1,18 @@ # Copyright (C) 2024 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 contextlib +import difflib +import importlib import io import os import shutil import sys -import unittest -from unittest import mock -from unittest import TestCase import tempfile -import importlib +import unittest from pathlib import Path +from unittest import TestCase +from unittest import mock +from unittest.mock import patch sys.path.append(str(Path(__file__).resolve().parents[2])) from init_paths import init_test_paths @@ -18,11 +20,30 @@ from init_paths import init_test_paths init_test_paths(False) +def file_diff(expected_file: Path, actual_file: Path) -> str: + """ + Get a unified diff between two files + """ + target_text = expected_file.read_text(encoding="utf-8").splitlines() + generated_text = actual_file.read_text(encoding="utf-8").splitlines() + + return "\n".join(difflib.unified_diff( + generated_text, target_text, + fromfile=str(actual_file), + tofile=str(expected_file), + lineterm="" + )) + + class PySide6ProjectTestBase(TestCase): + # If a project name is specified, on each the test, the project folder will be copy to the + # temp dir and the current dir will be changed to the project folder + # The project name must match an existing folder in the folder where this file is located + project_name: str | None = None + @classmethod def setUpClass(cls): cls.pyside_root = Path(__file__).parents[5].resolve() - cls.example_root = cls.pyside_root / "examples" tools_path = cls.pyside_root / "sources" / "pyside-tools" if tools_path not in sys.path: sys.path.append(str(tools_path)) @@ -32,36 +53,42 @@ class PySide6ProjectTestBase(TestCase): cls.current_dir = Path.cwd() # print no outputs to stdout sys.stdout = mock.MagicMock() + if cls.project_name: + cls.temp_project = Path(cls.temp_dir / cls.project_name).resolve() + os.chdir(cls.temp_dir) + + def setUp(self): + super().setUp() + if self.project_name: + shutil.copytree(Path(__file__).parent / self.project_name, self.temp_project) + os.chdir(self.temp_project) + + def tearDown(self): + super().tearDown() + if self.project_name: + os.chdir(self.temp_dir) + shutil.rmtree(self.temp_project) @classmethod def tearDownClass(cls): os.chdir(cls.current_dir) shutil.rmtree(cls.temp_dir) - def setUp(self): - os.chdir(self.temp_dir) - class TestPySide6ProjectDesignStudio(PySide6ProjectTestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - example_drumpad = Path(__file__).parent / "example_drumpad" - cls.temp_example_drumpad = Path( - shutil.copytree(example_drumpad, cls.temp_dir / "drumpad") - ).resolve() + project_name = "example_drumpad" def testDrumpadExample(self): # This test compiles the .qrc file into a .py file and checks whether the compilation is # carried out only when required - compiled_resources_path = self.temp_example_drumpad / "Python" / "autogen" / "resources.py" - resources_path = self.temp_example_drumpad / "Drumpad.qrc" - requires_rebuild = self.project_lib.utils.requires_rebuild + compiled_resources_path = Path("Python") / "autogen" / "resources.py" + resources_path = Path("Drumpad.qrc") + requires_rebuild = self.project_lib.utils.requires_rebuild + pyproject_path = Path("Python") / "Drumpad.pyproject" self.assertFalse(compiled_resources_path.exists()) - - os.chdir(self.temp_example_drumpad / "Python") - self.project.main(mode="build") + self.assertTrue(pyproject_path.exists()) + self.project.main(mode="build", project_path=pyproject_path) self.assertTrue(compiled_resources_path.exists()) self.assertFalse(requires_rebuild([resources_path], compiled_resources_path)) @@ -72,59 +99,96 @@ class TestPySide6ProjectDesignStudio(PySide6ProjectTestBase): self.assertTrue(requires_rebuild([resources_path], compiled_resources_path)) - self.project.main(mode="build") + self.project.main(mode="build", project_path=pyproject_path) self.assertFalse(requires_rebuild([resources_path], compiled_resources_path)) # Refresh the modification timestamp of one of the resources files - list((self.temp_example_drumpad / "Resources").glob("*.txt"))[0].touch() + list((Path("Resources").glob("*.txt")))[0].touch() self.assertTrue(requires_rebuild([resources_path], compiled_resources_path)) - self.project.main(mode="clean") + self.project.main(mode="clean", project_path=pyproject_path) self.assertFalse(compiled_resources_path.exists()) + def testMigrateDrumpadExample(self): + # The pyproject.toml file already contains the expected output + expected_pyproject_toml = Path("Python") / "pyproject.toml" + expected_pyproject_toml.rename(expected_pyproject_toml.parent / "expected_pyproject.toml") + existing_pyproject = Path("Python") / "Drumpad.pyproject" + + with self.assertRaises(SystemExit) as context: + with patch("builtins.input", return_value="y"): + self.project.main(mode="migrate-pyproject", + project_path=existing_pyproject.as_posix()) + + self.assertEqual(0, context.exception.code) + generated_pyproject_toml = Path("Python") / "pyproject.toml" + self.assertTrue(generated_pyproject_toml.exists()) + diff = file_diff(expected_pyproject_toml, generated_pyproject_toml) + self.assertFalse(diff, f"Generated pyproject.toml does not match:\n{diff}") + class TestPySide6ProjectNew(PySide6ProjectTestBase): def testNewUi(self): + test_project_path = self.temp_dir / "NewUiProject" with self.assertRaises(SystemExit) as context: - self.project.main(mode="new-ui", project_dir="TestProject") - test_project_path = Path("TestProject") + self.project.main(mode="new-ui", project_dir=test_project_path.as_posix()) + self.assertTrue((test_project_path / "pyproject.toml").exists()) self.assertTrue((test_project_path / "mainwindow.ui").exists()) self.assertTrue((test_project_path / "main.py").exists()) - self.assertEqual(context.exception.code, 0) + self.assertEqual(0, context.exception.code) shutil.rmtree(test_project_path) - def testRaiseErrorOnExistingProject(self): + def testRaiseErrorOnExistingNonEmptyProject(self): + # Create a project twice to ensure that an error is raised + project_name = "TestProject" with self.assertRaises(SystemExit) as context: - self.project.main(mode="new-ui", project_dir="TestProject") - self.assertEqual(context.exception.code, 0) + self.project.main(mode="new-ui", project_dir=project_name) + + self.assertEqual(0, context.exception.code) + error_message = io.StringIO() - with self.assertRaises(SystemExit) as context, contextlib.redirect_stderr(error_message): - self.project.main(mode="new-ui", project_dir="TestProject") - self.assertEqual(context.exception.code, 1) - self.assertTrue(error_message.getvalue()) # some error message is printed + with self.assertRaises(SystemExit) as context: + with contextlib.redirect_stderr(error_message): + self.project.main(mode="new-ui", project_dir=project_name) + + self.assertEqual(1, context.exception.code) + self.assertTrue(f"Can not create project at {project_name}: directory is not empty." in + error_message.getvalue()) shutil.rmtree(self.temp_dir / "TestProject") + def testRaiseErrorOnInvalidProjectName(self): + # Create a project with an empty project name + error_message = io.StringIO() + with self.assertRaises(SystemExit) as context: + with contextlib.redirect_stderr(error_message): + self.project.main(mode="new-ui", project_dir="asdf/?^%$#@!") + + self.assertEqual(1, context.exception.code) + self.assertTrue("Invalid project name" in error_message.getvalue()) + def testNewQuick(self): + test_project_path = Path("QuickProject") + with self.assertRaises(SystemExit) as context: - self.project.main(mode="new-quick", project_dir="TestProject") - test_project_path = Path("TestProject") + self.project.main(mode="new-quick", project_dir=str(test_project_path)) + self.assertTrue((test_project_path / "pyproject.toml").exists()) self.assertTrue((test_project_path / "main.qml").exists()) self.assertTrue((test_project_path / "main.py").exists()) - self.assertEqual(context.exception.code, 0) + self.assertEqual(0, context.exception.code) shutil.rmtree(test_project_path) def testNewWidget(self): + project_dir = self.temp_dir / "inner_folder" / "another_folder" / "WidgetProject" with self.assertRaises(SystemExit) as context: - self.project.main(mode="new-widget", project_dir="TestProject") - test_project_path = Path("TestProject") - self.assertTrue((test_project_path / "pyproject.toml").exists()) - self.assertTrue((test_project_path / "main.py").exists()) - self.assertEqual(context.exception.code, 0) - shutil.rmtree(test_project_path) + self.project.main(mode="new-widget", project_dir=project_dir.as_posix()) + self.assertTrue((project_dir / "pyproject.toml").exists()) + self.assertTrue((project_dir / "main.py").exists()) + self.assertEqual(0, context.exception.code) + shutil.rmtree(project_dir) def testRaiseErrorWhenNoProjectNameIsSpecified(self): mode = "new-widget" @@ -135,25 +199,273 @@ class TestPySide6ProjectNew(PySide6ProjectTestBase): expected_msg = f"Error creating new project: {mode} requires a directory name or path" self.assertTrue(expected_msg in error_message.getvalue()) + def testCreateProjectLegacyPyProjectFile(self): + project_path = Path("TestPyProjectJSON") + mode = "new-widget" + with self.assertRaises(SystemExit) as context: + self.project.main(mode=mode, project_dir=project_path.as_posix(), legacy_pyproject=True) + self.assertEqual(0, context.exception.code) + self.assertTrue((project_path / "main.py").exists()) + self.assertTrue((project_path / f"{project_path.name}.pyproject").exists()) + class TestPySide6ProjectRun(PySide6ProjectTestBase): - @classmethod - def setUpClass(cls): - super().setUpClass() - example_widgets = cls.example_root / "widgets" / "widgets" / "tetrix" - cls.temp_example_tetrix = Path( - shutil.copytree(example_widgets, Path(cls.temp_dir) / "tetrix") - ).resolve() - - def testRunEmptyProject(self): - project_folder = self.temp_dir / "TestProject" + project_name = "example_project" + + def testRaiseErrorWhenRunningEmptyProject(self): + # Create a new empty project in the temp dir + project_folder = self.temp_dir / "empty_project" project_folder.mkdir() os.chdir(project_folder) + error_message = io.StringIO() - with self.assertRaises(SystemExit) as context, contextlib.redirect_stderr(error_message): + with self.assertRaises(SystemExit) as context: + with contextlib.redirect_stderr(error_message): + self.project.main(mode="run") + + os.chdir(self.temp_dir) + shutil.rmtree(project_folder) + + self.assertEqual(1, context.exception.code) + self.assertTrue("No project file found" in error_message.getvalue()) + + def testRunExampleProject(self): + # The project is executed in a subprocess. The proejct code reads the PYSIDE_TESTING + # environment variable to avoid starting the Qt event loop + os.environ["PYSIDE_TESTING"] = "1" + with self.assertRaises(SystemExit) as context: self.project.main(mode="run") - self.assertEqual(context.exception.code, 1) - self.assertTrue(error_message.getvalue()) # some error message is printed + os.environ.pop("PYSIDE_TESTING") + self.assertEqual(0, context.exception.code) + + self.assertEqual(Path("pyproject.toml").resolve(), + self.project_lib.resolve_valid_project_file()) + + +class TestPySide6ProjectExampleProject(PySide6ProjectTestBase): + """ + Test of an example project with both pyproject.toml and .pyproject valid files. + Contains a subproject with its own pyproject.toml file and .pyproject file too + """ + project_name = "example_project" + + def testMigratePyProjectToToml(self): + # The existing pyproject.toml file contains the expected output + expected_pyproject_toml = Path("pyproject.toml").rename("expected_pyproject.toml") + expected_subproject_pyproject_toml = Path("subproject") / "pyproject.toml" + expected_subproject_pyproject_toml.rename( + expected_subproject_pyproject_toml.parent / "expected_subproject_pyproject.toml") + + with self.assertRaises(SystemExit) as context: + self.project.main(mode="migrate-pyproject") + + self.assertEqual(0, context.exception.code) + + generated_pyproject_toml = Path("pyproject.toml") + self.assertTrue(generated_pyproject_toml.exists()) + diff = file_diff(expected_pyproject_toml, generated_pyproject_toml) + self.assertFalse(diff, f"Generated pyproject.toml does not match:\n{diff}") + + generated_subproject_pyproject_toml = Path("subproject") / "pyproject.toml" + self.assertTrue(generated_subproject_pyproject_toml.exists()) + diff = file_diff(expected_subproject_pyproject_toml, generated_subproject_pyproject_toml) + self.assertFalse(diff, f"Generated subproject/pyproject.toml does not match:\n{diff}") + + def testMigratePyProjectToTomlSpecifyingPyProjectFile(self): + # The existing pyproject.toml file contains the expected output + existing_pyproject = Path("example_project.pyproject") + expected_pyproject_toml = Path("pyproject.toml") + expected_pyproject_toml.rename("example_project.toml") + + expected_subproject_pyproject_toml = Path("subproject") / "pyproject.toml" + expected_subproject_pyproject_toml.rename( + expected_subproject_pyproject_toml.parent / "expected_pyproject.toml") + + with self.assertRaises(SystemExit) as context: + with patch("builtins.input", return_value="y"): + self.project.main(mode="migrate-pyproject", + project_path=existing_pyproject.as_posix()) + + self.assertEqual(0, context.exception.code) + + generated_pyproject_toml = Path("pyproject.toml") + self.assertTrue(generated_pyproject_toml.exists()) + diff = file_diff(expected_pyproject_toml, generated_pyproject_toml) + self.assertFalse(diff, f"Generated pyproject.toml does not match:\n{diff}") + + generated_subproject_pyproject_toml = Path("subproject") / "pyproject.toml" + self.assertTrue(generated_subproject_pyproject_toml.exists()) + diff = file_diff(expected_subproject_pyproject_toml, generated_subproject_pyproject_toml) + self.assertFalse(diff, f"Generated subproject/pyproject.toml does not match:\n{diff}") + + +class TestPySide6ProjectExistingPyProjectToml(PySide6ProjectTestBase): + """ + Test for migrating a project with an existing pyproject.toml file which does not contain the + [tool.pyside6-project] section + """ + project_name = "existing_pyproject_toml" + + def testMigratePyProjectToTomlAlreadyExistingTomlFile(self): + with self.assertRaises(SystemExit) as context: + with patch("builtins.input", return_value="y"): + self.project.main(mode="migrate-pyproject") + + self.assertEqual(0, context.exception.code) + diff = file_diff(Path("expected_pyproject.toml"), + Path("pyproject.toml")) + self.assertFalse(diff, f"Updated pyproject.toml does not match:\n{diff}") + + +class TestPySide6ProjectInvalidPyProjectToml(PySide6ProjectTestBase): + """ + Check the current behavior in a project with an existing invalid pyproject.toml file and + invalid_pyproject.pyproject file + """ + + project_name = "invalid_pyproject" + + def testRunInvalidPyProjectTomlFile(self): + pyproject_toml = Path("pyproject.toml") + self.assertTrue(pyproject_toml.exists()) + self.assertTrue(self.project_lib.parse_pyproject_toml(pyproject_toml).errors) + + error_message = io.StringIO() + with contextlib.redirect_stderr(error_message): + with self.assertRaises(SystemExit) as context: + self.project.main(mode="run", project_path=pyproject_toml.as_posix()) + + self.assertEqual(1, context.exception.code) + self.assertTrue("Invalid project file" in error_message.getvalue()) + + def testRunSpecifyingPyProjectJsonFile(self): + # Check that the *.pyproject file is used if the pyproject.toml is invalid when using + # pyside6-project run + + pyproject_toml_file = Path("pyproject.toml") + self.assertTrue(pyproject_toml_file.exists()) + # Ensure that pyproject.toml is considered invalid + self.assertTrue(self.project_lib.parse_pyproject_toml(pyproject_toml_file).errors) + + valid_pyproject = Path("valid_pyproject.pyproject") + self.assertTrue(valid_pyproject.exists()) + + # Ensure that the project can still be run specifying a valid *.pyproject JSON file + with self.assertRaises(SystemExit) as context: + self.project.main(mode="run", project_path=valid_pyproject.as_posix()) + + self.assertEqual(0, context.exception.code) + self.assertTrue(Path("main.py").exists()) + + def testErrorRaisesWhenRunningWithoutSpecifyingProjectFile(self): + # The project folder contains two *.pyproject JSON files. + # The tool should raise an error because the project file is not specified + error_message = io.StringIO() + with contextlib.redirect_stderr(error_message): + with self.assertRaises(SystemExit) as context: + self.project.main(mode="run") + self.assertEqual(1, context.exception.code) + self.assertTrue("Multiple project files found" in error_message.getvalue()) + + def testRaiseErrorResolvingInvalidProjectFile(self): + # Simulate that the user is specifying an invalid project file + invalid_pyproject_file = Path("invalid_pyproject.pyproject") + self.assertTrue(invalid_pyproject_file.exists()) + + with self.assertRaises(ValueError) as context: + self.project_lib.resolve_valid_project_file(invalid_pyproject_file.as_posix()) + + exception_message = str(context.exception) + self.assertTrue("Invalid project file" in exception_message) + self.assertTrue(str(invalid_pyproject_file) in exception_message) + + def testResolveValidProjectFile(self): + # Simulate that the user is specifying a valid project file + valid_pyproject_file = Path("valid_pyproject.pyproject") + actual_project_file = self.project_lib.resolve_valid_project_file( + valid_pyproject_file.as_posix()) + self.assertEqual(valid_pyproject_file.resolve(), actual_project_file) + + def testRaiseErrorResolvingInvalidPyProjectToml(self): + # Simulate that the user is specifying an invalid pyproject.toml file + pyproject_toml_file = Path("pyproject.toml") + self.assertTrue(pyproject_toml_file.exists()) + + with self.assertRaises(ValueError) as context: + self.project_lib.resolve_valid_project_file(pyproject_toml_file.as_posix()) + + exception_message = str(context.exception) + self.assertTrue("Invalid project file" in exception_message) + self.assertTrue(str(pyproject_toml_file) in exception_message) + + def testMigrateInvalidPyProjectToml(self): + # Can not migrate a project with an invalid pyproject.toml file + error_message = io.StringIO() + with contextlib.redirect_stderr(error_message): + with self.assertRaises(SystemExit) as context: + with patch("builtins.input", return_value="y"): + self.project.main(mode="migrate-pyproject") + + self.assertEqual(1, context.exception.code) + self.assertTrue("Invalid project file" in error_message.getvalue()) + + def testMigrateInvalidPyProjectTomlSpecifyingWrongFile(self): + # Test specifying the pyproject.toml file as the project file to be migrated + existing_invalid_pyproject_toml = Path("pyproject.toml") + self.assertTrue( + bool(self.project_lib.parse_pyproject_toml(existing_invalid_pyproject_toml).errors)) + + error_message = io.StringIO() + with contextlib.redirect_stderr(error_message): + with self.assertRaises(SystemExit) as context: + self.project.main(mode="migrate-pyproject", + project_path=existing_invalid_pyproject_toml) + + self.assertEqual(1, context.exception.code) + self.assertTrue("Cannot migrate non \"*.pyproject\" file" in error_message.getvalue()) + self.assertTrue("pyproject.toml" in error_message.getvalue()) + + +def testRunInvalidPyProjectToml(self): + # Ensure that the .pyproject file is preferred over the invalid pyproject.toml file. + # This preserves the backward compatibility of the .pyproject file + + # Remove the invalid invalid_pyproject.pyproject file first + Path("invalid_pyproject.pyproject").unlink() + self.assertFalse(Path("invalid_pyproject.pyproject").exists()) + + with self.assertRaises(SystemExit) as context: + self.project.main(mode="run") + + self.assertEqual(0, context.exception.code) + self.assertEqual(Path("valid_pyproject.pyproject").resolve(), + self.project_lib.resolve_valid_project_file()) + + +class TestPySide6ProjectMultiplePyProject(PySide6ProjectTestBase): + project_name = "multiple_pyproject" + + def testCancelMigration(self): + # Ensure that the pyproject.toml is not created if the user cancels the operation + with self.assertRaises(SystemExit) as context: + with patch("builtins.input", return_value="n"): + self.project.main(mode="migrate-pyproject") + + self.assertEqual(0, context.exception.code) + self.assertFalse(Path("pyproject.toml").exists()) + + def testMigrateMultiplePyProjectFilesToToml(self): + expected_pyproject_toml = Path("expected_pyproject.toml") + generated_pyproject_toml = Path("pyproject.toml") + + with self.assertRaises(SystemExit) as context: + with patch("builtins.input", return_value="y"): + self.project.main(mode="migrate-pyproject") + + self.assertEqual(0, context.exception.code) + self.assertTrue(generated_pyproject_toml.exists()) + diff = file_diff(expected_pyproject_toml, generated_pyproject_toml) + self.assertFalse(diff, f"Generated pyproject.toml does not match:\n{diff}") if __name__ == "__main__": |