source: sasview/src/sas/qtgui/Perspectives/Fitting/UnitTesting/FittingWidgetTest.py @ dc5ef15

ESS_GUIESS_GUI_DocsESS_GUI_batch_fittingESS_GUI_bumps_abstractionESS_GUI_iss1116ESS_GUI_iss879ESS_GUI_iss959ESS_GUI_openclESS_GUI_orderingESS_GUI_sync_sascalc
Last change on this file since dc5ef15 was dc5ef15, checked in by Piotr Rozyczko <rozyczko@…>, 7 years ago

Removed qtgui dependency on sasgui and wx SASVIEW-590

  • Property mode set to 100644
File size: 23.3 KB
Line 
1import sys
2import unittest
3import time
4
5from PyQt4 import QtGui
6from PyQt4 import QtTest
7from PyQt4 import QtCore
8from mock import MagicMock
9from twisted.internet import threads
10
11# set up import paths
12import sas.qtgui.path_prepare
13
14# Local
15from sas.qtgui.Utilities.GuiUtils import *
16from sas.qtgui.Perspectives.Fitting.FittingWidget import *
17from sas.qtgui.UnitTesting.TestUtils import QtSignalSpy
18
19from sas.qtgui.Plotting.PlotterData import Data1D
20from sas.qtgui.Plotting.PlotterData import Data2D
21
22app = QtGui.QApplication(sys.argv)
23
24class dummy_manager(object):
25    communicate = Communicate()
26
27class FittingWidgetTest(unittest.TestCase):
28    """Test the fitting widget GUI"""
29
30    def setUp(self):
31        """Create the GUI"""
32        self.widget = FittingWidget(dummy_manager())
33
34    def tearDown(self):
35        """Destroy the GUI"""
36        self.widget.close()
37        self.widget = None
38
39    def testDefaults(self):
40        """Test the GUI in its default state"""
41        self.assertIsInstance(self.widget, QtGui.QWidget)
42        self.assertEqual(self.widget.windowTitle(), "Fitting")
43        self.assertEqual(self.widget.sizePolicy().Policy(), QtGui.QSizePolicy.Fixed)
44        self.assertIsInstance(self.widget.lstParams.model(), QtGui.QStandardItemModel)
45        self.assertIsInstance(self.widget.lstPoly.model(), QtGui.QStandardItemModel)
46        self.assertIsInstance(self.widget.lstMagnetic.model(), QtGui.QStandardItemModel)
47        self.assertFalse(self.widget.cbModel.isEnabled())
48        self.assertFalse(self.widget.cbStructureFactor.isEnabled())
49        self.assertFalse(self.widget.cmdFit.isEnabled())
50        self.assertTrue(self.widget.acceptsData())
51        self.assertFalse(self.widget.data_is_loaded)
52
53    def testSelectCategoryDefault(self):
54        """
55        Test if model categories have been loaded properly
56        """
57        fittingWindow =  self.widget
58
59        #Test loading from json categories
60        category_list = fittingWindow.master_category_dict.keys()
61
62        for category in category_list:
63            self.assertNotEqual(fittingWindow.cbCategory.findText(category),-1)
64
65        #Test what is current text in the combobox
66        self.assertEqual(fittingWindow.cbCategory.currentText(), CATEGORY_DEFAULT)
67
68    def testWidgetWithData(self):
69        """
70        Test the instantiation of the widget with initial data
71        """
72        data = Data1D(x=[1,2], y=[1,2])
73        GuiUtils.dataFromItem = MagicMock(return_value=data)
74        item = QtGui.QStandardItem("test")
75
76        widget_with_data = FittingWidget(dummy_manager(), data=item, tab_id=3)
77
78        self.assertEqual(widget_with_data.data, data)
79        self.assertTrue(widget_with_data.data_is_loaded)
80        # self.assertTrue(widget_with_data.cmdFit.isEnabled())
81        self.assertFalse(widget_with_data.acceptsData())
82
83    def testSelectPolydispersity(self):
84        """
85        Test if models have been loaded properly
86        :return:
87        """
88        fittingWindow =  self.widget
89
90        #Test loading from json categories
91        fittingWindow.SASModelToQModel("cylinder")
92        pd_index = fittingWindow.lstPoly.model().index(0,0)
93        self.assertEqual(str(pd_index.data().toString()), "Distribution of radius")
94        pd_index = fittingWindow.lstPoly.model().index(1,0)
95        self.assertEqual(str(pd_index.data().toString()), "Distribution of length")
96
97    def testSelectStructureFactor(self):
98        """
99        Test if structure factors have been loaded properly
100        :return:
101        """
102        fittingWindow =  self.widget
103
104        #Test for existence in combobox
105        self.assertNotEqual(fittingWindow.cbStructureFactor.findText("stickyhardsphere"),-1)
106        self.assertNotEqual(fittingWindow.cbStructureFactor.findText("hayter_msa"),-1)
107        self.assertNotEqual(fittingWindow.cbStructureFactor.findText("squarewell"),-1)
108        self.assertNotEqual(fittingWindow.cbStructureFactor.findText("hardsphere"),-1)
109
110        #Test what is current text in the combobox
111        self.assertTrue(fittingWindow.cbCategory.currentText(), "None")
112
113    def testSignals(self):
114        """
115        Test the widget emitted signals
116        """
117        pass
118
119    def testSelectCategory(self):
120        """
121        Assure proper behaviour on changing category
122        """
123        self.widget.show()
124        self.assertEqual(self.widget._previous_category_index, 0)
125        # confirm the model combo contains no models
126        self.assertEqual(self.widget.cbModel.count(), 0)
127
128        # invoke the method by changing the index
129        category_index = self.widget.cbCategory.findText("Shape Independent")
130        self.widget.cbCategory.setCurrentIndex(category_index)
131
132        # test the model combo content
133        self.assertEqual(self.widget.cbModel.count(), 29)
134
135        # Try to change back to default
136        self.widget.cbCategory.setCurrentIndex(0)
137
138        # Observe no such luck
139        self.assertEqual(self.widget.cbCategory.currentIndex(), 6)
140        self.assertEqual(self.widget.cbModel.count(), 29)
141
142        # Set the structure factor
143        structure_index=self.widget.cbCategory.findText(CATEGORY_STRUCTURE)
144        self.widget.cbCategory.setCurrentIndex(structure_index)
145        # check the enablement of controls
146        self.assertFalse(self.widget.cbModel.isEnabled())
147        self.assertTrue(self.widget.cbStructureFactor.isEnabled())
148
149    def testSelectModel(self):
150        """
151        Assure proper behaviour on changing model
152        """
153        self.widget.show()
154        # Change the category index so we have some models
155        category_index = self.widget.cbCategory.findText("Shape Independent")
156        self.widget.cbCategory.setCurrentIndex(category_index)
157
158        # check the enablement of controls
159        self.assertTrue(self.widget.cbModel.isEnabled())
160        self.assertFalse(self.widget.cbStructureFactor.isEnabled())
161
162        # set up the model update spy
163        # spy = QtSignalSpy(self.widget._model_model, self.widget._model_model.itemChanged)
164
165        # mock the tested methods
166        self.widget.SASModelToQModel = MagicMock()
167        self.widget.createDefaultDataset = MagicMock()
168        self.widget.calculateQGridForModel = MagicMock()
169        #
170        # Now change the model
171        self.widget.cbModel.setCurrentIndex(3)
172        self.assertEqual(self.widget.cbModel.currentText(),'dab')
173
174        # No data sent -> no index set, only createDefaultDataset called
175        self.assertTrue(self.widget.createDefaultDataset.called)
176        self.assertTrue(self.widget.SASModelToQModel.called)
177        self.assertFalse(self.widget.calculateQGridForModel.called)
178
179        # Let's tell the widget that data has been loaded
180        self.widget.data_is_loaded = True
181        # Reset the sasmodel index
182        self.widget.cbModel.setCurrentIndex(1)
183        self.assertEqual(self.widget.cbModel.currentText(),'broad_peak')
184
185        # Observe calculateQGridForModel called
186        self.assertTrue(self.widget.calculateQGridForModel.called)
187
188    def testSelectFactor(self):
189        """
190        Assure proper behaviour on changing structure factor
191        """
192        self.widget.show()
193        # Change the category index so we have some models
194        category_index = self.widget.cbCategory.findText("Shape Independent")
195        self.widget.cbCategory.setCurrentIndex(category_index)
196        # Change the model to one that supports structure factors
197        model_index = self.widget.cbModel.findText('fractal_core_shell')
198        self.widget.cbModel.setCurrentIndex(model_index)
199
200        # Check that the factor combo is active and the default is chosen
201        self.assertTrue(self.widget.cbStructureFactor.isEnabled())
202        self.assertEqual(self.widget.cbStructureFactor.currentText(), STRUCTURE_DEFAULT)
203
204        # We have this many rows in the model
205        rowcount = self.widget._model_model.rowCount()
206        #self.assertEqual(self.widget._model_model.rowCount(), 8)
207
208        # Change structure factor to something more exciting
209        structure_index = self.widget.cbStructureFactor.findText('squarewell')
210        self.widget.cbStructureFactor.setCurrentIndex(structure_index)
211
212        # We have 4 more rows now
213        self.assertEqual(self.widget._model_model.rowCount(), rowcount+4)
214
215        # Switch models
216        self.widget.cbModel.setCurrentIndex(0)
217
218        # Observe factor reset to None
219        self.assertEqual(self.widget.cbStructureFactor.currentText(), STRUCTURE_DEFAULT)
220
221
222        # TODO once functionality fixed
223        ## Switch category to structure factor
224        #structure_index=self.widget.cbCategory.findText(CATEGORY_STRUCTURE)
225        #self.widget.cbCategory.setCurrentIndex(structure_index)
226        ## Choose the last factor
227        #last_index = self.widget.cbStructureFactor.count()
228        #self.widget.cbStructureFactor.setCurrentIndex(last_index-1)
229
230    def testReadCategoryInfo(self):
231        """
232        Check the category file reader
233        """
234        # Tested in default checks
235        pass
236
237    def testUpdateParamsFromModel(self):
238        """
239        Checks the sasmodel parameter update from QModel items
240        """
241        # Tested in default checks
242        pass
243
244    def testCreateTheoryIndex(self):
245        """
246        Test the data->QIndex conversion
247        """
248        # set up the model update spy
249        spy = QtSignalSpy(self.widget._model_model, self.widget.communicate.updateTheoryFromPerspectiveSignal)
250
251        self.widget.show()
252        # Change the category index so we have some models
253        self.widget.cbCategory.setCurrentIndex(1)
254
255        # Create the index
256        self.widget.createTheoryIndex(Data1D(x=[1,2], y=[1,2]))
257
258        # Make sure the signal has been emitted
259        self.assertEqual(spy.count(), 1)
260
261        # Check the argument type
262        self.assertIsInstance(spy.called()[0]['args'][0], QtGui.QStandardItem)
263
264    def testCalculateQGridForModel(self):
265        """
266        Check that the fitting 1D data object is ready
267        """
268        # Mock the thread creation
269        threads.deferToThread = MagicMock()
270        # Model for theory
271        self.widget.SASModelToQModel("cylinder")
272        # Call the tested method
273        self.widget.calculateQGridForModel()
274        time.sleep(1)
275        # Test the mock
276        self.assertTrue(threads.deferToThread.called)
277        self.assertEqual(threads.deferToThread.call_args_list[0][0][0].__name__, "compute")
278
279    def testCalculateResiduals(self):
280        """
281        Check that the residuals are calculated and plots updated
282        """
283        test_data = Data1D(x=[1,2], y=[1,2])
284
285        # Model for theory
286        self.widget.SASModelToQModel("cylinder")
287        # Invoke the tested method
288        self.widget.calculateResiduals(test_data)
289        # Check the Chi2 value - should be undetermined
290        self.assertEqual(self.widget.lblChi2Value.text(), '---')
291
292        # Force same data into logic
293        self.widget.logic.data = test_data
294        self.widget.calculateResiduals(test_data)
295        # Now, the difference is 0, as data is the same
296        self.assertEqual(self.widget.lblChi2Value.text(), '0')
297
298        # Change data
299        test_data_2 = Data1D(x=[1,2], y=[2.1,3.49])
300        self.widget.logic.data = test_data_2
301        self.widget.calculateResiduals(test_data)
302        # Now, the difference is non-zero
303        self.assertEqual(float(self.widget.lblChi2Value.text()), 1.7151)
304
305    def testSetPolyModel(self):
306        """
307        Test the polydispersity model setup
308        """
309        self.widget.show()
310        # Change the category index so we have a model with no poly
311        category_index = self.widget.cbCategory.findText("Shape Independent")
312        self.widget.cbCategory.setCurrentIndex(category_index)
313        # Check the poly model
314        self.assertEqual(self.widget._poly_model.rowCount(), 0)
315        self.assertEqual(self.widget._poly_model.columnCount(), 0)
316
317        # Change the category index so we have a model available
318        self.widget.cbCategory.setCurrentIndex(2)
319
320        # Check the poly model
321        self.assertEqual(self.widget._poly_model.rowCount(), 4)
322        self.assertEqual(self.widget._poly_model.columnCount(), 7)
323
324        # Test the header
325        self.assertEqual(self.widget.lstPoly.horizontalHeader().count(), 7)
326        self.assertFalse(self.widget.lstPoly.horizontalHeader().stretchLastSection())
327
328        # Test presence of comboboxes in last column
329        for row in xrange(self.widget._poly_model.rowCount()):
330            func_index = self.widget._poly_model.index(row, 6)
331            #self.assertTrue(isinstance(self.widget.lstPoly.indexWidget(func_index), QtGui.QComboBox))
332            self.assertIn('Distribution of', self.widget._poly_model.item(row, 0).text())
333
334    def testSetMagneticModel(self):
335        """
336        Test the magnetic model setup
337        """
338        self.widget.show()
339        # Change the category index so we have a model available
340        category_index = self.widget.cbCategory.findText("Sphere")
341        self.widget.cbCategory.setCurrentIndex(category_index)
342
343        # Check the magnetic model
344        self.assertEqual(self.widget._magnet_model.rowCount(), 9)
345        self.assertEqual(self.widget._magnet_model.columnCount(), 5)
346
347        # Test the header
348        self.assertEqual(self.widget.lstMagnetic.horizontalHeader().count(), 5)
349        self.assertFalse(self.widget.lstMagnetic.horizontalHeader().stretchLastSection())
350
351        # Test rows
352        for row in xrange(self.widget._magnet_model.rowCount()):
353            func_index = self.widget._magnet_model.index(row, 0)
354            self.assertIn(':', self.widget._magnet_model.item(row, 0).text())
355
356
357    def testAddExtraShells(self):
358        """
359        Test how the extra shells are presented
360        """
361        pass
362
363    def testModifyShellsInList(self):
364        """
365        Test the additional rows added by modifying the shells combobox
366        """
367        self.widget.show()
368        # Change the model to multi shell
369        category_index = self.widget.cbCategory.findText("Sphere")
370        self.widget.cbCategory.setCurrentIndex(category_index)
371        model_index = self.widget.cbModel.findText("core_multi_shell")
372        self.widget.cbModel.setCurrentIndex(model_index)
373
374        # Assure we have the combobox available
375        last_row = self.widget._last_model_row
376        func_index = self.widget._model_model.index(last_row-1, 1)
377        self.assertIsInstance(self.widget.lstParams.indexWidget(func_index), QtGui.QComboBox)
378
379        # Change the combo box index
380        self.widget.lstParams.indexWidget(func_index).setCurrentIndex(3)
381
382        # Check that the number of rows increased
383        more_rows = self.widget._model_model.rowCount() - last_row
384        self.assertEqual(more_rows, 6) # 6 new rows: 2 params per index
385
386        # Back to 0
387        self.widget.lstParams.indexWidget(func_index).setCurrentIndex(0)
388        self.assertEqual(self.widget._model_model.rowCount(), last_row)
389
390    def testPlotTheory(self):
391        """
392        See that theory item can produce a chart
393        """
394        # By default, the compute/plot button is disabled
395        self.assertFalse(self.widget.cmdPlot.isEnabled())
396        self.assertEqual(self.widget.cmdPlot.text(), 'Show Plot')
397
398        # Assign a model
399        self.widget.show()
400        # Change the category index so we have a model available
401        category_index = self.widget.cbCategory.findText("Sphere")
402        self.widget.cbCategory.setCurrentIndex(category_index)
403
404        # Check the enablement/text
405        self.assertTrue(self.widget.cmdPlot.isEnabled())
406        self.assertEqual(self.widget.cmdPlot.text(), 'Calculate')
407
408        # Spying on plot update signal
409        spy = QtSignalSpy(self.widget, self.widget.communicate.plotRequestedSignal)
410
411        # Press Calculate
412        QtTest.QTest.mouseClick(self.widget.cmdPlot, QtCore.Qt.LeftButton)
413
414        # Observe cmdPlot caption change
415        self.assertEqual(self.widget.cmdPlot.text(), 'Show Plot')
416
417        # Make sure the signal has NOT been emitted
418        self.assertEqual(spy.count(), 0)
419
420        # Click again
421        QtTest.QTest.mouseClick(self.widget.cmdPlot, QtCore.Qt.LeftButton)
422
423        # This time, we got the update signal
424        self.assertEqual(spy.count(), 0)
425
426    def testPlotData(self):
427        """
428        See that data item can produce a chart
429        """
430        # By default, the compute/plot button is disabled
431        self.assertFalse(self.widget.cmdPlot.isEnabled())
432        self.assertEqual(self.widget.cmdPlot.text(), 'Show Plot')
433
434        self.widget.show()
435
436        # Set data
437        test_data = Data1D(x=[1,2], y=[1,2])
438
439        # Force same data into logic
440        self.widget.logic.data = test_data
441        self.widget.data_is_loaded = True
442
443        # Change the category index so we have a model available
444        category_index = self.widget.cbCategory.findText("Sphere")
445        self.widget.cbCategory.setCurrentIndex(category_index)
446
447        # Check the enablement/text
448        self.assertTrue(self.widget.cmdPlot.isEnabled())
449        self.assertEqual(self.widget.cmdPlot.text(), 'Show Plot')
450
451        # Spying on plot update signal
452        spy = QtSignalSpy(self.widget, self.widget.communicate.plotRequestedSignal)
453
454        # Press Calculate
455        QtTest.QTest.mouseClick(self.widget.cmdPlot, QtCore.Qt.LeftButton)
456
457        # Observe cmdPlot caption did not change
458        self.assertEqual(self.widget.cmdPlot.text(), 'Show Plot')
459
460        # Make sure the signal has been emitted == new plot
461        self.assertEqual(spy.count(), 1)
462
463    def testOnFit1D(self):
464        """
465        Test the threaded fitting call
466        """
467        # Set data
468        test_data = Data1D(x=[1,2], y=[1,2])
469
470        # Force same data into logic
471        self.widget.logic.data = test_data
472        self.widget.data_is_loaded = True
473        category_index = self.widget.cbCategory.findText("Sphere")
474        self.widget.cbCategory.setCurrentIndex(category_index)
475
476        self.widget.show()
477
478        # Test no fitting params
479        self.widget.parameters_to_fit = []
480
481        with self.assertRaises(ValueError) as error:
482            self.widget.onFit()
483        self.assertEqual(str(error.exception), 'no fitting parameters')
484
485        # Assing fitting params
486        self.widget.parameters_to_fit = ['scale']
487
488        # Spying on status update signal
489        update_spy = QtSignalSpy(self.widget, self.widget.communicate.statusBarUpdateSignal)
490
491        with threads.deferToThread as MagicMock:
492            self.widget.onFit()
493            # thread called
494            self.assertTrue(threads.deferToThread.called)
495            # thread method is 'compute'
496            self.assertEqual(threads.deferToThread.call_args_list[0][0][0].__name__, 'compute')
497
498            # the fit button changed caption and got disabled
499            self.assertEqual(self.widget.cmdFit.text(), 'Calculating...')
500            self.assertFalse(self.widget.cmdFit.isEnabled())
501
502            # Signal pushed up
503            self.assertEqual(update_spy.count(), 1)
504
505    def testOnFit2D(self):
506        """
507        Test the threaded fitting call
508        """
509        # Set data
510        test_data = Data2D(image=[1.0, 2.0, 3.0],
511                           err_image=[0.01, 0.02, 0.03],
512                           qx_data=[0.1, 0.2, 0.3],
513                           qy_data=[0.1, 0.2, 0.3],
514                           xmin=0.1, xmax=0.3, ymin=0.1, ymax=0.3,
515                           mask=[True, True, True])
516
517        # Force same data into logic
518        self.widget.logic.data = test_data
519        self.widget.data_is_loaded = True
520        category_index = self.widget.cbCategory.findText("Sphere")
521        self.widget.cbCategory.setCurrentIndex(category_index)
522
523        self.widget.show()
524
525        # Test no fitting params
526        self.widget.parameters_to_fit = []
527
528        with self.assertRaises(ValueError) as error:
529            self.widget.onFit()
530        self.assertEqual(str(error.exception), 'no fitting parameters')
531
532        # Assing fitting params
533        self.widget.parameters_to_fit = ['scale']
534
535        # Spying on status update signal
536        update_spy = QtSignalSpy(self.widget, self.widget.communicate.statusBarUpdateSignal)
537
538        with threads.deferToThread as MagicMock:
539            self.widget.onFit()
540            # thread called
541            self.assertTrue(threads.deferToThread.called)
542            # thread method is 'compute'
543            self.assertEqual(threads.deferToThread.call_args_list[0][0][0].__name__, 'compute')
544
545            # the fit button changed caption and got disabled
546            self.assertEqual(self.widget.cmdFit.text(), 'Calculating...')
547            self.assertFalse(self.widget.cmdFit.isEnabled())
548
549            # Signal pushed up
550            self.assertEqual(update_spy.count(), 1)
551
552    def testReadFitPage(self):
553        """
554        Read in the fitpage object and restore state
555        """
556        # Set data
557        test_data = Data1D(x=[1,2], y=[1,2])
558
559        # Force same data into logic
560        self.widget.logic.data = test_data
561        self.widget.data_is_loaded = True
562        category_index = self.widget.cbCategory.findText('Sphere')
563        self.widget.cbCategory.setCurrentIndex(category_index)
564        self.widget.parameters_to_fit = ['scale']
565        # Invoke the tested method
566        fp = self.widget.currentState()
567
568        # Prepare modified fit page
569        fp.current_model = 'onion'
570        fp.is_polydisperse = True
571
572        # Read in modified state
573        self.widget.readFitPage(fp)
574
575        # Check if the widget got updated accordingly
576        self.assertEqual(self.widget.cbModel.currentText(), 'onion')
577        self.assertTrue(self.widget.chkPolydispersity.isChecked())
578
579    def testCurrentState(self):
580        """
581        Set up the fitpage with current state
582        """
583        # Set data
584        test_data = Data1D(x=[1,2], y=[1,2])
585
586        # Force same data into logic
587        self.widget.logic.data = test_data
588        self.widget.data_is_loaded = True
589        category_index = self.widget.cbCategory.findText("Sphere")
590        self.widget.cbCategory.setCurrentIndex(category_index)
591        self.widget.parameters_to_fit = ['scale']
592
593        # Invoke the tested method
594        fp = self.widget.currentState()
595
596        # Test some entries. (Full testing of fp is done in FitPageTest)
597        self.assertIsInstance(fp.data, Data1D)
598        self.assertListEqual(list(fp.data.x), [1,2])
599        self.assertTrue(fp.data_is_loaded)
600        self.assertEqual(fp.current_category, "Sphere")
601        self.assertEqual(fp.current_model, "adsorbed_layer")
602        self.assertListEqual(fp.parameters_to_fit, ['scale'])
603
604    def testPushFitPage(self):
605        """
606        Push current state of fitpage onto stack
607        """
608        # Set data
609        test_data = Data1D(x=[1,2], y=[1,2])
610
611        # Force same data into logic
612        self.widget.logic.data = test_data
613        self.widget.data_is_loaded = True
614        category_index = self.widget.cbCategory.findText("Sphere")
615
616        # Asses the initial state of stack
617        self.assertEqual(self.widget.page_stack, [])
618
619        # Set the undo flag
620        self.widget.undo_supported = True
621        self.widget.cbCategory.setCurrentIndex(category_index)
622        self.widget.parameters_to_fit = ['scale']
623
624        # Check that the stack is updated
625        self.assertEqual(len(self.widget.page_stack), 1)
626
627        # Change another parameter
628        self.widget._model_model.item(2, 1).setText("3.0")
629        # Check that the stack is updated
630        self.assertEqual(len(self.widget.page_stack), 2)
631
632    def testPopFitPage(self):
633        """
634        Pop current state of fitpage from stack
635        """
636        # TODO: to be added when implementing UNDO/REDO
637        pass
638
639if __name__ == "__main__":
640    unittest.main()
Note: See TracBrowser for help on using the repository browser.