3385 lines
125 KiB
Python
3385 lines
125 KiB
Python
# coding=utf-8
|
|
import unittest
|
|
import functools
|
|
import warnings
|
|
import threading
|
|
import logging
|
|
import logging.handlers
|
|
import contextlib
|
|
import sys
|
|
import os
|
|
import queue
|
|
import requests
|
|
import time
|
|
import tempfile
|
|
import shutil
|
|
import textwrap
|
|
import importlib
|
|
import uuid
|
|
import json
|
|
|
|
if sys.version_info >= (3,):
|
|
# Need to force PySide2 import in python3. It's the only set of bindings I
|
|
# can seem to get to work here.
|
|
basestring = str
|
|
import unittest.mock as mock
|
|
else:
|
|
import mock
|
|
|
|
try:
|
|
import PyQt4
|
|
except ImportError:
|
|
import PySide2
|
|
|
|
import faulthandler
|
|
faulthandler.enable()
|
|
import qtpy
|
|
from qtpy import QtCore
|
|
from qtpy import QtGui
|
|
from qtpy import QtWidgets
|
|
from qtpy.QtTest import QTest
|
|
import six
|
|
|
|
LOGGER = logging.getLogger(__name__)
|
|
|
|
@contextlib.contextmanager
|
|
def wait_on_signal(qt_app, signal, timeout=250):
|
|
"""Block loop until signal emitted, or timeout (ms) elapses."""
|
|
loop = QtCore.QEventLoop()
|
|
signal.connect(loop.quit)
|
|
|
|
try:
|
|
yield
|
|
if qt_app.hasPendingEvents():
|
|
qt_app.processEvents()
|
|
except Exception as error:
|
|
LOGGER.exception('Error encountered while witing for signal %s',
|
|
signal)
|
|
raise error
|
|
finally:
|
|
if timeout is not None:
|
|
QtCore.QTimer.singleShot(timeout, loop.quit)
|
|
loop.exec_()
|
|
loop = None
|
|
|
|
|
|
class _QtTest(unittest.TestCase):
|
|
def setUp(self):
|
|
"""Set up a QAppplication on each test case."""
|
|
try:
|
|
QApplication = QtGui.QApplication
|
|
except AttributeError:
|
|
QApplication = QtWidgets.QApplication
|
|
|
|
self.qt_app = QApplication.instance()
|
|
|
|
# If we're running PyQt4, we need to instruct Qt to use UTF-8 strings
|
|
# internally.
|
|
if qtpy.API in ('pyqt', 'pyqt4'):
|
|
QtCore.QTextCodec.setCodecForCStrings(
|
|
QtCore.QTextCodec.codecForName('UTF-8'))
|
|
|
|
if self.qt_app is None:
|
|
self.qt_app = QApplication(sys.argv)
|
|
|
|
def tearDown(self):
|
|
"""Clear the QApplication's event queue."""
|
|
# After each test, empty the event queue.
|
|
# This should help to make sure that there aren't any event-based race
|
|
# conditions where a C/C++ object is deleted before a slot is called.
|
|
self.qt_app.sendPostedEvents()
|
|
|
|
class _SettingsSandbox(_QtTest):
|
|
def setUp(self):
|
|
_QtTest.setUp(self)
|
|
from natcap.invest.ui import inputs
|
|
|
|
# back up the QSettings options for the test run so we don't disrupt
|
|
# whatever settings exist on this computer
|
|
self.settings = dict(
|
|
(key, inputs.INVEST_SETTINGS.value(key)) for key in
|
|
inputs.INVEST_SETTINGS.allKeys())
|
|
inputs.INVEST_SETTINGS.clear()
|
|
|
|
def tearDown(self):
|
|
_QtTest.tearDown(self)
|
|
from natcap.invest.ui import inputs
|
|
inputs.INVEST_SETTINGS.clear()
|
|
for key, value in self.settings.items():
|
|
inputs.INVEST_SETTINGS.setValue(key, value)
|
|
|
|
|
|
class InVESTModelInputTest(_QtTest):
|
|
@staticmethod
|
|
def create_input(*args, **kwargs):
|
|
from natcap.invest.ui.inputs import InVESTModelInput
|
|
return InVESTModelInput(*args, **kwargs)
|
|
|
|
def test_label(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
self.assertEqual(input_instance.label, 'foo')
|
|
|
|
def test_helptext(self):
|
|
input_instance = self.__class__.create_input(label='foo', helptext='bar')
|
|
self.assertEqual(input_instance.helptext, 'bar')
|
|
|
|
def test_clear(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
if input_instance.__class__.__name__ == 'InVESTModelInput':
|
|
with self.assertRaises(NotImplementedError):
|
|
input_instance.clear()
|
|
else:
|
|
self.fail('Test class must reimplement this test method.')
|
|
|
|
def test_interactive(self):
|
|
input_instance = self.__class__.create_input(label='foo', interactive=True)
|
|
self.assertEqual(input_instance.interactive, True)
|
|
|
|
def test_noninteractive(self):
|
|
input_instance = self.__class__.create_input(label='foo', interactive=False)
|
|
# Silence notimplementederror exceptions on input.value in some cases.
|
|
try:
|
|
input_instance.value()
|
|
except NotImplementedError:
|
|
input_instance.value = lambda: 'Value!'
|
|
self.assertEqual(input_instance.interactive, False)
|
|
|
|
def test_set_interactive(self):
|
|
input_instance = self.__class__.create_input(label='foo', interactive=False)
|
|
self.assertEqual(input_instance.interactive, False)
|
|
# Silence notimplementederror exceptions on input.value in some cases.
|
|
try:
|
|
input_instance.value()
|
|
except NotImplementedError:
|
|
input_instance.value = lambda: 'Value!'
|
|
input_instance.set_interactive(True)
|
|
self.assertEqual(input_instance.interactive, True)
|
|
|
|
def test_interactivity_changed(self):
|
|
input_instance = self.__class__.create_input(label='foo', interactive=False)
|
|
callback = mock.Mock()
|
|
input_instance.interactivity_changed.connect(callback)
|
|
|
|
with wait_on_signal(self.qt_app, input_instance.interactivity_changed):
|
|
try:
|
|
input_instance.value()
|
|
except NotImplementedError:
|
|
input_instance.value = lambda: 'Value!'
|
|
input_instance.set_interactive(True)
|
|
|
|
callback.assert_called_with(True)
|
|
|
|
def test_add_to_layout(self):
|
|
base_widget = QtWidgets.QWidget()
|
|
base_widget.setLayout(QtWidgets.QGridLayout())
|
|
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
input_instance.add_to(base_widget.layout())
|
|
|
|
def test_value(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
if input_instance.__class__.__name__ in ('InVESTModelInput', 'GriddedInput'):
|
|
with self.assertRaises(NotImplementedError):
|
|
input_instance.value()
|
|
else:
|
|
self.fail('Test class must reimplement this test method')
|
|
|
|
def test_set_value(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
if input_instance.__class__.__name__ in ('InVESTModelInput', 'GriddedInput'):
|
|
with self.assertRaises(NotImplementedError):
|
|
input_instance.set_value('foo')
|
|
else:
|
|
self.fail('Test class must reimplement this test method')
|
|
|
|
def test_value_changed_signal_emitted(self):
|
|
input_instance = self.__class__.create_input(label='some_label')
|
|
callback = mock.Mock()
|
|
input_instance.value_changed.connect(callback)
|
|
|
|
if input_instance.__class__.__name__ in ('InVESTModelInput', 'GriddedInput'):
|
|
try:
|
|
with self.assertRaises(NotImplementedError):
|
|
self.assertEqual(input_instance.value(), '')
|
|
with wait_on_signal(self.qt_app, input_instance.value_changed):
|
|
input_instance.set_value('foo')
|
|
callback.assert_called_with(u'foo')
|
|
finally:
|
|
input_instance.value_changed.disconnect(callback)
|
|
else:
|
|
self.fail('Test class must reimplement this test method')
|
|
|
|
def test_value_changed_signal(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
callback = mock.Mock()
|
|
input_instance.value_changed.connect(callback)
|
|
|
|
try:
|
|
with wait_on_signal(self.qt_app, input_instance.value_changed):
|
|
try:
|
|
input_instance.value()
|
|
except NotImplementedError:
|
|
input_instance.value = lambda: 'Value!'
|
|
input_instance.value_changed.emit('value')
|
|
|
|
callback.assert_called_with('value')
|
|
finally:
|
|
input_instance.value_changed.disconnect(callback)
|
|
|
|
def test_interactivity_changed_signal(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
callback = mock.Mock()
|
|
input_instance.interactivity_changed.connect(callback)
|
|
|
|
with wait_on_signal(self.qt_app, input_instance.interactivity_changed):
|
|
try:
|
|
input_instance.value()
|
|
except NotImplementedError:
|
|
input_instance.value = lambda: 'Value!'
|
|
|
|
input_instance.interactivity_changed.emit(True)
|
|
|
|
callback.assert_called_with(True)
|
|
|
|
def test_args_key(self):
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
args_key='some_key')
|
|
self.assertEqual(input_instance.args_key, 'some_key')
|
|
|
|
def test_no_args_key(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
self.assertEqual(input_instance.args_key, None)
|
|
|
|
def test_add_to_container(self):
|
|
from natcap.invest.ui.inputs import Container
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
container = Container(label='Some container')
|
|
container.add_input(input_instance)
|
|
|
|
def test_visibility(self):
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
interactive=False)
|
|
self.assertEqual(input_instance.visible(), True)
|
|
|
|
input_instance.set_visible(False)
|
|
if len(input_instance.widgets) > 0: # only works if input has widgets
|
|
self.assertEqual(input_instance.visible(), False)
|
|
|
|
input_instance.set_visible(True)
|
|
if len(input_instance.widgets) > 0: # only works if input has widgets
|
|
self.assertEqual(input_instance.visible(), True)
|
|
|
|
def test_visibility_when_shown(self):
|
|
from natcap.invest.ui import inputs
|
|
container = inputs.Container(label='sample container')
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
interactive=False)
|
|
container.add_input(input_instance)
|
|
container.show()
|
|
|
|
self.assertEqual(input_instance.visible(), True)
|
|
|
|
input_instance.set_visible(False)
|
|
if len(input_instance.widgets) > 0: # only works if input has widgets
|
|
self.assertEqual(input_instance.visible(), False)
|
|
|
|
input_instance.set_visible(True)
|
|
if len(input_instance.widgets) > 0: # only works if input has widgets
|
|
self.assertEqual(input_instance.visible(), True)
|
|
|
|
|
|
class GriddedInputTest(InVESTModelInputTest):
|
|
@staticmethod
|
|
def create_input(*args, **kwargs):
|
|
from natcap.invest.ui.inputs import GriddedInput
|
|
return GriddedInput(*args, **kwargs)
|
|
|
|
def test_label(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
label_text = input_instance.label
|
|
self.assertEqual(label_text, 'foo')
|
|
|
|
def test_validator(self):
|
|
_callback = mock.Mock()
|
|
input_instance = self.__class__.create_input(
|
|
label='foo', validator=_callback)
|
|
self.assertEqual(input_instance.validator_ref, _callback)
|
|
|
|
def test_helptext(self):
|
|
from natcap.invest.ui.inputs import HelpButton
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
helptext='bar')
|
|
self.assertTrue(isinstance(input_instance.help_button, HelpButton))
|
|
|
|
def test_no_helptext(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
self.assertTrue(isinstance(input_instance.help_button,
|
|
QtWidgets.QWidget))
|
|
|
|
def test_clear(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
# simulate successful validation completion.
|
|
input_instance._validation_finished([])
|
|
self.assertEqual(input_instance._valid, True)
|
|
input_instance.clear()
|
|
self.assertEqual(input_instance.valid_button.whatsThis(), '')
|
|
self.assertEqual(input_instance.valid_button.toolTip(), '')
|
|
self.assertEqual(input_instance._valid, None)
|
|
self.assertEqual(input_instance.sufficient, False)
|
|
|
|
def test_validate_passes(self):
|
|
#"""UI: Validation that passes should affect validity."""
|
|
_validation_func = mock.Mock(return_value=[])
|
|
input_instance = self.__class__.create_input(
|
|
label='some_label', args_key='some_key',
|
|
validator=_validation_func)
|
|
try:
|
|
input_instance.value()
|
|
except NotImplementedError:
|
|
input_instance.value = lambda: 'value!'
|
|
|
|
input_instance._validate()
|
|
|
|
# Wait for validation to finish.
|
|
self.assertEqual(input_instance.valid(), True)
|
|
|
|
def test_validate_missing_args_key(self):
|
|
from natcap.invest.ui import inputs
|
|
input_instance = self.__class__.create_input(
|
|
label='some_label')
|
|
|
|
input_instance.value = mock.Mock(
|
|
input_instance, return_value=u'something')
|
|
|
|
# Verify we're starting with an unvalidated input
|
|
self.assertEqual(input_instance.valid(), None)
|
|
with warnings.catch_warnings(record=True) as messages:
|
|
input_instance._validate()
|
|
time.sleep(0.25) # wait for warnings to register
|
|
self.qt_app.processEvents()
|
|
|
|
# Validation still passes
|
|
self.assertEqual(input_instance.valid(), True)
|
|
|
|
def test_validate_fails(self):
|
|
#"""UI: Validation that fails should affect validity."""
|
|
_validation_func = mock.Mock(
|
|
return_value=[('some_key', 'some warning')])
|
|
input_instance = self.__class__.create_input(
|
|
label='some_label', args_key='some_key',
|
|
validator=_validation_func)
|
|
try:
|
|
input_instance.value()
|
|
except NotImplementedError:
|
|
input_instance.value = lambda: 'value!'
|
|
|
|
input_instance._validate()
|
|
|
|
# Wait for validation to finish and assert Failure.
|
|
self.assertEqual(input_instance.valid(), False)
|
|
|
|
def test_validate_required_validator(self):
|
|
from natcap.invest.ui import inputs
|
|
input_instance = self.__class__.create_input(
|
|
label='some_label', args_key='foo'
|
|
)
|
|
|
|
input_instance.value = mock.Mock(
|
|
input_instance, return_value=u'something')
|
|
|
|
# Verify we're starting with an unvalidated input
|
|
self.assertEqual(input_instance.valid(), None)
|
|
with warnings.catch_warnings(record=True) as messages:
|
|
input_instance._validate()
|
|
time.sleep(0.25) # wait for warnings to register
|
|
self.qt_app.processEvents()
|
|
|
|
# Validation still passes, but verify warning raised
|
|
self.assertEqual(len(messages), 1)
|
|
self.assertEqual(input_instance.valid(), True)
|
|
|
|
def test_validate_error(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='some_label', args_key='foo',
|
|
validator=lambda args, limit_to=None: []
|
|
)
|
|
|
|
input_instance.value = mock.Mock(
|
|
input_instance, return_value=u'something')
|
|
|
|
input_instance._validator.validate = mock.Mock(
|
|
input_instance._validator.validate, side_effect=ValueError('foo'))
|
|
|
|
with self.assertRaises(ValueError):
|
|
input_instance._validate()
|
|
|
|
def test_nonhideable_default_state(self):
|
|
sample_widget = QtWidgets.QWidget()
|
|
sample_widget.setLayout(QtWidgets.QGridLayout())
|
|
input_instance = self.__class__.create_input(
|
|
label='some_label', hideable=False)
|
|
input_instance.add_to(sample_widget.layout())
|
|
sample_widget.show()
|
|
|
|
self.assertEqual(input_instance.hideable, False)
|
|
self.assertEqual(input_instance.hidden(), False)
|
|
|
|
for widget, hidden in zip(input_instance.widgets,
|
|
[False, False, False, False, False]):
|
|
if not widget:
|
|
continue
|
|
if not widget.isHidden() == hidden:
|
|
self.fail('Widget %s hidden: %s, expected: %s' % (
|
|
widget, widget.isHidden(), hidden))
|
|
|
|
def test_nonhideable_set_hidden_fails(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='some_label', hideable=False)
|
|
with self.assertRaises(ValueError):
|
|
input_instance.set_hidden(False)
|
|
|
|
def test_hideable_set_hidden(self):
|
|
sample_widget = QtWidgets.QWidget()
|
|
sample_widget.setLayout(QtWidgets.QGridLayout())
|
|
input_instance = self.__class__.create_input(
|
|
label='some_label', hideable=True)
|
|
input_instance.add_to(sample_widget.layout())
|
|
sample_widget.show()
|
|
|
|
self.assertEqual(input_instance.hidden(), True) # default is hidden
|
|
input_instance.set_hidden(False)
|
|
self.assertEqual(input_instance.hidden(), False)
|
|
for widget, hidden in zip(input_instance.widgets,
|
|
[False, False, False, False, False]):
|
|
if not widget:
|
|
continue
|
|
if not widget.isHidden() == hidden:
|
|
self.fail('Widget %s hidden: %s, expected: %s' % (
|
|
widget, widget.isHidden(), hidden))
|
|
|
|
input_instance.set_hidden(True)
|
|
self.assertEqual(input_instance.hidden(), True)
|
|
for widget, hidden in zip(input_instance.widgets,
|
|
[False, False, True, True, True]):
|
|
if not widget:
|
|
continue
|
|
if not widget.isHidden() == hidden:
|
|
self.fail('Widget %s hidden: %s, expected: %s' % (
|
|
widget, widget.isHidden(), hidden))
|
|
|
|
def test_hidden_change_signal(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='some_label', hideable=True)
|
|
callback = mock.Mock()
|
|
input_instance.hidden_changed.connect(callback)
|
|
self.assertEqual(input_instance.hidden(), True)
|
|
|
|
with wait_on_signal(self.qt_app, input_instance.hidden_changed):
|
|
input_instance.set_hidden(False)
|
|
|
|
callback.assert_called_with(True)
|
|
|
|
def test_hidden_when_not_hideable(self):
|
|
"""UI: Verify non-hideable Text input has expected behavior."""
|
|
input_instance = self.__class__.create_input(
|
|
label='Some label', hideable=False)
|
|
|
|
self.assertEqual(input_instance.hideable, False)
|
|
self.assertEqual(input_instance.hidden(), False)
|
|
|
|
with self.assertRaises(ValueError):
|
|
input_instance.set_hidden(True)
|
|
|
|
|
|
class TextTest(GriddedInputTest):
|
|
@staticmethod
|
|
def create_input(*args, **kwargs):
|
|
from natcap.invest.ui.inputs import Text
|
|
return Text(*args, **kwargs)
|
|
|
|
def test_value(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
self.assertEqual(input_instance.value(), '')
|
|
self.assertTrue(isinstance(input_instance.value(), six.text_type))
|
|
|
|
def test_set_value(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
self.assertEqual(input_instance.value(), '')
|
|
input_instance.set_value('foo')
|
|
self.assertEqual(input_instance.value(), u'foo')
|
|
self.assertTrue(isinstance(input_instance.value(), six.text_type))
|
|
|
|
def test_set_value_latin1(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
self.assertEqual(input_instance.value(), '')
|
|
input_instance.set_value('gr\xe9gory') # Latin-1 encoded string
|
|
self.assertEqual(input_instance.value().encode('utf-8'),
|
|
b'gr\xc3\xa9gory')
|
|
self.assertTrue(isinstance(input_instance.value(), six.text_type))
|
|
|
|
def test_set_value_int(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
input_instance.set_value(1)
|
|
self.assertEqual(input_instance.value(), u'1')
|
|
self.assertTrue(isinstance(input_instance.value(), six.text_type))
|
|
|
|
def test_set_value_float(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
input_instance.set_value(3.14159)
|
|
self.assertEqual(input_instance.value(), u'3.14159')
|
|
self.assertTrue(isinstance(input_instance.value(), six.text_type))
|
|
|
|
def test_set_value_when_hideable(self):
|
|
input_instance = self.__class__.create_input(label='text',
|
|
hideable=True)
|
|
self.assertEqual(input_instance.value(), '')
|
|
self.assertEqual(input_instance.hideable, True)
|
|
self.assertEqual(input_instance.hidden(), True)
|
|
input_instance.set_value('foo')
|
|
self.assertEqual(input_instance.value(), u'foo')
|
|
self.assertTrue(isinstance(input_instance.value(), six.text_type))
|
|
self.assertFalse(input_instance.hidden())
|
|
|
|
def test_value_changed_signal_emitted(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
callback = mock.Mock()
|
|
input_instance.value_changed.connect(callback)
|
|
|
|
self.assertEqual(input_instance.value(), '')
|
|
|
|
with wait_on_signal(self.qt_app, input_instance.value_changed):
|
|
input_instance.set_value('foo')
|
|
|
|
callback.assert_called_with(u'foo')
|
|
|
|
def test_textfield_settext(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
|
|
input_instance.textfield.setText('foo')
|
|
self.assertEqual(input_instance.value(), u'foo')
|
|
self.assertTrue(isinstance(input_instance.value(), six.text_type))
|
|
|
|
def test_textfield_settext_signal(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
callback = mock.Mock()
|
|
input_instance.value_changed.connect(callback)
|
|
|
|
with wait_on_signal(self.qt_app, input_instance.value_changed):
|
|
input_instance.textfield.setText('foo')
|
|
|
|
callback.assert_called_with(u'foo')
|
|
|
|
def test_textfield_drag_n_drop(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
|
|
mime_data = QtCore.QMimeData()
|
|
mime_data.setText('Hello world!')
|
|
|
|
event = QtGui.QDragEnterEvent(
|
|
input_instance.textfield.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
|
|
input_instance.textfield.dragEnterEvent(event)
|
|
self.assertEqual(event.isAccepted(), True)
|
|
|
|
def test_textfield_drag_n_drop_urls(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
|
|
mime_data = QtCore.QMimeData()
|
|
mime_data.setText('Hello world!')
|
|
mime_data.setUrls([QtCore.QUrl('/foo/bar')])
|
|
|
|
event = QtGui.QDragEnterEvent(
|
|
input_instance.textfield.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
|
|
input_instance.textfield.dragEnterEvent(event)
|
|
self.assertEqual(event.isAccepted(), False)
|
|
|
|
def test_textfield_drop(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
|
|
mime_data = QtCore.QMimeData()
|
|
mime_data.setText('Hello world!')
|
|
mime_data.setUrls([QtCore.QUrl('/foo/bar')])
|
|
|
|
event = QtGui.QDropEvent(
|
|
input_instance.textfield.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
|
|
input_instance.textfield.dropEvent(event)
|
|
self.assertEqual(event.isAccepted(), True)
|
|
self.assertEqual(input_instance.value(), 'Hello world!')
|
|
|
|
def test_clear(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
input_instance.set_value('foo')
|
|
input_instance.clear()
|
|
self.assertEqual(input_instance.value(), '')
|
|
self.assertEqual(input_instance.valid(), None)
|
|
|
|
|
|
class PathTest(TextTest):
|
|
@staticmethod
|
|
def create_input(*args, **kwargs):
|
|
from natcap.invest.ui.inputs import _Path
|
|
return _Path(*args, **kwargs)
|
|
|
|
def test_path_context_menu_coverage(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
|
|
_callback = mock.Mock()
|
|
input_instance.textfield.textChanged.connect(_callback)
|
|
|
|
event = QtGui.QContextMenuEvent(
|
|
QtGui.QContextMenuEvent.Mouse,
|
|
input_instance.textfield.mapToGlobal(
|
|
input_instance.textfield.pos()))
|
|
|
|
def _click_out_of_contextmenu():
|
|
# In case the popup isn't shown until after the callback is called,
|
|
# we should be sure to wait for when it is shown.
|
|
popup = None
|
|
n_tries = 0
|
|
while True:
|
|
if n_tries > 5:
|
|
raise RuntimeError(
|
|
'Something happened where we could not get the '
|
|
'context menu')
|
|
popup = self.qt_app.activePopupWidget()
|
|
try:
|
|
popup.close()
|
|
break
|
|
except AttributeError:
|
|
# When popup is None
|
|
n_tries += 1
|
|
self.qt_app.processEvents()
|
|
time.sleep(0.25)
|
|
|
|
|
|
QtCore.QTimer.singleShot(25, _click_out_of_contextmenu)
|
|
input_instance.textfield.contextMenuEvent(event)
|
|
|
|
# simulate textchanged signal (expects a bool)
|
|
input_instance.textfield._emit_textchanged(True)
|
|
self.qt_app.processEvents()
|
|
_callback.assert_called_once()
|
|
|
|
def test_path_selected(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
# Only run this test on subclasses of path
|
|
if input_instance.__class__.__name__ != '_Path':
|
|
input_instance.path_select_button.path_selected.emit(u'/tmp/foo')
|
|
self.assertTrue(input_instance.value(), '/tmp/foo')
|
|
|
|
def test_path_dialog_cancelled(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
# Only run this test on subclasses of path
|
|
if input_instance.__class__.__name__ != '_Path':
|
|
# initial value should not be overwritten by the new value.
|
|
input_instance.set_value('some_value')
|
|
|
|
# path is blank when the dialog was cancelled.
|
|
input_instance.path_select_button.path_selected.emit(u'')
|
|
self.assertTrue(input_instance.value(), 'some_value')
|
|
|
|
def test_path_selected_cyrillic(self):
|
|
input_instance = self.__class__.create_input(label='foo')
|
|
# Only run this test on subclasses of path
|
|
if input_instance.__class__.__name__ != '_Path':
|
|
input_instance.set_value(u'/tmp/fooДЖЩя')
|
|
input_instance.path_select_button.path_selected.emit(
|
|
u'/tmp/fooДЖЩя')
|
|
self.assertEqual(input_instance.value(), u'/tmp/fooДЖЩя')
|
|
|
|
def test_textfield_drag_n_drop(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
|
|
mime_data = QtCore.QMimeData()
|
|
mime_data.setText('Hello world!ДЖЩя')
|
|
|
|
event = QtGui.QDragEnterEvent(
|
|
input_instance.textfield.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
|
|
input_instance.textfield.dragEnterEvent(event)
|
|
self.assertEqual(event.isAccepted(), False)
|
|
|
|
def test_textfield_drag_n_drop_urls(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
|
|
mime_data = QtCore.QMimeData()
|
|
mime_data.setText(u'Hello world!ДЖЩя')
|
|
mime_data.setUrls([QtCore.QUrl('/foo/bar/ДЖЩя')])
|
|
|
|
event = QtGui.QDragEnterEvent(
|
|
input_instance.textfield.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
|
|
input_instance.textfield.dragEnterEvent(event)
|
|
self.assertEqual(event.isAccepted(), True)
|
|
|
|
def test_textfield_drop(self):
|
|
pass
|
|
|
|
def test_textfield_drop_windows(self):
|
|
input_instance = self.__class__.create_input(label='text')
|
|
|
|
mime_data = QtCore.QMimeData()
|
|
mime_data.setText(u'Hello world!ДЖЩя')
|
|
# this is what paths look like when Qt receives them.
|
|
mime_data.setUrls([QtCore.QUrl(u'/C:/foo/bar/ДЖЩя')])
|
|
|
|
event = QtGui.QDropEvent(
|
|
input_instance.textfield.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
|
|
with mock.patch('platform.system', return_value='Windows'):
|
|
input_instance.textfield.dropEvent(event)
|
|
|
|
self.assertEqual(event.isAccepted(), True)
|
|
self.assertEqual(input_instance.value(), u'C:/foo/bar/ДЖЩя')
|
|
|
|
def test_textfield_drop_mac(self):
|
|
# NOTE: Mac OS's filesystem is UTF-8.
|
|
input_instance = self.__class__.create_input(label='text')
|
|
|
|
text_path = u'/foo/bar/ДЖЩя'
|
|
mime_data = QtCore.QMimeData()
|
|
mime_data.setText(u'Hello world!ДЖЩя')
|
|
mime_data.setUrls([QtCore.QUrl(text_path)])
|
|
|
|
event = QtGui.QDropEvent(
|
|
input_instance.textfield.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
|
|
with mock.patch('platform.system', return_value='Darwin'):
|
|
with mock.patch('subprocess.Popen') as mock_popen:
|
|
mock_process = mock.Mock()
|
|
mock_process.configure_mock(
|
|
**{'communicate.return_value': [text_path]})
|
|
mock_popen.return_value = mock_process
|
|
|
|
input_instance.textfield.dropEvent(event)
|
|
|
|
self.assertTrue(mock_popen.called)
|
|
self.assertTrue(mock_popen.call_args[0][0].startswith('osascript'))
|
|
self.assertEqual(event.isAccepted(), True)
|
|
self.assertEqual(input_instance.value(), u'/foo/bar/ДЖЩя')
|
|
|
|
|
|
class FolderTest(PathTest):
|
|
@staticmethod
|
|
def create_input(*args, **kwargs):
|
|
from natcap.invest.ui.inputs import Folder
|
|
return Folder(*args, **kwargs)
|
|
|
|
|
|
class FileTest(PathTest):
|
|
@staticmethod
|
|
def create_input(*args, **kwargs):
|
|
from natcap.invest.ui.inputs import File
|
|
return File(*args, **kwargs)
|
|
|
|
|
|
class SaveFileTest(PathTest):
|
|
@staticmethod
|
|
def create_input(*args, **kwargs):
|
|
from natcap.invest.ui.inputs import SaveFile
|
|
return SaveFile(*args, **kwargs)
|
|
|
|
|
|
class CheckboxTest(GriddedInputTest):
|
|
@staticmethod
|
|
def create_input(*args, **kwargs):
|
|
from natcap.invest.ui.inputs import Checkbox
|
|
return Checkbox(*args, **kwargs)
|
|
|
|
def test_value(self):
|
|
input_instance = self.__class__.create_input(label='new_label')
|
|
self.assertEqual(input_instance.value(), False) # default value
|
|
|
|
# set the value using the qt method
|
|
input_instance.checkbox.setChecked(True)
|
|
self.assertEqual(input_instance.value(), True)
|
|
|
|
def test_set_value(self):
|
|
input_instance = self.__class__.create_input(label='new_label')
|
|
self.assertEqual(input_instance.value(), False)
|
|
input_instance.set_value(True)
|
|
self.assertEqual(input_instance.value(), True)
|
|
|
|
def test_value_changed_signal_emitted(self):
|
|
input_instance = self.__class__.create_input(label='new_label')
|
|
callback = mock.Mock()
|
|
input_instance.value_changed.connect(callback)
|
|
self.assertEqual(input_instance.value(), False)
|
|
|
|
with wait_on_signal(self.qt_app, input_instance.value_changed):
|
|
input_instance.set_value(True)
|
|
|
|
callback.assert_called_with(True)
|
|
|
|
def test_value_changed_signal(self):
|
|
input_instance = self.__class__.create_input(label='new_label')
|
|
callback = mock.Mock()
|
|
input_instance.value_changed.connect(callback)
|
|
|
|
with wait_on_signal(self.qt_app, input_instance.value_changed):
|
|
input_instance.value_changed.emit(True)
|
|
|
|
callback.assert_called_with(True)
|
|
|
|
def test_clear(self):
|
|
input_instance = self.__class__.create_input(label='new_label')
|
|
input_instance.set_value(True)
|
|
input_instance.clear()
|
|
self.assertEqual(input_instance.value(), False)
|
|
|
|
def test_valid(self):
|
|
input_instance = self.__class__.create_input(label='new_label')
|
|
self.assertEqual(input_instance.value(), False)
|
|
self.assertEqual(input_instance.valid(), True)
|
|
input_instance.set_value(True)
|
|
self.assertEqual(input_instance.valid(), True)
|
|
|
|
def test_validate_required_validator(self):
|
|
# Override from GriddedInputTest, as checkbox is always valid.
|
|
from natcap.invest.ui import inputs
|
|
input_instance = self.__class__.create_input(
|
|
label='some_label', args_key='foo'
|
|
)
|
|
|
|
input_instance.value = mock.Mock(
|
|
input_instance, return_value=u'something')
|
|
|
|
# Verify we're starting with an unvalidated input
|
|
self.assertEqual(input_instance.valid(), True)
|
|
with warnings.catch_warnings(record=True) as messages:
|
|
input_instance._validate()
|
|
time.sleep(0.25) # wait for warnings to register
|
|
self.qt_app.processEvents()
|
|
|
|
# Validation still passes, but verify warning raised
|
|
self.assertEqual(len(messages), 1)
|
|
self.assertEqual(input_instance.valid(), True)
|
|
|
|
def test_validate_missing_args_key(self):
|
|
# Override from GriddedInputTest, as checkbox is always valid.
|
|
from natcap.invest.ui import inputs
|
|
input_instance = self.__class__.create_input(
|
|
label='some_label')
|
|
|
|
input_instance.value = mock.Mock(
|
|
input_instance, return_value=u'something')
|
|
|
|
# Verify we're starting with an unvalidated input
|
|
self.assertEqual(input_instance.valid(), True)
|
|
with warnings.catch_warnings(record=True) as messages:
|
|
input_instance._validate()
|
|
time.sleep(0.25) # wait for warnings to register
|
|
self.qt_app.processEvents()
|
|
|
|
# Validation still passes
|
|
self.assertEqual(input_instance.valid(), True)
|
|
|
|
|
|
def test_label(self):
|
|
# Override, sinve 'Optional' is irrelevant for Checkbox.
|
|
pass
|
|
|
|
def test_validator(self):
|
|
pass
|
|
|
|
def test_validate_required(self):
|
|
pass
|
|
|
|
def test_validate_passes(self):
|
|
pass
|
|
|
|
def test_validate_fails(self):
|
|
pass
|
|
|
|
def test_required(self):
|
|
pass
|
|
|
|
def test_set_required(self):
|
|
pass
|
|
|
|
def test_nonrequired(self):
|
|
pass
|
|
|
|
def test_nonhideable_set_hidden_fails(self):
|
|
pass
|
|
|
|
def test_nonhideable_default_state(self):
|
|
pass
|
|
|
|
def test_label_required(self):
|
|
pass
|
|
|
|
def test_hideable_set_hidden(self):
|
|
pass
|
|
|
|
def test_hidden_when_not_hideable(self):
|
|
pass
|
|
|
|
def test_hidden_change_signal(self):
|
|
pass
|
|
|
|
def test_validate_required_args_key(self):
|
|
pass
|
|
|
|
def test_validate_error(self):
|
|
pass
|
|
|
|
|
|
class DropdownTest(GriddedInputTest):
|
|
@staticmethod
|
|
def create_input(*args, **kwargs):
|
|
from natcap.invest.ui.inputs import Dropdown
|
|
return Dropdown(*args, **kwargs)
|
|
|
|
def test_options(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=('foo', 'bar', 'baz'))
|
|
self.assertEqual(input_instance.options, [u'foo', u'bar', u'baz'])
|
|
|
|
def test_options_with_return_value_map(self):
|
|
return_value_map = {'foo': 1, 'bar': 2, 'baz': 3}
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=('foo', 'bar', 'baz'),
|
|
return_value_map=return_value_map)
|
|
self.assertEqual(input_instance.options, [u'foo', u'bar', u'baz'])
|
|
self.assertEqual(input_instance.return_value_map,
|
|
{'foo': '1', 'bar': '2', 'baz': '3'})
|
|
|
|
def test_options_return_value_mismatch(self):
|
|
with self.assertRaises(ValueError):
|
|
self.__class__.create_input(
|
|
label='label', options=(1, 2, 3),
|
|
return_value_map={'foo': 4, 1: 'bar'})
|
|
|
|
def test_options_typecast(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=(1, 2, 3))
|
|
self.assertEqual(input_instance.options, [u'1', u'2', u'3'])
|
|
|
|
def test_set_options_unicode(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=(u'Þingvellir',))
|
|
self.assertEqual(input_instance.options, [u'Þingvellir'])
|
|
|
|
def test_clear(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=('foo', 'bar', 'baz'))
|
|
input_instance.set_value('bar')
|
|
self.assertEqual(input_instance.value(), 'bar')
|
|
input_instance.clear()
|
|
self.assertEqual(input_instance.value(), 'foo')
|
|
|
|
def test_clear_no_options(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=())
|
|
try:
|
|
input_instance.clear()
|
|
except Exception as e:
|
|
self.fail("Unexpected exception: %s" % repr(e))
|
|
|
|
def test_set_value(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=('foo', 'bar', 'baz'))
|
|
input_instance.set_value('foo')
|
|
self.assertEqual(input_instance.value(), u'foo')
|
|
|
|
def test_set_value_noncast(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=(1, 2, 3))
|
|
input_instance.set_value(1)
|
|
self.assertEqual(input_instance.value(), u'1')
|
|
|
|
def test_set_value_not_in_options(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=(1, 2, 3))
|
|
with self.assertRaises(ValueError):
|
|
input_instance.set_value('foo')
|
|
|
|
def test_set_value_from_return_map(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=(1, 2, 3),
|
|
return_value_map={1: 'a', 2: 'b', 3: 'c'}
|
|
)
|
|
input_instance.set_value('a')
|
|
self.assertEqual(input_instance.value(), 'a')
|
|
self.assertEqual(input_instance.dropdown.currentIndex(), 0)
|
|
|
|
input_instance.set_value(3)
|
|
self.assertEqual(input_instance.value(), 'c')
|
|
self.assertEqual(input_instance.dropdown.currentIndex(), 2)
|
|
|
|
def test_value(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=('foo', 'bar', 'baz'))
|
|
self.assertEqual(input_instance.value(), u'foo')
|
|
self.assertTrue(isinstance(input_instance.value(), six.text_type))
|
|
|
|
def test_value_changed_signal_emitted(self):
|
|
input_instance = self.__class__.create_input(
|
|
label='label', options=('foo', 'bar', 'baz'))
|
|
callback = mock.Mock()
|
|
input_instance.value_changed.connect(callback)
|
|
self.assertEqual(input_instance.value(), u'foo')
|
|
|
|
with wait_on_signal(self.qt_app, input_instance.value_changed):
|
|
input_instance.set_value('bar')
|
|
|
|
callback.assert_called_with('bar')
|
|
|
|
def test_label(self):
|
|
# Override, since 'Optional' is irrelevant for Dropdown.
|
|
pass
|
|
|
|
def test_validator(self):
|
|
pass
|
|
|
|
def test_validate_required(self):
|
|
pass
|
|
|
|
def test_validate_passes(self):
|
|
pass
|
|
|
|
def test_validate_fails(self):
|
|
pass
|
|
|
|
def test_required(self):
|
|
pass
|
|
|
|
def test_set_required(self):
|
|
pass
|
|
|
|
def test_nonrequired(self):
|
|
pass
|
|
|
|
def test_label_required(self):
|
|
pass
|
|
|
|
def test_validate_required_args_key(self):
|
|
pass
|
|
|
|
def test_validate_error(self):
|
|
pass
|
|
|
|
def test_validate_missing_args_key(self):
|
|
pass
|
|
|
|
def test_validate_required_validator(self):
|
|
pass
|
|
|
|
|
|
class LabelTest(_QtTest):
|
|
def test_add_to_layout(self):
|
|
from natcap.invest.ui.inputs import Label
|
|
|
|
super_widget = QtWidgets.QWidget()
|
|
super_widget.setLayout(QtWidgets.QGridLayout())
|
|
label = Label('Hello, World!')
|
|
label.add_to(super_widget.layout())
|
|
|
|
|
|
class ContainerTest(InVESTModelInputTest):
|
|
@staticmethod
|
|
def create_input(*args, **kwargs):
|
|
from natcap.invest.ui.inputs import Container
|
|
return Container(*args, **kwargs)
|
|
|
|
def test_expandable(self):
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
expandable=False)
|
|
|
|
self.assertEqual(input_instance.expandable, False)
|
|
self.assertEqual(input_instance.expanded, True)
|
|
|
|
input_instance.expandable = True
|
|
self.assertEqual(input_instance.expandable, True)
|
|
|
|
def test_expanded(self):
|
|
from natcap.invest.ui import inputs
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
expandable=True,
|
|
expanded=True)
|
|
input_instance.show()
|
|
|
|
# Add an input so we can text that the input becomes visible.
|
|
contained_input = inputs.Text(label='some text!')
|
|
input_instance.add_input(contained_input)
|
|
|
|
self.assertEqual(input_instance.expandable, True)
|
|
self.assertEqual(input_instance.expanded, True)
|
|
|
|
input_instance.expanded = False
|
|
self.assertEqual(input_instance.expanded, False)
|
|
|
|
def test_clear(self):
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
expandable=True)
|
|
input_instance.expanded = True
|
|
input_instance.clear()
|
|
self.assertEqual(input_instance.expanded, False)
|
|
|
|
def test_value_changed_signal(self):
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
expandable=True)
|
|
callback = mock.Mock()
|
|
input_instance.value_changed.connect(callback)
|
|
|
|
with wait_on_signal(self.qt_app, input_instance.value_changed):
|
|
input_instance.value_changed.emit(True)
|
|
|
|
callback.assert_called_with(True)
|
|
|
|
def test_value_changed_signal_emitted(self):
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
expandable=True,
|
|
expanded=False)
|
|
callback = mock.Mock()
|
|
input_instance.value_changed.connect(callback)
|
|
self.assertEqual(input_instance.value(), False)
|
|
|
|
with wait_on_signal(self.qt_app, input_instance.value_changed):
|
|
input_instance.set_value(True)
|
|
|
|
callback.assert_called_with(True)
|
|
|
|
def test_value(self):
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
expandable=True)
|
|
|
|
input_instance.setChecked(False)
|
|
self.assertEqual(input_instance.value(), False)
|
|
input_instance.setChecked(True)
|
|
self.assertEqual(input_instance.value(), True)
|
|
|
|
def test_set_value(self):
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
expandable=True,
|
|
expanded=False)
|
|
|
|
self.assertEqual(input_instance.value(), False)
|
|
input_instance.set_value(True)
|
|
self.assertEqual(input_instance.value(), True)
|
|
|
|
def test_set_value_nonexpandable(self):
|
|
input_instance = self.__class__.create_input(label='foo',
|
|
expandable=False)
|
|
with self.assertRaises(ValueError):
|
|
input_instance.set_value(False)
|
|
|
|
def test_helptext(self):
|
|
pass
|
|
|
|
def test_nonrequired(self):
|
|
pass
|
|
|
|
def test_required(self):
|
|
pass
|
|
|
|
def test_set_required(self):
|
|
pass
|
|
|
|
|
|
class ValidationWorkerTest(_QtTest):
|
|
def test_run(self):
|
|
from natcap.invest.ui.inputs import ValidationWorker
|
|
_callable = mock.Mock(return_value=[])
|
|
worker = ValidationWorker(
|
|
target=_callable,
|
|
args={'foo': 'bar'},
|
|
limit_to='foo')
|
|
worker.start()
|
|
while not worker.isFinished():
|
|
QTest.qWait(50)
|
|
self.assertEqual(worker.warnings, [])
|
|
self.assertEqual(worker.error, None)
|
|
|
|
def test_error(self):
|
|
from natcap.invest.ui.inputs import ValidationWorker
|
|
_callable = mock.Mock(side_effect=KeyError('missing'))
|
|
worker = ValidationWorker(
|
|
target=_callable,
|
|
args={'foo': 'bar'},
|
|
limit_to='foo')
|
|
worker.start()
|
|
while not worker.isFinished():
|
|
QTest.qWait(50)
|
|
self.assertEqual(worker.warnings, [])
|
|
self.assertEqual(worker.error, "'missing'")
|
|
|
|
|
|
class FileButtonTest(_QtTest):
|
|
def test_button_clicked(self):
|
|
from natcap.invest.ui.inputs import FileButton
|
|
button = FileButton('Some title')
|
|
|
|
# Patch up the open_method to return a known path.
|
|
# Would block on user input otherwise.
|
|
button.open_method = mock.Mock(return_value='/some/path')
|
|
_callback = mock.Mock()
|
|
button.path_selected.connect(_callback)
|
|
|
|
QTest.mouseClick(button, QtCore.Qt.LeftButton)
|
|
self.qt_app.processEvents()
|
|
|
|
_callback.assert_called_with('/some/path')
|
|
self.qt_app.processEvents()
|
|
|
|
def test_button_title(self):
|
|
from natcap.invest.ui.inputs import FileButton
|
|
button = FileButton('Some title')
|
|
self.assertEqual(button.dialog_title, 'Some title')
|
|
|
|
|
|
class FolderButtonTest(_QtTest):
|
|
def test_button_clicked(self):
|
|
from natcap.invest.ui.inputs import FolderButton
|
|
button = FolderButton('Some title')
|
|
|
|
# Patch up the open_method to return a known path.
|
|
# Would block on user input otherwise.
|
|
button.open_method = mock.Mock(return_value='/some/path')
|
|
_callback = mock.Mock()
|
|
button.path_selected.connect(_callback)
|
|
|
|
QTest.mouseClick(button, QtCore.Qt.LeftButton)
|
|
self.qt_app.processEvents()
|
|
|
|
_callback.assert_called_with('/some/path')
|
|
|
|
def test_button_title(self):
|
|
from natcap.invest.ui.inputs import FolderButton
|
|
button = FolderButton('Some title')
|
|
self.assertEqual(button.dialog_title, 'Some title')
|
|
|
|
|
|
class FileDialogTest(_SettingsSandbox):
|
|
def test_save_file_title_and_last_selection(self):
|
|
from natcap.invest.ui.inputs import FileDialog, INVEST_SETTINGS
|
|
dialog = FileDialog()
|
|
dialog.file_dialog.getSaveFileName = mock.Mock(
|
|
spec=dialog.file_dialog.getSaveFileName,
|
|
return_value='/new/file')
|
|
|
|
INVEST_SETTINGS.setValue('last_dir', '/tmp/foo/bar')
|
|
|
|
out_file = dialog.save_file(title='foo', start_dir=None)
|
|
self.assertEqual(
|
|
dialog.file_dialog.getSaveFileName.call_args[0], # pos. args
|
|
(dialog.file_dialog, 'foo', '/tmp/foo/bar'))
|
|
self.assertEqual(out_file, '/new/file')
|
|
self.assertEqual(INVEST_SETTINGS.value('last_dir', ''),
|
|
u'/new')
|
|
|
|
def test_save_file_defined_savefile(self):
|
|
from natcap.invest.ui.inputs import FileDialog
|
|
dialog = FileDialog()
|
|
dialog.file_dialog.getSaveFileName = mock.Mock(
|
|
spec=dialog.file_dialog.getSaveFileName,
|
|
return_value=os.path.join('/new','file'))
|
|
|
|
out_file = dialog.save_file(title='foo', start_dir='/tmp',
|
|
savefile='file.txt')
|
|
self.assertEqual(
|
|
dialog.file_dialog.getSaveFileName.call_args[0], # pos. args
|
|
(dialog.file_dialog, 'foo', os.path.join('/tmp', 'file.txt')))
|
|
|
|
self.assertEqual(out_file, os.path.join('/new', 'file'))
|
|
|
|
def test_open_file_qt5(self):
|
|
from natcap.invest.ui.inputs import FileDialog, INVEST_SETTINGS
|
|
dialog = FileDialog()
|
|
|
|
# patch up the Qt method to get the path to the file to open
|
|
# Qt4 and Qt5 have different return values. Mock up accordingly.
|
|
# Simulate Qt5 return value.
|
|
try:
|
|
_old_qtpy_version = qtpy.QT_VERSION
|
|
qtpy.QT_VERSION = ('5', '5', '5')
|
|
dialog.file_dialog.getOpenFileName = mock.Mock(
|
|
spec=dialog.file_dialog.getOpenFileName,
|
|
return_value=('/new/file', 'filter'))
|
|
|
|
INVEST_SETTINGS.setValue('last_dir', '/tmp/foo/bar')
|
|
out_file = dialog.open_file(title='foo')
|
|
finally:
|
|
qtpy.QT_VERSION = _old_qtpy_version
|
|
|
|
self.assertEqual(
|
|
dialog.file_dialog.getOpenFileName.call_args[0], # pos. args
|
|
(dialog.file_dialog, 'foo', '/tmp/foo/bar', ''))
|
|
self.assertEqual(out_file, '/new/file')
|
|
self.assertEqual(INVEST_SETTINGS.value('last_dir', ''), '/new')
|
|
|
|
def test_open_file_qt4(self):
|
|
from natcap.invest.ui.inputs import FileDialog, INVEST_SETTINGS
|
|
dialog = FileDialog()
|
|
|
|
# patch up the Qt method to get the path to the file to open
|
|
# Qt4 and Qt5 have different return values. Mock up accordingly.
|
|
# Simulate Qt4 return value.
|
|
try:
|
|
_old_qtpy_version = qtpy.QT_VERSION
|
|
qtpy.QT_VERSION = ('4', '5', '6')
|
|
dialog.file_dialog.getOpenFileName = mock.Mock(
|
|
spec=dialog.file_dialog.getOpenFileName,
|
|
return_value='/new/file')
|
|
|
|
INVEST_SETTINGS.setValue('last_dir', '/tmp/foo/bar')
|
|
out_file = dialog.open_file(title='foo')
|
|
finally:
|
|
qtpy.QT_VERSION = _old_qtpy_version
|
|
|
|
self.assertEqual(
|
|
dialog.file_dialog.getOpenFileName.call_args[0], # pos. args
|
|
(dialog.file_dialog, 'foo', '/tmp/foo/bar', ''))
|
|
self.assertEqual(out_file, '/new/file')
|
|
self.assertEqual(INVEST_SETTINGS.value('last_dir', ''), '/new')
|
|
|
|
def test_open_folder(self):
|
|
from natcap.invest.ui.inputs import FileDialog, INVEST_SETTINGS
|
|
dialog = FileDialog()
|
|
|
|
# patch up the Qt method to get the path to the file to open
|
|
dialog.file_dialog.getExistingDirectory = mock.Mock(
|
|
spec=dialog.file_dialog.getExistingDirectory,
|
|
return_value='/existing/folder')
|
|
|
|
INVEST_SETTINGS.setValue('last_dir', '/tmp/foo/bar')
|
|
new_folder = dialog.open_folder('foo', start_dir=None)
|
|
|
|
self.assertEqual(dialog.file_dialog.getExistingDirectory.call_args[0],
|
|
(dialog.file_dialog, 'Select folder: foo', '/tmp/foo/bar'))
|
|
self.assertEqual(new_folder, '/existing/folder')
|
|
self.assertEqual(INVEST_SETTINGS.value('last_dir', ''),
|
|
'/existing/folder')
|
|
|
|
|
|
class InfoButtonTest(_QtTest):
|
|
def test_buttonpress(self):
|
|
from natcap.invest.ui.inputs import InfoButton
|
|
button = InfoButton('some text')
|
|
self.assertEqual(button.whatsThis(), 'some text')
|
|
|
|
# Necessary to mock up the QWhatsThis module because it always
|
|
# segfaults in a test if I don't. Haven't yet been able to figure out
|
|
# why or how to work around, and this allows me to have the test
|
|
# coverage.
|
|
with mock.patch('qtpy.QtWidgets.QWhatsThis'):
|
|
button.show()
|
|
QTest.mouseClick(button, QtCore.Qt.LeftButton)
|
|
|
|
|
|
class FormTest(_QtTest):
|
|
@staticmethod
|
|
def validate(args, limit_to=None):
|
|
return []
|
|
|
|
@staticmethod
|
|
def execute(args, limit_to=None):
|
|
pass
|
|
|
|
@staticmethod
|
|
def make_ui():
|
|
from natcap.invest.ui.inputs import Form
|
|
|
|
return Form()
|
|
|
|
def test_run_button_pressed(self):
|
|
form = FormTest.make_ui()
|
|
mock_object = mock.Mock()
|
|
form.submitted.connect(mock_object)
|
|
|
|
QTest.mouseClick(form.run_button,
|
|
QtCore.Qt.LeftButton)
|
|
|
|
self.qt_app.processEvents()
|
|
mock_object.assert_called_once()
|
|
|
|
def test_run_noerror(self):
|
|
form = FormTest.make_ui()
|
|
def _target():
|
|
return
|
|
with wait_on_signal(self.qt_app, form.run_finished, timeout=250):
|
|
form.run(target=_target)
|
|
|
|
self.qt_app.processEvents()
|
|
# At the end of the run, the button should be visible.
|
|
self.assertTrue(form.run_dialog.openWorkspaceButton.isVisible())
|
|
|
|
# close the window by pressing the back button.
|
|
QTest.mouseClick(form.run_dialog.backButton,
|
|
QtCore.Qt.LeftButton)
|
|
|
|
def test_run_target_error(self):
|
|
form = FormTest.make_ui()
|
|
with self.assertRaises(ValueError):
|
|
form.run(target='str does not have a __call__()')
|
|
|
|
def test_open_workspace_on_success(self):
|
|
class _SampleTarget(object):
|
|
@staticmethod
|
|
def validate(args, limit_to=None):
|
|
return []
|
|
|
|
@staticmethod
|
|
def execute(args):
|
|
pass
|
|
|
|
form = FormTest.make_ui()
|
|
target = _SampleTarget().execute
|
|
|
|
# patch open_workspace to avoid lots of open file dialogs.
|
|
with mock.patch('natcap.invest.ui.inputs.open_workspace',
|
|
mock.Mock(return_value=None)) as open_workspace:
|
|
with wait_on_signal(self.qt_app, form.run_finished):
|
|
form.run(target=target)
|
|
|
|
self.assertTrue(form.run_dialog.openWorkspaceCB.isVisible())
|
|
self.assertFalse(
|
|
form.run_dialog.openWorkspaceButton.isVisible())
|
|
|
|
form.run_dialog.openWorkspaceCB.setChecked(True)
|
|
self.assertTrue(form.run_dialog.openWorkspaceCB.isChecked())
|
|
|
|
def _close_modal_dialog():
|
|
# close the window by pressing the back button.
|
|
QTest.mouseClick(form.run_dialog.backButton,
|
|
QtCore.Qt.LeftButton)
|
|
|
|
QtCore.QTimer.singleShot(0, _close_modal_dialog)
|
|
self.qt_app.processEvents()
|
|
open_workspace.assert_called()
|
|
|
|
def test_run_prevent_dialog_close_esc(self):
|
|
thread_event = threading.Event()
|
|
|
|
class _SampleTarget(object):
|
|
@staticmethod
|
|
def validate(args, limit_to=None):
|
|
return []
|
|
|
|
@staticmethod
|
|
def execute(args):
|
|
thread_event.wait()
|
|
|
|
target_mod = _SampleTarget().execute
|
|
form = FormTest.make_ui()
|
|
form.run(target=target_mod)
|
|
QTest.keyPress(form.run_dialog, QtCore.Qt.Key_Escape)
|
|
self.assertTrue(form.run_dialog.isVisible())
|
|
|
|
# when the execute function finishes, pressing escape should
|
|
# close the window.
|
|
thread_event.set()
|
|
QTest.keyPress(form.run_dialog, QtCore.Qt.Key_Escape)
|
|
self.assertEqual(form.run_dialog.result(), QtWidgets.QDialog.Rejected)
|
|
self.assertEqual(form.run_dialog.result(), QtWidgets.QDialog.Rejected)
|
|
|
|
def test_run_prevent_dialog_close_event(self):
|
|
thread_event = threading.Event()
|
|
|
|
class _SampleTarget(object):
|
|
@staticmethod
|
|
def validate(args, limit_to=None):
|
|
return []
|
|
|
|
@staticmethod
|
|
def execute(args):
|
|
thread_event.wait()
|
|
|
|
form = FormTest.make_ui()
|
|
target_mod = _SampleTarget().execute
|
|
try:
|
|
form.run(target=target_mod, kwargs={'args': {'a': 1}})
|
|
if self.qt_app.hasPendingEvents():
|
|
self.qt_app.processEvents()
|
|
self.assertTrue(form.run_dialog.isVisible())
|
|
form.run_dialog.close()
|
|
if self.qt_app.hasPendingEvents():
|
|
self.qt_app.processEvents()
|
|
self.assertTrue(form.run_dialog.isVisible())
|
|
|
|
# when the execute function finishes, pressing escape should
|
|
# close the window.
|
|
thread_event.set()
|
|
form._thread.join()
|
|
if self.qt_app.hasPendingEvents():
|
|
self.qt_app.processEvents()
|
|
form.run_dialog.close()
|
|
if self.qt_app.hasPendingEvents():
|
|
self.qt_app.processEvents()
|
|
self.assertFalse(form.run_dialog.isVisible())
|
|
except Exception as error:
|
|
LOGGER.exception('Something failed')
|
|
# If something happens while executing, be sure the thread executes
|
|
# cleanly.
|
|
thread_event.set()
|
|
form._thread.join()
|
|
self.fail(error)
|
|
|
|
def test_run_error(self):
|
|
class _SampleTarget(object):
|
|
@staticmethod
|
|
def validate(args, limit_to=None):
|
|
return []
|
|
|
|
@staticmethod
|
|
def execute(args):
|
|
raise RuntimeError('Something broke!')
|
|
|
|
target_mod = _SampleTarget().execute
|
|
form = FormTest.make_ui()
|
|
form.run(target=target_mod, kwargs={'args': {}})
|
|
form._thread.join()
|
|
if self.qt_app.hasPendingEvents():
|
|
self.qt_app.processEvents()
|
|
|
|
self.assertTrue('encountered' in form.run_dialog.messageArea.text())
|
|
|
|
def test_show(self):
|
|
form = FormTest.make_ui()
|
|
form.show()
|
|
|
|
def test_resize_scrollbar(self):
|
|
form = FormTest.make_ui()
|
|
form.show()
|
|
self.assertTrue('border: None' in form.scroll_area.styleSheet())
|
|
form.scroll_area.update_scroll_border(50, 50) # simulate form resize
|
|
self.assertTrue(len(form.scroll_area.styleSheet()) == 0)
|
|
|
|
def test_add_input(self):
|
|
from natcap.invest.ui import inputs
|
|
form = FormTest.make_ui()
|
|
text_input = inputs.Text('hello there')
|
|
form.add_input(text_input)
|
|
|
|
|
|
class OpenWorkspaceTest(_QtTest):
|
|
def test_windows(self):
|
|
from natcap.invest.ui.inputs import open_workspace
|
|
with mock.patch('natcap.invest.ui.inputs.subprocess.Popen') as method:
|
|
with mock.patch('platform.system', return_value='Windows'):
|
|
with mock.patch('os.path.normpath', return_value='/foo\\bar'):
|
|
open_workspace(os.path.join('/foo', 'bar'))
|
|
method.assert_called_with('explorer "/foo\\bar"')
|
|
|
|
def test_mac(self):
|
|
from natcap.invest.ui.inputs import open_workspace
|
|
with mock.patch('natcap.invest.ui.inputs.subprocess.Popen') as method:
|
|
with mock.patch('platform.system', return_value='Darwin'):
|
|
with mock.patch('os.path.normpath', return_value='/foo/bar'):
|
|
open_workspace('/foo/bar')
|
|
method.assert_called_with('open /foo/bar', shell=True)
|
|
|
|
def test_linux(self):
|
|
from natcap.invest.ui.inputs import open_workspace
|
|
with mock.patch('natcap.invest.ui.inputs.subprocess.Popen') as method:
|
|
with mock.patch('platform.system', return_value='Linux'):
|
|
with mock.patch('os.path.normpath', return_value='/foo/bar'):
|
|
open_workspace('/foo/bar')
|
|
method.assert_called_with(['xdg-open', '/foo/bar'])
|
|
|
|
def test_error_in_subprocess(self):
|
|
from natcap.invest.ui.inputs import open_workspace
|
|
popen_import_path = 'natcap.invest.ui.inputs.subprocess.Popen'
|
|
normpath_import_path = 'natcap.invest.ui.inputs.os.path.normpath'
|
|
with mock.patch(popen_import_path,
|
|
side_effect=OSError('error message')) as patch:
|
|
with mock.patch(normpath_import_path, return_value='/foo/bar'):
|
|
open_workspace('/foo/bar')
|
|
patch.assert_called()
|
|
|
|
|
|
@contextlib.contextmanager
|
|
def _capture_logging_messages(logger):
|
|
"""Capture logging messages and return them as a list.
|
|
|
|
This context manager also sets the root logger's level to
|
|
``logging.NOTSET`` and restores the former log level on completion of the
|
|
context being managed.
|
|
|
|
Args:
|
|
logger (logging.Logger): A logger instance from python's logging system
|
|
from which log records should be captured.
|
|
|
|
Returns:
|
|
``None``."""
|
|
# The root logger has a default level of WARNING, but for tests we
|
|
# want to capture everything.
|
|
root_logger = logging.getLogger()
|
|
root_logger_prior_level = root_logger.level
|
|
root_logger.setLevel(logging.NOTSET)
|
|
|
|
log_queue = queue.Queue()
|
|
log_queue_handler = logging.handlers.QueueHandler(log_queue)
|
|
log_queue_handler.setLevel(logging.NOTSET) # capture all logging
|
|
logger.addHandler(log_queue_handler)
|
|
|
|
records = []
|
|
try:
|
|
yield records
|
|
finally:
|
|
# No matter what happens, always restore root logger level.
|
|
root_logger.setLevel(root_logger_prior_level)
|
|
|
|
try:
|
|
while True:
|
|
records.append(log_queue.get_nowait())
|
|
except queue.Empty:
|
|
logger.removeHandler(log_queue_handler)
|
|
|
|
|
|
class ExecutionTest(_QtTest):
|
|
def test_executor_run(self):
|
|
from natcap.invest.ui.execution import Executor
|
|
from natcap.invest.ui.execution import LOGGER as execution_logger
|
|
|
|
thread_event = threading.Event()
|
|
|
|
def _waiting_func(*args, **kwargs):
|
|
thread_event.wait()
|
|
|
|
target = mock.Mock(wraps=_waiting_func)
|
|
callback = mock.Mock()
|
|
args = ('a', 'b', 'c')
|
|
kwargs = {'d': 1, 'e': 2, 'f': 3}
|
|
|
|
executor = Executor(
|
|
target=target,
|
|
args=args,
|
|
kwargs=kwargs,
|
|
log_events=False) # produce no extra logging
|
|
|
|
self.assertEqual(executor.target, target)
|
|
self.assertEqual(executor.args, args)
|
|
self.assertEqual(executor.kwargs, kwargs)
|
|
|
|
# register the callback with the finished signal.
|
|
executor.finished.connect(callback)
|
|
|
|
with _capture_logging_messages(execution_logger) as log_records:
|
|
executor.start()
|
|
thread_event.set()
|
|
executor.join()
|
|
if self.qt_app.hasPendingEvents():
|
|
self.qt_app.processEvents()
|
|
|
|
callback.assert_called_once()
|
|
target.assert_called_once()
|
|
target.assert_called_with(*args, **kwargs)
|
|
|
|
self.assertEqual(len(log_records), 0)
|
|
|
|
def test_executor_exception(self):
|
|
from natcap.invest.ui.execution import Executor
|
|
from natcap.invest.ui.execution import LOGGER as execution_logger
|
|
|
|
thread_event = threading.Event()
|
|
|
|
def _waiting_func(*args, **kwargs):
|
|
thread_event.wait()
|
|
raise ValueError('Some demo exception')
|
|
|
|
target = mock.Mock(wraps=_waiting_func)
|
|
callback = mock.Mock()
|
|
args = ('a', 'b', 'c')
|
|
kwargs = {'d': 1, 'e': 2, 'f': 3}
|
|
|
|
executor = Executor(
|
|
target=target,
|
|
args=args,
|
|
kwargs=kwargs)
|
|
|
|
self.assertEqual(executor.target, target)
|
|
self.assertEqual(executor.args, args)
|
|
self.assertEqual(executor.kwargs, kwargs)
|
|
|
|
# register the callback with the finished signal.
|
|
executor.finished.connect(callback)
|
|
|
|
with _capture_logging_messages(execution_logger) as log_records:
|
|
executor.start()
|
|
thread_event.set()
|
|
executor.join()
|
|
if self.qt_app.hasPendingEvents():
|
|
self.qt_app.processEvents()
|
|
|
|
callback.assert_called_once()
|
|
target.assert_called_once()
|
|
target.assert_called_with(*args, **kwargs)
|
|
|
|
self.assertTrue(executor.failed)
|
|
self.assertEqual(str(executor.exception),
|
|
'Some demo exception')
|
|
self.assertTrue(isinstance(executor.traceback, basestring))
|
|
|
|
self.assertEqual(len(log_records), 2)
|
|
self.assertIn('failed with exception', log_records[0].msg)
|
|
self.assertIn('Execution finished', log_records[1].msg)
|
|
|
|
def test_default_args(self):
|
|
from natcap.invest.ui.execution import Executor
|
|
|
|
executor = Executor(target=mock.Mock())
|
|
|
|
# We didn't define args or kwargs (which default to None), so verify
|
|
# that the parameters are set correctly.
|
|
self.assertEqual(executor.args, ())
|
|
self.assertEqual(executor.kwargs, {})
|
|
|
|
|
|
class IntegrationTests(_QtTest):
|
|
def test_checkbox_enables_collapsible_container(self):
|
|
from natcap.invest.ui import inputs
|
|
checkbox = inputs.Checkbox(label='Trigger')
|
|
container = inputs.Container(label='Container',
|
|
expandable=True,
|
|
expanded=False,
|
|
interactive=False)
|
|
# Interactivity of the contained file is dependent upon the
|
|
# collapsed state of the container.
|
|
contained_file = inputs.File(label='some file input')
|
|
container.add_input(contained_file)
|
|
checkbox.value_changed.connect(container.set_interactive)
|
|
|
|
# Assert everything starts out fine.
|
|
self.assertTrue(checkbox.interactive)
|
|
self.assertFalse(container.interactive)
|
|
self.assertFalse(container.expanded)
|
|
self.assertFalse(contained_file.interactive)
|
|
self.assertFalse(contained_file.visible())
|
|
|
|
# When the checkbox is enabled, the container should become enabled,
|
|
# but the container's contained widgets should still be noninteractive
|
|
checkbox.set_value(True)
|
|
if self.qt_app.hasPendingEvents():
|
|
self.qt_app.processEvents()
|
|
|
|
self.assertTrue(container.interactive)
|
|
self.assertFalse(container.expanded)
|
|
self.assertFalse(contained_file.interactive)
|
|
self.assertFalse(contained_file.visible())
|
|
|
|
# When the container is expanded, the contained input should become
|
|
# interactive and visible
|
|
container.set_value(True)
|
|
if self.qt_app.hasPendingEvents():
|
|
self.qt_app.processEvents()
|
|
|
|
self.assertTrue(container.interactive)
|
|
self.assertTrue(container.expanded)
|
|
self.assertTrue(contained_file.interactive)
|
|
self.assertTrue(contained_file.visible())
|
|
|
|
|
|
class OptionsDialogTest(_QtTest):
|
|
def test_postprocess_not_implemented_coverage(self):
|
|
"""UI OptionsDialog: Coverage for postprocess method."""
|
|
from natcap.invest.ui import model
|
|
from natcap.invest.ui import inputs
|
|
|
|
options_dialog = model.OptionsDialog()
|
|
options_dialog.open()
|
|
options_dialog.accept()
|
|
self.qt_app.processEvents()
|
|
|
|
def test_postprocess_not_implemented(self):
|
|
"""UI OptionsDialog: postprocess() raises NotImplementedError."""
|
|
from natcap.invest.ui import model
|
|
|
|
options_dialog = model.OptionsDialog()
|
|
with self.assertRaises(NotImplementedError):
|
|
options_dialog.postprocess(0)
|
|
|
|
|
|
class SettingsDialogTest(_SettingsSandbox):
|
|
def test_cache_dir_initialized_correctly(self):
|
|
"""UI SettingsDialog: check initialization of inputs."""
|
|
from natcap.invest.ui import model
|
|
|
|
settings_dialog = model.SettingsDialog()
|
|
try:
|
|
# Qt4
|
|
cache_dir = QtGui.QDesktopServices.storageLocation(
|
|
QtGui.QDesktopServices.CacheLocation)
|
|
except AttributeError:
|
|
# Package location changed in Qt5
|
|
cache_dir = QtCore.QStandardPaths.writableLocation(
|
|
QtCore.QStandardPaths.CacheLocation)
|
|
|
|
self.assertEqual(settings_dialog.cache_directory.value(),
|
|
cache_dir)
|
|
|
|
def test_cache_dir_setting_set_correctly(self):
|
|
"""UI SettingsDialog: check settings values when input changed."""
|
|
from natcap.invest.ui import model
|
|
from natcap.invest.ui import inputs
|
|
|
|
settings_dialog = model.SettingsDialog()
|
|
settings_dialog.show()
|
|
settings_dialog.cache_directory.set_value('new_dir')
|
|
QTest.mouseClick(settings_dialog.ok_button,
|
|
QtCore.Qt.LeftButton)
|
|
self.qt_app.processEvents()
|
|
try:
|
|
self.assertEqual(settings_dialog.cache_directory.value(),
|
|
'new_dir')
|
|
finally:
|
|
settings_dialog.close()
|
|
|
|
def test_dialog_log_level_initialized(self):
|
|
"""UI SettingsDialog: check initialization of dialog log level."""
|
|
from natcap.invest.ui import model
|
|
|
|
settings_dialog = model.SettingsDialog()
|
|
self.assertEqual(settings_dialog.dialog_logging_level.value(),
|
|
'INFO')
|
|
|
|
|
|
def test_dialog_log_level_set_correctly(self):
|
|
"""UI SettingsDialog: check settings value for dialog threshold."""
|
|
from natcap.invest.ui import model
|
|
|
|
settings_dialog = model.SettingsDialog()
|
|
|
|
settings_dialog.show()
|
|
settings_dialog.dialog_logging_level.set_value('CRITICAL')
|
|
QTest.mouseClick(settings_dialog.ok_button,
|
|
QtCore.Qt.LeftButton)
|
|
self.qt_app.processEvents()
|
|
try:
|
|
self.assertEqual(settings_dialog.dialog_logging_level.value(),
|
|
'CRITICAL')
|
|
finally:
|
|
settings_dialog.close()
|
|
|
|
def test_logfile_log_level_initialized(self):
|
|
"""UI SettingsDialog: check initialization of logfile log level."""
|
|
from natcap.invest.ui import model
|
|
|
|
settings_dialog = model.SettingsDialog()
|
|
self.assertEqual(settings_dialog.logfile_logging_level.value(),
|
|
'NOTSET')
|
|
|
|
def test_logfile_log_level_set_correctly(self):
|
|
"""UI SettingsDialog: check settings value for logfile threshold."""
|
|
from natcap.invest.ui import model
|
|
|
|
settings_dialog = model.SettingsDialog()
|
|
|
|
settings_dialog.show()
|
|
settings_dialog.logfile_logging_level.set_value('CRITICAL')
|
|
QTest.mouseClick(settings_dialog.ok_button,
|
|
QtCore.Qt.LeftButton)
|
|
self.qt_app.processEvents()
|
|
try:
|
|
self.assertEqual(settings_dialog.logfile_logging_level.value(),
|
|
'CRITICAL')
|
|
finally:
|
|
settings_dialog.close()
|
|
|
|
def test_taskgraph_log_level_initialized(self):
|
|
"""UI SettingsDialog: check initialization of taskgraph log level."""
|
|
from natcap.invest.ui import model
|
|
|
|
settings_dialog = model.SettingsDialog()
|
|
self.assertEqual(settings_dialog.taskgraph_logging_level.value(),
|
|
'ERROR')
|
|
|
|
def test_taskgraph_log_level_set_correctly(self):
|
|
"""UI SettingsDialog: check settings value for taskgraph threshold."""
|
|
from natcap.invest.ui import model
|
|
|
|
settings_dialog = model.SettingsDialog()
|
|
|
|
settings_dialog.show()
|
|
settings_dialog.taskgraph_logging_level.set_value('CRITICAL')
|
|
QTest.mouseClick(settings_dialog.ok_button,
|
|
QtCore.Qt.LeftButton)
|
|
self.qt_app.processEvents()
|
|
try:
|
|
self.assertEqual(settings_dialog.taskgraph_logging_level.value(),
|
|
'CRITICAL')
|
|
finally:
|
|
settings_dialog.close()
|
|
|
|
def test_n_workers_initialized(self):
|
|
"""UI SettingsDialog: check initialization of n_workers."""
|
|
from natcap.invest.ui import model
|
|
|
|
settings_dialog = model.SettingsDialog()
|
|
self.assertEqual(settings_dialog.taskgraph_n_workers.value(),
|
|
'-1')
|
|
|
|
def test_n_workers_set_correctly(self):
|
|
"""UI SettingsDialog: check settings value for n_workers."""
|
|
from natcap.invest.ui import model
|
|
|
|
settings_dialog = model.SettingsDialog()
|
|
|
|
settings_dialog.show()
|
|
settings_dialog.taskgraph_n_workers.set_value('3')
|
|
QTest.mouseClick(settings_dialog.ok_button,
|
|
QtCore.Qt.LeftButton)
|
|
self.qt_app.processEvents()
|
|
try:
|
|
self.assertEqual(settings_dialog.taskgraph_n_workers.value(),
|
|
'3')
|
|
finally:
|
|
settings_dialog.close()
|
|
|
|
|
|
class DatastackOptionsDialogTests(_QtTest):
|
|
def setUp(self):
|
|
_QtTest.setUp(self)
|
|
self.workspace = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
_QtTest.tearDown(self)
|
|
shutil.rmtree(self.workspace)
|
|
|
|
def test_dialog_save_button_enable(self):
|
|
"""UI Datastack Options: verify save button enable/disable."""
|
|
from natcap.invest.ui import model
|
|
|
|
options_dialog = model.DatastackOptionsDialog(
|
|
paramset_basename='test_model')
|
|
new_paramset_path = os.path.join(
|
|
self.workspace, 'testdir1', 'test.invs.json')
|
|
|
|
options_dialog.save_parameters.set_value(new_paramset_path)
|
|
self.assertTrue(options_dialog.ok_button.isEnabled())
|
|
|
|
# whitespace and empty string are the only values that disable
|
|
invalids = [' ', '']
|
|
for val in invalids:
|
|
options_dialog.save_parameters.set_value(val)
|
|
self.assertFalse(options_dialog.ok_button.isEnabled())
|
|
|
|
def test_dialog_return_value(self):
|
|
"""UI Datastack Options: Verify return value of dialog."""
|
|
from natcap.invest.ui import model
|
|
from natcap.invest.ui import inputs
|
|
|
|
options_dialog = model.DatastackOptionsDialog(
|
|
paramset_basename='test_model')
|
|
|
|
# set this option to ensure coverage of the slot
|
|
options_dialog.datastack_type.set_value(model._DATASTACK_DATA_ARCHIVE)
|
|
options_dialog.datastack_type.set_value(model._DATASTACK_PARAMETER_SET)
|
|
self.qt_app.processEvents()
|
|
|
|
new_paramset_path = os.path.join(self.workspace, 'test.invs.json')
|
|
options_dialog.save_parameters.set_value(new_paramset_path)
|
|
|
|
def _press_accept():
|
|
options_dialog.accept()
|
|
|
|
QtCore.QTimer.singleShot(25, _press_accept)
|
|
return_options = options_dialog.exec_()
|
|
|
|
self.assertEqual(
|
|
model.DatastackSaveOpts(
|
|
model._DATASTACK_PARAMETER_SET, # datastack type
|
|
False, # use relative paths
|
|
False, # include workpace
|
|
new_paramset_path), # datastack path
|
|
return_options)
|
|
|
|
# verify that the options have been cleared.
|
|
for input_obj, expected_value in (
|
|
(options_dialog.datastack_type,
|
|
options_dialog.datastack_type.options[0]),
|
|
(options_dialog.use_relative_paths, False),
|
|
(options_dialog.include_workspace, False),
|
|
(options_dialog.save_parameters, '')):
|
|
self.assertEqual(input_obj.value(), expected_value)
|
|
|
|
|
|
def test_dialog_cancelled(self):
|
|
"""UI Datastack Options: Verify return value when dialog cancelled."""
|
|
from natcap.invest.ui import model
|
|
from natcap.invest.ui import inputs
|
|
|
|
options_dialog = model.DatastackOptionsDialog(
|
|
paramset_basename='test_model')
|
|
|
|
# set this option to ensure coverage of the slot
|
|
options_dialog.datastack_type.set_value(model._DATASTACK_DATA_ARCHIVE)
|
|
options_dialog.datastack_type.set_value(model._DATASTACK_PARAMETER_SET)
|
|
self.qt_app.processEvents()
|
|
|
|
def _press_accept():
|
|
options_dialog.reject()
|
|
|
|
QtCore.QTimer.singleShot(25, _press_accept)
|
|
return_options = options_dialog.exec_()
|
|
|
|
self.assertEqual(return_options, None)
|
|
|
|
@unittest.skip('These tests are segfaulting. '
|
|
'See github.com/natcap/invest/issues/72')
|
|
class ModelTests(_QtTest):
|
|
def setUp(self):
|
|
_QtTest.setUp(self)
|
|
self.workspace = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
_QtTest.tearDown(self)
|
|
shutil.rmtree(self.workspace)
|
|
|
|
@staticmethod
|
|
def build_model(validate_func=None, target_func=None):
|
|
from natcap.invest.ui import model
|
|
from natcap.invest import validation
|
|
|
|
if target_func is None:
|
|
def _target(args):
|
|
pass
|
|
target_func = _target
|
|
|
|
if validate_func is None:
|
|
@validation.invest_validator
|
|
def _validate(args, limit_to=None):
|
|
return []
|
|
validate_func = _validate
|
|
|
|
# Fetch settings and clear them before the UI constructs.
|
|
# This avoids a scenario where invalid settings have been
|
|
# constructed and not destroyed properly before the test model object
|
|
# could complete its setup, causing lots of test failures.
|
|
label = 'Test model'
|
|
settings = model.SETTINGS_TEMPLATE(label)
|
|
settings.clear()
|
|
|
|
class _TestInVESTModel(model.InVESTModel):
|
|
def __init__(self):
|
|
model.InVESTModel.__init__(
|
|
self,
|
|
label=label,
|
|
target=target_func,
|
|
validator=validate_func,
|
|
localdoc='testmodel.html')
|
|
|
|
# Default model class already has workspace and suffix input.
|
|
def assemble_args(self):
|
|
return {
|
|
self.workspace.args_key: self.workspace.value(),
|
|
self.suffix.args_key: self.suffix.value(),
|
|
}
|
|
|
|
def __del__(self):
|
|
# clear the settings for future runs.
|
|
try:
|
|
self.settings.clear()
|
|
except RuntimeError:
|
|
pass
|
|
|
|
return _TestInVESTModel()
|
|
|
|
def test_model_defaults(self):
|
|
"""UI Model: Check that workspace and suffix are added."""
|
|
from natcap.invest.ui import inputs
|
|
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
workspace_input = getattr(model_ui, 'workspace')
|
|
self.assertTrue(isinstance(workspace_input, inputs.Folder))
|
|
|
|
suffix_input = getattr(model_ui, 'suffix')
|
|
self.assertTrue(isinstance(suffix_input, inputs.Text))
|
|
except AttributeError as missing_input:
|
|
self.fail(str(missing_input))
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_lastrun(self):
|
|
"""UI Model: Check that lastrun saving/loading works."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
# Set input values and save the lastrun.
|
|
model_ui.workspace.set_value('foo')
|
|
model_ui.suffix.set_value('bar')
|
|
model_ui.save_lastrun()
|
|
|
|
# change the input values
|
|
model_ui.workspace.set_value('new workspace')
|
|
model_ui.suffix.set_value('new suffix')
|
|
self.assertEqual(model_ui.workspace.value(), 'new workspace')
|
|
self.assertEqual(model_ui.suffix.value(), 'new suffix')
|
|
|
|
# load the values from lastrun and assert that the values are correct.
|
|
model_ui.load_lastrun()
|
|
self.assertEqual(model_ui.workspace.value(), 'foo')
|
|
self.assertEqual(model_ui.suffix.value(), 'bar')
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_close_window_confirm(self):
|
|
"""UI Model: Close confirmation dialog 'remember lastrun' checkbox."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
model_ui.show()
|
|
try:
|
|
QTest.qWaitForWindowShown(model_ui)
|
|
except AttributeError:
|
|
# pyqt5 has different wait methods.
|
|
QTest.qWaitForWindowExposed(model_ui)
|
|
|
|
threading_event = threading.Event()
|
|
|
|
def _tests():
|
|
# verify 'remember inputs' is checked by default.
|
|
self.assertTrue(model_ui.quit_confirm_dialog.checkbox.isChecked())
|
|
|
|
# click yes.
|
|
QTest.mouseClick(
|
|
model_ui.quit_confirm_dialog.button(QtWidgets.QMessageBox.Yes),
|
|
QtCore.Qt.LeftButton)
|
|
|
|
threading_event.set()
|
|
|
|
QtCore.QTimer.singleShot(25, _tests)
|
|
model_ui.close()
|
|
self.qt_app.processEvents()
|
|
|
|
threading_event.wait(0.5)
|
|
self.qt_app.processEvents()
|
|
|
|
# verify the 'remember inputs' state
|
|
self.assertEqual(model_ui.settings.value('remember_lastrun'),
|
|
True)
|
|
self.assertFalse(model_ui.quit_confirm_dialog.isVisible())
|
|
self.assertFalse(model_ui.isVisible())
|
|
finally:
|
|
model_ui.destroy()
|
|
|
|
def test_close_window_cancel(self):
|
|
"""UI Model: Close confirmation dialog cancel"""
|
|
model_ui = ModelTests.build_model()
|
|
model_ui.show()
|
|
|
|
threading_event = threading.Event()
|
|
|
|
def _tests():
|
|
# click cancel.
|
|
button = QtWidgets.QMessageBox.Cancel
|
|
QTest.mouseClick(
|
|
model_ui.quit_confirm_dialog.button(button),
|
|
QtCore.Qt.LeftButton)
|
|
threading_event.set()
|
|
|
|
QtCore.QTimer.singleShot(25, _tests)
|
|
model_ui.close()
|
|
threading_event.wait()
|
|
|
|
self.assertFalse(model_ui.quit_confirm_dialog.isVisible())
|
|
self.assertTrue(model_ui.isVisible())
|
|
# then close it for real so it doesn't hang around
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_validation_passes(self):
|
|
"""UI Model: Check what happens when validation passes."""
|
|
from natcap.invest import validation
|
|
from natcap.invest.ui import inputs
|
|
|
|
@validation.invest_validator
|
|
def _sample_validate(args, limit_to=None):
|
|
# no validation errors!
|
|
return []
|
|
|
|
model_ui = ModelTests.build_model(_sample_validate)
|
|
try:
|
|
model_ui.show()
|
|
|
|
model_ui.validate(block=True)
|
|
self.qt_app.processEvents()
|
|
self.assertEqual(len(model_ui.validation_report_dialog.warnings), 0)
|
|
self.assertTrue(model_ui.is_valid())
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_validate_blocking(self):
|
|
"""UI Model: Validate that the blocking validation call works."""
|
|
from natcap.invest import validation
|
|
from natcap.invest.ui import inputs
|
|
|
|
@validation.invest_validator
|
|
def _sample_validate(args, limit_to=None):
|
|
return [(('workspace_dir',), 'some error')]
|
|
|
|
model_ui = ModelTests.build_model(_sample_validate)
|
|
try:
|
|
model_ui.show()
|
|
|
|
model_ui.validate(block=True)
|
|
self.qt_app.processEvents()
|
|
self.assertEqual(len(model_ui.validation_report_dialog.warnings), 1)
|
|
self.assertFalse(model_ui.is_valid())
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_validate_nonblocking(self):
|
|
"""UI Model: Validate that the nonblocking validation call works."""
|
|
from natcap.invest import validation
|
|
from natcap.invest.ui import inputs
|
|
|
|
@validation.invest_validator
|
|
def _sample_validate(args, limit_to=None):
|
|
return [(('workspace_dir',), 'some error')]
|
|
|
|
model_ui = ModelTests.build_model(_sample_validate)
|
|
try:
|
|
model_ui.show()
|
|
|
|
model_ui.validate(block=False)
|
|
self.qt_app.processEvents()
|
|
self.assertEqual(len(model_ui.validation_report_dialog.warnings), 1)
|
|
self.assertFalse(model_ui.is_valid())
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_assemble_args_not_implemented(self):
|
|
"""UI Model: Validate exception when assemble_args not implemented."""
|
|
from natcap.invest.ui import model
|
|
|
|
with self.assertRaises(NotImplementedError):
|
|
try:
|
|
model_ui = model.InVESTModel(
|
|
label='foo',
|
|
target=lambda args: None,
|
|
validator=lambda args, limit_to=None: [],
|
|
localdoc='sometextfile.html'
|
|
)
|
|
model_ui.assemble_args()
|
|
except Exception:
|
|
LOGGER.exception('Could not create the model UI')
|
|
raise
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_load_args(self):
|
|
"""UI Model: Check that we can load args as expected."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
args = {
|
|
'workspace_dir': 'new workspace!',
|
|
'results_suffix': 'a',
|
|
}
|
|
model_ui.load_args(args)
|
|
self.assertEqual(model_ui.workspace.value(), args['workspace_dir'])
|
|
self.assertEqual(model_ui.suffix.value(), args['results_suffix'])
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_load_args_bad_key(self):
|
|
"""UI Model: Check that we can handle loading of bad keys."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
model_ui.workspace.set_value('')
|
|
args = {
|
|
'bad_key': 'something unexpected!',
|
|
'results_suffix': 'a',
|
|
}
|
|
model_ui.load_args(args)
|
|
self.assertEqual(model_ui.workspace.value(), '') # was never changed
|
|
self.assertEqual(model_ui.suffix.value(), args['results_suffix'])
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_load_args_error(self):
|
|
"""UI Model: Check that we can handle errors when loading args."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
model_ui.workspace.set_value('')
|
|
args = {
|
|
'workspace_dir': 'workspace',
|
|
'results_suffix': 'a',
|
|
}
|
|
|
|
def _raise_valueerror(new_value):
|
|
raise ValueError('foo!')
|
|
|
|
model_ui.workspace.set_value = _raise_valueerror
|
|
model_ui.load_args(args)
|
|
|
|
self.assertEqual(model_ui.workspace.value(), '') # was never changed
|
|
self.assertEqual(model_ui.suffix.value(), args['results_suffix'])
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_run(self):
|
|
"""UI Model: Check that we can run the model."""
|
|
from natcap.invest.ui import inputs
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
model_ui.test_container = inputs.Container('test')
|
|
model_ui.add_input(model_ui.test_container)
|
|
|
|
def _close_window():
|
|
# trigger whole-model validation for coverage of callback.
|
|
model_ui.workspace.set_value('foo')
|
|
|
|
self.qt_app.processEvents()
|
|
|
|
model_ui.close(prompt=False)
|
|
|
|
model_ui.run()
|
|
self.assertTrue(model_ui.isVisible())
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_execute_with_n_workers(self):
|
|
"""UI Model: Check that model runs with n_workers parameter."""
|
|
|
|
from natcap.invest.ui import inputs
|
|
|
|
n_workers_setting = inputs.INVEST_SETTINGS.value(
|
|
'taskgraph/n_workers', '-1')
|
|
|
|
def target_func(args):
|
|
"""n_workers is required in args."""
|
|
if 'n_workers' not in args:
|
|
raise ValueError('n_workers should be in args but is not.')
|
|
|
|
if args['n_workers'] != n_workers_setting:
|
|
raise ValueError('n_workers value is not set correctly')
|
|
|
|
model_ui = ModelTests.build_model(target_func=target_func)
|
|
model_ui.workspace.set_value(os.path.join(self.workspace, 'new_dir'))
|
|
|
|
try:
|
|
# Show the window
|
|
model_ui.run()
|
|
self.assertTrue(model_ui.isVisible())
|
|
|
|
# This should execute without exception.
|
|
model_ui.execute_model()
|
|
|
|
# I don't particularly like spinning here, but it should be
|
|
# reliable.
|
|
while model_ui.form.run_dialog.is_executing:
|
|
self.qt_app.processEvents()
|
|
time.sleep(0.1)
|
|
self.assertFalse(model_ui.form._thread.failed)
|
|
finally:
|
|
model_ui.form.run_dialog.close()
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_execute_error_with_n_workers(self):
|
|
"""UI Model: Check that model fails with n_workers parameter."""
|
|
from natcap.invest.ui import inputs, model
|
|
from natcap.invest import validation
|
|
|
|
n_workers_setting = inputs.INVEST_SETTINGS.value(
|
|
'taskgraph/n_workers', 1)
|
|
|
|
def target_func(args):
|
|
raise AssertionError(
|
|
'Target should not have been called due to n_workers being '
|
|
'in args.')
|
|
|
|
@validation.invest_validator
|
|
def _validate(args, limit_to=None):
|
|
return []
|
|
|
|
class _TestInVESTModel(model.InVESTModel):
|
|
def __init__(self):
|
|
model.InVESTModel.__init__(
|
|
self,
|
|
label='Test model',
|
|
target=target_func,
|
|
validator=_validate,
|
|
localdoc='testmodel.html')
|
|
|
|
# Default model class already has workspace and suffix input.
|
|
def assemble_args(self):
|
|
return {
|
|
self.workspace.args_key: self.workspace.value(),
|
|
self.suffix.args_key: self.suffix.value(),
|
|
'n_workers': n_workers_setting # this should cause an error.
|
|
}
|
|
|
|
def __del__(self):
|
|
# clear the settings for future runs.
|
|
try:
|
|
self.settings.clear()
|
|
except RuntimeError:
|
|
pass
|
|
|
|
model_ui = _TestInVESTModel()
|
|
model_ui.workspace.set_value(os.path.join(self.workspace, 'new_dir'))
|
|
|
|
try:
|
|
# Show the window
|
|
model_ui.run()
|
|
self.assertTrue(model_ui.isVisible())
|
|
|
|
# This should execute without exception.
|
|
model_ui.execute_model()
|
|
|
|
# I don't particularly like spinning here, but it should be
|
|
# reliable.
|
|
while model_ui.form.run_dialog.is_executing:
|
|
self.qt_app.processEvents()
|
|
time.sleep(0.1)
|
|
self.assertTrue(model_ui.form._thread.failed)
|
|
|
|
# We expect RuntimeError to be raised by the UI in the function
|
|
# that wraps the target, not by the target itself.
|
|
self.assertTrue(isinstance(model_ui.form._thread.exception,
|
|
RuntimeError))
|
|
self.assertTrue('n_workers defined in args.' in
|
|
repr(model_ui.form._thread.exception))
|
|
finally:
|
|
model_ui.form.run_dialog.close()
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_local_docs_from_hyperlink(self):
|
|
"""UI Model: Check that we can open the local docs missing dialog."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
model_ui.run()
|
|
|
|
def _check_dialog_and_close():
|
|
self.assertTrue(model_ui.local_docs_missing_dialog.isVisible())
|
|
model_ui.local_docs_missing_dialog.accept()
|
|
|
|
QtCore.QTimer.singleShot(25, _check_dialog_and_close)
|
|
|
|
if hasattr(QtCore, 'QDesktopServices'):
|
|
patch_object = mock.patch('natcap.invest.ui.inputs'
|
|
'.QtCore.QDesktopServices.openUrl')
|
|
else:
|
|
# PyQt5 changed the location of this.
|
|
patch_object = mock.patch('natcap.invest.ui.inputs'
|
|
'.QtGui.QDesktopServices.openUrl')
|
|
|
|
# simulate a mouse click on the localdocs hyperlink.
|
|
with patch_object as patched_openurl:
|
|
# simulate a mouse click on the localdocs hyperlink.
|
|
model_ui._check_local_docs('localdocs')
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_local_docs_launch(self):
|
|
"""UI Model: Check that we can launch local documentation."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
model_ui.run()
|
|
|
|
if hasattr(QtCore, 'QDesktopServices'):
|
|
patch_object = mock.patch('natcap.invest.ui.inputs'
|
|
'.QtCore.QDesktopServices.openUrl')
|
|
else:
|
|
# PyQt5 changed the location of this.
|
|
patch_object = mock.patch('natcap.invest.ui.inputs'
|
|
'.QtGui.QDesktopServices.openUrl')
|
|
|
|
# simulate a mouse click on the localdocs hyperlink.
|
|
with patch_object as patched_openurl:
|
|
with mock.patch('os.path.exists', return_value=True):
|
|
# simulate about --> view documentation menu.
|
|
model_ui._check_local_docs('http://some_file_that_exists')
|
|
|
|
patched_openurl.assert_called_once_with(
|
|
QtCore.QUrl('http://some_file_that_exists'))
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_forums_link_launch(self):
|
|
"""UI Model: Check that we can link to the forums."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
model_ui.run()
|
|
|
|
if hasattr(QtCore, 'QDesktopServices'):
|
|
patch_object = mock.patch('natcap.invest.ui.inputs'
|
|
'.QtCore.QDesktopServices.openUrl')
|
|
else:
|
|
# PyQt5 changed the location of this.
|
|
patch_object = mock.patch('natcap.invest.ui.inputs'
|
|
'.QtGui.QDesktopServices.openUrl')
|
|
|
|
# simulate a mouse click on the localdocs hyperlink.
|
|
with patch_object as patched_openurl:
|
|
# simulate forums link clicked
|
|
model_ui._check_local_docs(
|
|
'https://forums.naturalcapitalproject.org')
|
|
|
|
patched_openurl.assert_called_once_with(
|
|
QtCore.QUrl('https://forums.naturalcapitalproject.org'))
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_version_update(self):
|
|
"""UI Model: Check update button & dialog exist with a new version."""
|
|
import natcap.invest
|
|
current_version = natcap.invest.__version__
|
|
|
|
try:
|
|
# Assert that update button and dialog do not exist if the current
|
|
# version is already the latest
|
|
with mock.patch('natcap.invest.ui.model.'
|
|
'InVESTModel._get_latest_version',
|
|
return_value=current_version):
|
|
model_ui = ModelTests.build_model()
|
|
assert not all([hasattr(model_ui, 'update_button'),
|
|
hasattr(model_ui, 'version_update_dialog')])
|
|
|
|
# Assert that button and dialog exists if there is a new version,
|
|
# and the page can be linked successfully
|
|
with mock.patch('natcap.invest.ui.model.'
|
|
'InVESTModel._get_latest_version',
|
|
return_value='1' + current_version):
|
|
model_ui = ModelTests.build_model()
|
|
assert all([hasattr(model_ui, 'update_button'),
|
|
hasattr(model_ui, 'version_update_dialog')])
|
|
|
|
if hasattr(QtCore, 'QDesktopServices'):
|
|
patch_object = mock.patch('natcap.invest.ui.inputs.QtCore.'
|
|
'QDesktopServices.openUrl')
|
|
else:
|
|
# PyQt5 changed the location of this.
|
|
patch_object = mock.patch('natcap.invest.ui.inputs.QtGui.'
|
|
'QDesktopServices.openUrl')
|
|
|
|
# Simulate InVEST download link clicked
|
|
with patch_object as patched_openurl:
|
|
model_ui._activate_link(
|
|
model_ui.version_update_dialog.download_qurl)
|
|
|
|
patched_openurl.assert_called_once_with(QtCore.QUrl(
|
|
'https://naturalcapitalproject.stanford.edu/invest/'))
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_version_update_slow_connection(self):
|
|
"""UI Model: Check no/slow Internet connection won't interrupt UI."""
|
|
import natcap.invest
|
|
try:
|
|
# Assert that update button and dialog don't exist if connection
|
|
# is slow
|
|
with mock.patch('requests.get', side_effect=requests.Timeout):
|
|
model_ui = ModelTests.build_model()
|
|
assert not all([hasattr(model_ui, 'update_button'),
|
|
hasattr(model_ui, 'version_update_dialog')])
|
|
|
|
# Assert that update button and dialog don't exist if no connection
|
|
with mock.patch('requests.get',
|
|
side_effect=requests.ConnectionError):
|
|
model_ui = ModelTests.build_model()
|
|
assert not all([hasattr(model_ui, 'update_button'),
|
|
hasattr(model_ui, 'version_update_dialog')])
|
|
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_load_datastack_paramset(self):
|
|
"""UI Model: Check that we can load a parameter set datastack."""
|
|
from natcap.invest import datastack
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
args = {
|
|
'workspace_dir': 'foodir',
|
|
'results_suffix': 'suffix',
|
|
}
|
|
datastack_filepath = os.path.join(self.workspace, 'paramset.json')
|
|
datastack.build_parameter_set(
|
|
args=args,
|
|
model_name=model_ui.target.__module__,
|
|
paramset_path=datastack_filepath,
|
|
relative=False)
|
|
|
|
model_ui.load_datastack(datastack_filepath)
|
|
|
|
self.assertEqual(model_ui.workspace.value(), args['workspace_dir'])
|
|
self.assertEqual(model_ui.suffix.value(), args['results_suffix'])
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_load_datastack_archive(self):
|
|
"""UI Model: Check that we can load a parameter archive."""
|
|
from natcap.invest import datastack
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
args = {
|
|
'workspace_dir': 'foodir',
|
|
'results_suffix': 'suffix',
|
|
}
|
|
datastack_filepath = os.path.join(self.workspace, 'archive.tar.gz')
|
|
datastack.build_datastack_archive(args, model_ui.target.__module__,
|
|
datastack_filepath)
|
|
|
|
extracted_archive = os.path.join(self.workspace, 'archive_dir')
|
|
def _set_extraction_dir():
|
|
model_ui.datastack_archive_extract_dialog.extraction_point.set_value(
|
|
extracted_archive)
|
|
model_ui.datastack_archive_extract_dialog.accept()
|
|
|
|
QtCore.QTimer.singleShot(25, _set_extraction_dir)
|
|
|
|
# close the archive progress dialog automatically when extraction
|
|
# finishes.
|
|
model_ui.datastack_progress_dialog.checkbox.setChecked(True)
|
|
|
|
model_ui.load_datastack(datastack_filepath)
|
|
|
|
# Workspace isn't saved in a parameter archive, so just test suffix
|
|
self.assertEqual(model_ui.suffix.value(), args['results_suffix'])
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_load_datastack_from_logfile(self):
|
|
"""UI Model: Check that we can load parameters from a logfile."""
|
|
import natcap.invest
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
# write a sample logfile
|
|
logfile_path = os.path.join(self.workspace, 'logfile')
|
|
with open(logfile_path, 'w') as logfile:
|
|
logfile.write(textwrap.dedent("""
|
|
07/20/2017 16:37:48 natcap.invest.ui.model INFO
|
|
Arguments for InVEST %s %s:
|
|
results_suffix foo
|
|
workspace_dir some_workspace_dir
|
|
|
|
""" % (model_ui.target.__module__, natcap.invest.__version__)))
|
|
|
|
model_ui.load_datastack(logfile_path)
|
|
|
|
self.assertEqual(model_ui.workspace.value(), 'some_workspace_dir')
|
|
self.assertEqual(model_ui.suffix.value(), 'foo')
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_load_datastack_extraction_dialog_cancelled(self):
|
|
"""UI Model: coverage when user clicks cancel in datastack dialog."""
|
|
from natcap.invest import datastack
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
args = {
|
|
'workspace_dir': 'foodir',
|
|
'results_suffix': 'suffix',
|
|
}
|
|
datastack_filepath = os.path.join(self.workspace, 'archive.tar.gz')
|
|
datastack.build_datastack_archive(args, model_ui.target.__module__,
|
|
datastack_filepath)
|
|
|
|
def _cancel_dialog():
|
|
model_ui.datastack_archive_extract_dialog.reject()
|
|
|
|
QtCore.QTimer.singleShot(25, _cancel_dialog)
|
|
model_ui.load_datastack(datastack_filepath)
|
|
self.assertFalse(model_ui.isVisible())
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_load_datastack_file_dialog_cancelled(self):
|
|
"""UI Model: coverage for when the file select dialog is cancelled."""
|
|
# I'm mocking up the file dialog because I can't figure out how to
|
|
# programmatically press the cancel button in a way that works on both
|
|
# mac and linux.
|
|
with mock.patch('qtpy.QtWidgets.QFileDialog.getOpenFileName',
|
|
return_value=(None, None)):
|
|
try:
|
|
model_ui = ModelTests.build_model()
|
|
model_ui.load_datastack()
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_model_quickrun(self):
|
|
"""UI Model: Test the quickrun path through model.run()."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
def _confirm_workspace_overwrite():
|
|
# Just using dialog.accept() didn't work here, and I can't seem to
|
|
# figure out why.
|
|
QTest.mouseClick(
|
|
model_ui.workspace_overwrite_confirm_dialog.button(
|
|
QtWidgets.QMessageBox.Yes),
|
|
QtCore.Qt.LeftButton)
|
|
|
|
# this line used to be in a singleshot, but i don't see why we
|
|
# can't set it directly, works when i do it.
|
|
model_ui.workspace.set_value(self.workspace)
|
|
# Need to wait a little longer on this one to compensate for other
|
|
# singleshot timers in model.run().
|
|
QtCore.QTimer.singleShot(1000, _confirm_workspace_overwrite)
|
|
model_ui.run(quickrun=True)
|
|
try:
|
|
QTest.qWaitForWindowShown(model_ui)
|
|
except AttributeError:
|
|
# pyqt5 has different wait methods.
|
|
QTest.qWaitForWindowExposed(model_ui)
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_save_datastack_cancel_coverage(self):
|
|
"""UI Model: Test coverage for cancelling save datastack dialog."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
def _cancel_datastack_dialog():
|
|
model_ui.datastack_options_dialog.reject()
|
|
|
|
QtCore.QTimer.singleShot(25, _cancel_datastack_dialog)
|
|
model_ui._save_datastack_as()
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_save_datastack_as_archive(self):
|
|
"""UI Model: Test coverage for saving parameter archives."""
|
|
from natcap.invest.ui import model
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
|
|
starting_window_title = model_ui.windowTitle()
|
|
|
|
archive_path = os.path.join(self.workspace, 'archive.invs.tar.gz')
|
|
|
|
def _set_archive_options():
|
|
model_ui.datastack_options_dialog.datastack_type.set_value(
|
|
model._DATASTACK_DATA_ARCHIVE)
|
|
model_ui.datastack_options_dialog.save_parameters.set_value(
|
|
archive_path)
|
|
self.qt_app.processEvents()
|
|
model_ui.datastack_options_dialog.accept()
|
|
|
|
# close the archive progress dialog automatically when extraction
|
|
# finishes.
|
|
model_ui.datastack_progress_dialog.checkbox.setChecked(True)
|
|
|
|
QtCore.QTimer.singleShot(25, _set_archive_options)
|
|
model_ui._save_datastack_as()
|
|
self.assertNotEqual(starting_window_title, model_ui.windowTitle())
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_save_datastack_as_parameter_set(self):
|
|
"""UI Model: Test coverage for saving parameter set."""
|
|
from natcap.invest.ui import model
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
starting_window_title = model_ui.windowTitle()
|
|
|
|
archive_path = os.path.join(self.workspace, 'parameters.invs.json')
|
|
|
|
def _set_archive_options():
|
|
model_ui.datastack_options_dialog.datastack_type.set_value(
|
|
model._DATASTACK_PARAMETER_SET)
|
|
model_ui.datastack_options_dialog.use_relative_paths.set_value(
|
|
True)
|
|
model_ui.datastack_options_dialog.include_workspace.set_value(
|
|
True)
|
|
model_ui.datastack_options_dialog.save_parameters.set_value(
|
|
archive_path)
|
|
self.qt_app.processEvents()
|
|
model_ui.datastack_options_dialog.accept()
|
|
|
|
QtCore.QTimer.singleShot(25, _set_archive_options)
|
|
model_ui._save_datastack_as()
|
|
self.assertNotEqual(starting_window_title, model_ui.windowTitle())
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_settings_saved_message(self):
|
|
"""UI Model: Verify that saving settings posts status to statusbar."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
# this used to have a singleeshot to get the dialog to exec_, but
|
|
# open seems to work just fine
|
|
model_ui.settings_dialog.open()
|
|
model_ui.settings_dialog.accept()
|
|
self.assertEqual(
|
|
model_ui.statusBar().currentMessage(), 'Settings saved')
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_run_with_validation_errors(self):
|
|
"""UI Model: Verify coverage when validation errors before a run."""
|
|
from natcap.invest import validation
|
|
|
|
@validation.invest_validator
|
|
def _validate(args, limit_to=None):
|
|
return [(['workspace_dir'], 'Some error message')]
|
|
|
|
model_ui = ModelTests.build_model(_validate)
|
|
try:
|
|
model_ui.workspace.set_value('')
|
|
model_ui.validate(block=True)
|
|
self.assertEqual(len(model_ui.validation_report_dialog.warnings), 1)
|
|
|
|
def _close_validation_report():
|
|
model_ui.validation_report_dialog.accept()
|
|
|
|
QtCore.QTimer.singleShot(25, _close_validation_report)
|
|
model_ui.execute_model()
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_exception_raised_in_target(self):
|
|
"""UI Model: Verify coverage when exception raised in target."""
|
|
def _target(args):
|
|
raise Exception('foo!')
|
|
|
|
model_ui = ModelTests.build_model(target_func=_target)
|
|
try:
|
|
model_ui.workspace.set_value(os.path.join(self.workspace,
|
|
'dir_not_there'))
|
|
self.qt_app.processEvents()
|
|
|
|
# Wait until the InVESTModel object is about to be deleted.
|
|
with wait_on_signal(self.qt_app, model_ui.destroyed):
|
|
model_ui.execute_model()
|
|
|
|
self.assertEqual(str(model_ui.form._thread.exception), 'foo!')
|
|
model_ui.form.run_dialog.close()
|
|
model_ui.form.run_dialog.destroy()
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_workspace_overwrite_reject(self):
|
|
"""UI Model: Verify coverage when overwrite dialog is rejected."""
|
|
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
def _cancel_workspace_overwrite():
|
|
model_ui.workspace_overwrite_confirm_dialog.reject()
|
|
|
|
QtCore.QTimer.singleShot(50, _cancel_workspace_overwrite)
|
|
model_ui.execute_model()
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_save_to_python(self):
|
|
"""UI Model: Verify that we can make a python script from params."""
|
|
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
python_file = os.path.join(self.workspace, 'python_script.py')
|
|
|
|
model_ui.save_to_python(python_file)
|
|
|
|
self.assertTrue(os.path.exists(python_file))
|
|
|
|
module_name = str(uuid.uuid4()) + 'testscript'
|
|
try:
|
|
spec = importlib.util.spec_from_file_location(module_name, python_file)
|
|
module = importlib.util.module_from_spec(spec)
|
|
spec.loader.exec_module(module)
|
|
self.assertEqual(module.args, model_ui.assemble_args())
|
|
finally:
|
|
del sys.modules[module_name]
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_drag_n_drop_datastack(self):
|
|
"""UI Model: Verify that we can drag-n-drop a valid datastack."""
|
|
model = ModelTests.build_model()
|
|
try:
|
|
# Write a sample parameter set file to drop
|
|
datastack_filepath = os.path.join(self.workspace, 'datastack.invest.json')
|
|
with open(datastack_filepath, 'w') as sample_datastack:
|
|
sample_datastack.write(json.dumps(
|
|
{'args': {'workspace_dir': '/foo/bar',
|
|
'results_suffix': 'baz'},
|
|
'model_name': model.target.__module__,
|
|
'invest_version': 'testing'}))
|
|
|
|
mime_data = QtCore.QMimeData()
|
|
mime_data.setText('Some datastack')
|
|
mime_data.setUrls([QtCore.QUrl.fromLocalFile(datastack_filepath)])
|
|
|
|
drag_event = QtGui.QDragEnterEvent(
|
|
model.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
model.dragEnterEvent(drag_event)
|
|
self.assertTrue(drag_event.isAccepted())
|
|
|
|
event = QtGui.QDropEvent(
|
|
model.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
|
|
# When the datastack is dropped, the datastack is loaded.
|
|
model.dropEvent(event)
|
|
self.assertEqual(model.workspace.value(),
|
|
os.path.normpath('/foo/bar'))
|
|
self.assertEqual(model.suffix.value(), 'baz')
|
|
finally:
|
|
model.close(prompt=False)
|
|
model.destroy()
|
|
|
|
def test_drag_n_drop_datastack_no_mime_text(self):
|
|
"""UI Model: Verify drag-n-drop a datastack without MIME text."""
|
|
model = ModelTests.build_model()
|
|
try:
|
|
# Write a sample parameter set file to drop
|
|
datastack_filepath = os.path.join(self.workspace, 'datastack.invest.json')
|
|
with open(datastack_filepath, 'w') as sample_datastack:
|
|
sample_datastack.write(json.dumps(
|
|
{'args': {'workspace_dir': '/foo/bar',
|
|
'results_suffix': 'baz'},
|
|
'model_name': model.target.__module__,
|
|
'invest_version': 'testing'}))
|
|
|
|
# Deliberately not setting any text in the mimedata. We should
|
|
# still be able to load the datastack.
|
|
mime_data = QtCore.QMimeData()
|
|
mime_data.setUrls([QtCore.QUrl.fromLocalFile(datastack_filepath)])
|
|
|
|
drag_event = QtGui.QDragEnterEvent(
|
|
model.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
model.dragEnterEvent(drag_event)
|
|
self.assertTrue(drag_event.isAccepted())
|
|
|
|
event = QtGui.QDropEvent(
|
|
model.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
|
|
# When the datastack is dropped, the datastack is loaded.
|
|
model.dropEvent(event)
|
|
self.assertEqual(model.workspace.value(),
|
|
os.path.normpath('/foo/bar'))
|
|
self.assertEqual(model.suffix.value(), 'baz')
|
|
finally:
|
|
model.close(prompt=False)
|
|
model.destroy()
|
|
|
|
def test_drag_n_drop_rejected_multifile(self):
|
|
"""UI Model: Drag-n-drop fails when dragging several files."""
|
|
model = ModelTests.build_model()
|
|
try:
|
|
mime_data = QtCore.QMimeData()
|
|
mime_data.setText('Some datastack')
|
|
mime_data.setUrls([QtCore.QUrl('/path/1'),
|
|
QtCore.QUrl('/path/2')])
|
|
|
|
drag_event = QtGui.QDragEnterEvent(
|
|
model.pos(),
|
|
QtCore.Qt.CopyAction,
|
|
mime_data,
|
|
QtCore.Qt.LeftButton,
|
|
QtCore.Qt.NoModifier)
|
|
model.dragEnterEvent(drag_event)
|
|
self.assertFalse(drag_event.isAccepted())
|
|
finally:
|
|
model.close(prompt=False)
|
|
model.destroy()
|
|
|
|
def test_open_recent_menu(self):
|
|
"""UI Model: Check for correct behavior of the open-recent menu."""
|
|
from natcap.invest import datastack
|
|
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
|
|
datastack_created = []
|
|
for datastack_index in range(11):
|
|
datastack_path = os.path.join(self.workspace,
|
|
'datastack_%s.invest.json' %
|
|
datastack_index)
|
|
args = {
|
|
'workspace_dir': 'workspace_%s' % datastack_index,
|
|
}
|
|
|
|
datastack.build_parameter_set(args,
|
|
model_ui.target.__module__,
|
|
datastack_path)
|
|
datastack_created.append(datastack_path)
|
|
model_ui.load_datastack(datastack_path)
|
|
|
|
previous_datastack_actions = []
|
|
for action in model_ui.open_menu.actions():
|
|
if action.isSeparator() or action is model_ui.open_file_action:
|
|
continue
|
|
previous_datastack_actions.append(action.data())
|
|
|
|
# We should only have the 10 most recent datastack
|
|
self.assertEqual(len(previous_datastack_actions), 10)
|
|
self.assertEqual(max(previous_datastack_actions),
|
|
max(datastack_created))
|
|
|
|
# The earliest datastack should have been booted off the list since
|
|
# we're only keeping the 10 most recent.
|
|
self.assertEqual(min(previous_datastack_actions),
|
|
sorted(datastack_created)[1])
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_load_datastack_display_short_filepaths(self):
|
|
"""UI Model: Check handling of long filepaths in open-recent menu."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
# synthesize a recent datastack path by adding it to the right setting.
|
|
model_ui._add_to_open_menu('/foo.invest.json')
|
|
|
|
last_run_datastack_actions = []
|
|
for action in model_ui.open_menu.actions():
|
|
if action.isSeparator() or action is model_ui.open_file_action:
|
|
continue
|
|
last_run_datastack_actions.append((action.text(), action.data()))
|
|
|
|
self.assertEqual(len(last_run_datastack_actions), 1)
|
|
self.assertTrue('/foo.invest.json' in last_run_datastack_actions[0][0])
|
|
self.assertEqual(last_run_datastack_actions[0][1], '/foo.invest.json')
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_load_datastack_display_long_filepaths(self):
|
|
"""UI Model: Check handling of long filepaths in open-recent menu."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
# synthesize a recent datastack path by adding it to the right setting.
|
|
deep_directory = os.path.join(*[str(uuid.uuid4()) for x in range(10)])
|
|
filepath = os.path.join(deep_directory, 'something.invest.json')
|
|
model_ui._add_to_open_menu(filepath)
|
|
|
|
last_run_datastack_actions = []
|
|
for action in model_ui.open_menu.actions():
|
|
if action.isSeparator() or action is model_ui.open_file_action:
|
|
continue
|
|
last_run_datastack_actions.append((action.text(), action.data()))
|
|
|
|
self.assertEqual(len(last_run_datastack_actions), 1)
|
|
self.assertTrue('something.invest.json' in last_run_datastack_actions[0][0])
|
|
self.assertEqual(last_run_datastack_actions[0][1], filepath)
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_load_datastack_from_open_recent(self):
|
|
"""UI Model: Check loading of datastack via open-recent menu."""
|
|
from natcap.invest import datastack
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
|
|
datastack_filepath = os.path.join(self.workspace,
|
|
'datastack.invest.json')
|
|
args = {
|
|
'workspace_dir': 'workspace_foo',
|
|
}
|
|
datastack.build_parameter_set(args,
|
|
model_ui.target.__module__,
|
|
datastack_filepath)
|
|
|
|
model_ui.load_datastack(datastack_filepath)
|
|
self.assertEqual(model_ui.workspace.value(), 'workspace_foo')
|
|
model_ui.workspace.set_value('some_other_workspace')
|
|
self.assertEqual(model_ui.workspace.value(), 'some_other_workspace')
|
|
|
|
# Check to make sure that the loaded datastack was added to the
|
|
# last-run menu.
|
|
last_run_datastack_actions = []
|
|
for action in model_ui.open_menu.actions():
|
|
if action.isSeparator() or action is model_ui.open_file_action:
|
|
continue
|
|
last_run_datastack_actions.append(action)
|
|
|
|
# There should be exactly one recently-loaded datastack
|
|
self.assertEqual(len(last_run_datastack_actions), 1)
|
|
self.assertEqual(last_run_datastack_actions[0].data(), datastack_filepath)
|
|
|
|
# When we trigger the action and process events, the datastack should
|
|
# be loaded into the UI.
|
|
action = last_run_datastack_actions[0]
|
|
def _accept_parameter_overwrite():
|
|
QTest.mouseClick(
|
|
model_ui.input_overwrite_confirm_dialog.button(
|
|
QtWidgets.QMessageBox.Yes),
|
|
QtCore.Qt.LeftButton)
|
|
|
|
QtCore.QTimer.singleShot(25, _accept_parameter_overwrite)
|
|
action.activate(QtWidgets.QAction.Trigger)
|
|
self.qt_app.processEvents()
|
|
time.sleep(0.25)
|
|
self.qt_app.processEvents()
|
|
|
|
self.assertEqual(model_ui.workspace.value(), args['workspace_dir'])
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_clear_local_settings(self):
|
|
"""UI Model: Check that we can clear local settings."""
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
# write something to settings and check it's been saved
|
|
model_ui.save_lastrun()
|
|
self.assertEqual(model_ui.settings.allKeys(), ['lastrun'])
|
|
|
|
# clear settings and verify it's been cleared.
|
|
model_ui.clear_local_settings()
|
|
self.assertEqual(model_ui.settings.allKeys(), [])
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
def test_reject_on_modelname_mismatch(self):
|
|
"""UI Model: confirm when datastack modelname != model modelname."""
|
|
from natcap.invest.ui import model
|
|
from natcap.invest import datastack
|
|
|
|
filepath = os.path.join(self.workspace, 'paramset.json')
|
|
args = {'foo': 'foo', 'bar': 'bar'}
|
|
datastack.build_parameter_set(args, 'test_model', filepath)
|
|
|
|
model_ui = ModelTests.build_model()
|
|
try:
|
|
|
|
def _confirm_datastack_load():
|
|
self.assertTrue(
|
|
model_ui.model_mismatch_confirm_dialog.isVisible())
|
|
|
|
# Both the parameter's model name ('test_model') and the target
|
|
# model's module name ('test_ui_inputs') should be in the
|
|
# informative text.
|
|
info_text = model_ui.model_mismatch_confirm_dialog.informativeText()
|
|
self.assertTrue(model_ui.target.__module__ in info_text)
|
|
self.assertTrue('test_model' in info_text)
|
|
|
|
# Reject the dialog.
|
|
QTest.mouseClick(
|
|
model_ui.model_mismatch_confirm_dialog.button(
|
|
QtWidgets.QMessageBox.Cancel),
|
|
QtCore.Qt.LeftButton)
|
|
|
|
QtCore.QTimer.singleShot(500, _confirm_datastack_load)
|
|
|
|
# Verify that we haven't changed anything when the dialog is cancelled.
|
|
previous_args = model_ui.assemble_args()
|
|
model_ui.load_datastack(filepath)
|
|
self.assertEqual(previous_args, model_ui.assemble_args())
|
|
finally:
|
|
model_ui.close(prompt=False)
|
|
model_ui.destroy()
|
|
|
|
|
|
class ValidatorTest(_QtTest):
|
|
def test_in_progress(self):
|
|
from natcap.invest.ui import inputs
|
|
|
|
parent_widget = QtWidgets.QWidget()
|
|
validator = inputs.Validator(parent_widget)
|
|
|
|
self.assertFalse(validator.in_progress())
|
|
|
|
# validator should be in progress while _validate() is executing.
|
|
def _validate(args, limit_to=None):
|
|
self.assertTrue(validator.in_progress())
|
|
return []
|
|
|
|
validator.validate(target=_validate, args={})
|
|
|
|
|
|
class IsProbablyDatastackTests(unittest.TestCase):
|
|
"""Tests for our quick check for whether a file is a datastack."""
|
|
|
|
def setUp(self):
|
|
"""Create a new workspace for each test."""
|
|
self.workspace = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
"""Clean up the workspace created for each test."""
|
|
shutil.rmtree(self.workspace)
|
|
|
|
def test_directory(self):
|
|
"""Model UI datastack: directory is not a datastack."""
|
|
from natcap.invest.ui import model
|
|
|
|
dirpath = os.path.join(self.workspace, 'foo_dir')
|
|
os.makedirs(dirpath)
|
|
self.assertFalse(model.is_probably_datastack(dirpath))
|
|
|
|
def test_json_extension(self):
|
|
"""Model UI datastack: invest.json extension is a datastack"""
|
|
from natcap.invest.ui import model
|
|
|
|
filepath = 'some_model.invest.json'
|
|
self.assertTrue(model.is_probably_datastack(filepath))
|
|
|
|
def test_tar_gz_extension(self):
|
|
"""Model UI datastack: invest.tar.gz extension is a datastack"""
|
|
from natcap.invest.ui import model
|
|
|
|
filepath = 'some_model.invest.tar.gz'
|
|
self.assertTrue(model.is_probably_datastack(filepath))
|
|
|
|
def test_parameter_set(self):
|
|
"""Model UI datastack: a parameter set should be a datastack."""
|
|
from natcap.invest.ui import model
|
|
from natcap.invest import datastack
|
|
|
|
filepath = os.path.join(self.workspace, 'paramset.json')
|
|
args = {'foo': 'foo', 'bar': 'bar'}
|
|
datastack.build_parameter_set(args, 'test_model', filepath)
|
|
|
|
self.assertTrue(model.is_probably_datastack(filepath))
|
|
|
|
def test_parameter_archive(self):
|
|
"""Model UI datastack: a parameter archive should be a datastack."""
|
|
from natcap.invest.ui import model
|
|
from natcap.invest import datastack
|
|
|
|
filepath = os.path.join(self.workspace, 'paramset.tar.gz')
|
|
args = {'foo': 'foo', 'bar': 'bar'}
|
|
datastack.build_datastack_archive(args, 'test_model', filepath)
|
|
|
|
self.assertTrue(model.is_probably_datastack(filepath))
|
|
|
|
def test_parameter_logfile(self):
|
|
"""Model UI datastack: a logfile should be a datastack."""
|
|
from natcap.invest.ui import model
|
|
|
|
filepath = os.path.join(self.workspace, 'logfile.txt')
|
|
with open(filepath, 'w') as logfile:
|
|
logfile.write(textwrap.dedent("""
|
|
07/20/2017 16:37:48 natcap.invest.ui.model INFO
|
|
Arguments:
|
|
suffix foo
|
|
some_int 1
|
|
some_float 2.33
|
|
workspace_dir some_workspace_dir
|
|
|
|
07/20/2017 16:37:48 natcap.invest.ui.model INFO post args.
|
|
"""))
|
|
|
|
self.assertTrue(model.is_probably_datastack(filepath))
|
|
|
|
def test_parameter_logfile_startswith_args(self):
|
|
"""Model UI datastack: logfile starting with arguments is datastack."""
|
|
from natcap.invest.ui import model
|
|
|
|
filepath = os.path.join(self.workspace, 'logfile.txt')
|
|
with open(filepath, 'w') as logfile:
|
|
logfile.write(textwrap.dedent(
|
|
"""Arguments:
|
|
carbon_pools_path file_a.csv
|
|
lulc_cur_path file_b.tif
|
|
workspace_dir new_workspace_dir"""))
|
|
self.assertTrue(model.is_probably_datastack(filepath))
|
|
|
|
|
|
def test_csv_not_a_parameter(self):
|
|
"""Model UI datastack: a CSV is probably not a parameter set."""
|
|
from natcap.invest.ui import model
|
|
|
|
filepath = os.path.join(self.workspace, 'sample.csv')
|
|
with open(filepath, 'w') as sample_csv:
|
|
sample_csv.write('A,B,C\n')
|
|
sample_csv.write('"aaa","bbb","ccc"\n')
|
|
|
|
self.assertFalse(model.is_probably_datastack(filepath))
|
|
|
|
|
|
class FileSystemRunDialogTests(_QtTest):
|
|
def setUp(self):
|
|
_QtTest.setUp(self)
|
|
self.workspace = tempfile.mkdtemp()
|
|
|
|
def tearDown(self):
|
|
_QtTest.tearDown(self)
|
|
shutil.rmtree(self.workspace)
|
|
|
|
def test_window_close(self):
|
|
from natcap.invest.ui import inputs
|
|
|
|
dialog = inputs.FileSystemRunDialog()
|
|
dialog.show()
|
|
dialog.start('Window title', self.workspace)
|
|
self.qt_app.processEvents()
|
|
|
|
# Try to close the dialog, verify that it did not close.
|
|
dialog.close() # This shouldn't do anything.
|
|
self.qt_app.processEvents()
|
|
self.assertTrue(dialog.isVisible())
|
|
|
|
# Finish the dialog, verify it is no longer visible.
|
|
dialog.finish(None) # None = no exception encountered.
|
|
dialog.close() # this should work now.
|
|
self.qt_app.processEvents()
|
|
self.assertFalse(dialog.isVisible())
|
|
|
|
# launch the window again and verify that the things that should have
|
|
# been cleared have been cleared.
|
|
dialog.show()
|
|
self.assertTrue(dialog.openWorkspaceCB.isVisible())
|
|
self.assertFalse(dialog.openWorkspaceButton.isVisible())
|
|
self.assertEqual(dialog.messageArea.text(), '')
|
|
self.assertEqual(dialog.cancel, False)
|