Metadata-Version: 2.1
Name: pyqt-autotest
Version: 0.1.0
Summary: A command line tool for finding system-level bugs that cause a python Qt application to terminate.
Author-email: Robert Applin <robertapplin.developer@gmail.com>
License: MIT License
        
        Copyright (c) 2022 Rob Applin
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
Project-URL: Homepage, https://github.com/robertapplin/pyqt-autotest
Project-URL: Bug Tracker, https://github.com/robertapplin/pyqt-autotest/issues
Keywords: python,qt,random,auto,test
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: LICENSE

# pyqt-autotest ⚙️🧪
A command line tool for finding system-level bugs that cause a python Qt application to terminate. This is achieved by simulating user actions on a widget using [QtTest](https://doc.qt.io/qt-5/qtest-overview.html).

This tool requires you to provide a python class which inherits from the provided `RandomAutoTest` class. The widget defined inside this class will be opened a specified number of times, and a random selection of actions is performed on the children widgets within the encompassing widget. The result of each run is recorded, and warnings/errors are captured, before a report is generated to provide reliable instructions for how to reproduce a bug.

## Table of contents
* [Installation](#installation)
* [Options](#options)
* [Test class](#test-class)
* [Built-in Actions](#built-in-actions)
* [Creating your own Actions](#creating-your-own-actions)
* [Usage](#usage)
* [Output](#output)

## Installation

This package can be installed from [PyPI](https://pypi.org/) using pip.

```sh
pip install pyqt-autotest
```

## Options

The following table details the options that can be provided on the command line.

| Option            | Description                                                                       | Default            | Command line option     |
|-------------------|-----------------------------------------------------------------------------------|--------------------|-------------------------|
| Test class        | A module path to a python class which inherits from `RandomAutoTest`.             | REQUIRED           | -t, --test-class        |
| Number of runs    | The number of times to open the widget and perform a random selection of actions. | 1                  | -r, --number-of-runs    |
| Number of actions | The number of random actions to perform each time the widget is opened.           | 10                 | -a, --number-of-actions |
| Wait time         | The number of milliseconds to wait between executing two consecutive actions.     | 50                 | -w, --wait-time         |
| Output directory  | The relative directory to store the output files in.                              | "autotest_results" | -o, --output-directory  |

## Test class

The first thing to do is to create a python class which inherits from `RandomAutoTest`, and implement the `setup_widget` method. Inside this method, you should instantiate your `QWidget` and assign it to the member variable `self.widget`as follows. Any additional setup, such as loading data into your widget, should also be done in the same method.

```py
from usercode.model import ExampleModel
from usercode.presenter import ExamplePresenter
from usercode.view import ExampleView

from pyqt_autotest.random_auto_test import RandomAutoTest


class ExampleTest(RandomAutoTest):

    def setup_widget(self):
        # The 'self.widget' member variable MUST be instantiated, and it must be a QWidget.
        self.widget = ExampleView()

        # Other relevant setup should be done here too. This example refers to the Model-View-Presenter (MVP) pattern
        self.model = ExampleModel()
        self.presenter = ExamplePresenter(self.widget, self.model)
        
        # Your test might be more interesting if you first load data into the widget
        self.presenter.load_data("fake_data_file.dat")
```

Optionally, you can also create a `setup_options` method to specify commonly used options. However, these options will be overridden if provided on the command line.

```py
    def setup_options(self):
        self.options.number_of_runs = 2
        self.options.number_of_actions = 15
        self.options.wait_time = 200  # milliseconds
        self.options.output_directory = "output_results"
```

## Built-in Actions

The following actions are built-in to this package. Only these actions can be performed if you do not create your own (see the section below on [how to create your own](#creating-your-own-actions)).

| Built-in Action       | Description                                               | Python Callable                                          |
|-----------------------|-----------------------------------------------------------|----------------------------------------------------------|
| Action.KeyDownClick   | Simulates the Down key being pressed on an active widget. | `lambda widget: QTest.keyClick(widget, Qt.Key_Down)`     |
| Action.KeyUpClick     | Simulates the Up key being pressed on an active widget.   | `lambda widget: QTest.keyClick(widget, Qt.Key_Up)`       |
| Action.MouseLeftClick | Simulates a widget being left clicked by the mouse.       | `lambda widget: QTest.mouseClick(widget, Qt.LeftButton)` |

These actions will by default be performed for the following widget types. This has been made minimalistic on purpose because this command line tool is arguably more useful if you carefully customize your own actions as seen in the next section.

| Widget type       | Built-in Actions      |
|-------------------|-----------------------|
| QPushButton       | Action.MouseLeftClick |

## Creating your own Actions
This package provides a great amount of flexibility for you to create and customize the actions you want to be available during a 'random auto test'. It also allows you to ignore certain widget types by not providing any actions for them.

The following code provides a basic example for how to define your own custom actions:

```py
    def setup_options(self):
        # The 'actions' dictionary requires a string to describe an action, and a callable function which uses QtTest to
        # perform an action. Note that the 'Left click in centre' action is useful for testing a QCheckBox.
        actions = {
            "Enter dummy text": lambda widget: QTest.keyClicks(widget, "dummy text"),
            "Left click in centre": lambda widget: QTest.mouseClick(widget, Qt.LeftButton,
                                                                    pos=QPoint(2, widget.height() / 2))
        }
        
        # The 'widget_actions' dictionary requires you to specify which actions can be performed on which widget types.
        # Note that you can use the built-in actions, or create your own with a string name. The string name is used to 
        # describe what an action does when generating the output instructions, so make sure it is short but descriptive.
        widget_actions = {
            QCheckBox: ["Left click in centre"],
            QLineEdit: ["Enter dummy text"],
            QSpinBox: [Action.KeyDownClick, Action.KeyUpClick]
        }

        # This will overwrite the built-in actions. You could use 'dict::update' to keep the built-in actions.
        self.options.actions = actions
        self.options.widget_actions = widget_actions
```

## Usage

It is trivial to run your test class from the command line as follows (depending on its module location):

```sh
autotest -t pyqt_autotest.examples.simple.test.ExampleTest
```
or with more arguments:
```
autotest -t pyqt_autotest.examples.simple.test.ExampleTest -r 5 -a 10 -w 150 -o autotest_results
```

## Output

For a basic example of what the output looks like, see the [simple example output](https://github.com/robertapplin/pyqt-autotest/tree/main/pyqt_autotest/examples/simple/output).
