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.
Files changed (54) hide show
  1. testexecutor/VERSION +1 -0
  2. testexecutor/__init__.py +6 -0
  3. testexecutor/control/TestListenerApi.py +56 -0
  4. testexecutor/control/TestSuiteListener.py +249 -0
  5. testexecutor/control/__init__.py +0 -0
  6. testexecutor/model/GenericListModel.py +63 -0
  7. testexecutor/model/InstructionsModel.py +351 -0
  8. testexecutor/model/KeyValueModel.py +177 -0
  9. testexecutor/model/MultiTestWindowModel.py +184 -0
  10. testexecutor/model/ResultModel.py +314 -0
  11. testexecutor/model/TestSuiteControlModel.py +139 -0
  12. testexecutor/model/TestSuiteGroup.py +105 -0
  13. testexecutor/model/TestSuiteModel.py +301 -0
  14. testexecutor/model/__init__.py +0 -0
  15. testexecutor/model/selector/FilterGroupModel.py +90 -0
  16. testexecutor/model/selector/FilterModel.py +251 -0
  17. testexecutor/model/selector/MultiSelectDetails.py +106 -0
  18. testexecutor/model/selector/MultiSelectListModel.py +48 -0
  19. testexecutor/model/selector/__init__.py +0 -0
  20. testexecutor/ui/IdentificationWidget.qml +40 -0
  21. testexecutor/ui/InputWidget.qml +74 -0
  22. testexecutor/ui/InstructionWidget.qml +185 -0
  23. testexecutor/ui/KeyValueItem.qml +194 -0
  24. testexecutor/ui/KeyValueItemConfig.qml +41 -0
  25. testexecutor/ui/KeyValueList.qml +49 -0
  26. testexecutor/ui/MultiTestWidget.qml +48 -0
  27. testexecutor/ui/MultiTestWindow.qml +88 -0
  28. testexecutor/ui/ResultItem.qml +199 -0
  29. testexecutor/ui/ResultItemConfig.qml +46 -0
  30. testexecutor/ui/ResultList.qml +58 -0
  31. testexecutor/ui/TestSuiteConfig.qml +95 -0
  32. testexecutor/ui/TestSuiteController.qml +54 -0
  33. testexecutor/ui/TestSuiteWidget.qml +350 -0
  34. testexecutor/ui/selector/FilterItem.qml +75 -0
  35. testexecutor/ui/selector/FilterItemConfig.qml +24 -0
  36. testexecutor/ui/selector/FilterMultiSelectorItem.qml +169 -0
  37. testexecutor/ui/selector/FilterMultiSelectorItemConfig.qml +22 -0
  38. testexecutor/ui/selector/FilterMultiSelectorList.qml +50 -0
  39. testexecutor/ui/selector/HeaderConfig.qml +24 -0
  40. testexecutor/ui/selector/SectionConfig.qml +24 -0
  41. testexecutor/ui/selector/SectionDelegate.qml +85 -0
  42. testexecutor/ui/selector/SelectorHeader.qml +43 -0
  43. testexecutor/ui/selector/SuiteList.qml +150 -0
  44. testexecutor/ui/selector/SuiteSelector.qml +71 -0
  45. testexecutor/ui/selector/SuiteSelectorConfig.qml +38 -0
  46. testexecutor/ui/selector/TestConfig.qml +22 -0
  47. testexecutor/ui/selector/TestDelegate.qml +75 -0
  48. testexecutor/ui/te-64x64.ico +0 -0
  49. testexecutor_ui-0.6.1.dist-info/LICENSE +204 -0
  50. testexecutor_ui-0.6.1.dist-info/METADATA +27 -0
  51. testexecutor_ui-0.6.1.dist-info/NOTICE +3 -0
  52. testexecutor_ui-0.6.1.dist-info/RECORD +54 -0
  53. testexecutor_ui-0.6.1.dist-info/WHEEL +5 -0
  54. testexecutor_ui-0.6.1.dist-info/top_level.txt +1 -0
testexecutor/VERSION ADDED
@@ -0,0 +1 @@
1
+ 0.6.1
@@ -0,0 +1,6 @@
1
+ import os
2
+ te_current_path = os.path.dirname(os.path.abspath(__file__))
3
+ with open(os.path.join(te_current_path, 'VERSION')) as version_file:
4
+ VERSION = version_file.read().strip()
5
+
6
+ __version__ = VERSION
@@ -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()