From 3afb6288462010489fb11b14d1aa13817cdde188 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Thu, 21 May 2026 14:19:41 +0200 Subject: [PATCH 1/9] add missing PyQtWebEngine to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index 54bfee5..b8e825e 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,4 +1,5 @@ PyQt5 +PyQtWebEngine guidata pylint black From 0af63f6557c26ac77f1338f8f5c728a9a16470f4 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Thu, 21 May 2026 14:20:27 +0200 Subject: [PATCH 2/9] change launch configuration type from python to debugpy --- .vscode/launch.json | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/.vscode/launch.json b/.vscode/launch.json index 89a2cf2..3f7bd2f 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -6,15 +6,12 @@ "configurations": [ { "name": "Run ModuleTester", - "type": "python", + "type": "debugpy", "request": "launch", "module": "moduletester.gui.main", "console": "integratedTerminal", "envFile": "${workspaceFolder}/.env", "justMyCode": false, - "args": [ - "--unattended", - ], "env": { "LANG": "en", "QT_COLOR_MODE": "light", @@ -22,7 +19,7 @@ }, { "name": "Run current file", - "type": "python", + "type": "debugpy", "request": "launch", "program": "${file}", "console": "integratedTerminal", From 10feb858a5d3e9bfb5d0261df1211cf50519aa87 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Thu, 21 May 2026 14:29:20 +0200 Subject: [PATCH 3/9] add pypandoc_binary to requirements --- requirements.txt | 1 + 1 file changed, 1 insertion(+) diff --git a/requirements.txt b/requirements.txt index b8e825e..2c51579 100644 --- a/requirements.txt +++ b/requirements.txt @@ -11,4 +11,5 @@ pydata_sphinx_theme build twine pypandoc +pypandoc_binary jinja2 \ No newline at end of file From 83e0083135f87dcabe7a36b1fee3e18e33966840 Mon Sep 17 00:00:00 2001 From: Pierre Raybaut <1311787+PierreRaybaut@users.noreply.github.com> Date: Thu, 21 May 2026 14:31:58 +0200 Subject: [PATCH 4/9] set fixed font for result label in ResultError and ResultOutput widgets --- moduletester/gui/widgets/result_error_widget.py | 1 + moduletester/gui/widgets/result_output_widget.py | 1 + 2 files changed, 2 insertions(+) diff --git a/moduletester/gui/widgets/result_error_widget.py b/moduletester/gui/widgets/result_error_widget.py index 54f2a77..76ded72 100644 --- a/moduletester/gui/widgets/result_error_widget.py +++ b/moduletester/gui/widgets/result_error_widget.py @@ -37,6 +37,7 @@ def __init__(self, parent: Optional[QW.QWidget] = None): ) self.label.setAlignment(QC.Qt.AlignmentFlag.AlignTop) self.label.setFrameStyle(0) + self.label.setFont(QG.QFontDatabase.systemFont(QG.QFontDatabase.FixedFont)) self.icon.setFixedWidth(32) self.icon.setAlignment(QC.Qt.AlignmentFlag.AlignTop) diff --git a/moduletester/gui/widgets/result_output_widget.py b/moduletester/gui/widgets/result_output_widget.py index 60f7b2d..055be03 100644 --- a/moduletester/gui/widgets/result_output_widget.py +++ b/moduletester/gui/widgets/result_output_widget.py @@ -29,6 +29,7 @@ def __init__(self, parent: Optional[QW.QWidget] = None): self.label.setTextInteractionFlags(QC.Qt.TextSelectableByMouse) self.label.setFrameStyle(0) self.label.setAlignment(QC.Qt.AlignTop) + self.label.setFont(QG.QFontDatabase.systemFont(QG.QFontDatabase.FixedFont)) self.icon.setFixedWidth(32) self.icon.setAlignment(QC.Qt.AlignTop) From 46952f6017f4a0c050d453a7ed064c869a7d17bf Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Thu, 21 May 2026 17:22:34 +0200 Subject: [PATCH 5/9] bump version to 1.0.1 --- CHANGELOG.md | 10 ++++++++++ doc/changelog.rst | 8 ++++++++ moduletester/__init__.py | 2 +- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index b62cfe1..de09456 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,15 @@ # ModuleTester Releases # +## Version 1.0.1 ## + +### Bug fixes + +*To be documented* + +### Improvements + +*To be documented* + ## Version 1.0.0 ## First stable release of ModuleTester. diff --git a/doc/changelog.rst b/doc/changelog.rst index 3a34dc5..d2b74a7 100644 --- a/doc/changelog.rst +++ b/doc/changelog.rst @@ -4,6 +4,14 @@ Changelog See the full changelog on `GitHub `_. +Version 1.0.1 +------------- + +Bug fixes and improvements โ€” see the +`CHANGELOG.md `_ +for the complete list. + + Version 1.0.0 ------------- diff --git a/moduletester/__init__.py b/moduletester/__init__.py index 3c49739..65c310f 100644 --- a/moduletester/__init__.py +++ b/moduletester/__init__.py @@ -16,7 +16,7 @@ .. _PlotPyStack: https://github.com/PlotPyStack """ -__version__ = "1.0.0" +__version__ = "1.0.1" __docurl__ = "https://moduletester.readthedocs.io/en/latest/" __homeurl__ = "https://codra-ingenierie-informatique.github.io/moduletester/" __supporturl__ = ( From ad1fa88bf2200f55d8013673313664935d951321 Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Thu, 21 May 2026 17:45:08 +0200 Subject: [PATCH 6/9] update changelog and add deps requirement sync script/task --- .vscode/tasks.json | 24 +++++++++++++ CHANGELOG.md | 7 ++-- pyproject.toml | 2 +- requirements.txt | 22 ++++++------ scripts/update_requirements.py | 61 ++++++++++++++++++++++++++++++++++ 5 files changed, 102 insertions(+), 14 deletions(-) create mode 100644 scripts/update_requirements.py diff --git a/.vscode/tasks.json b/.vscode/tasks.json index ccdf3a1..203be85 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -368,6 +368,30 @@ "๐Ÿงน Clean Up", ], }, + { + "label": "๐Ÿ“‹ Update requirements.txt", + "type": "shell", + "command": "${command:python.interpreterPath}", + "args": [ + "scripts/update_requirements.py", + ], + "options": { + "cwd": "${workspaceFolder}", + }, + "group": { + "kind": "build", + "isDefault": false, + }, + "presentation": { + "clear": true, + "echo": true, + "focus": false, + "panel": "dedicated", + "reveal": "always", + "showReuseMessage": true, + }, + "problemMatcher": [], + }, { "label": "โ” Untracked files", "type": "shell", diff --git a/CHANGELOG.md b/CHANGELOG.md index de09456..490e2c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,11 +4,14 @@ ### Bug fixes -*To be documented* +- Set fixed font for result label in `ResultError` and `ResultOutput` widgets +- Change launch configuration type from `python` to `debugpy` ### Improvements -*To be documented* +- Add `pypandoc_binary` to requirements +- Add missing `PyQtWebEngine` to requirements +- Add deps requirements sync script `scripts\update_requirements.py` ## Version 1.0.0 ## diff --git a/pyproject.toml b/pyproject.toml index 2b13163..f1b58db 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -48,7 +48,7 @@ moduletester-cli = "moduletester.manager:cli" moduletester = "moduletester.gui.main:run_gui" [project.optional-dependencies] -dev = ["ruff", "pylint", "pytest", "Coverage", "build"] +dev = ["ruff", "pylint", "pytest", "Coverage", "build", "pypandoc_binary"] doc = ["PyQt5", "sphinx>6", "pydata_sphinx_theme"] [tool.setuptools.packages.find] diff --git a/requirements.txt b/requirements.txt index 2c51579..dc8f1b0 100644 --- a/requirements.txt +++ b/requirements.txt @@ -1,15 +1,15 @@ -PyQt5 -PyQtWebEngine +beautifulsoup4 +build +Coverage guidata -pylint -black -coverage -pycodestyle -pyinstaller -sphinx +jinja2 pydata_sphinx_theme -build -twine +pylint pypandoc pypandoc_binary -jinja2 \ No newline at end of file +PyQt5 +pyqtwebengine +pytest +QtPy +ruff +sphinx diff --git a/scripts/update_requirements.py b/scripts/update_requirements.py new file mode 100644 index 0000000..1b14417 --- /dev/null +++ b/scripts/update_requirements.py @@ -0,0 +1,61 @@ +# -*- coding: utf-8 -*- + +"""Update requirements.txt from pyproject.toml. + +Combines [project.dependencies] and all [project.optional-dependencies] +groups into a single requirements.txt file at the repository root. +""" + +import os +import re +import sys + +if sys.version_info >= (3, 11): + import tomllib +else: + try: + import tomllib + except ImportError: + import tomli as tomllib # type: ignore + + +def strip_version_specifiers(dep: str) -> str: + """Return the package name without version specifiers. + + Example: 'guidata >= 3.14' -> 'guidata' + """ + return re.split(r"[><=!~\s;]", dep)[0].strip() + + +def generate_requirements_txt(pyproject_path: str, output_path: str) -> None: + """Generate requirements.txt from pyproject.toml.""" + with open(pyproject_path, "rb") as f: + data = tomllib.load(f) + + project = data.get("project", {}) + all_deps: dict[str, str] = {} + + # Collect main dependencies + for dep in project.get("dependencies", []): + name = strip_version_specifiers(dep) + all_deps[name.lower()] = name + + # Collect all optional-dependencies groups + for group, deps in project.get("optional-dependencies", {}).items(): + for dep in deps: + name = strip_version_specifiers(dep) + all_deps[name.lower()] = name + + # Sort case-insensitively and write + sorted_deps = sorted(all_deps.values(), key=str.lower) + with open(output_path, "w", encoding="utf-8") as f: + f.write("\n".join(sorted_deps) + "\n") + + +if __name__ == "__main__": + root_dir = os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + pyproject_path = os.path.join(root_dir, "pyproject.toml") + output_path = os.path.join(root_dir, "requirements.txt") + print("Updating requirements.txt from pyproject.toml...", end=" ") + generate_requirements_txt(pyproject_path, output_path) + print("done.") From 48bedf2c30ff77b913118d633203c04683bc8a61 Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Thu, 21 May 2026 18:50:19 +0200 Subject: [PATCH 7/9] implement example --- .vscode/tasks.json | 25 ++ example/README.md | 138 ++++++ .../performance/performance_report.txt | 19 + .../2026-05-21/precision/precision_report.txt | 23 + example/example_calculator/__init__.py | 13 + example/example_calculator/app.py | 166 +++++++ example/example_calculator/converter.py | 63 +++ example/example_calculator/moduletester.ini | 83 ++++ example/example_calculator/operations.py | 60 +++ .../Test Plan/Manual GUI Tests/test-001.py | 51 +++ .../Test Plan/Manual GUI Tests/test-002.py | 45 ++ .../Test Plan/Manual GUI Tests/test-003.py | 44 ++ .../Qualification Tests/001-precision.py | 121 ++++++ .../Qualification Tests/002-performance.py | 125 ++++++ .../Test Plan/Unit Tests/001-operations.py | 64 +++ .../Test Plan/Unit Tests/002-converter.py | 64 +++ example/example_calculator/tests/__init__.py | 1 + .../tests/moduletester_launcher.py | 76 ++++ .../tests/processing/test_converter.py | 57 +++ .../tests/processing/test_operations.py | 89 ++++ .../tests/templates/custom-reference.docx | Bin 0 -> 21744 bytes .../tests/templates/custom-reference.odt | Bin 0 -> 9589 bytes .../tests/templates/default_style.css | 408 ++++++++++++++++++ .../tests/templates/test_list_template.j2 | 39 ++ .../tests/templates/test_results_template.j2 | 116 +++++ example/pyproject.toml | 22 + scripts/run_example.py | 45 ++ 27 files changed, 1957 insertions(+) create mode 100644 example/README.md create mode 100644 example/example_calculator/TestPlan/reports/2026-05-21/performance/performance_report.txt create mode 100644 example/example_calculator/TestPlan/reports/2026-05-21/precision/precision_report.txt create mode 100644 example/example_calculator/__init__.py create mode 100644 example/example_calculator/app.py create mode 100644 example/example_calculator/converter.py create mode 100644 example/example_calculator/moduletester.ini create mode 100644 example/example_calculator/operations.py create mode 100644 example/example_calculator/tests/Test Plan/Manual GUI Tests/test-001.py create mode 100644 example/example_calculator/tests/Test Plan/Manual GUI Tests/test-002.py create mode 100644 example/example_calculator/tests/Test Plan/Manual GUI Tests/test-003.py create mode 100644 example/example_calculator/tests/Test Plan/Qualification Tests/001-precision.py create mode 100644 example/example_calculator/tests/Test Plan/Qualification Tests/002-performance.py create mode 100644 example/example_calculator/tests/Test Plan/Unit Tests/001-operations.py create mode 100644 example/example_calculator/tests/Test Plan/Unit Tests/002-converter.py create mode 100644 example/example_calculator/tests/__init__.py create mode 100644 example/example_calculator/tests/moduletester_launcher.py create mode 100644 example/example_calculator/tests/processing/test_converter.py create mode 100644 example/example_calculator/tests/processing/test_operations.py create mode 100644 example/example_calculator/tests/templates/custom-reference.docx create mode 100644 example/example_calculator/tests/templates/custom-reference.odt create mode 100644 example/example_calculator/tests/templates/default_style.css create mode 100644 example/example_calculator/tests/templates/test_list_template.j2 create mode 100644 example/example_calculator/tests/templates/test_results_template.j2 create mode 100644 example/pyproject.toml create mode 100644 scripts/run_example.py diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 203be85..24143e8 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -392,6 +392,31 @@ }, "problemMatcher": [], }, + { + "label": "๐Ÿงช Run Example Calculator (ModuleTester)", + "type": "shell", + "command": "${command:python.interpreterPath}", + "args": [ + "scripts/run_with_env.py", + "${command:python.interpreterPath}", + "scripts/run_example.py", + ], + "options": { + "cwd": "${workspaceFolder}", + }, + "group": { + "kind": "build", + "isDefault": true, + }, + "presentation": { + "clear": true, + "echo": true, + "focus": false, + "panel": "dedicated", + "reveal": "always", + "showReuseMessage": true, + }, + }, { "label": "โ” Untracked files", "type": "shell", diff --git a/example/README.md b/example/README.md new file mode 100644 index 0000000..8ab9ebe --- /dev/null +++ b/example/README.md @@ -0,0 +1,138 @@ +# Example Calculator โ€” ModuleTester Reference Implementation + +This project is a minimal, self-contained example demonstrating how to integrate +[ModuleTester](https://github.com/Codra-Ingenierie-Informatique/ModuleTester/) +into a Python project with a Qt GUI. It mirrors the integration pattern used in +[X-GRID](https://github.com/Codra-Ingenierie-Informatique/X-GRID/). + +## Project structure + +``` +example/ +โ”œโ”€โ”€ pyproject.toml # Project metadata & dependencies +โ”œโ”€โ”€ README.md # This file +โ””โ”€โ”€ example_calculator/ + โ”œโ”€โ”€ __init__.py # Package metadata + โ”œโ”€โ”€ app.py # Qt GUI application (QMainWindow) + โ”œโ”€โ”€ operations.py # Arithmetic functions + โ”œโ”€โ”€ converter.py # Unit conversion functions + โ”œโ”€โ”€ moduletester.ini # ModuleTester configuration + โ””โ”€โ”€ tests/ + โ”œโ”€โ”€ __init__.py + โ”œโ”€โ”€ moduletester_launcher.py # Launcher for ModuleTester GUI + โ”œโ”€โ”€ Manual GUI Tests/ + โ”‚ โ”œโ”€โ”€ test-001.py # Application startup + โ”‚ โ”œโ”€โ”€ test-002.py # Arithmetic operations via GUI + โ”‚ โ””โ”€โ”€ test-003.py # Unit conversions via GUI + โ”œโ”€โ”€ Unit Tests/ + โ”‚ โ”œโ”€โ”€ 001-operations.py # pytest + coverage wrapper + โ”‚ โ””โ”€โ”€ 002-converter.py # pytest + coverage wrapper + โ”œโ”€โ”€ Qualification Tests/ + โ”‚ โ”œโ”€โ”€ 001-precision.py # Numerical precision verification + โ”‚ โ””โ”€โ”€ 002-performance.py # Performance benchmark + โ””โ”€โ”€ processing/ + โ”œโ”€โ”€ test_operations.py # Actual pytest test cases + โ””โ”€โ”€ test_converter.py # Actual pytest test cases +``` + +## Prerequisites + +- Python >= 3.9 +- A Qt binding (PyQt5, PyQt6, PySide2, or PySide6) +- ModuleTester installed (from the parent directory: `pip install ..`) + +## Installation + +```bash +cd example +pip install -e ".[test]" +``` + +## Running the application + +```bash +python -m example_calculator.app +``` + +## Running tests + +### With pytest directly + +```bash +pytest example_calculator/tests/processing/ -v +``` + +### With ModuleTester + +```bash +python example_calculator/tests/moduletester_launcher.py +``` + +This will: +1. Discover all tests in `example_calculator.tests` (manual GUI, unit, qualification) +2. Generate a `.moduletester` template in `TestPlan/` +3. Open the ModuleTester GUI where you can run and manage tests + +## How to add ModuleTester to your own project + +### Step 1: Install ModuleTester + +```bash +pip install moduletester +``` + +### Step 2: Organize your tests + +Create a `tests/` subpackage inside your main package with subdirectories for +each test category. Use the `# guitest:` directives at the top of each test file: + +- `# guitest: show` โ€” Test is visible in ModuleTester GUI (manual GUI tests, + wrapper scripts) +- `# guitest: skip` โ€” File is ignored by ModuleTester (actual pytest files, + utility modules) +- `# guitest: hide` โ€” Test exists but is hidden from the default "visible" + category + +### Step 3: Write test docstrings in RST + +ModuleTester extracts the module docstring to display test instructions. +Use RST `.. list-table::` for step-by-step instructions: + +```python +""" +TEST-001: My test description + +.. list-table:: Test steps + :header-rows: 1 + :widths: 50 50 + + * - Action + - Expected result + * - Do something + - Something happens +""" +# guitest: show +``` + +### Step 4: Create a `moduletester.ini` + +Place a `moduletester.ini` file next to your package's `__init__.py`. See +[example_calculator/moduletester.ini](example_calculator/moduletester.ini) for a +fully commented reference. + +### Step 5: Create a launcher + +Write a launcher script that uses `TestManager` to discover tests and open the +ModuleTester GUI. See +[moduletester_launcher.py](example_calculator/tests/moduletester_launcher.py). + +### Step 6: Create pytest wrappers (for unit tests) + +For unit tests, create wrapper scripts that call `pytest.main()` with the +`coverage` API. These wrappers use `# guitest: show` so they appear in +ModuleTester, while the actual pytest files use `# guitest: skip`. + +### Step 7: Create qualification scripts (optional) + +For qualification or acceptance tests, create standalone scripts that run +computations, compare results to references, and generate text or HTML reports. diff --git a/example/example_calculator/TestPlan/reports/2026-05-21/performance/performance_report.txt b/example/example_calculator/TestPlan/reports/2026-05-21/performance/performance_report.txt new file mode 100644 index 0000000..709f814 --- /dev/null +++ b/example/example_calculator/TestPlan/reports/2026-05-21/performance/performance_report.txt @@ -0,0 +1,19 @@ +================================================================================ +QUALIFICATION REPORT โ€” Performance Benchmark +Date: 2026-05-21 18:42:45 +================================================================================ + +Function Iterations Total (s) Per call Max (s) Status +------------------------------------------------------------------------------------- +add 100,000 0.0042 4.19e-08 1.0 PASS +subtract 100,000 0.0042 4.17e-08 1.0 PASS +multiply 100,000 0.0045 4.54e-08 1.0 PASS +divide 100,000 0.0062 6.24e-08 1.0 PASS +power 100,000 0.0068 6.83e-08 1.0 PASS +sqrt 100,000 0.0082 8.16e-08 1.0 PASS +factorial(20) 100,000 0.0113 1.13e-07 2.0 PASS +celsius_to_fahrenheit 100,000 0.0054 5.41e-08 1.0 PASS +km_to_miles 100,000 0.0039 3.94e-08 1.0 PASS +------------------------------------------------------------------------------------- +Results: 9/9 passed +Overall: PASS diff --git a/example/example_calculator/TestPlan/reports/2026-05-21/precision/precision_report.txt b/example/example_calculator/TestPlan/reports/2026-05-21/precision/precision_report.txt new file mode 100644 index 0000000..18a7231 --- /dev/null +++ b/example/example_calculator/TestPlan/reports/2026-05-21/precision/precision_report.txt @@ -0,0 +1,23 @@ +======================================================================== +QUALIFICATION REPORT โ€” Arithmetic Precision +Date: 2026-05-21 18:42:32 +======================================================================== + +Test Computed Expected Error Status +------------------------------------------------------------------------------------------ +add(0.1, 0.2) 0.3 0.3 5.55e-17 PASS +add(1e15, 1e-15) 1e+15 1e+15 0.00e+00 PASS +subtract(1.0, 0.9) 0.1 0.1 2.78e-17 PASS +multiply(0.1, 0.1) 0.01 0.01 1.73e-18 PASS +multiply(1e8, 1e8) 1e+16 1e+16 0.00e+00 PASS +divide(1, 3) 0.3333333333 0.3333333333 0.00e+00 PASS +divide(1e-10, 1e10) 1e-20 1e-20 1.50e-36 PASS +power(2, 10) 1024 1024 0.00e+00 PASS +power(2, 0.5) 1.414213562 1.414213562 0.00e+00 PASS +sqrt(2) 1.414213562 1.414213562 0.00e+00 PASS +sqrt(1e-20) 1e-10 1e-10 0.00e+00 PASS +factorial(10) 3628800 3628800 0.00e+00 PASS +factorial(20) 2.432902008e+18 2.432902008e+18 0.00e+00 PASS +------------------------------------------------------------------------------------------ +Results: 13/13 passed +Overall: PASS diff --git a/example/example_calculator/__init__.py b/example/example_calculator/__init__.py new file mode 100644 index 0000000..08fb80a --- /dev/null +++ b/example/example_calculator/__init__.py @@ -0,0 +1,13 @@ +# -*- coding: utf-8 -*- + +""" +Example Calculator +------------------ + +A minimal Python package with a Qt GUI, used as a reference implementation +for ModuleTester integration. This example demonstrates how to set up +ModuleTester with manual GUI tests, unit tests (pytest + coverage), +and qualification tests (custom scripts). +""" + +__version__ = "1.0.0" diff --git a/example/example_calculator/app.py b/example/example_calculator/app.py new file mode 100644 index 0000000..66c5adc --- /dev/null +++ b/example/example_calculator/app.py @@ -0,0 +1,166 @@ +# -*- coding: utf-8 -*- + +""" +Example Calculator โ€” Qt GUI Application +---------------------------------------- + +A minimal QMainWindow-based calculator demonstrating a typical Qt application +that can be tested with ModuleTester manual GUI tests. +""" + +import sys + +from qtpy import QtCore as QC +from qtpy import QtWidgets as QW + +from example_calculator import converter, operations + + +class CalculatorWindow(QW.QMainWindow): + """Main window for the Example Calculator application.""" + + def __init__(self): + super().__init__() + self.setWindowTitle("Example Calculator") + self.setMinimumSize(400, 350) + + central = QW.QWidget() + self.setCentralWidget(central) + layout = QW.QVBoxLayout(central) + + # --- Calculator tab --- + tabs = QW.QTabWidget() + layout.addWidget(tabs) + + # Tab 1: Arithmetic operations + calc_widget = QW.QWidget() + calc_layout = QW.QFormLayout(calc_widget) + + self.input_a = QW.QDoubleSpinBox() + self.input_a.setRange(-1e9, 1e9) + self.input_a.setDecimals(6) + calc_layout.addRow("A:", self.input_a) + + self.input_b = QW.QDoubleSpinBox() + self.input_b.setRange(-1e9, 1e9) + self.input_b.setDecimals(6) + calc_layout.addRow("B:", self.input_b) + + self.operation_combo = QW.QComboBox() + self.operation_combo.addItems( + ["Add", "Subtract", "Multiply", "Divide", "Power"] + ) + calc_layout.addRow("Operation:", self.operation_combo) + + self.calc_button = QW.QPushButton("Compute") + self.calc_button.clicked.connect(self._on_compute) + calc_layout.addRow(self.calc_button) + + self.result_label = QW.QLabel("Result: โ€”") + self.result_label.setAlignment(QC.Qt.AlignCenter) + self.result_label.setStyleSheet("font-size: 16px; font-weight: bold;") + calc_layout.addRow(self.result_label) + + tabs.addTab(calc_widget, "Operations") + + # Tab 2: Unit converter + conv_widget = QW.QWidget() + conv_layout = QW.QFormLayout(conv_widget) + + self.conv_input = QW.QDoubleSpinBox() + self.conv_input.setRange(-1e9, 1e9) + self.conv_input.setDecimals(6) + conv_layout.addRow("Value:", self.conv_input) + + self.conv_combo = QW.QComboBox() + self.conv_combo.addItems( + [ + "Celsius โ†’ Fahrenheit", + "Fahrenheit โ†’ Celsius", + "Celsius โ†’ Kelvin", + "Kelvin โ†’ Celsius", + "Meters โ†’ Feet", + "Feet โ†’ Meters", + "Km โ†’ Miles", + "Miles โ†’ Km", + ] + ) + conv_layout.addRow("Conversion:", self.conv_combo) + + self.conv_button = QW.QPushButton("Convert") + self.conv_button.clicked.connect(self._on_convert) + conv_layout.addRow(self.conv_button) + + self.conv_result_label = QW.QLabel("Result: โ€”") + self.conv_result_label.setAlignment(QC.Qt.AlignCenter) + self.conv_result_label.setStyleSheet("font-size: 16px; font-weight: bold;") + conv_layout.addRow(self.conv_result_label) + + tabs.addTab(conv_widget, "Converter") + + # Status bar + self.statusBar().showMessage("Ready") + + def _on_compute(self): + """Execute the selected arithmetic operation.""" + a = self.input_a.value() + b = self.input_b.value() + op = self.operation_combo.currentText() + + op_map = { + "Add": operations.add, + "Subtract": operations.subtract, + "Multiply": operations.multiply, + "Divide": operations.divide, + "Power": operations.power, + } + + try: + result = op_map[op](a, b) + self.result_label.setText(f"Result: {result}") + self.statusBar().showMessage(f"{a} {op} {b} = {result}") + except (ZeroDivisionError, ValueError, OverflowError) as e: + self.result_label.setText(f"Error: {e}") + self.statusBar().showMessage(f"Error: {e}") + + def _on_convert(self): + """Execute the selected unit conversion.""" + value = self.conv_input.value() + conversion = self.conv_combo.currentText() + + conv_map = { + "Celsius โ†’ Fahrenheit": converter.celsius_to_fahrenheit, + "Fahrenheit โ†’ Celsius": converter.fahrenheit_to_celsius, + "Celsius โ†’ Kelvin": converter.celsius_to_kelvin, + "Kelvin โ†’ Celsius": converter.kelvin_to_celsius, + "Meters โ†’ Feet": converter.meters_to_feet, + "Feet โ†’ Meters": converter.feet_to_meters, + "Km โ†’ Miles": converter.km_to_miles, + "Miles โ†’ Km": converter.miles_to_km, + } + + try: + result = conv_map[conversion](value) + self.conv_result_label.setText(f"Result: {result:.6f}") + self.statusBar().showMessage(f"{value} โ†’ {result:.6f}") + except ValueError as e: + self.conv_result_label.setText(f"Error: {e}") + self.statusBar().showMessage(f"Error: {e}") + + +def run(): + """Launch the Example Calculator application.""" + app = QW.QApplication.instance() + standalone = app is None + if standalone: + app = QW.QApplication(sys.argv) + + window = CalculatorWindow() + window.show() + + if standalone: + app.exec() + + +if __name__ == "__main__": + run() diff --git a/example/example_calculator/converter.py b/example/example_calculator/converter.py new file mode 100644 index 0000000..92b5439 --- /dev/null +++ b/example/example_calculator/converter.py @@ -0,0 +1,63 @@ +# -*- coding: utf-8 -*- + +"""Unit conversion functions (temperature, distance).""" + +from __future__ import annotations + +# --- Temperature conversions --- + + +def celsius_to_fahrenheit(celsius: float) -> float: + """Convert Celsius to Fahrenheit.""" + return celsius * 9.0 / 5.0 + 32.0 + + +def fahrenheit_to_celsius(fahrenheit: float) -> float: + """Convert Fahrenheit to Celsius.""" + return (fahrenheit - 32.0) * 5.0 / 9.0 + + +def celsius_to_kelvin(celsius: float) -> float: + """Convert Celsius to Kelvin. + + Raises: + ValueError: If the result would be below absolute zero. + """ + kelvin = celsius + 273.15 + if kelvin < 0: + raise ValueError("Temperature below absolute zero is not physical") + return kelvin + + +def kelvin_to_celsius(kelvin: float) -> float: + """Convert Kelvin to Celsius. + + Raises: + ValueError: If kelvin is negative. + """ + if kelvin < 0: + raise ValueError("Kelvin temperature cannot be negative") + return kelvin - 273.15 + + +# --- Distance conversions --- + + +def meters_to_feet(meters: float) -> float: + """Convert meters to feet.""" + return meters * 3.28084 + + +def feet_to_meters(feet: float) -> float: + """Convert feet to meters.""" + return feet / 3.28084 + + +def km_to_miles(km: float) -> float: + """Convert kilometers to miles.""" + return km * 0.621371 + + +def miles_to_km(miles: float) -> float: + """Convert miles to kilometers.""" + return miles / 0.621371 diff --git a/example/example_calculator/moduletester.ini b/example/example_calculator/moduletester.ini new file mode 100644 index 0000000..1a1d5df --- /dev/null +++ b/example/example_calculator/moduletester.ini @@ -0,0 +1,83 @@ +# ModuleTester configuration for the Example Calculator project. +# +# This file is read by ModuleTester when loading a test suite for the +# example_calculator package. Place it at the root of the package directory +# (next to __init__.py). +# +# See moduletester/config.py for the full list of configuration options. + +[general] +# Format used for parsing test docstrings. +# Supported values: "rst" (reStructuredText), "md" (Markdown) +docstring_fmt = rst + +# Default test category filter used when discovering tests. +# Supported values: "all", "visible", "batch" +# - "all" : every discovered test script +# - "visible" : tests marked with "# guitest: show" (default) +# - "batch" : tests intended for unattended execution +category = visible + +[export] +# Relative path (from this .ini file) to the directory containing +# Jinja2 templates and assets used for report generation. +template_dir = tests/templates + +# Name of the Jinja2 template used for test results (RTV) reports. +test_results_template_name = test_results_template.j2 + +# Name of the Jinja2 template used for test list (DTV) reports. +test_list_template_name = test_list_template.j2 + +# Reference document for DOCX export (styles and formatting). +docx_reference = custom-reference.docx + +# Reference document for ODT export (styles and formatting). +odt_reference = custom-reference.odt + +# CSS stylesheet applied to HTML exports. +css_style = default_style.css + +# Comma-separated list of export formats to generate. +# Supported values: html, docx +export_fmts = html + +# If 1, Jinja2 templates are reloaded from disk on every export +# (useful during template development). +reload_templates_on_export = 0 + +# Number of heading levels to shift when embedding docstrings in reports. +# For example, 3 means an RST "=" heading becomes an

. +docstrings_header_shift = 3 + +# Depth of the table of contents in generated reports. +toc_depth = 2 + +[gui] +# Visibility and position of each ModuleTester GUI panel. +# Visibility: 1 = visible, 0 = hidden +# Position: "left", "right", "top", "bottom" + +# Test list panel (tree of discovered tests) +test_list_visible = 1 +test_list_pos = left + +# Test properties panel (metadata of the selected test) +test_props_visible = 0 +test_props_pos = right + +# Result tab panel (execution output) +result_tab_visible = 1 +result_tab_pos = bottom + +# Result properties panel (details of test result) +result_props_visible = 1 +result_props_pos = bottom + +# CLI output panel +cli_visible = 0 +cli_pos = bottom + +# Toolbox panel +toolbox_visible = 0 +toolbox_pos = bottom diff --git a/example/example_calculator/operations.py b/example/example_calculator/operations.py new file mode 100644 index 0000000..84443c0 --- /dev/null +++ b/example/example_calculator/operations.py @@ -0,0 +1,60 @@ +# -*- coding: utf-8 -*- + +"""Basic arithmetic and mathematical operations.""" + +from __future__ import annotations + +import math + + +def add(a: float, b: float) -> float: + """Return the sum of two numbers.""" + return a + b + + +def subtract(a: float, b: float) -> float: + """Return the difference of two numbers.""" + return a - b + + +def multiply(a: float, b: float) -> float: + """Return the product of two numbers.""" + return a * b + + +def divide(a: float, b: float) -> float: + """Return the quotient of two numbers. + + Raises: + ZeroDivisionError: If b is zero. + """ + if b == 0: + raise ZeroDivisionError("Cannot divide by zero") + return a / b + + +def power(base: float, exponent: float) -> float: + """Return base raised to the power of exponent.""" + return math.pow(base, exponent) + + +def sqrt(value: float) -> float: + """Return the square root of a non-negative number. + + Raises: + ValueError: If value is negative. + """ + if value < 0: + raise ValueError("Cannot compute square root of a negative number") + return math.sqrt(value) + + +def factorial(n: int) -> int: + """Return the factorial of a non-negative integer. + + Raises: + ValueError: If n is negative. + """ + if n < 0: + raise ValueError("Factorial is not defined for negative numbers") + return math.factorial(n) diff --git a/example/example_calculator/tests/Test Plan/Manual GUI Tests/test-001.py b/example/example_calculator/tests/Test Plan/Manual GUI Tests/test-001.py new file mode 100644 index 0000000..5a8d57f --- /dev/null +++ b/example/example_calculator/tests/Test Plan/Manual GUI Tests/test-001.py @@ -0,0 +1,51 @@ +# -*- coding: utf-8 -*- + +""" +Example Calculator โ€” Manual GUI Test + +Basic tests and application launch + +TEST-001: Application startup + +This test verifies that the Example Calculator application starts correctly +and that the main window is displayed with all expected UI components. + +.. list-table:: Test steps + :header-rows: 1 + :widths: 50 50 + + * - Action + - Expected result + * - Launch the Example Calculator application (in ModuleTester, select + Manual GUI Tests/test-001 and click "Run Script"). + - The application starts and the main window appears with the title + "Example Calculator". + * - Verify that the main window contains two tabs: "Operations" and + "Converter". + - Both tabs are visible and can be selected. + * - On the "Operations" tab, verify the presence of: + + - Two numeric input fields (A and B) + - An operation selector (Add, Subtract, Multiply, Divide, Power) + - A "Compute" button + - A result label + - All components are present and the result label shows "Result: โ€”". + * - On the "Converter" tab, verify the presence of: + + - One numeric input field (Value) + - A conversion selector (8 conversions available) + - A "Convert" button + - A result label + - All components are present and the result label shows "Result: โ€”". + * - Verify the status bar at the bottom of the window. + - The status bar displays "Ready". + * - Close the application. + - The application closes without errors. +""" + +# guitest: show + +import example_calculator.app as app + +if __name__ == "__main__": + app.run() diff --git a/example/example_calculator/tests/Test Plan/Manual GUI Tests/test-002.py b/example/example_calculator/tests/Test Plan/Manual GUI Tests/test-002.py new file mode 100644 index 0000000..d2803a0 --- /dev/null +++ b/example/example_calculator/tests/Test Plan/Manual GUI Tests/test-002.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +""" +Example Calculator โ€” Manual GUI Test + +Arithmetic operations + +TEST-002: Basic arithmetic operations via the GUI + +This test verifies that the calculator correctly performs basic arithmetic +operations through the graphical user interface. + +.. list-table:: Test steps + :header-rows: 1 + :widths: 50 50 + + * - Action + - Expected result + * - Launch the application (in ModuleTester, select + Manual GUI Tests/test-002 and click "Run Script"). + - The application starts and the "Operations" tab is displayed. + * - Set A = 10 and B = 5. Select "Add" and click "Compute". + - The result label displays "Result: 15.0". + * - Select "Subtract" and click "Compute". + - The result label displays "Result: 5.0". + * - Select "Multiply" and click "Compute". + - The result label displays "Result: 50.0". + * - Select "Divide" and click "Compute". + - The result label displays "Result: 2.0". + * - Select "Power" and click "Compute". + - The result label displays "Result: 100000.0". + * - Set B = 0 and select "Divide". Click "Compute". + - The result label displays "Error: Cannot divide by zero". + * - Verify the status bar after each operation. + - The status bar shows a summary of the last operation or error. + * - Close the application. + - The application closes without errors. +""" + +# guitest: show + +import example_calculator.app as app + +if __name__ == "__main__": + app.run() diff --git a/example/example_calculator/tests/Test Plan/Manual GUI Tests/test-003.py b/example/example_calculator/tests/Test Plan/Manual GUI Tests/test-003.py new file mode 100644 index 0000000..c1b4c11 --- /dev/null +++ b/example/example_calculator/tests/Test Plan/Manual GUI Tests/test-003.py @@ -0,0 +1,44 @@ +# -*- coding: utf-8 -*- + +""" +Example Calculator โ€” Manual GUI Test + +Unit conversions + +TEST-003: Unit conversions via the GUI + +This test verifies that the calculator correctly performs unit conversions +through the graphical user interface. + +.. list-table:: Test steps + :header-rows: 1 + :widths: 50 50 + + * - Action + - Expected result + * - Launch the application (in ModuleTester, select + Manual GUI Tests/test-003 and click "Run Script"). + - The application starts. Navigate to the "Converter" tab. + * - Set Value = 100. Select "Celsius โ†’ Fahrenheit" and click "Convert". + - The result label displays "Result: 212.000000". + * - Select "Fahrenheit โ†’ Celsius" and click "Convert". + - The result label displays "Result: 37.777778". + * - Set Value = 0. Select "Celsius โ†’ Kelvin" and click "Convert". + - The result label displays "Result: 273.150000". + * - Set Value = 1. Select "Km โ†’ Miles" and click "Convert". + - The result label displays "Result: 0.621371". + * - Set Value = 1. Select "Meters โ†’ Feet" and click "Convert". + - The result label displays "Result: 3.280840". + * - Set Value = -300. Select "Celsius โ†’ Kelvin" and click "Convert". + - The result label displays "Error: Temperature below absolute zero + is not physical". + * - Close the application. + - The application closes without errors. +""" + +# guitest: show + +import example_calculator.app as app + +if __name__ == "__main__": + app.run() diff --git a/example/example_calculator/tests/Test Plan/Qualification Tests/001-precision.py b/example/example_calculator/tests/Test Plan/Qualification Tests/001-precision.py new file mode 100644 index 0000000..48c6c1e --- /dev/null +++ b/example/example_calculator/tests/Test Plan/Qualification Tests/001-precision.py @@ -0,0 +1,121 @@ +# -*- coding: utf-8 -*- + +""" +Example Calculator โ€” Qualification Test + +QUAL-001: Arithmetic precision verification + +This qualification test verifies the numerical precision of all arithmetic +operations by comparing computed results against known reference values. +A detailed report is generated. + +.. list-table:: Test steps + :header-rows: 1 + :widths: 50 50 + + * - Action + - Expected result + * - Launch the qualification script (in ModuleTester, select + Qualification Tests/001-precision and click "Run Script"). + - The script runs and displays results in the console. A text report + is saved in the ``TestPlan/reports/YYYY-MM-DD/precision`` folder. +""" + +# guitest: show + +import math +import os +from datetime import datetime + +from example_calculator import operations + +# Reference values: (description, function, args, expected, tolerance) +REFERENCE_DATA = [ + ("add(0.1, 0.2)", operations.add, (0.1, 0.2), 0.3, 1e-15), + ("add(1e15, 1e-15)", operations.add, (1e15, 1e-15), 1e15, 1.0), + ("subtract(1.0, 0.9)", operations.subtract, (1.0, 0.9), 0.1, 1e-15), + ("multiply(0.1, 0.1)", operations.multiply, (0.1, 0.1), 0.01, 1e-16), + ("multiply(1e8, 1e8)", operations.multiply, (1e8, 1e8), 1e16, 1.0), + ("divide(1, 3)", operations.divide, (1, 3), 1 / 3, 1e-15), + ("divide(1e-10, 1e10)", operations.divide, (1e-10, 1e10), 1e-20, 1e-35), + ("power(2, 10)", operations.power, (2, 10), 1024.0, 1e-10), + ("power(2, 0.5)", operations.power, (2, 0.5), math.sqrt(2), 1e-15), + ("sqrt(2)", operations.sqrt, (2,), math.sqrt(2), 1e-15), + ("sqrt(1e-20)", operations.sqrt, (1e-20,), 1e-10, 1e-25), + ("factorial(10)", operations.factorial, (10,), 3628800, 0), + ("factorial(20)", operations.factorial, (20,), 2432902008176640000, 0), +] + + +def run(mode="print", save_path=None): + """Run precision qualification tests. + + Args: + mode: "print", "save", or "print_save" + save_path: directory where the report will be saved + """ + results = [] + all_passed = True + + for desc, func, args, expected, tolerance in REFERENCE_DATA: + computed = func(*args) + error = abs(computed - expected) + passed = error <= tolerance + if not passed: + all_passed = False + results.append((desc, computed, expected, error, tolerance, passed)) + + # Build report + lines = [] + lines.append("=" * 72) + lines.append("QUALIFICATION REPORT โ€” Arithmetic Precision") + lines.append(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + lines.append("=" * 72) + lines.append("") + lines.append(f"{'Test':<30} {'Computed':>18} {'Expected':>18} {'Error':>12} {'Status':>8}") + lines.append("-" * 90) + + for desc, computed, expected, error, tolerance, passed in results: + status = "PASS" if passed else "FAIL" + lines.append( + f"{desc:<30} {computed:>18.10g} {expected:>18.10g} {error:>12.2e} {status:>8}" + ) + + lines.append("-" * 90) + total = len(results) + passed_count = sum(1 for *_, p in results if p) + lines.append(f"Results: {passed_count}/{total} passed") + lines.append(f"Overall: {'PASS' if all_passed else 'FAIL'}") + lines.append("") + + report_text = "\n".join(lines) + + if "print" in mode: + print(report_text) + + if "save" in mode and save_path is not None: + os.makedirs(save_path, exist_ok=True) + report_file = os.path.join(save_path, "precision_report.txt") + with open(report_file, "w", encoding="utf-8") as f: + f.write(report_text) + print(f"Report saved to: {report_file}") + + return all_passed + + +if __name__ == "__main__": + project_root = os.path.dirname( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + ) + report_path = os.path.join( + project_root, + "TestPlan", + "reports", + datetime.now().strftime("%Y-%m-%d"), + "precision", + ) + success = run("print_save", save_path=report_path) + if not success: + print("\n*** QUALIFICATION FAILED ***") diff --git a/example/example_calculator/tests/Test Plan/Qualification Tests/002-performance.py b/example/example_calculator/tests/Test Plan/Qualification Tests/002-performance.py new file mode 100644 index 0000000..7971ea1 --- /dev/null +++ b/example/example_calculator/tests/Test Plan/Qualification Tests/002-performance.py @@ -0,0 +1,125 @@ +# -*- coding: utf-8 -*- + +""" +Example Calculator โ€” Qualification Test + +QUAL-002: Performance benchmark + +This qualification test measures the execution time of all arithmetic and +conversion operations to verify they remain within acceptable performance +thresholds. + +.. list-table:: Test steps + :header-rows: 1 + :widths: 50 50 + + * - Action + - Expected result + * - Launch the qualification script (in ModuleTester, select + Qualification Tests/002-performance and click "Run Script"). + - The script runs and displays benchmark results in the console. + A text report is saved in the + ``TestPlan/reports/YYYY-MM-DD/performance`` folder. +""" + +# guitest: show + +import os +import time +from datetime import datetime + +from example_calculator import converter, operations + +# Benchmark definitions: (name, function, args, iterations, max_time_seconds) +BENCHMARKS = [ + ("add", operations.add, (1.5, 2.5), 100_000, 1.0), + ("subtract", operations.subtract, (10.0, 3.0), 100_000, 1.0), + ("multiply", operations.multiply, (3.0, 4.0), 100_000, 1.0), + ("divide", operations.divide, (10.0, 3.0), 100_000, 1.0), + ("power", operations.power, (2.0, 10.0), 100_000, 1.0), + ("sqrt", operations.sqrt, (144.0,), 100_000, 1.0), + ("factorial(20)", operations.factorial, (20,), 100_000, 2.0), + ("celsius_to_fahrenheit", converter.celsius_to_fahrenheit, (100.0,), 100_000, 1.0), + ("km_to_miles", converter.km_to_miles, (42.195,), 100_000, 1.0), +] + + +def run(mode="print", save_path=None): + """Run performance benchmark. + + Args: + mode: "print", "save", or "print_save" + save_path: directory where the report will be saved + """ + results = [] + all_passed = True + + for name, func, args, iterations, max_time in BENCHMARKS: + start = time.perf_counter() + for _ in range(iterations): + func(*args) + elapsed = time.perf_counter() - start + passed = elapsed <= max_time + if not passed: + all_passed = False + per_call = elapsed / iterations + results.append((name, iterations, elapsed, per_call, max_time, passed)) + + # Build report + lines = [] + lines.append("=" * 80) + lines.append("QUALIFICATION REPORT โ€” Performance Benchmark") + lines.append(f"Date: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}") + lines.append("=" * 80) + lines.append("") + lines.append( + f"{'Function':<25} {'Iterations':>12} {'Total (s)':>12} " + f"{'Per call':>14} {'Max (s)':>10} {'Status':>8}" + ) + lines.append("-" * 85) + + for name, iterations, elapsed, per_call, max_time, passed in results: + status = "PASS" if passed else "FAIL" + lines.append( + f"{name:<25} {iterations:>12,} {elapsed:>12.4f} " + f"{per_call:>14.2e} {max_time:>10.1f} {status:>8}" + ) + + lines.append("-" * 85) + total = len(results) + passed_count = sum(1 for *_, p in results if p) + lines.append(f"Results: {passed_count}/{total} passed") + lines.append(f"Overall: {'PASS' if all_passed else 'FAIL'}") + lines.append("") + + report_text = "\n".join(lines) + + if "print" in mode: + print(report_text) + + if "save" in mode and save_path is not None: + os.makedirs(save_path, exist_ok=True) + report_file = os.path.join(save_path, "performance_report.txt") + with open(report_file, "w", encoding="utf-8") as f: + f.write(report_text) + print(f"Report saved to: {report_file}") + + return all_passed + + +if __name__ == "__main__": + project_root = os.path.dirname( + os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + ) + report_path = os.path.join( + project_root, + "TestPlan", + "reports", + datetime.now().strftime("%Y-%m-%d"), + "performance", + ) + success = run("print_save", save_path=report_path) + if not success: + print("\n*** QUALIFICATION FAILED ***") diff --git a/example/example_calculator/tests/Test Plan/Unit Tests/001-operations.py b/example/example_calculator/tests/Test Plan/Unit Tests/001-operations.py new file mode 100644 index 0000000..06bfb0e --- /dev/null +++ b/example/example_calculator/tests/Test Plan/Unit Tests/001-operations.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +""" +Example Calculator โ€” Unit Tests + +UT-001: Arithmetic operations (pytest + coverage) + +These unit tests verify the arithmetic functions in the +``example_calculator.operations`` module using pytest. Code coverage is +collected and an HTML report is generated. + +.. list-table:: Test steps + :header-rows: 1 + :widths: 50 50 + + * - Action + - Expected result + * - Launch the test script (in ModuleTester, select + Unit Tests/001-operations and click "Run Script"). + - The test script runs. Unit test results are displayed in the console. + An HTML coverage report is generated in the + ``TestPlan/reports/YYYY-MM-DD/operations`` folder. +""" + +# guitest: show + +import os +from datetime import datetime + +import coverage +import pytest + +if __name__ == "__main__": + current_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(current_dir))) + ) + test_dir = os.path.join(project_root, "example_calculator", "tests") + + cov = coverage.Coverage( + include=["*/example_calculator/operations.py"], + ) + cov.start() + + pytest.main( + [ + os.path.join(test_dir, "processing", "test_operations.py"), + "-v", + ] + ) + + cov.stop() + cov.save() + cov.report(show_missing=False) + + report_dir = os.path.join( + project_root, + "TestPlan", + "reports", + datetime.now().strftime("%Y-%m-%d"), + "operations", + ) + cov.html_report(directory=report_dir) + print(f"\nCoverage HTML report saved to: {report_dir}") diff --git a/example/example_calculator/tests/Test Plan/Unit Tests/002-converter.py b/example/example_calculator/tests/Test Plan/Unit Tests/002-converter.py new file mode 100644 index 0000000..c2b6c3f --- /dev/null +++ b/example/example_calculator/tests/Test Plan/Unit Tests/002-converter.py @@ -0,0 +1,64 @@ +# -*- coding: utf-8 -*- + +""" +Example Calculator โ€” Unit Tests + +UT-002: Unit converter (pytest + coverage) + +These unit tests verify the conversion functions in the +``example_calculator.converter`` module using pytest. Code coverage is +collected and an HTML report is generated. + +.. list-table:: Test steps + :header-rows: 1 + :widths: 50 50 + + * - Action + - Expected result + * - Launch the test script (in ModuleTester, select + Unit Tests/002-converter and click "Run Script"). + - The test script runs. Unit test results are displayed in the console. + An HTML coverage report is generated in the + ``TestPlan/reports/YYYY-MM-DD/converter`` folder. +""" + +# guitest: show + +import os +from datetime import datetime + +import coverage +import pytest + +if __name__ == "__main__": + current_dir = os.path.dirname(os.path.abspath(__file__)) + project_root = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(current_dir))) + ) + test_dir = os.path.join(project_root, "example_calculator", "tests") + + cov = coverage.Coverage( + include=["*/example_calculator/converter.py"], + ) + cov.start() + + pytest.main( + [ + os.path.join(test_dir, "processing", "test_converter.py"), + "-v", + ] + ) + + cov.stop() + cov.save() + cov.report(show_missing=False) + + report_dir = os.path.join( + project_root, + "TestPlan", + "reports", + datetime.now().strftime("%Y-%m-%d"), + "converter", + ) + cov.html_report(directory=report_dir) + print(f"\nCoverage HTML report saved to: {report_dir}") diff --git a/example/example_calculator/tests/__init__.py b/example/example_calculator/tests/__init__.py new file mode 100644 index 0000000..a3b3a2f --- /dev/null +++ b/example/example_calculator/tests/__init__.py @@ -0,0 +1 @@ +# guitest: skip diff --git a/example/example_calculator/tests/moduletester_launcher.py b/example/example_calculator/tests/moduletester_launcher.py new file mode 100644 index 0000000..1d9183d --- /dev/null +++ b/example/example_calculator/tests/moduletester_launcher.py @@ -0,0 +1,76 @@ +# -*- coding: utf-8 -*- + +""" +ModuleTester Integration Launcher for Example Calculator + +This script creates a .moduletester template file for the example_calculator +package and launches the ModuleTester GUI. It follows the same pattern as +the X-GRID launcher (xgrid/tests/module_tester/moduleTester_launcher.py). + +Usage:: + + # Generate a new template and launch ModuleTester GUI + python moduletester_launcher.py + + # Open an existing .moduletester file + python moduletester_launcher.py path/to/file.moduletester +""" + +# guitest: skip + +import os +import sys +from importlib import import_module + +from qtpy import QtWidgets as QW + +from example_calculator import __version__ +from moduletester.gui.main import run +from moduletester.manager import TestManager +from moduletester.model import Module + + +def create_template(): + """Create a .moduletester template file for Example Calculator tests. + + Returns: + str: Path to the generated .moduletester file. + """ + mod = import_module("example_calculator") + + project_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + test_plan_dir = os.path.join(project_dir, "TestPlan") + os.makedirs(test_plan_dir, exist_ok=True) + + output_path = os.path.join( + test_plan_dir, + f"example_calculator_v{__version__}_.moduletester", + ) + print(f"Creating template at: {output_path}") + + manager = TestManager(Module(mod), _template_path=output_path, _category="visible") + + print(f"\nTemplate created successfully: {output_path}") + print(f"Found {len(manager.test_suite.tests)} tests") + + return output_path + + +if __name__ == "__main__": + app = QW.QApplication.instance() + if not app: + app = QW.QApplication(sys.argv) + + if len(sys.argv) > 1: + moduletester_file = sys.argv[1] + if not os.path.exists(moduletester_file): + print(f"Error: File not found: {moduletester_file}") + sys.exit(1) + else: + moduletester_file = create_template() + + moduletester = run(path=moduletester_file) + moduletester.window.show() + app.exec_() diff --git a/example/example_calculator/tests/processing/test_converter.py b/example/example_calculator/tests/processing/test_converter.py new file mode 100644 index 0000000..7472c25 --- /dev/null +++ b/example/example_calculator/tests/processing/test_converter.py @@ -0,0 +1,57 @@ +# -*- coding: utf-8 -*- + +# guitest: skip + +"""Pytest unit tests for example_calculator.converter.""" + +import pytest +from example_calculator.converter import ( + celsius_to_fahrenheit, + celsius_to_kelvin, + fahrenheit_to_celsius, + feet_to_meters, + kelvin_to_celsius, + km_to_miles, + meters_to_feet, + miles_to_km, +) + + +class TestTemperature: + def test_celsius_to_fahrenheit(self): + assert celsius_to_fahrenheit(0) == 32.0 + assert celsius_to_fahrenheit(100) == 212.0 + + def test_fahrenheit_to_celsius(self): + assert fahrenheit_to_celsius(32) == 0.0 + assert fahrenheit_to_celsius(212) == 100.0 + + def test_celsius_to_kelvin(self): + assert celsius_to_kelvin(0) == 273.15 + assert celsius_to_kelvin(-273.15) == 0.0 + + def test_celsius_to_kelvin_below_absolute_zero(self): + with pytest.raises(ValueError, match="absolute zero"): + celsius_to_kelvin(-300) + + def test_kelvin_to_celsius(self): + assert kelvin_to_celsius(273.15) == 0.0 + assert kelvin_to_celsius(0) == -273.15 + + def test_kelvin_negative(self): + with pytest.raises(ValueError, match="negative"): + kelvin_to_celsius(-1) + + +class TestDistance: + def test_meters_to_feet(self): + assert abs(meters_to_feet(1) - 3.28084) < 1e-4 + + def test_feet_to_meters(self): + assert abs(feet_to_meters(3.28084) - 1.0) < 1e-4 + + def test_km_to_miles(self): + assert abs(km_to_miles(1) - 0.621371) < 1e-4 + + def test_miles_to_km(self): + assert abs(miles_to_km(0.621371) - 1.0) < 1e-4 diff --git a/example/example_calculator/tests/processing/test_operations.py b/example/example_calculator/tests/processing/test_operations.py new file mode 100644 index 0000000..7845313 --- /dev/null +++ b/example/example_calculator/tests/processing/test_operations.py @@ -0,0 +1,89 @@ +# -*- coding: utf-8 -*- + +# guitest: skip + +"""Pytest unit tests for example_calculator.operations.""" + +import math + +import pytest +from example_calculator.operations import ( + add, + divide, + factorial, + multiply, + power, + sqrt, + subtract, +) + + +class TestAdd: + def test_positive_numbers(self): + assert add(2, 3) == 5 + + def test_negative_numbers(self): + assert add(-1, -2) == -3 + + def test_zero(self): + assert add(0, 0) == 0 + + +class TestSubtract: + def test_basic(self): + assert subtract(10, 4) == 6 + + def test_negative_result(self): + assert subtract(3, 7) == -4 + + +class TestMultiply: + def test_basic(self): + assert multiply(3, 4) == 12 + + def test_by_zero(self): + assert multiply(5, 0) == 0 + + +class TestDivide: + def test_basic(self): + assert divide(10, 2) == 5.0 + + def test_float_result(self): + assert divide(7, 2) == 3.5 + + def test_divide_by_zero(self): + with pytest.raises(ZeroDivisionError, match="Cannot divide by zero"): + divide(1, 0) + + +class TestPower: + def test_basic(self): + assert power(2, 3) == 8.0 + + def test_zero_exponent(self): + assert power(5, 0) == 1.0 + + +class TestSqrt: + def test_basic(self): + assert sqrt(9) == 3.0 + + def test_zero(self): + assert sqrt(0) == 0.0 + + def test_negative(self): + with pytest.raises(ValueError, match="negative"): + sqrt(-1) + + +class TestFactorial: + def test_basic(self): + assert factorial(5) == 120 + + def test_zero(self): + assert factorial(0) == 1 + + def test_negative(self): + with pytest.raises(ValueError, match="negative"): + factorial(-1) diff --git a/example/example_calculator/tests/templates/custom-reference.docx b/example/example_calculator/tests/templates/custom-reference.docx new file mode 100644 index 0000000000000000000000000000000000000000..045165cc66d715de1b1584aacc9878d01ce1a9cb GIT binary patch literal 21744 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 literal 0 HcmV?d00001 diff --git a/example/example_calculator/tests/templates/custom-reference.odt b/example/example_calculator/tests/templates/custom-reference.odt new file mode 100644 index 0000000000000000000000000000000000000000..63ad93d59d9caddaf967061fc30893685f009423 GIT binary patch literal 9589 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 YRpj9y>L&n169o^@gETPVsQ(`QFAYw&ssI20 literal 0 HcmV?d00001 diff --git a/example/example_calculator/tests/templates/default_style.css b/example/example_calculator/tests/templates/default_style.css new file mode 100644 index 0000000..bb40de9 --- /dev/null +++ b/example/example_calculator/tests/templates/default_style.css @@ -0,0 +1,408 @@ +/* + * I add this to html files generated with pandoc. + */ + +html { + font-size: 100%; + overflow-y: scroll; + -webkit-text-size-adjust: 100%; + -ms-text-size-adjust: 100%; +} + +body { + color: #444; + font-family: Georgia, Palatino, 'Palatino Linotype', Times, 'Times New Roman', serif; + font-size: 12px; + line-height: 1.7; + padding: 2em; + margin: auto; + background: #fefefe; +} + +a { + color: #0645ad; + text-decoration: none; +} + +a:visited { + color: #0b0080; +} + +a:hover { + color: #06e; +} + +a:active { + color: #faa700; +} + +a:focus { + outline: thin dotted; +} + +*::-moz-selection { + background: rgba(255, 255, 0, 0.3); + color: #000; +} + +*::selection { + background: rgba(255, 255, 0, 0.3); + color: #000; +} + +a::-moz-selection { + background: rgba(255, 255, 0, 0.3); + color: #0645ad; +} + +a::selection { + background: rgba(255, 255, 0, 0.3); + color: #0645ad; +} + +p { + margin: 1em 0; +} + +img { + max-width: 100%; +} + +h1, +h2, +h3, +h4, +h5, +h6 { + color: #111; + line-height: 125%; + margin-top: 2em; + font-weight: normal; +} + +h4, +h5, +h6 { + font-weight: bold; +} + +h1 { + font-size: 2.5em; +} + +h2 { + font-size: 2em; +} + +h3 { + font-size: 1.5em; +} + +h4 { + font-size: 1.2em; +} + +h5 { + font-size: 1em; +} + +h6 { + font-size: 0.9em; +} + +blockquote { + color: #666666; + margin: 0; + padding-left: 3em; + border-left: 0.5em #EEE solid; +} + +hr { + display: block; + height: 2px; + border: 0; + border-top: 1px solid #aaa; + border-bottom: 1px solid #eee; + margin: 1em 0; + padding: 0; +} + +pre, +code, +kbd, +samp { + color: #000; + font-family: monospace, monospace; + _font-family: 'courier new', monospace; + font-size: 0.98em; +} + +pre { + white-space: pre; + white-space: pre-wrap; + word-wrap: break-word; +} + +b, +strong { + font-weight: bold; +} + +dfn { + font-style: italic; +} + +ins { + background: #ff9; + color: #000; + text-decoration: none; +} + +mark { + background: #ff0; + color: #000; + font-style: italic; + font-weight: bold; +} + +sub, +sup { + font-size: 75%; + line-height: 0; + position: relative; + vertical-align: baseline; +} + +sup { + top: -0.5em; +} + +sub { + bottom: -0.25em; +} + +ul, +ol { + margin: 1em 0; + padding: 0 0 0 2em; +} + +li p:last-child { + margin-bottom: 0; +} + +ul ul, +ol ol { + margin: .3em 0; +} + +dl { + margin-bottom: 1em; +} + +dt { + font-weight: bold; + margin-bottom: .8em; +} + +dd { + margin: 0 0 .8em 2em; +} + +dd:last-child { + margin-bottom: 0; +} + +img { + border: 0; + -ms-interpolation-mode: bicubic; + vertical-align: middle; +} + +figure { + display: block; + text-align: center; + margin: 1em 0; +} + +figure img { + border: none; + margin: 0 auto; +} + +figcaption { + font-size: 0.8em; + font-style: italic; + margin: 0 0 .8em; +} + +table { + margin-bottom: 2em; + border-bottom: 1px solid #ddd; + border-right: 1px solid #ddd; + border-spacing: 0; + border-collapse: collapse; + width: 100%; +} + +table th { + padding: .2em 1em; + background-color: #eee; + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; +} + +table td { + padding: .2em 1em; + border-top: 1px solid #ddd; + border-left: 1px solid #ddd; + vertical-align: top; +} + +.author { + font-size: 1.2em; + text-align: center; +} + +@media only screen and (min-width: 480px) { + body { + font-size: 14px; + } +} + +@media only screen and (min-width: 768px) { + body { + font-size: 16px; + } +} + +@media print { + * { + background: transparent !important; + color: black !important; + filter: none !important; + -ms-filter: none !important; + } + + body { + font-size: 12pt; + max-width: 100%; + } + + a, + a:visited { + text-decoration: underline; + } + + hr { + height: 1px; + border: 0; + border-bottom: 1px solid black; + } + + a[href]:after { + content: " (" attr(href) ")"; + } + + abbr[title]:after { + content: " (" attr(title) ")"; + } + + .ir a:after, + a[href^="javascript:"]:after, + a[href^="#"]:after { + content: ""; + } + + pre, + blockquote { + border: 1px solid #999; + padding-right: 1em; + page-break-inside: avoid; + } + + tr, + img { + page-break-inside: avoid; + } + + img { + max-width: 100% !important; + } + + @page :left { + margin: 15mm 20mm 15mm 10mm; + } + + @page :right { + margin: 15mm 10mm 15mm 20mm; + } + + p, + h2, + h3 { + orphans: 3; + widows: 3; + } + + h2, + h3 { + page-break-after: avoid; + } +} + +/* ADDITIONAL CUSTOM STYLING */ + +.section-block { + border: 1px solid #ccc; + border-radius: 5px; + padding: 1em; + margin: 1em 0; + box-shadow: 5px 5px 10px lightgrey; +} + +.result-block { + display: flex; + align-items: center; +} + +.result-block img { + margin-right: 1em; +} + +.new-page { + page-break-before: always; +} + + +nav#TOC { + border: 1px solid #ccc; + border-radius: 5px; + padding: 1em; + margin: 1em 0; + box-shadow: 5px 5px 10px lightgrey; +} + +/* Common code properties */ +code { + background-color: WhiteSmoke; +} + +/* Code block */ +:not(p):not(td)>code { + box-shadow: 5px 5px 10px lightgrey; + border-radius: 10px; + display: block; + padding: 1em; +} + +/* Inline code */ +p code { + border-radius: 5px; + padding: 0.2em; +} + +td code { + border-radius: 5px; + padding: 0em; +} \ No newline at end of file diff --git a/example/example_calculator/tests/templates/test_list_template.j2 b/example/example_calculator/tests/templates/test_list_template.j2 new file mode 100644 index 0000000..dd1cad3 --- /dev/null +++ b/example/example_calculator/tests/templates/test_list_template.j2 @@ -0,0 +1,39 @@ + + + + + {{ _("Test List Document") }}: {{ doc_obj.test_suite.package.full_name.capitalize() }} + + + +

+

{{ doc_obj.test_suite.author or "Unknown author " }}
+

+

{{ _("Last execution date:") }} {{ doc_obj.test_suite.last_run }}

+

Description

+
+ {{ doc_obj.test_suite.get_fmt_description("html").strip() or "No package description found."}}
+ {% for section_name, tests in doc_obj.test_suite.group_tests().items() %} +
+ +

Test sub-module: {{ section_name.rsplit(".", 1)[-1] }}

+ + {% for test in tests %} +
+ {% set test_name_split = test.package.last_name.split("_") %} +

{{test_name_split[0].capitalize()}}: {{" ".join(test_name_split[1:])}}

+
+

Description

+ {{ test.get_html_description(standalone=False, embeded=True, + shift_header=doc_obj.docstrings_header_shift)|safe + }} + +
+
+
+ {% endfor %} +
+ {% endfor %} + + + \ No newline at end of file diff --git a/example/example_calculator/tests/templates/test_results_template.j2 b/example/example_calculator/tests/templates/test_results_template.j2 new file mode 100644 index 0000000..59241d0 --- /dev/null +++ b/example/example_calculator/tests/templates/test_results_template.j2 @@ -0,0 +1,116 @@ + + + + + {{ _("Test Results Document") }}: {{ doc_obj.test_suite.package.full_name.capitalize() }} + + + +

+

{{ doc_obj.test_suite.author or "Unknown author " }}
+

+

{{ _("Last execution date:") }} {{ doc_obj.test_suite.last_run }}

+

Description

+
+ {% set extra_desc_gen_args = ["--shift-heading-level-by={s}".format(s=doc_obj.docstrings_header_shift)] %} + {{ doc_obj.test_suite.get_fmt_description("html", extra_desc_gen_args + ).strip() or "No package description found."}} +
+ {% for section_name, tests in doc_obj.test_suite.group_tests().items() %} +
+ +

Test sub-module: {{ section_name.rsplit(".", 1)[-1] }}

+ + {% for test in tests %} +
+ {% set test_name_split = test.package.last_name.split("_") %} +

{{test_name_split[0].capitalize()}}: {{" ".join(test_name_split[1:])}}

+
+

Description

+ {{ test.get_html_description(standalone=False, embeded=True, + shift_header=doc_obj.docstrings_header_shift)|safe + }} + +
+ +
+

Command

+ {{ test.command or "-" }} +
+ +
+

Result

+ {% if test.result != None %} +
+ +

{{ test.result.result_name }}, executed on {{ + test.result.last_run or "-"}}

+
+ {% else %} +

No result found.

+ {% endif %} +

{{ test.comment|safe }}

+ +
+ {% for image_path in doc_obj.get_images_paths(test) %} + Test Image + {% endfor %} +
+
+
+
+ {% endfor %} +
+ {% endfor %} + +
+
+

Summary

+

{{_("%s test results summary") % doc_obj.test_suite.package.last_name.capitalize() }}

+ + + + + + + + + + + {% for test in doc_obj.test_suite.tests %} + + {% set result_bin = test.result_binary_label() %} + + + + + + + + + {% endfor %} +
Test{{ _("No result") }}{{ _("Accepted") }}{{ _("Accepted with reserve") }}{{ _("Skipped") }}{{ _("Rejected") }}Execution date
{{ test.package.full_name }}{{ "X" if result_bin[0] == 1 else "" }}{{ "X" if result_bin[1] == 1 else "" }}{{ "X" if result_bin[2] == 1 else "" }}{{ "X" if result_bin[3] == 1 else "" }}{{ "X" if result_bin[4] == 1 else "" }}{{ test.result.last_run }}
+

{{_("Count by result cateogry")}}

+ {% set result_count = doc_obj.test_suite.results_count() %} + + + + + + + + + + + + + + + +
{{ _("No result") }}{{ _("Accepted") }}{{ _("Accepted with reserve") }}{{ _("Skipped") }}{{ _("Rejected") }}
{{ result_count[0] }}{{ result_count[1] }}{{ result_count[2] }}{{ result_count[3] }}{{ result_count[4] }}
+
+
+ + + + \ No newline at end of file diff --git a/example/pyproject.toml b/example/pyproject.toml new file mode 100644 index 0000000..fa95b83 --- /dev/null +++ b/example/pyproject.toml @@ -0,0 +1,22 @@ +[build-system] +requires = ["setuptools"] +build-backend = "setuptools.build_meta" + +[project] +name = "example-calculator" +version = "1.0.0" +description = "A minimal calculator project demonstrating ModuleTester integration" +readme = "README.md" +requires-python = ">=3.9" +dependencies = [ + "QtPy >= 1.9", +] + +[project.optional-dependencies] +test = [ + "pytest", + "coverage", +] + +[tool.setuptools.packages.find] +include = ["example_calculator*"] diff --git a/scripts/run_example.py b/scripts/run_example.py new file mode 100644 index 0000000..db62183 --- /dev/null +++ b/scripts/run_example.py @@ -0,0 +1,45 @@ +# -*- coding: utf-8 -*- + +"""Launch the ModuleTester GUI with the Example Calculator test suite. + +This script mirrors the pattern used in X-GRID's ``run_test_plan.bat`` / +``moduleTester_launcher.py``, but as a standalone Python script suitable +for use as a VS Code task. + +Usage:: + + python scripts/run_example.py +""" + +import subprocess +import sys +from pathlib import Path + +SCRIPT_DIR = Path(__file__).resolve().parent +PROJECT_ROOT = SCRIPT_DIR.parent + +LAUNCHER = ( + PROJECT_ROOT + / "example" + / "example_calculator" + / "tests" + / "moduletester_launcher.py" +) + + +def main(): + """Run the Example Calculator ModuleTester launcher.""" + if not LAUNCHER.exists(): + print(f"Error: launcher not found at {LAUNCHER}") + sys.exit(1) + + print(f"Launching ModuleTester example from: {LAUNCHER}") + result = subprocess.run( + [sys.executable, str(LAUNCHER)], + cwd=str(PROJECT_ROOT / "example"), + ) + sys.exit(result.returncode) + + +if __name__ == "__main__": + main() From b2e119bdf8e1780d7d832fd4819757d223de848a Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Fri, 22 May 2026 11:55:00 +0200 Subject: [PATCH 8/9] update changelog and add specific step-by-step implementation documentation throught the new example given --- CHANGELOG.md | 9 + README.md | 9 +- doc/example.rst | 501 +++++++++++++++++++++++++++++++++++++++++++++- example/README.md | 28 +-- 4 files changed, 522 insertions(+), 25 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 490e2c2..0e09419 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,14 @@ # ModuleTester Releases # +## Version 1.1.0 ## + +### New features + +- Add Example Calculator reference implementation (`example/`) demonstrating + full ModuleTester integration with manual GUI tests, unit tests (pytest + + coverage), and qualification scripts +- Add step-by-step integration guide in documentation (`doc/example.rst`) + ## Version 1.0.1 ## ### Bug fixes diff --git a/README.md b/README.md index a34892c..06651e4 100644 --- a/README.md +++ b/README.md @@ -75,9 +75,12 @@ and is used to test [PlotPyStack](https://github.com/PlotPyStack) libraries. ## Example -Using ModuleTester on the `guidata` Python package โ€” the tree view shows test -hierarchy and execution status, while dockable panels display test properties, -output, and errors: +ModuleTester ships with a complete **Example Calculator** project in the +[`example/`](example/) directory that demonstrates all three test categories: +manual GUI tests, unit tests with coverage, and qualification scripts. + +See the [integration guide](https://moduletester.readthedocs.io/en/latest/example.html) +for a step-by-step tutorial on adding ModuleTester to your own project. ![ModuleTester โ€” guidata tests](https://raw.githubusercontent.com/Codra-Ingenierie-Informatique/ModuleTester/main/doc/images/shots/guidata.moduletester.png) diff --git a/doc/example.rst b/doc/example.rst index 965fec5..904172d 100644 --- a/doc/example.rst +++ b/doc/example.rst @@ -1,13 +1,496 @@ -Example -======= +Example โ€” Integrating ModuleTester into your project +===================================================== -.. figure:: images/shots/empty.png - :align: center +This guide walks you through integrating ModuleTester into an existing Python +project, step by step. It uses the **Example Calculator** shipped in the +``example/`` directory of the ModuleTester repository as a reference +implementation. By the end, you will have a fully functional ModuleTester +setup with manual GUI tests, automated unit tests with coverage, and +qualification scripts. - ModuleTester main window with dockable panels and tree view navigation +.. contents:: Steps + :local: + :depth: 1 -.. figure:: images/shots/guidata.moduletester.png - :align: center - Running tests on the ``guidata`` package โ€” tree view with status icons, test - properties, and execution results panels +Overview of the example project +-------------------------------- + +The Example Calculator is a minimal Python package with a Qt GUI that +demonstrates the three test categories ModuleTester supports: + +- **Manual GUI Tests** โ€” launch the application and follow step-by-step + instructions displayed in ModuleTester. +- **Unit Tests** โ€” wrapper scripts that run ``pytest`` with ``coverage`` + and generate HTML coverage reports. +- **Qualification Tests** โ€” standalone scripts that verify numerical + precision and performance against reference values. + +.. code-block:: text + + example/ + โ”œโ”€โ”€ pyproject.toml + โ”œโ”€โ”€ README.md + โ””โ”€โ”€ example_calculator/ + โ”œโ”€โ”€ __init__.py + โ”œโ”€โ”€ app.py # Qt GUI (QMainWindow) + โ”œโ”€โ”€ operations.py # Arithmetic functions + โ”œโ”€โ”€ converter.py # Unit conversion functions + โ”œโ”€โ”€ moduletester.ini # ModuleTester configuration + โ””โ”€โ”€ tests/ + โ”œโ”€โ”€ __init__.py + โ”œโ”€โ”€ moduletester_launcher.py # Launches ModuleTester GUI + โ”œโ”€โ”€ templates/ # Export templates and assets + โ”œโ”€โ”€ processing/ # Actual pytest test files + โ”‚ โ”œโ”€โ”€ test_operations.py + โ”‚ โ””โ”€โ”€ test_converter.py + โ””โ”€โ”€ Test Plan/ + โ”œโ”€โ”€ Manual GUI Tests/ + โ”‚ โ”œโ”€โ”€ test-001.py + โ”‚ โ”œโ”€โ”€ test-002.py + โ”‚ โ””โ”€โ”€ test-003.py + โ”œโ”€โ”€ Unit Tests/ + โ”‚ โ”œโ”€โ”€ 001-operations.py + โ”‚ โ””โ”€โ”€ 002-converter.py + โ””โ”€โ”€ Qualification Tests/ + โ”œโ”€โ”€ 001-precision.py + โ””โ”€โ”€ 002-performance.py + + +Step 1 โ€” Make your package importable +-------------------------------------- + +ModuleTester discovers tests by importing your package and scanning its +sub-modules. Your package must be importable from the Python environment +where ModuleTester runs. + +For the example project, install it in editable mode: + +.. code-block:: console + + $ cd example + $ pip install -e ".[test]" + +.. tip:: + + If your project is already installed in your environment (via ``pip install + -e .`` or similar), you can skip this step. + + +Step 2 โ€” Organise your tests +------------------------------ + +Create a ``tests/`` sub-package inside your main package. ModuleTester scans +this sub-package recursively and groups tests by directory. + +Use the ``# guitest:`` directive at the top of each Python file to control +how ModuleTester treats it: + +.. list-table:: + :header-rows: 1 + :widths: 25 75 + + * - Directive + - Effect + * - ``# guitest: show`` + - The script appears in the ModuleTester tree view. Use this for all + test files that should be visible to testers. + * - ``# guitest: skip`` + - The script is completely ignored during discovery. Use this for + utility modules, ``pytest`` files, and launchers. + * - ``# guitest: hide`` + - The script is discovered but hidden from the default "visible" + category. Useful for batch-only tests. + +In the example, the directory structure under ``Test Plan/`` determines how +tests are grouped in the tree view. You are free to choose any directory +names โ€” ModuleTester uses them as-is. + + +Step 3 โ€” Write manual GUI tests +--------------------------------- + +Manual GUI tests launch your application and display step-by-step +instructions to the tester. ModuleTester renders the module docstring as +HTML, so write it in reStructuredText with a ``.. list-table::`` describing +actions and expected results. + +Here is an annotated example from the calculator project: + +.. code-block:: python + + """ + Example Calculator โ€” Manual GUI Test + + TEST-001: Application startup + + This test verifies that the application starts correctly. + + .. list-table:: Test steps + :header-rows: 1 + :widths: 50 50 + + * - Action + - Expected result + * - Launch the application. + - The main window appears with the title "Example Calculator". + * - Verify that both tabs ("Operations" and "Converter") are present. + - Both tabs are visible and selectable. + * - Close the application. + - The application closes without errors. + """ + + # guitest: show + + import example_calculator.app as app + + if __name__ == "__main__": + app.run() + +Key points: + +- The docstring must come **before** the ``# guitest: show`` directive. +- The ``if __name__ == "__main__":`` guard is required โ€” ModuleTester + executes each test in a subprocess. +- Your application must expose a ``run()`` function (or equivalent) that + starts the Qt event loop. + + +Step 4 โ€” Write unit test wrappers with coverage +------------------------------------------------- + +You could add ``# guitest: show`` and a ``if __name__`` block directly to +your existing ``pytest`` files, but a cleaner approach is to keep them +untouched and create thin **wrapper scripts** instead. This way your test +files remain standard ``pytest`` modules โ€” the only change is adding +``# guitest: skip`` so ModuleTester ignores them during discovery. The +wrappers call ``pytest.main()`` and optionally collect code coverage. + +Wrapper example (``Unit Tests/001-operations.py``): + +.. code-block:: python + + """ + UT-001: Arithmetic operations (pytest + coverage) + + .. list-table:: Test steps + :header-rows: 1 + :widths: 50 50 + + * - Action + - Expected result + * - Launch the test script. + - Unit test results and a coverage report are generated. + """ + + # guitest: show + + import os + from datetime import datetime + import coverage + import pytest + + if __name__ == "__main__": + current_dir = os.path.dirname(os.path.abspath(__file__)) + # Navigate to the project root + project_root = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.dirname(current_dir))) + ) + test_dir = os.path.join(project_root, "example_calculator", "tests") + + cov = coverage.Coverage( + include=["*/example_calculator/operations.py"], + ) + cov.start() + pytest.main([ + os.path.join(test_dir, "processing", "test_operations.py"), + "-v", + ]) + cov.stop() + cov.save() + cov.report(show_missing=False) + cov.html_report(directory=os.path.join( + project_root, "TestPlan", "reports", + datetime.now().strftime("%Y-%m-%d"), "operations", + )) + +The actual ``pytest`` file (``processing/test_operations.py``) must start +with ``# guitest: skip`` to stay hidden from ModuleTester: + +.. code-block:: python + + # guitest: skip + + import pytest + from example_calculator.operations import add, divide + + class TestAdd: + def test_positive_numbers(self): + assert add(2, 3) == 5 + + def test_negative_numbers(self): + assert add(-1, -2) == -3 + + class TestDivide: + def test_divide_by_zero(self): + with pytest.raises(ZeroDivisionError): + divide(1, 0) + + +Step 5 โ€” Write qualification scripts +-------------------------------------- + +Qualification tests are standalone scripts that run computations, compare +results against reference values, and generate reports. They are useful for +performance benchmarks, numerical accuracy checks, or any test that does not +fit the ``pytest`` model. + +Because ModuleTester executes every test as a **subprocess**, it is entirely +agnostic to what the script does internally. This makes it straightforward to +integrate any custom test script your project already has โ€” numerical +simulations, hardware-in-the-loop checks, data-processing pipelines, etc. โ€” +without modifying them beyond adding the ``# guitest: show`` directive and a +docstring. This approach even extends to **non-Python projects** (C++, C#, +web applications, โ€ฆ): the wrapper script just needs to call the external +tool via ``subprocess.run()`` or equivalent. The only requirement is that the +wrapper itself is a ``.py`` file so ModuleTester can discover it. + +Example (``Qualification Tests/001-precision.py``): + +.. code-block:: python + + """ + QUAL-001: Arithmetic precision verification + + .. list-table:: Test steps + :header-rows: 1 + :widths: 50 50 + + * - Action + - Expected result + * - Launch the qualification script. + - The script displays results and saves a report. + """ + + # guitest: show + + import os + from example_calculator import operations + + REFERENCE_DATA = [ + ("add(0.1, 0.2)", operations.add, (0.1, 0.2), 0.3, 1e-15), + # ... more test cases + ] + + def run(mode="print", save_path=None): + results = [] + for desc, func, args, expected, tol in REFERENCE_DATA: + computed = func(*args) + error = abs(computed - expected) + results.append((desc, computed, expected, error, error <= tol)) + # ... build and save report + + if __name__ == "__main__": + run("print_save", save_path="TestPlan/reports/precision") + + +Step 6 โ€” Customise export templates +-------------------------------------- + +ModuleTester uses **Jinja2 templates** to generate reports. The default +templates are shipped in ``moduletester/default_templates/`` โ€” copy them into +your project's ``tests/templates/`` directory so you can customise them. + +Two templates control the exported documents: + +- ``test_list_template.j2`` โ€” the **test list** report (test catalogue with + descriptions only, no results). +- ``test_results_template.j2`` โ€” the **test results** report (full campaign + output with descriptions, statuses, comments, images, and a summary table). + +Both templates have access to the ``doc_obj`` context object, which exposes +the full ``test_suite`` โ€” including the package description, grouped tests, +results, and execution dates. + +**Typical customisations:** + +- **Project description tracking** โ€” edit the ``

Description

`` + section to include project-specific metadata (version, author, release + date, reference documents, โ€ฆ). +- **Choosing which information to display** โ€” add or remove sections in each + test block: description, command, result status, execution date, comments, + screenshots, etc. +- **Summary table columns** โ€” adjust the summary table at the end of the + results template to match your reporting needs (e.g. add a "Duration" + column, remove unused result categories). +- **Styling** โ€” edit ``default_style.css`` to match your organisation's + branding. + +When exporting to **DOCX** or **ODT**, ModuleTester first renders the +Jinja2 template to HTML, then converts that HTML via Pandoc using a +``--reference-doc`` flag. The reference files (``custom-reference.docx`` and +``custom-reference.odt``) act as **style templates**: Pandoc extracts fonts, +heading styles, page layout, headers/footers, and margins from them, then +applies those styles to the generated content. In other words, the +**structure and data** come from the ``.j2`` templates, while the **visual +formatting** comes from the reference document. To match your organisation's +formatting, open the reference file in Word or LibreOffice, adjust the +styles (e.g. ``Heading 1``, ``Normal``, page margins), and save it back. + +For the Example Calculator, the templates are used as-is from the defaults. +See :doc:`templates` for the full template API reference. + + +Step 7 โ€” Create the configuration file +---------------------------------------- + +Place a ``moduletester.ini`` file next to your package's ``__init__.py``. +This file must declare **all** options in every section โ€” ModuleTester raises +a ``ConfigConflictError`` if any field is missing. + +The easiest way to start is to copy the example configuration file and +adapt it: + +.. code-block:: ini + + [general] + docstring_fmt = rst + category = visible + + [export] + template_dir = tests/templates + test_results_template_name = test_results_template.j2 + test_list_template_name = test_list_template.j2 + docx_reference = custom-reference.docx + odt_reference = custom-reference.odt + css_style = default_style.css + export_fmts = html + reload_templates_on_export = 0 + docstrings_header_shift = 3 + toc_depth = 2 + + [gui] + test_list_visible = 1 + test_list_pos = left + test_props_visible = 0 + test_props_pos = right + result_tab_visible = 1 + result_tab_pos = bottom + result_props_visible = 1 + result_props_pos = bottom + cli_visible = 0 + cli_pos = bottom + toolbox_visible = 0 + toolbox_pos = bottom + +The ``template_dir`` path is relative to the package directory. You need to +provide the template and asset files in that directory (see +:doc:`templates`). The simplest approach is to copy ModuleTester's default +templates from ``moduletester/default_templates/``. + +See :doc:`configuration` for the full reference of all options. + + +Step 8 โ€” Create a launcher script (optional) +----------------------------------------------- + +The launcher script is a convenience tool that generates a ``.moduletester`` +template file by discovering all tests in your package, then opens the +ModuleTester GUI. It is typically used by developers to keep the test plan +up to date or to prepare a test campaign before a milestone release. + +.. note:: + + This step is **optional**. You can achieve the same result by launching + ModuleTester directly on your package: + + .. code-block:: console + + $ moduletester -p my_package + + Or by opening an existing ``.moduletester`` file: + + .. code-block:: console + + $ moduletester -f TestPlan/my_package_v1.0.0_.moduletester + + The launcher script simply automates the template generation step and can + be wired to a VS Code task or a CI script for convenience. + +.. code-block:: python + + # guitest: skip + + import os + import sys + from importlib import import_module + + from qtpy import QtWidgets as QW + from moduletester.gui.main import run + from moduletester.manager import TestManager + from moduletester.model import Module + + from my_package import __version__ + + def create_template(): + mod = import_module("my_package") + project_dir = os.path.dirname( + os.path.dirname(os.path.dirname(os.path.abspath(__file__))) + ) + test_plan_dir = os.path.join(project_dir, "TestPlan") + os.makedirs(test_plan_dir, exist_ok=True) + + output_path = os.path.join( + test_plan_dir, + f"my_package_v{__version__}_.moduletester", + ) + manager = TestManager( + Module(mod), _template_path=output_path, _category="visible" + ) + print(f"Found {len(manager.test_suite.tests)} tests") + return output_path + + if __name__ == "__main__": + app = QW.QApplication.instance() + if not app: + app = QW.QApplication(sys.argv) + + if len(sys.argv) > 1: + moduletester_file = sys.argv[1] + else: + moduletester_file = create_template() + + moduletester = run(path=moduletester_file) + moduletester.window.show() + app.exec_() + +Run it to launch ModuleTester: + +.. code-block:: console + + $ python my_package/tests/moduletester_launcher.py + +The launcher generates a ``.moduletester`` file in ``TestPlan/`` and opens +the GUI. You can also pass an existing ``.moduletester`` file as an argument +to reload a previous test session. + +.. note:: + + The launcher must use ``# guitest: skip`` to avoid appearing in the test + tree itself. + + +Running the example +-------------------- + +To try the full example shipped with ModuleTester: + +.. code-block:: console + + $ cd example + $ pip install -e ".[test]" + $ python example_calculator/tests/moduletester_launcher.py + +This discovers 7 tests (3 manual GUI, 2 unit, 2 qualification) and opens +the ModuleTester GUI. You can run each test, inspect its output, and export +a report. diff --git a/example/README.md b/example/README.md index 8ab9ebe..c4ec6fe 100644 --- a/example/README.md +++ b/example/README.md @@ -20,19 +20,21 @@ example/ โ””โ”€โ”€ tests/ โ”œโ”€โ”€ __init__.py โ”œโ”€โ”€ moduletester_launcher.py # Launcher for ModuleTester GUI - โ”œโ”€โ”€ Manual GUI Tests/ - โ”‚ โ”œโ”€โ”€ test-001.py # Application startup - โ”‚ โ”œโ”€โ”€ test-002.py # Arithmetic operations via GUI - โ”‚ โ””โ”€โ”€ test-003.py # Unit conversions via GUI - โ”œโ”€โ”€ Unit Tests/ - โ”‚ โ”œโ”€โ”€ 001-operations.py # pytest + coverage wrapper - โ”‚ โ””โ”€โ”€ 002-converter.py # pytest + coverage wrapper - โ”œโ”€โ”€ Qualification Tests/ - โ”‚ โ”œโ”€โ”€ 001-precision.py # Numerical precision verification - โ”‚ โ””โ”€โ”€ 002-performance.py # Performance benchmark - โ””โ”€โ”€ processing/ - โ”œโ”€โ”€ test_operations.py # Actual pytest test cases - โ””โ”€โ”€ test_converter.py # Actual pytest test cases + โ”œโ”€โ”€ templates/ # Export templates (copied from defaults) + โ”œโ”€โ”€ processing/ + โ”‚ โ”œโ”€โ”€ test_operations.py # Actual pytest test cases + โ”‚ โ””โ”€โ”€ test_converter.py # Actual pytest test cases + โ””โ”€โ”€ Test Plan/ + โ”œโ”€โ”€ Manual GUI Tests/ + โ”‚ โ”œโ”€โ”€ test-001.py # Application startup + โ”‚ โ”œโ”€โ”€ test-002.py # Arithmetic operations via GUI + โ”‚ โ””โ”€โ”€ test-003.py # Unit conversions via GUI + โ”œโ”€โ”€ Unit Tests/ + โ”‚ โ”œโ”€โ”€ 001-operations.py # pytest + coverage wrapper + โ”‚ โ””โ”€โ”€ 002-converter.py # pytest + coverage wrapper + โ””โ”€โ”€ Qualification Tests/ + โ”œโ”€โ”€ 001-precision.py # Numerical precision verification + โ””โ”€โ”€ 002-performance.py # Performance benchmark ``` ## Prerequisites From 8693491fb58caba18a9dd0535d9dc7bb65504223 Mon Sep 17 00:00:00 2001 From: Thomas MALLET Date: Fri, 22 May 2026 12:02:02 +0200 Subject: [PATCH 9/9] update and add doc image screenshot with example project --- README.md | 2 ++ doc/cli.rst | 4 ++-- doc/images/shots/example.moduletester.png | Bin 0 -> 75974 bytes doc/user_guide.rst | 14 +++++++++++--- 4 files changed, 15 insertions(+), 5 deletions(-) create mode 100644 doc/images/shots/example.moduletester.png diff --git a/README.md b/README.md index 06651e4..8ad357c 100644 --- a/README.md +++ b/README.md @@ -82,6 +82,8 @@ manual GUI tests, unit tests with coverage, and qualification scripts. See the [integration guide](https://moduletester.readthedocs.io/en/latest/example.html) for a step-by-step tutorial on adding ModuleTester to your own project. +![ModuleTester โ€” example tests](https://raw.githubusercontent.com/Codra-Ingenierie-Informatique/ModuleTester/main/doc/images/shots/example.moduletester.png) + ![ModuleTester โ€” guidata tests](https://raw.githubusercontent.com/Codra-Ingenierie-Informatique/ModuleTester/main/doc/images/shots/guidata.moduletester.png) ## Documentation diff --git a/doc/cli.rst b/doc/cli.rst index 690bc47..a85af0c 100644 --- a/doc/cli.rst +++ b/doc/cli.rst @@ -28,7 +28,7 @@ Launch the graphical interface. - Python package to load on startup. The package must be importable. * - ``-f``, ``--file`` - str - - Path to a ``.mt`` project file to open on startup. + - Path to a ``.moduletester`` project file to open on startup. **Examples** @@ -41,7 +41,7 @@ Launch the graphical interface. $ moduletester -p guidata # Open a saved project file - $ moduletester -f /path/to/project.mt + $ moduletester -f /path/to/project.moduletester .. note:: diff --git a/doc/images/shots/example.moduletester.png b/doc/images/shots/example.moduletester.png new file mode 100644 index 0000000000000000000000000000000000000000..67adee786f5156e866bcfe902f952d47670200d5 GIT binary patch literal 75974 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⩔Y&RY zVp|^;hocRi8HPO8k=88Y|Qcd`ZNyY9V6?M zvcz;+7I3b`U1_iV%9FF^=^SpsDKnZT$@btGf9py6h{}2!@jY6Jg{2X}`b4DmmbN@j zD}1uym8W@38-x(3{AxMkRHlT*vGqnWXWB=#6`#f&HHi_wY`Iy>v1#kK;ket@mg0s| zxS1!-;bYwkF+-|xKMW~zCQppE?b>xgXB>DuuW~Ub)M_IW1sHvEC_D>%oT$$ zg?>mEq`5zOnT~Mwt*l{;vSD1x)(+{)<;f&uvO1PI*c1`y_O8U&U@$oO<_>W97;3 zdQ7(de#<8-+4VU5?kg!fLl<9mo*t=t#u{VrvWvUe63F~RWW1nXD)Xj^-~Q_EaSKc3 zoQpGW(${{RG5`R`U`C*8hRWti4X!7Hx1L_7SI)ak7kfP%m_daYu0W67gnNlIx@Np( z&lsm-ck}pX>LP=77EaW8?uQq5o@A`k`U_%sos!;qaG}otpN=zh!=ib5<+gK;zi>UH zh0XxDF@w?DqPkxs*q6^Nj2^(WaH7sTZ=woQt+QV_&S$erNL=nnUGTXYpCv&{I!teVpZXKO@{LhaV@ybhf-zTfP`zjlcl^4J7o@DFqw|uhK za>Dg=@wqJtR;$z{x_+H>ndd{L$#^EQ{}36jU-Ndo5fp|cS?3wLA-G1x&N@#z zN$0Mc=pD3ko3TtEy?UP-T+O;qI%&4FDA8C1(cLfN?#mfvQT00M+;zI_*Ei^lRO{?p zPI!jYy1H~#+~eXW-nN>@(qV3o>wG-ddX;!E{Fmx@=bW|fd5_Gt*L|*T(>QDYFN&Od z*Xyz*w!=%jZHvd)?(Q7l8OvPzA}S-!eu;DKCB1LDy6uX;tna=s&y_f3r0SW4_S>vAG01FlDbYfrs+GT?tST3##?%@^jzIm?2Ppk*LryjS#!qW^gM%)*nR z?z2&E)r?|oyPSL3Y#Gm|ZmG;R5HMe?jQ6#~xFD z{zx;=!!ufV(h^O4H5fe*av$~Hd++JwlTS92yXTpOI;pPAXAG0$Boph@9oe=$-NpXM zJ{;5Z$%uWK=I_po*rpzk5!;pKV+HghU45?Z>T&=8xNY6KRb$7VqHlfcTgo|d`5uyzKh! z%x)YjpsTw2T;0{>008})drYo#&N)Y0w{BHKLxZaC|NklH(4m9+{hBq}vu97#Lf&R* z*+)G50RWIZzB~I4juij^Kws3_+N$5LS))UT4l3u!k#@be=RGZ7zTBJ-000000078n z>({T>dwbr~kt6NO9Wr$2Pd@qdQ+@Ek2YTn7cQj$b1f6)|iRLm{slRL|0002cle+p` z-PPp)0R7v^C!eg7Pd-_lot;{`bgBOG(o5R={`-=oz^GB9Lm_*{&wS=H`rOGU>#D0} ztFW+8n!F zPMk}UG$=1m!%iHg!Gi~D(4avYIADOHW