testexecutor-ui 0.6.1__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- testexecutor/VERSION +1 -0
- testexecutor/__init__.py +6 -0
- testexecutor/control/TestListenerApi.py +56 -0
- testexecutor/control/TestSuiteListener.py +249 -0
- testexecutor/control/__init__.py +0 -0
- testexecutor/model/GenericListModel.py +63 -0
- testexecutor/model/InstructionsModel.py +351 -0
- testexecutor/model/KeyValueModel.py +177 -0
- testexecutor/model/MultiTestWindowModel.py +184 -0
- testexecutor/model/ResultModel.py +314 -0
- testexecutor/model/TestSuiteControlModel.py +139 -0
- testexecutor/model/TestSuiteGroup.py +105 -0
- testexecutor/model/TestSuiteModel.py +301 -0
- testexecutor/model/__init__.py +0 -0
- testexecutor/model/selector/FilterGroupModel.py +90 -0
- testexecutor/model/selector/FilterModel.py +251 -0
- testexecutor/model/selector/MultiSelectDetails.py +106 -0
- testexecutor/model/selector/MultiSelectListModel.py +48 -0
- testexecutor/model/selector/__init__.py +0 -0
- testexecutor/ui/IdentificationWidget.qml +40 -0
- testexecutor/ui/InputWidget.qml +74 -0
- testexecutor/ui/InstructionWidget.qml +185 -0
- testexecutor/ui/KeyValueItem.qml +194 -0
- testexecutor/ui/KeyValueItemConfig.qml +41 -0
- testexecutor/ui/KeyValueList.qml +49 -0
- testexecutor/ui/MultiTestWidget.qml +48 -0
- testexecutor/ui/MultiTestWindow.qml +88 -0
- testexecutor/ui/ResultItem.qml +199 -0
- testexecutor/ui/ResultItemConfig.qml +46 -0
- testexecutor/ui/ResultList.qml +58 -0
- testexecutor/ui/TestSuiteConfig.qml +95 -0
- testexecutor/ui/TestSuiteController.qml +54 -0
- testexecutor/ui/TestSuiteWidget.qml +350 -0
- testexecutor/ui/selector/FilterItem.qml +75 -0
- testexecutor/ui/selector/FilterItemConfig.qml +24 -0
- testexecutor/ui/selector/FilterMultiSelectorItem.qml +169 -0
- testexecutor/ui/selector/FilterMultiSelectorItemConfig.qml +22 -0
- testexecutor/ui/selector/FilterMultiSelectorList.qml +50 -0
- testexecutor/ui/selector/HeaderConfig.qml +24 -0
- testexecutor/ui/selector/SectionConfig.qml +24 -0
- testexecutor/ui/selector/SectionDelegate.qml +85 -0
- testexecutor/ui/selector/SelectorHeader.qml +43 -0
- testexecutor/ui/selector/SuiteList.qml +150 -0
- testexecutor/ui/selector/SuiteSelector.qml +71 -0
- testexecutor/ui/selector/SuiteSelectorConfig.qml +38 -0
- testexecutor/ui/selector/TestConfig.qml +22 -0
- testexecutor/ui/selector/TestDelegate.qml +75 -0
- testexecutor/ui/te-64x64.ico +0 -0
- testexecutor_ui-0.6.1.dist-info/LICENSE +204 -0
- testexecutor_ui-0.6.1.dist-info/METADATA +27 -0
- testexecutor_ui-0.6.1.dist-info/NOTICE +3 -0
- testexecutor_ui-0.6.1.dist-info/RECORD +54 -0
- testexecutor_ui-0.6.1.dist-info/WHEEL +5 -0
- testexecutor_ui-0.6.1.dist-info/top_level.txt +1 -0
testexecutor/VERSION
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
0.6.1
|
testexecutor/__init__.py
ADDED
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
# Copyright 2020 Direkt, Australia
|
|
2
|
+
# Copyright 2021 Direkt Embedded Pty Ltd
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
"""
|
|
17
|
+
Interface class as an interface between test runners/executors and user interfaces and other listeners.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class TestListenerApi(object):
|
|
22
|
+
def test_started(self, name):
|
|
23
|
+
pass
|
|
24
|
+
|
|
25
|
+
def test_completed(self, name, result):
|
|
26
|
+
pass
|
|
27
|
+
|
|
28
|
+
def test_progress(self, name, progress):
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
def feedback(self, name, data):
|
|
32
|
+
pass
|
|
33
|
+
|
|
34
|
+
def user_input(self, title, message, control=None, default_value=None, hidden=False, *values):
|
|
35
|
+
pass
|
|
36
|
+
|
|
37
|
+
def user_decision(self, title, message, control):
|
|
38
|
+
pass
|
|
39
|
+
|
|
40
|
+
def user_instructions(self, title, message, expect_response=True, control=None):
|
|
41
|
+
pass
|
|
42
|
+
|
|
43
|
+
def suite_start(self, name, tests=None):
|
|
44
|
+
pass
|
|
45
|
+
|
|
46
|
+
def suite_end(self, name, failures=-1, message=None):
|
|
47
|
+
pass
|
|
48
|
+
|
|
49
|
+
def suite_abort(self, name, message):
|
|
50
|
+
pass
|
|
51
|
+
|
|
52
|
+
def async_instructions(self, title, message, callback=None, control=None, response=None):
|
|
53
|
+
pass
|
|
54
|
+
|
|
55
|
+
def clear_instructions(self):
|
|
56
|
+
pass
|
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
# Copyright 2020 Direkt, Australia
|
|
2
|
+
# Copyright 2021 Direkt Embedded Pty Ltd
|
|
3
|
+
#
|
|
4
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
5
|
+
# you may not use this file except in compliance with the License.
|
|
6
|
+
# You may obtain a copy of the License at
|
|
7
|
+
#
|
|
8
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
9
|
+
#
|
|
10
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
11
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
12
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
13
|
+
# See the License for the specific language governing permissions and
|
|
14
|
+
# limitations under the License.
|
|
15
|
+
|
|
16
|
+
from PySide6.QtCore import Signal
|
|
17
|
+
from PySide6.QtCore import Slot
|
|
18
|
+
from PySide6.QtCore import Qt
|
|
19
|
+
from PySide6.QtCore import QObject
|
|
20
|
+
from PySide6.QtCore import QMutex
|
|
21
|
+
|
|
22
|
+
from testexecutor.model.TestSuiteModel import TestSuiteModel
|
|
23
|
+
from testexecutor.control.TestListenerApi import TestListenerApi
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class Link(QObject):
|
|
27
|
+
"""
|
|
28
|
+
A class which is the link between this modules 'Python" TestSuiteListener and the Qt/Qml UI.
|
|
29
|
+
It simply provides some Qt Signals and Slots which correspond to the PySide6 models Slots and Signals.
|
|
30
|
+
"""
|
|
31
|
+
addSignal = Signal(str, str)
|
|
32
|
+
startSignal = Signal(str)
|
|
33
|
+
populateSignal = Signal(list, bool)
|
|
34
|
+
feedbackSignal = Signal(str, str)
|
|
35
|
+
endSignal = Signal(str, str)
|
|
36
|
+
progressSignal = Signal(str, int)
|
|
37
|
+
userDecisionSignal = Signal(str, str, list, bool)
|
|
38
|
+
userInputRequestSignal = Signal(str, str, list, str, bool, tuple, bool)
|
|
39
|
+
_userCallback = None
|
|
40
|
+
|
|
41
|
+
def __init__(self, instruction_signal):
|
|
42
|
+
QObject.__init__(self)
|
|
43
|
+
instruction_signal.connect(self._asyncInstructionCallback, Qt.QueuedConnection)
|
|
44
|
+
|
|
45
|
+
def setAsyncInstructionCallback(self, callback):
|
|
46
|
+
self._userCallback = callback
|
|
47
|
+
|
|
48
|
+
@Slot(str)
|
|
49
|
+
def _asyncInstructionCallback(self, response, input):
|
|
50
|
+
if self._userCallback:
|
|
51
|
+
if self._userCallback(response):
|
|
52
|
+
self._userCallback = None
|
|
53
|
+
|
|
54
|
+
|
|
55
|
+
class TestSuiteListener(TestListenerApi):
|
|
56
|
+
|
|
57
|
+
PASS = "Pass"
|
|
58
|
+
FAIL = "Fail"
|
|
59
|
+
|
|
60
|
+
def __init__(self, model=None):
|
|
61
|
+
"""
|
|
62
|
+
Connect the Qml UI with our listener through a Link object which holds our connection Qt signals and slots.
|
|
63
|
+
:param model: A testexecutor.model.TestSuiteModel which this listener will connect to.
|
|
64
|
+
"""
|
|
65
|
+
self.model = model
|
|
66
|
+
self.clear_results_on_start = True
|
|
67
|
+
self.link = Link(self.model.instructions.control.onUserInput)
|
|
68
|
+
self.link.addSignal.connect(self.model.results.add, Qt.QueuedConnection)
|
|
69
|
+
self.link.startSignal.connect(self.model.results.start, Qt.QueuedConnection)
|
|
70
|
+
self.link.populateSignal.connect(self.model.results.populateTests, Qt.QueuedConnection)
|
|
71
|
+
self.link.feedbackSignal.connect(self.model.results.setFeedback, Qt.QueuedConnection)
|
|
72
|
+
self.link.endSignal.connect(self.model.results.end, Qt.QueuedConnection)
|
|
73
|
+
self.link.progressSignal.connect(self.model.results.progress, Qt.QueuedConnection)
|
|
74
|
+
self.link.userDecisionSignal.connect(self.model.instructions.userDecision, Qt.QueuedConnection)
|
|
75
|
+
self.link.userInputRequestSignal.connect(self.model.instructions.userInputRequest, Qt.QueuedConnection)
|
|
76
|
+
|
|
77
|
+
# Test Listener Api methods
|
|
78
|
+
def test_started(self, name):
|
|
79
|
+
"""
|
|
80
|
+
Should be called by the underlying test whenever it is started, allowing the listener (likely UI) to add it
|
|
81
|
+
or indicate it is running.
|
|
82
|
+
:param name: Name of the test that has been started
|
|
83
|
+
:return: None
|
|
84
|
+
"""
|
|
85
|
+
self.clear_instructions()
|
|
86
|
+
self.link.startSignal.emit(name)
|
|
87
|
+
|
|
88
|
+
def test_completed(self, name, result):
|
|
89
|
+
resultStr = result
|
|
90
|
+
if type(result) is bool:
|
|
91
|
+
resultStr = self.FAIL
|
|
92
|
+
if result:
|
|
93
|
+
resultStr = self.PASS
|
|
94
|
+
self.link.endSignal.emit(name, resultStr)
|
|
95
|
+
|
|
96
|
+
def test_progress(self, name, progress):
|
|
97
|
+
"""
|
|
98
|
+
Update the progress of the test with name.
|
|
99
|
+
:param name: Name of the test to update the progress on
|
|
100
|
+
:param progress: Percentage of progress, 0 to 100
|
|
101
|
+
:return: None
|
|
102
|
+
"""
|
|
103
|
+
self.link.progressSignal.emit(name, progress)
|
|
104
|
+
|
|
105
|
+
def feedback(self, name, data):
|
|
106
|
+
"""
|
|
107
|
+
Update the feedback information (likely shown to user on UI) for the test with name.
|
|
108
|
+
:param name: Name of test to show feedback for
|
|
109
|
+
:param data: string containing feedback
|
|
110
|
+
:return: None
|
|
111
|
+
"""
|
|
112
|
+
self.link.feedbackSignal.emit(name, self._convert_message(data))
|
|
113
|
+
|
|
114
|
+
def user_input(self, title, message, control=None, default_value=None, hidden=False, *values):
|
|
115
|
+
"""
|
|
116
|
+
Place holder for allowing user to return a value
|
|
117
|
+
:return: (input, decision) e.g. (None, "cancel") or ("Hello", "ok")
|
|
118
|
+
"""
|
|
119
|
+
if control is None:
|
|
120
|
+
control = ["Cancel"]
|
|
121
|
+
buttons = control
|
|
122
|
+
# if exception occurs here we get no error and test seems unrecoverable
|
|
123
|
+
self.link.userInputRequestSignal.emit(title, self._convert_message(message), buttons,
|
|
124
|
+
default_value, hidden, values, False)
|
|
125
|
+
self.model.instructions.userDecisionWait()
|
|
126
|
+
self.clear_instructions()
|
|
127
|
+
input_value = self.model.instructions.control.lastUserInput()
|
|
128
|
+
decision = self.model.instructions.control.lastUserDecision()
|
|
129
|
+
return input_value, decision
|
|
130
|
+
|
|
131
|
+
def user_decision(self, title, message, control=None):
|
|
132
|
+
"""
|
|
133
|
+
Request a yes or no user decision. This is a blocking call and will not return until user has made a decision
|
|
134
|
+
or test is cancelled/stopped.
|
|
135
|
+
:param title: Top line of the question posed to the user
|
|
136
|
+
:param message: message asked of the user
|
|
137
|
+
:param control: Alternate button/control values. First two only used.
|
|
138
|
+
:return: The decision made as "yes" or "no" string by default, or control values
|
|
139
|
+
"""
|
|
140
|
+
if control is None:
|
|
141
|
+
control = ["Yes", "No"]
|
|
142
|
+
buttons = control
|
|
143
|
+
self.link.userDecisionSignal.emit(title, self._convert_message(message), buttons, False)
|
|
144
|
+
self.model.instructions.userDecisionWait()
|
|
145
|
+
self.clear_instructions()
|
|
146
|
+
decision = self.model.instructions.control.lastUserDecision()
|
|
147
|
+
if self.model.instructions.control.lastWasUserInput():
|
|
148
|
+
# If last entry was user input then interpret it as a button press
|
|
149
|
+
decision = self.model.instructions.control.lastUserInput()
|
|
150
|
+
return decision
|
|
151
|
+
|
|
152
|
+
def user_instructions(self, title, message, expectResponse=True, control=None, highlight=True):
|
|
153
|
+
"""
|
|
154
|
+
Provides user instructions. By default acknowledgement is requested (paramter expectResponse is true) and the
|
|
155
|
+
user should press "Ok" to continue
|
|
156
|
+
:param title: Top line of the question posed to the user
|
|
157
|
+
:param message: message asked of the user
|
|
158
|
+
:param expectResponse: set to false if not response is required from user.
|
|
159
|
+
i.e. if this is only a test step detail.
|
|
160
|
+
:param control: list of control buttons strings to show, e.g. ["Yes", "No"]
|
|
161
|
+
:param highlight: enable instruction widget (to show colour) even if no control buttons provided
|
|
162
|
+
:return: "ok" if response requested, else None or ""
|
|
163
|
+
"""
|
|
164
|
+
if control is None:
|
|
165
|
+
control = []
|
|
166
|
+
buttons = []
|
|
167
|
+
if expectResponse:
|
|
168
|
+
buttons = ["Ok"]
|
|
169
|
+
if control and type(control) is list and len(control) > 0:
|
|
170
|
+
buttons = control
|
|
171
|
+
self.link.userDecisionSignal.emit(title, self._convert_message(message), buttons, highlight)
|
|
172
|
+
if expectResponse:
|
|
173
|
+
self.model.instructions.userDecisionWait()
|
|
174
|
+
self.clear_instructions()
|
|
175
|
+
return self.model.instructions.control.lastUserDecision()
|
|
176
|
+
|
|
177
|
+
def suite_start(self, name="test run", tests=None):
|
|
178
|
+
"""
|
|
179
|
+
This should be called by test execution module/thread whenever a new suite is started to ensure the listener
|
|
180
|
+
(likely a UI) can update its state to indicate the suite is in progress.
|
|
181
|
+
:param name: The name of the test suite run. Currently not used.
|
|
182
|
+
:param tests: The tests to run
|
|
183
|
+
:return: nothing
|
|
184
|
+
"""
|
|
185
|
+
if tests is None:
|
|
186
|
+
tests = []
|
|
187
|
+
self.link.populateSignal.emit(tests, self.clear_results_on_start)
|
|
188
|
+
self.model.suitestate = TestSuiteModel.STATE_RUNNING
|
|
189
|
+
|
|
190
|
+
def suite_end(self, name="test run", failures=-1, message=""):
|
|
191
|
+
"""
|
|
192
|
+
This should be called by the test execution module/thread whenever a suite has been completed to
|
|
193
|
+
ensure the listener (likely a UI) can update its state to indicate the suite has been completed.
|
|
194
|
+
If the suite is cancelled/aborted use suiteAbort.
|
|
195
|
+
:param name: The name of the test suite run. Currently not used.
|
|
196
|
+
:param failures: The number of test failures
|
|
197
|
+
:param message: A message to display to the user. Currently not used.
|
|
198
|
+
:return: None
|
|
199
|
+
"""
|
|
200
|
+
self.model.suitestate = TestSuiteModel.STATE_END
|
|
201
|
+
|
|
202
|
+
def suite_abort(self, name="test run", message=""):
|
|
203
|
+
"""
|
|
204
|
+
This should be called by the test execution module/thread whenever a suite has been cancelled or aborted to
|
|
205
|
+
ensure the listener (likely a UI) can update its state to indicate this.
|
|
206
|
+
:param name: The name of the test suite run. Currently not used.
|
|
207
|
+
:param message: A message to display to the user. Currently not used.
|
|
208
|
+
:return: None
|
|
209
|
+
"""
|
|
210
|
+
self.model.suitestate = TestSuiteModel.STATE_STOPPED
|
|
211
|
+
|
|
212
|
+
def async_instructions(self, title, message, callback=None, control=None, response=None):
|
|
213
|
+
"""
|
|
214
|
+
A non blocking instruction provided to the user. The callback will be called when the instruction control
|
|
215
|
+
action is taken.
|
|
216
|
+
Usage would be
|
|
217
|
+
listener.asyncInstructions("Do something", "Press the start button", callback=self.myAction, control=["Start", "Stop"])
|
|
218
|
+
and the callback would be
|
|
219
|
+
def myAction(self, response=None):
|
|
220
|
+
if response and response == "start":
|
|
221
|
+
self.start()
|
|
222
|
+
else:
|
|
223
|
+
self.stop()
|
|
224
|
+
:param title: The first line of the instructions provided to user
|
|
225
|
+
:param message: The message request made of the user
|
|
226
|
+
:param callback: The function to call of type callback("response") where "response" is the control text if a
|
|
227
|
+
string is not specified in the response array.
|
|
228
|
+
:param control: An array of strings to display in the control buttons.
|
|
229
|
+
:param response: a set of response strings to use. Should correspond to the control entries. Currently not used.
|
|
230
|
+
:return: None
|
|
231
|
+
"""
|
|
232
|
+
if control is None:
|
|
233
|
+
control = []
|
|
234
|
+
if response is None:
|
|
235
|
+
response = []
|
|
236
|
+
self.link.setAsyncInstructionCallback(callback)
|
|
237
|
+
self.link.userDecisionSignal.emit(title, self._convert_message(message), control, True)
|
|
238
|
+
|
|
239
|
+
def clear_instructions(self):
|
|
240
|
+
"""
|
|
241
|
+
Infor the listener to clear the instruction window.
|
|
242
|
+
:return: None
|
|
243
|
+
"""
|
|
244
|
+
self.link.userDecisionSignal.emit(None, None, None, False)
|
|
245
|
+
|
|
246
|
+
def _convert_message(self, message):
|
|
247
|
+
if type(message) is list:
|
|
248
|
+
message = "\n".join(message)
|
|
249
|
+
return message
|
|
File without changes
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
# Copyright 2021 Direkt Embedded Pty Ltd
|
|
2
|
+
#
|
|
3
|
+
# Licensed under the Apache License, Version 2.0 (the "License");
|
|
4
|
+
# you may not use this file except in compliance with the License.
|
|
5
|
+
# You may obtain a copy of the License at
|
|
6
|
+
#
|
|
7
|
+
# http://www.apache.org/licenses/LICENSE-2.0
|
|
8
|
+
#
|
|
9
|
+
# Unless required by applicable law or agreed to in writing, software
|
|
10
|
+
# distributed under the License is distributed on an "AS IS" BASIS,
|
|
11
|
+
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
12
|
+
# See the License for the specific language governing permissions and
|
|
13
|
+
# limitations under the License.
|
|
14
|
+
|
|
15
|
+
"""
|
|
16
|
+
A QAbstractListModel with a single user role for a variant object, which should be given properties.
|
|
17
|
+
These properties can be accessed within qml models through model.item.<property_name>
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
import threading
|
|
21
|
+
from PySide6.QtCore import QAbstractListModel
|
|
22
|
+
from PySide6.QtCore import Qt
|
|
23
|
+
from PySide6.QtCore import QModelIndex
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class GenericListModel(QAbstractListModel):
|
|
27
|
+
|
|
28
|
+
SelectionRole = Qt.UserRole
|
|
29
|
+
NameKey = b"model"
|
|
30
|
+
|
|
31
|
+
def __init__(self, parent=None):
|
|
32
|
+
QAbstractListModel.__init__(self, parent)
|
|
33
|
+
self.lock = threading.RLock()
|
|
34
|
+
self._currentTestIndex = -1
|
|
35
|
+
self._data = []
|
|
36
|
+
|
|
37
|
+
def rowCount(self, parent=QModelIndex()):
|
|
38
|
+
return len(self._data)
|
|
39
|
+
|
|
40
|
+
def roleNames(self):
|
|
41
|
+
return {self.SelectionRole: self.NameKey}
|
|
42
|
+
|
|
43
|
+
def data(self, index, role=None):
|
|
44
|
+
d = self._data[index.row()]
|
|
45
|
+
if role == self.SelectionRole:
|
|
46
|
+
return d
|
|
47
|
+
return d
|
|
48
|
+
|
|
49
|
+
def setData(self, index, value, role=None):
|
|
50
|
+
self._data[index.row()] = value
|
|
51
|
+
self.dataChanged.emit(index, index, self.roleNames())
|
|
52
|
+
|
|
53
|
+
def add(self, item):
|
|
54
|
+
rowCount = self.rowCount()
|
|
55
|
+
self.beginInsertRows(QModelIndex(), rowCount, rowCount)
|
|
56
|
+
self._data.append(item)
|
|
57
|
+
self.endInsertRows()
|
|
58
|
+
return item
|
|
59
|
+
|
|
60
|
+
def clear(self):
|
|
61
|
+
self.beginRemoveRows(QModelIndex(), 0, self.rowCount(QModelIndex()))
|
|
62
|
+
self._data.clear()
|
|
63
|
+
self.endRemoveRows()
|