1 | import sys |
---|
2 | import os |
---|
3 | import unittest |
---|
4 | import webbrowser |
---|
5 | import tempfile |
---|
6 | from unittest.mock import MagicMock, patch |
---|
7 | |
---|
8 | from PyQt5 import QtGui |
---|
9 | from PyQt5 import QtWidgets |
---|
10 | |
---|
11 | # set up import paths |
---|
12 | import path_prepare |
---|
13 | |
---|
14 | from sas.qtgui.Utilities.GuiUtils import Communicate |
---|
15 | |
---|
16 | # Local |
---|
17 | from sas.qtgui.Utilities.AddMultEditor import AddMultEditor |
---|
18 | |
---|
19 | if not QtWidgets.QApplication.instance(): |
---|
20 | app = QtWidgets.QApplication(sys.argv) |
---|
21 | |
---|
22 | class dummy_manager(object): |
---|
23 | HELP_DIRECTORY_LOCATION = "html" |
---|
24 | communicate = Communicate() |
---|
25 | _parent = QtWidgets.QDialog() |
---|
26 | |
---|
27 | class AddMultEditorTest(unittest.TestCase): |
---|
28 | """ Test the simple AddMultEditor dialog """ |
---|
29 | @patch.object(AddMultEditor, 'readModels') |
---|
30 | def setUp(self, mock_list_models): |
---|
31 | """ Create AddMultEditor dialog """ |
---|
32 | |
---|
33 | # mock models from plugin folder |
---|
34 | mock_list_models.return_value = ['cylinder', 'rpa', |
---|
35 | 'core_shell_cylinder', 'sphere'] |
---|
36 | |
---|
37 | self.widget = AddMultEditor(dummy_manager()) |
---|
38 | |
---|
39 | def tearDown(self): |
---|
40 | """ Destroy the GUI """ |
---|
41 | |
---|
42 | self.widget.close() |
---|
43 | self.widget = None |
---|
44 | |
---|
45 | def testDefaults(self): |
---|
46 | """ Test the GUI in its default state """ |
---|
47 | |
---|
48 | self.assertIsInstance(self.widget, QtWidgets.QDialog) |
---|
49 | |
---|
50 | self.assertEqual(self.widget.sizePolicy().verticalPolicy(), 0) |
---|
51 | self.assertEqual(self.widget.sizePolicy().horizontalPolicy(), 5) |
---|
52 | |
---|
53 | # Default title |
---|
54 | self.assertEqual(self.widget.windowTitle(), "Easy Add/Multiply Editor") |
---|
55 | |
---|
56 | # Default types |
---|
57 | self.assertIsInstance(self.widget.cbOperator, QtWidgets.QComboBox) |
---|
58 | self.assertIsInstance(self.widget.cbModel1, QtWidgets.QComboBox) |
---|
59 | self.assertIsInstance(self.widget.cbModel2, QtWidgets.QComboBox) |
---|
60 | |
---|
61 | # Modal window |
---|
62 | self.assertFalse(self.widget.isModal()) |
---|
63 | |
---|
64 | # Checkbox not tristate, not checked |
---|
65 | self.assertFalse(self.widget.chkOverwrite.isTristate()) |
---|
66 | self.assertFalse(self.widget.chkOverwrite.isChecked()) |
---|
67 | |
---|
68 | # Push buttons disabled |
---|
69 | self.assertFalse(self.widget.buttonBox.button(QtWidgets.QDialogButtonBox.Apply).isEnabled()) |
---|
70 | |
---|
71 | # Text of labels... |
---|
72 | self.assertEqual(self.widget.chkOverwrite.text(), |
---|
73 | "Overwrite existing model") |
---|
74 | |
---|
75 | # Allowed operators |
---|
76 | self.assertListEqual([str(self.widget.cbOperator.itemText(i)) for i in |
---|
77 | range(self.widget.cbOperator.count())], |
---|
78 | ['+', '*']) |
---|
79 | |
---|
80 | # Default operator |
---|
81 | self.assertEqual(self.widget.cbOperator.currentText(), '+') |
---|
82 | |
---|
83 | # checkbox unchecked |
---|
84 | self.assertFalse(self.widget.chkOverwrite.isChecked()) |
---|
85 | |
---|
86 | # Default 'good_name' flag, name for plugin file and plugin dir |
---|
87 | self.assertFalse(self.widget.good_name) |
---|
88 | self.assertIsNone(self.widget.plugin_filename) |
---|
89 | |
---|
90 | # default content of displayed equation (to create the new model) |
---|
91 | self.assertEqual(self.widget.lblEquation.text(), |
---|
92 | "<html><head/><body><p>Plugin_model = " |
---|
93 | "scale_factor * (model_1 + model_2) + background" |
---|
94 | "</p></body></html>") |
---|
95 | |
---|
96 | # Tooltips |
---|
97 | self.assertEqual(self.widget.cbOperator.toolTip(), |
---|
98 | "Add: +\nMultiply: *") |
---|
99 | self.assertEqual(self.widget.txtName.toolTip(), |
---|
100 | "Sum / Multiply model function name.") |
---|
101 | self.widget.chkOverwrite.setChecked(True) |
---|
102 | |
---|
103 | self.assertNotEqual(len(self.widget.chkOverwrite.toolTip()), 0) |
---|
104 | |
---|
105 | def testOnModelComboboxes(self): |
---|
106 | """ Test on Model1 and Model2 comboboxes """ |
---|
107 | |
---|
108 | # content of model_1 and model_2 comboboxes |
---|
109 | # same content for the two comboboxes |
---|
110 | self.assertEqual(self.widget.cbModel1.count(), |
---|
111 | self.widget.cbModel2.count()) |
---|
112 | |
---|
113 | self.assertEqual(self.widget.cbModel1.count(), 4) |
---|
114 | |
---|
115 | # default of cbModel1 and cbModel2 |
---|
116 | self.assertEqual(self.widget.cbModel1.currentText(), 'sphere') |
---|
117 | self.assertEqual(self.widget.cbModel2.currentText(), 'cylinder') |
---|
118 | |
---|
119 | def testValidateName(self): |
---|
120 | """ Test validity of output name (syntax only) """ |
---|
121 | |
---|
122 | # Invalid plugin name |
---|
123 | self.widget.txtName.setText('+++') |
---|
124 | |
---|
125 | state = self.widget.txtName.validator().validate(self.widget.txtName.text(), 0)[0] |
---|
126 | self.assertEqual(state, QtGui.QValidator.Invalid) |
---|
127 | |
---|
128 | # Valid new plugin name |
---|
129 | self.widget.txtName.setText('cylinder_test_case') |
---|
130 | state = \ |
---|
131 | self.widget.txtName.validator().validate(self.widget.txtName.text(), |
---|
132 | 0)[0] |
---|
133 | self.assertEqual(state, QtGui.QValidator.Acceptable) |
---|
134 | |
---|
135 | def testOnApply(self): |
---|
136 | """ Test onApply """ |
---|
137 | |
---|
138 | self.widget.txtName.setText("new_model") |
---|
139 | self.widget.updateModels = MagicMock() |
---|
140 | |
---|
141 | # make sure the flag is set correctly |
---|
142 | self.widget.is_modified = True |
---|
143 | |
---|
144 | # Mock self.write_new_model_to_file |
---|
145 | self.widget.plugin_dir = tempfile.gettempdir() |
---|
146 | self.widget.plugin_filename = \ |
---|
147 | os.path.join(self.widget.plugin_dir, 'new_model.py') |
---|
148 | |
---|
149 | self.widget.write_new_mode_to_file = MagicMock() |
---|
150 | |
---|
151 | # invoke the tested method |
---|
152 | self.widget.onApply() |
---|
153 | |
---|
154 | self.assertTrue(self.widget.write_new_mode_to_file.called_once_with( |
---|
155 | os.path.join(self.widget.plugin_dir, 'new_model.py'), |
---|
156 | self.widget.cbModel1.currentText(), |
---|
157 | self.widget.cbModel2.currentText(), |
---|
158 | self.widget.cbOperator.currentText())) |
---|
159 | |
---|
160 | self.assertTrue(self.widget.updateModels.called_once()) |
---|
161 | |
---|
162 | def testWriteNewModelToFile(self): |
---|
163 | """ Test content of generated plugin""" |
---|
164 | |
---|
165 | dummy_file_path = os.path.join(tempfile.gettempdir(), |
---|
166 | "test_datafile.py") |
---|
167 | |
---|
168 | # prepare data to create file and check its content: names of 2 models, |
---|
169 | # operator and description (default or not) |
---|
170 | model1_name = self.widget.cbModel1.currentText() |
---|
171 | model2_name = self.widget.cbModel2.currentText() |
---|
172 | symbol_operator = self.widget.cbOperator.currentText() |
---|
173 | |
---|
174 | # default description |
---|
175 | desc_line = "{} {} {}".format(model1_name, |
---|
176 | symbol_operator, |
---|
177 | model2_name) |
---|
178 | |
---|
179 | self.widget.write_new_model_to_file(dummy_file_path, model1_name, |
---|
180 | model2_name, symbol_operator) |
---|
181 | # check content of dummy file path |
---|
182 | with open(dummy_file_path, 'r') as f_in: |
---|
183 | lines_from_generated_file = [line.strip() for line in f_in] |
---|
184 | |
---|
185 | # SUM_TEMPLATE with updated entries |
---|
186 | completed_template = ["from sasmodels.core import load_model_info", |
---|
187 | "from sasmodels.sasview_model import make_model_from_info", |
---|
188 | "model_info = load_model_info('{model1}{operator}{model2}')". |
---|
189 | format(model1=model1_name, |
---|
190 | operator=symbol_operator, |
---|
191 | model2=model2_name), |
---|
192 | "model_info.name = '{}'".format("test_datafile"), |
---|
193 | "model_info.description = '{}'".format(desc_line), |
---|
194 | "Model = make_model_from_info(model_info)"] |
---|
195 | |
---|
196 | for item in completed_template: |
---|
197 | self.assertIn(item, lines_from_generated_file) |
---|
198 | |
---|
199 | # check content with description entered by user |
---|
200 | self.widget.txtDescription.setText('New description for test ') |
---|
201 | |
---|
202 | new_desc_line = "model_info.description = '{}'".format('New description for test') |
---|
203 | |
---|
204 | # re-run function to test |
---|
205 | self.widget.write_new_model_to_file(dummy_file_path, model1_name, |
---|
206 | model2_name, symbol_operator) |
---|
207 | |
---|
208 | # check content of dummy file path |
---|
209 | with open(dummy_file_path, 'r') as f_in: |
---|
210 | lines_from_generated_file = [line.strip() for line in f_in] |
---|
211 | |
---|
212 | # update completed template |
---|
213 | completed_template[4] = new_desc_line |
---|
214 | |
---|
215 | # check content of generated file |
---|
216 | for item in completed_template: |
---|
217 | self.assertIn(item, lines_from_generated_file) |
---|
218 | |
---|
219 | def testOnOperatorChange(self): |
---|
220 | """ |
---|
221 | Test modification of displayed equation |
---|
222 | when selected operator is changed |
---|
223 | """ |
---|
224 | |
---|
225 | # check default |
---|
226 | self.assertIn(self.widget.cbOperator.currentText(), |
---|
227 | self.widget.lblEquation.text()) |
---|
228 | |
---|
229 | # change operator |
---|
230 | if self.widget.cbOperator.currentIndex() == 0: |
---|
231 | self.widget.cbOperator.setCurrentIndex(1) |
---|
232 | else: |
---|
233 | self.widget.cbOperator.setCurrentIndex(0) |
---|
234 | |
---|
235 | self.assertIn(self.widget.cbOperator.currentText(), |
---|
236 | self.widget.lblEquation.text()) |
---|
237 | |
---|
238 | def testOnHelp(self): |
---|
239 | """ Test the default help renderer """ |
---|
240 | |
---|
241 | webbrowser.open = MagicMock() |
---|
242 | |
---|
243 | # invoke the tested method |
---|
244 | self.widget.onHelp() |
---|
245 | |
---|
246 | # see that webbrowser open was attempted |
---|
247 | webbrowser.open.assert_called() |
---|
248 | |
---|
249 | def testOnNameCheck(self): |
---|
250 | """ Test onNameCheck """ |
---|
251 | |
---|
252 | # Enter plugin name already present in list of existing models |
---|
253 | self.widget.txtName.setText("cylinder") |
---|
254 | |
---|
255 | # Scenario 1 |
---|
256 | # overwrite not checked -> message box should appear |
---|
257 | # and good_name set to False, 'Apply' button disabled |
---|
258 | |
---|
259 | # mock QMessageBox |
---|
260 | QtWidgets.QMessageBox.critical = MagicMock() |
---|
261 | |
---|
262 | self.widget.chkOverwrite.setChecked(False) |
---|
263 | self.widget.txtName.editingFinished.emit() |
---|
264 | self.assertFalse(self.widget.good_name) |
---|
265 | self.assertFalse(self.widget.buttonBox.button( |
---|
266 | QtWidgets.QDialogButtonBox.Apply).isEnabled()) |
---|
267 | |
---|
268 | self.assertTrue(QtWidgets.QMessageBox.critical.called_once()) |
---|
269 | |
---|
270 | msg = "Plugin with specified name already exists.\n" |
---|
271 | msg += "Please specify different filename or allow file overwrite." |
---|
272 | |
---|
273 | self.assertIn('Plugin Error', QtWidgets.QMessageBox.critical.call_args[0][1]) |
---|
274 | self.assertIn(msg, QtWidgets.QMessageBox.critical.call_args[0][2]) |
---|
275 | |
---|
276 | # Scenario 2 |
---|
277 | # overwrite checked -> no message box displayed |
---|
278 | # and good_name set to True, Apply button enabled, output name created |
---|
279 | |
---|
280 | # mock QMessageBox |
---|
281 | QtWidgets.QMessageBox.critical = MagicMock() |
---|
282 | # create dummy plugin_dir for output file |
---|
283 | self.widget.plugin_dir = tempfile.gettempdir() |
---|
284 | |
---|
285 | self.widget.chkOverwrite.setChecked(True) |
---|
286 | self.widget.txtName.editingFinished.emit() |
---|
287 | self.assertTrue(self.widget.good_name) |
---|
288 | self.assertTrue(self.widget.buttonBox.button( |
---|
289 | QtWidgets.QDialogButtonBox.Apply).isEnabled()) |
---|
290 | |
---|
291 | self.assertFalse(QtWidgets.QMessageBox.critical.called) |
---|
292 | |
---|
293 | self.assertIn('cylinder.py', self.widget.plugin_filename) |
---|
294 | |
---|
295 | # Scenario 3 Enter valid new plugin name -> filename created and Apply |
---|
296 | # button enabled |
---|
297 | # forbidding overwriting should not change anything |
---|
298 | # since it's a new name |
---|
299 | self.widget.txtName.setText(" cylinder0 ") |
---|
300 | self.widget.chkOverwrite.setChecked(False) |
---|
301 | self.widget.txtName.editingFinished.emit() |
---|
302 | self.assertIn("cylinder0.py", self.widget.plugin_filename) |
---|
303 | self.assertTrue(self.widget.buttonBox.button( |
---|
304 | QtWidgets.QDialogButtonBox.Apply).isEnabled()) |
---|
305 | |
---|
306 | @patch.object(AddMultEditor, 'readModels') |
---|
307 | def testOnUpdateModels(self, mock_new_list_models): |
---|
308 | """ Test onUpdateModels """ |
---|
309 | |
---|
310 | ini_count_models = self.widget.cbModel1.count() |
---|
311 | |
---|
312 | mock_new_list_models.return_value = ['cylinder', 'rpa', |
---|
313 | 'core_shell_cylinder', 'sphere', |
---|
314 | 'cylinder0'] |
---|
315 | |
---|
316 | self.widget.updateModels() |
---|
317 | # check that the number of models in the comboboxes |
---|
318 | # has been incremented by 1 |
---|
319 | self.assertEqual(self.widget.cbModel1.count(), ini_count_models + 1) |
---|
320 | self.assertEqual(self.widget.cbModel2.count(), ini_count_models + 1) |
---|
321 | |
---|
322 | # check that the new model is in the list |
---|
323 | combobox = self.widget.cbModel1 |
---|
324 | self.assertIn('cylinder0', [combobox.itemText(indx) |
---|
325 | for indx in range(combobox.count())]) |
---|
326 | |
---|
327 | def testOnReadModels(self): |
---|
328 | """ The output of ReadModels is the return value of MagicMock defined |
---|
329 | in the SetUp of these tests. """ |
---|
330 | |
---|
331 | self.assertEqual(self.widget.list_models, ['cylinder', 'rpa', |
---|
332 | 'core_shell_cylinder', |
---|
333 | 'sphere']) |
---|
334 | |
---|
335 | def testCheckModel(self): |
---|
336 | """ Test CheckModel""" |
---|
337 | |
---|
338 | # TODO first: solve problem with empty 'test' |
---|
339 | pass |
---|