Index: src/sas/qtgui/Perspectives/Invariant/UnitTesting/InvariantPerspectiveTest.py
===================================================================
--- src/sas/qtgui/Perspectives/Invariant/UnitTesting/InvariantPerspectiveTest.py	(revision f1f3e6a66c47bce134b59fe9f166fe14eac3a61d)
+++ src/sas/qtgui/Perspectives/Invariant/UnitTesting/InvariantPerspectiveTest.py	(revision f1f3e6a66c47bce134b59fe9f166fe14eac3a61d)
@@ -0,0 +1,474 @@
+import sys
+import time
+import numpy
+import logging
+import unittest
+from PyQt5 import QtGui, QtWidgets
+from PyQt5 import QtCore
+from PyQt5.QtTest import QTest
+from PyQt5.QtCore import Qt
+from mock import MagicMock
+from mock import patch
+from mock import create_autospec
+
+from twisted.internet import threads
+
+from sas.qtgui.Perspectives.Invariant.InvariantPerspective import InvariantWindow
+from sas.qtgui.Perspectives.Invariant.InvariantDetails import DetailsDialog
+from sas.qtgui.Perspectives.Invariant.InvariantUtils import WIDGETS
+from sas.qtgui.Plotting.PlotterData import Data1D
+from sas.qtgui.MainWindow.GuiManager import GuiManager
+from sas.qtgui.MainWindow.DataExplorer import DataExplorerWindow
+
+import sas.qtgui.Utilities.GuiUtils as GuiUtils
+
+if not QtWidgets.QApplication.instance():
+    app = QtWidgets.QApplication(sys.argv)
+
+BG_COLOR_ERR = 'background-color: rgb(244, 170, 164);'
+
+class InvariantPerspectiveTest(unittest.TestCase):
+    """Test the Invariant Perspective Window"""
+    def setUp(self):
+        """Create the Invariant Perspective Window"""
+
+        class MainWindow(object):
+            def __init__(self):
+                self.model = QtGui.QStandardItemModel()
+
+        class dummy_manager(object):
+            def __init__(self):
+                self.filesWidget = MainWindow()
+
+            def communicator(self):
+                return GuiUtils.Communicate()
+
+            def communicate(self):
+                return GuiUtils.Communicate()
+
+        self.widget = InvariantWindow(dummy_manager())
+
+    def tearDown(self):
+        """Destroy the DataOperationUtility"""
+        self.widget.close()
+        self.widget = None
+
+    def testDefaults(self):
+        """Test the GUI in its default state"""
+
+        self.assertIsInstance(self.widget, QtWidgets.QDialog)
+
+        self.assertEqual(self.widget.windowTitle(), "Invariant Perspective")
+
+        # name for displaying in the DataExplorer combo box
+        self.assertEqual(self.widget.name, "Invariant")
+
+        self.assertIsInstance(self.widget.model, QtGui.QStandardItemModel)
+
+        self.assertIsNone(self.widget._data)
+        self.assertEqual(self.widget._path, '')
+        self.assertFalse(self.widget._allow_close)
+
+        # disabled pushbuttons
+        self.assertFalse(self.widget.cmdStatus.isEnabled())
+        self.assertFalse(self.widget.cmdCalculate.isEnabled())
+
+        # disabled, read only line edits
+        self.assertFalse(self.widget.txtName.isEnabled())
+        self.assertTrue(self.widget.txtVolFract.isReadOnly())
+        self.assertTrue(self.widget.txtVolFractErr.isReadOnly())
+
+        self.assertTrue(self.widget.txtSpecSurf.isReadOnly())
+        self.assertTrue(self.widget.txtSpecSurfErr.isReadOnly())
+
+        self.assertTrue(self.widget.txtInvariantTot.isReadOnly())
+        self.assertTrue(self.widget.txtInvariantTotErr.isReadOnly())
+
+        self.assertFalse(self.widget.txtBackgd.isReadOnly())
+        self.assertFalse(self.widget.txtScale.isReadOnly())
+        self.assertFalse(self.widget.txtContrast.isReadOnly())
+        self.assertFalse(self.widget.txtPorodCst.isReadOnly())
+
+        self.assertFalse(self.widget.txtExtrapolQMin.isEnabled())
+        self.assertFalse(self.widget.txtExtrapolQMax.isEnabled())
+
+        self.assertFalse(self.widget.txtNptsLowQ.isReadOnly())
+        self.assertFalse(self.widget.txtNptsHighQ.isReadOnly())
+
+        # content of line edits
+        self.assertEqual(self.widget.txtName.text(), '')
+        self.assertEqual(self.widget.txtTotalQMin.text(), '0.0')
+        self.assertEqual(self.widget.txtTotalQMax.text(), '0.0')
+        self.assertEqual(self.widget.txtBackgd.text(), '0.0')
+        self.assertEqual(self.widget.txtScale.text(), '1.0')
+        self.assertEqual(self.widget.txtContrast.text(), '1.0')
+        self.assertEqual(self.widget.txtExtrapolQMin.text(), '1e-05')
+        self.assertEqual(self.widget.txtExtrapolQMax.text(), '10')
+        self.assertEqual(self.widget.txtPowerLowQ.text(), '4')
+        self.assertEqual(self.widget.txtPowerHighQ.text(), '4')
+
+        # unchecked checkboxes
+        self.assertFalse(self.widget.chkLowQ.isChecked())
+        self.assertFalse(self.widget.chkHighQ.isChecked())
+
+        # number of tabs
+        self.assertEqual(self.widget.tabWidget.count(), 2)
+        # default tab
+        self.assertEqual(self.widget.tabWidget.currentIndex(), 0)
+        # tab's title
+        self.assertEqual(self.widget.tabWidget.tabText(0), 'Invariant')
+        self.assertEqual(self.widget.tabWidget.tabText(1), 'Options')
+
+        # Tooltips
+        self.assertEqual(self.widget.cmdStatus.toolTip(), "Get more details of computation such as fraction from extrapolation" )
+
+        self.assertEqual(self.widget.txtInvariantTot.toolTip(), "Total invariant [Q*], including extrapolated regions.")
+
+        self.assertEqual(self.widget.txtExtrapolQMin.toolTip(), "The minimum extrapolated q value.")
+
+        self.assertEqual(self.widget.txtPowerHighQ.toolTip(),"Exponent to apply to the Power_law function.")
+
+        self.assertEqual(self.widget.txtNptsHighQ.toolTip(), "Number of Q points to consider\n while extrapolating the high-Q region")
+
+        self.assertEqual(self.widget.chkHighQ.toolTip(), "Check to extrapolate data at high-Q")
+
+        self.assertEqual(self.widget.txtNptsLowQ.toolTip(), "Number of Q points to consider\nwhile extrapolating the low-Q region")
+
+        self.assertEqual(self.widget.chkLowQ.toolTip(), "Check to extrapolate data at low-Q")
+
+        self.assertEqual(self.widget.cmdCalculate.toolTip(), "Compute invariant")
+
+        self.assertEqual(self.widget.txtPowerLowQ.toolTip(), "Exponent to apply to the Power_law function." )
+
+        # Validators
+        self.assertIsInstance(self.widget.txtNptsLowQ.validator(), QtGui.QRegExpValidator)
+        self.assertIsInstance(self.widget.txtNptsHighQ.validator(), QtGui.QRegExpValidator)
+        self.assertIsInstance(self.widget.txtPowerLowQ.validator(), QtGui.QRegExpValidator)
+        self.assertIsInstance(self.widget.txtPowerHighQ.validator(), QtGui.QRegExpValidator)
+
+        self.assertIsInstance(self.widget.txtBackgd.validator(), QtGui.QDoubleValidator)
+        self.assertIsInstance(self.widget.txtContrast.validator(), QtGui.QDoubleValidator)
+        self.assertIsInstance(self.widget.txtScale.validator(), QtGui.QDoubleValidator)
+        self.assertIsInstance(self.widget.txtPorodCst.validator(), QtGui.QDoubleValidator)
+
+        # Test autoexclusivity of radiobuttons
+        # Low Q
+        self.assertFalse(self.widget.rbGuinier.autoExclusive())
+        self.assertFalse(self.widget.rbPowerLawLowQ.autoExclusive())
+        self.assertTrue(self.widget.rbFixLowQ.autoExclusive())
+        self.assertTrue(self.widget.rbFitLowQ.autoExclusive())
+        # High Q
+        self.assertTrue(self.widget.rbFixHighQ.autoExclusive())
+        self.assertTrue(self.widget.rbFitHighQ.autoExclusive())
+
+    def testOnCalculate(self):
+        """ Test onCompute function """
+        self.widget.calculateInvariant = MagicMock()
+        self.widget.cmdCalculate.setEnabled(True)
+        QTest.mouseClick(self.widget.cmdCalculate, Qt.LeftButton)
+        self.assertTrue(self.widget.calculateInvariant.called_once())
+
+    def testCalculateInvariant(self):
+        """ """
+        threads.deferToThread = MagicMock()
+        self.widget.calculateInvariant()
+        self.assertTrue(threads.deferToThread.called)
+        self.assertEqual(threads.deferToThread.call_args_list[0][0][0].__name__, 'calculateThread')
+
+        self.assertEqual(self.widget.cmdCalculate.text(), 'Calculating...')
+        self.assertFalse(self.widget.cmdCalculate.isEnabled())
+
+    # TODO
+    def testPlotResult(self):
+        """ """
+        pass
+        # create fake input
+        # data = Data1D(x=[1, 2], y=[3, 4])
+        # GuiUtils.dataFromItem = MagicMock(return_value=data)
+        # # self.widget._manager.filesWidget.model = MagicMock()
+        # item = QtGui.QStandardItem("test")
+
+        # run function
+        # self.widget.plotResult = MagicMock(return_value=None) # (item)
+        # self.widget.plotResult(item)
+        # self.assertTrue(self.widget.plotResult.called_once())
+
+
+        # self.assertTrue(self.widget.cmdCalculate.isEnabled())
+        # self.assertEqual(self.widget.cmdCalculate.text(), 'Calculate')
+        # self.assertEqual(self.widget._data.x[0], 1)
+        # self.assertEqual(self.widget._data.x[1], 2)
+        # self.assertEqual(self.widget._data.y[0], 3)
+        # self.assertEqual(self.widget._data.y[1], 4)
+
+    def testHelp(self):
+        """ Assure help file is shown """
+        # this should not rise
+        self.widget.onHelp()
+
+    def testAllowBatch(self):
+        """ """
+        self.assertFalse(self.widget.allowBatch())
+
+    def testTitle(self):
+        """
+        Test Perspective name
+        """
+        self.assertEqual(self.widget.title(), "Invariant panel")
+
+    def testOnStatus(self):
+        """
+        Test Display of Invariant Details
+        """
+        # enable click on Calculate button
+        self.widget.cmdStatus.setEnabled(True)
+
+        invariant_details_dialog = create_autospec(DetailsDialog)
+
+        self.widget.detailsDialog = invariant_details_dialog
+
+        # click on button
+        QTest.mouseClick(self.widget.cmdStatus, Qt.LeftButton)
+
+        invariant_details_dialog.showDialog.assert_called_once_with()
+
+    def testUpdateFromModel(self):
+        """
+        update the globals based on the data in the model
+        """
+        self.widget.updateFromModel()
+        self.assertEqual(self.widget._background,
+                         float(self.widget.model.item(WIDGETS.W_BACKGROUND).text()))
+        self.assertEqual(self.widget._contrast,
+                         float(self.widget.model.item(WIDGETS.W_CONTRAST).text()))
+        self.assertEqual(self.widget._scale, float(self.widget.model.item(WIDGETS.W_SCALE).text()))
+        self.assertEqual(self.widget._low_extrapolate,
+                         str(self.widget.model.item(WIDGETS.W_ENABLE_LOWQ).text()) == 'true')
+        self.assertEqual(self.widget._low_points,
+                         float(self.widget.model.item(WIDGETS.W_NPTS_LOWQ).text()))
+
+        self.assertEqual(self.widget._low_guinier, str(self.widget.model.item(WIDGETS.W_LOWQ_GUINIER).text()) == 'true' )
+
+        self.assertEqual(self.widget._low_fit,str(self.widget.model.item(WIDGETS.W_LOWQ_FIT).text()) == 'true')
+        self.assertEqual(self.widget._low_power_value, float(self.widget.model.item(WIDGETS.W_LOWQ_POWER_VALUE).text()))
+
+        self.assertEqual(self.widget._high_extrapolate,str(self.widget.model.item(WIDGETS.W_ENABLE_HIGHQ).text()) == 'true')
+        self.assertEqual(self.widget._high_points,
+                         float(self.widget.model.item(WIDGETS.W_NPTS_HIGHQ).text()))
+        self.assertEqual(self.widget._high_fit,str(self.widget.model.item(WIDGETS.W_HIGHQ_FIT).text()) == 'true')
+
+        self.assertEqual(self.widget._high_power_value, float(self.widget.model.item(WIDGETS.W_HIGHQ_POWER_VALUE).text()))
+
+    def testEnabling(self):
+        """ """
+
+        self.widget.cmdStatus.setEnabled(False)
+
+        self.widget.enabling()
+
+        self.assertTrue(self.widget.cmdStatus.isEnabled())
+
+    def testCheckLength(self):
+        """
+        Test validator for number of points for extrapolation
+         Error if it is larger than the distribution length
+        """
+        logging.warning = MagicMock()
+        self.widget.txtNptsLowQ.setEnabled(True)
+
+        self.widget._data = Data1D(x=[1, 2], y=[1, 2])
+        self.widget.txtNptsLowQ.setText('9')
+
+        # QTest.keyClicks(self.widget.txtNptsLowQ, '9')
+        # QTest.keyClick(self.widget.txtNptsLowQ, QtCore.Qt.Key_Return)
+
+        BG_COLOR_ERR = 'background-color: rgb(244, 170, 164);'
+        # print 'style ',self.widget.txtNptsLowQ.styleSheet()
+        self.assertIn(BG_COLOR_ERR, self.widget.txtNptsLowQ.styleSheet())
+        self.assertTrue(logging.warning.called_once_with())
+        self.assertFalse(self.widget.cmdCalculate.isEnabled())
+
+    def testModelChanged(self):
+        """ """
+        self.widget.lowQToggle = MagicMock()
+        status_ini = self.widget.model.item(WIDGETS.W_ENABLE_LOWQ).text()
+        if status_ini == 'true':
+            status_fin = 'false'
+        else:
+            status_fin = 'true'
+
+        self.widget.model.setItem(WIDGETS.W_ENABLE_LOWQ, QtGui.QStandardItem(status_fin))
+
+        if status_fin:
+            self.assertTrue(self.widget._low_extrapolate)
+        else:
+            self.assertFalse(self.widget._low_extrapolate)
+
+        self.assertTrue(self.widget.lowQToggle.called_once_with())
+
+    def testUpdateFromGui(self):
+        """ """
+        self.widget.txtBackgd.setText('0.22')
+        self.assertEqual(str(self.widget.model.item(WIDGETS.W_BACKGROUND).text()), '0.22')
+
+    def testLowGuinierAndPowerToggle(self):
+        """ """
+        # enable all tested radiobuttons
+        self.widget.rbGuinier.setEnabled(True)
+        self.widget.rbPowerLawLowQ.setEnabled(True)
+        self.widget.txtNptsLowQ.setEnabled(True)
+
+        # record initial status
+        status_ini = self.widget.rbGuinier.isChecked()
+
+        # mouse click to run function
+        QTest.mouseClick(self.widget.rbGuinier, Qt.LeftButton)
+
+        # check that status changed
+        self.assertNotEqual(self.widget.rbGuinier.isChecked(), status_ini)
+
+        status_fin = self.widget.rbGuinier.isChecked()
+
+        self.assertEqual(self.widget.rbPowerLawLowQ.isChecked(), not status_fin)
+
+        self.assertEqual(self.widget.txtPowerLowQ.isEnabled(),
+                         all([not status_fin, not self.widget._low_fit]))
+
+    def testLowFitAndFixToggle(self):
+        """ """
+        status = True
+        # run function to test
+        self.widget.lowFitAndFixToggle(status)
+        self.assertEqual(self.widget._low_fit, status)
+        self.assertNotEqual(self.widget.txtPowerLowQ.isEnabled(), status)
+
+    def testHiFitAndFixToggle(self):
+        status = True
+        self.widget.hiFitAndFixToggle(status)
+        self.assertEqual(self.widget.txtPowerHighQ.isEnabled(), not status)
+
+    def testHighQToggle(self):
+        """ Test enabling / disabling for check box High Q extrapolation """
+        status_chkHighQ = self.widget.chkHighQ.isChecked()
+        self.widget.highQToggle(status_chkHighQ)
+
+        self.assertEqual(self.widget.rbFitHighQ.isEnabled(), status_chkHighQ)
+        self.assertEqual(self.widget.rbFixHighQ.isEnabled(), status_chkHighQ)
+        self.assertEqual(self.widget.txtNptsHighQ.isEnabled(), status_chkHighQ)
+        self.assertEqual(self.widget.txtPowerHighQ.isEnabled(), status_chkHighQ)
+
+        # change checked status of chkHighQ
+        self.widget.chkHighQ.setChecked(True)
+        status_chkHighQ = self.widget.chkHighQ.isChecked()
+        self.assertEqual(self.widget.rbFitHighQ.isEnabled(), status_chkHighQ)
+        self.assertEqual(self.widget.rbFixHighQ.isEnabled(), status_chkHighQ)
+        self.assertEqual(self.widget.txtNptsHighQ.isEnabled(), status_chkHighQ)
+        self.assertEqual(self.widget.txtPowerHighQ.isEnabled(),
+                         status_chkHighQ)
+
+    def testLowQToggle(self):
+        """ Test enabling / disabling for check box Low Q extrapolation """
+        status_chkLowQ = self.widget.chkLowQ.isChecked()
+
+        self.assertEqual(self.widget.rbGuinier.isEnabled(), status_chkLowQ)
+        self.assertEqual(self.widget.rbPowerLawLowQ.isEnabled(), status_chkLowQ)
+        self.assertEqual(self.widget.txtNptsLowQ.isEnabled(), status_chkLowQ)
+
+        self.assertEqual(self.widget.rbFitLowQ.isVisible(), self.widget.rbPowerLawLowQ.isChecked())
+        self.assertEqual(self.widget.rbFixLowQ.isVisible(), self.widget.rbPowerLawLowQ.isChecked())
+        self.assertEqual(self.widget.rbFitLowQ.isEnabled(), status_chkLowQ)
+        self.assertEqual(self.widget.rbFixLowQ.isEnabled(), status_chkLowQ)
+
+        self.assertEqual(self.widget.txtNptsLowQ.isEnabled(),
+                         all([status_chkLowQ, not self.widget._low_guinier, not self.widget._low_fit]))
+
+    def testSetupModel(self):
+        """ Test default settings of model"""
+        self.assertEqual(self.widget.model.item(WIDGETS.W_FILENAME).text(),
+                         self.widget._path)
+
+        self.assertEqual(self.widget.model.item(WIDGETS.W_QMIN).text(), '0.0')
+
+        self.assertEqual(self.widget.model.item(WIDGETS.W_QMAX).text(), '0.0')
+
+        self.assertEqual(self.widget.model.item(WIDGETS.W_BACKGROUND).text(),
+                         str(self.widget._background))
+
+        self.assertEqual(self.widget.model.item(WIDGETS.W_CONTRAST).text(),
+                         str(self.widget._contrast))
+
+        self.assertEqual(self.widget.model.item(WIDGETS.W_SCALE).text(),
+                         str(self.widget._scale))
+
+        self.assertIn(str(self.widget.model.item(WIDGETS.W_POROD_CST).text()),
+                      ['', str(self.widget._porod)])
+
+        self.assertEqual(
+            str(self.widget.model.item(WIDGETS.W_ENABLE_HIGHQ).text()).lower(),
+            'false')
+
+        self.assertEqual(
+            str(self.widget.model.item(WIDGETS.W_ENABLE_LOWQ).text()).lower(),
+            'false')
+
+        self.assertEqual(
+            str(self.widget.model.item(WIDGETS.W_NPTS_LOWQ).text()),
+            str(10))
+
+        self.assertEqual(self.widget.model.item(WIDGETS.W_NPTS_HIGHQ).text(),
+                         str(10))
+
+        self.assertEqual(
+            str(self.widget.model.item(WIDGETS.W_LOWQ_GUINIER).text()).lower(),
+            'true')
+
+        self.assertEqual(
+            str(self.widget.model.item(WIDGETS.W_LOWQ_FIT).text()).lower(),
+            'true')
+
+        self.assertEqual(
+            str(self.widget.model.item(WIDGETS.W_LOWQ_POWER_VALUE).text()), '4')
+
+        self.assertEqual(
+            str(self.widget.model.item(WIDGETS.W_HIGHQ_FIT).text()).lower(),
+            'true')
+
+        self.assertEqual(
+            str(self.widget.model.item(WIDGETS.W_HIGHQ_POWER_VALUE).text()),
+            '4')
+
+    # TODO
+    def testSetupMapper(self):
+        """ """
+        self.assertIsInstance(self.widget.mapper, QtWidgets.QDataWidgetMapper)
+
+        self.assertEqual(self.widget.mapper.orientation(), 2)
+
+        self.assertEqual(self.widget.mapper.model(), self.widget.model)
+
+    def testSetData(self):
+        """ """
+        self.widget.updateGuiFromFile = MagicMock()
+
+        data = Data1D(x=[1, 2], y=[1, 2])
+        GuiUtils.dataFromItem = MagicMock(return_value=data)
+        item = QtGui.QStandardItem("test")
+        self.widget.setData([item])
+
+        self.assertTrue(self.widget.updateGuiFromFile.called_once())
+
+    def TestCheckQExtrapolatedData(self):
+        """
+        Test Match status of low or high-Q extrapolated data checkbox in
+        DataExplorer with low or high-Q extrapolation checkbox in invariant
+        panel
+        """
+        # Low-Q check box ticked
+        self.widget.chkLowQ.setCheckStatus(QtCore.Qt.Checked)
+        GuiUtils.updateModelItemStatus = MagicMock()
+
+        self.assertTrue(GuiUtils.updateModelItemStatus.called_once())
+
+
+if __name__ == "__main__":
+    unittest.main()
