Repository: spyder-ide/spyder-unittest (language: python)

Issue:
Use python interpreter from preferences to run tests
Use `<python> -m pytest` (for py.test) to run the tests, where `<python>` is the Python executable set in the preferences. This is useful for systems with more than one Python interpreter and it also resolves the Debian issue from #64.


Discussion / hints:
@ccordoba12 Is there a signal emiitted when a user changes a configuration option?

I am wondering how the unittest widget can retrieve the Python executable set in the preferences without referencing the main Spyder application. The plugin can read the Spyder configuration and use that to set a field in the unittest widget to contain the Python executable. If there is a signal when the user changes the configuration, then the plugin can read the configuration again and update the widget with the new value of the executable.
Is there a workaround? I just installed the plugin but it is basically useless. I installed spyder into its own conda env, and then I install the spyder kernels into my other conda environments. All tests fail because they use the spyder environment, which doesn't have any packages installed. And the main spyder website indicates that installing spyder into its own environment seems to be preferred.

https://docs.spyder-ide.org/current/installation.html
I started to tackle this issue...but it's ugly.

I added a file picker to the configdialog so you could manually choose the python executable, then made a few other tweaks so that the start function in RunnerBase would use this executable. Unfortunately, that threw a couple errors because the pytestworker file needs to import from spyder (get_translation) and spyder_unittest (zmqstreamwriter).

If I comment those out and just set the writer to FileStub, the tests execute properly with the chosen python executable. However, I only can see that if I choose the Show Output option from the test menu. Looks like that ZMQ stuff is important for making the nice outputs, which would require me to install spyder and spyter-unittest in every conda environment I need it... :-/

~As a last ditch effort, I just copied the contents of ZmqStreamWriter into the pytestworker file.~ I changed the import to `from zmqstream import ZmqStreamWriter`  That also had some dependencies, such as qtpy and pyqt5, so I had to install those in my desired env. At this point I almost might as well just install spyder and spyder-unittest in each environment, because **this DID work**. I now get the fancy results in the unit testing pane.

Fix patch:
diff --git a/requirements/conda.txt b/requirements/conda.txt
index 94d0ae01..cb8205f8 100644
--- a/requirements/conda.txt
+++ b/requirements/conda.txt
@@ -1,2 +1,2 @@
 lxml
-spyder>=5.2,<6
+spyder>=5.3.1,<6
diff --git a/setup.py b/setup.py
index 3a87b1e9..8931c86c 100644
--- a/setup.py
+++ b/setup.py
@@ -37,7 +37,7 @@ def get_package_data(name, extlist):
 
 
 # Requirements
-REQUIREMENTS = ['lxml', 'spyder>=5.2,<6', 'pyzmq']
+REQUIREMENTS = ['lxml', 'spyder>=5.3.1,<6', 'pyzmq']
 EXTLIST = ['.jpg', '.png', '.json', '.mo', '.ini']
 LIBNAME = 'spyder_unittest'
 


Test patch:
diff --git a/.github/workflows/linux-tests.yml b/.github/workflows/linux-tests.yml
index f2acae52..05b9c6e4 100644
--- a/.github/workflows/linux-tests.yml
+++ b/.github/workflows/linux-tests.yml
@@ -25,8 +25,8 @@ jobs:
         uses: actions/checkout@v2
       - name: Install System Packages
         run: | 
-          sudo apt-get update
-          sudo apt-get install libegl1-mesa
+          sudo apt-get update --fix-missing
+          sudo apt-get install -qq pyqt5-dev-tools libxcb-xinerama0 xterm --fix-missing
       - name: Install Conda
         uses: conda-incubator/setup-miniconda@v2
         with:
diff --git a/requirements/tests.txt b/requirements/tests.txt
index 92191942..4a80c451 100644
--- a/requirements/tests.txt
+++ b/requirements/tests.txt
@@ -1,7 +1,7 @@
 codecov
 flaky
 nose
-pytest
+pytest>=5
 pytest-cov
 pytest-mock
 pytest-qt
diff --git a/spyder_unittest/backend/pytestrunner.py b/spyder_unittest/backend/pytestrunner.py
index dbc45596..59a34837 100644
--- a/spyder_unittest/backend/pytestrunner.py
+++ b/spyder_unittest/backend/pytestrunner.py
@@ -45,12 +45,12 @@ def create_argument_list(self):
         pyfile = os.path.join(os.path.dirname(__file__), 'pytestworker.py')
         return [pyfile, str(self.reader.port)]
 
-    def start(self, config, pythonpath):
+    def start(self, config, executable, pythonpath):
         """Start process which will run the unit test suite."""
         self.config = config
         self.reader = ZmqStreamReader()
         self.reader.sig_received.connect(self.process_output)
-        RunnerBase.start(self, config, pythonpath)
+        RunnerBase.start(self, config, executable, pythonpath)
 
     def process_output(self, output):
         """
diff --git a/spyder_unittest/backend/pytestworker.py b/spyder_unittest/backend/pytestworker.py
index 7e65023d..ea74ec9b 100644
--- a/spyder_unittest/backend/pytestworker.py
+++ b/spyder_unittest/backend/pytestworker.py
@@ -17,16 +17,14 @@
 # Third party imports
 import pytest
 
-# Local imports
-from spyder.config.base import get_translation
-from spyder_unittest.backend.zmqstream import ZmqStreamWriter
-
+# Local imports, needs to be relative otherwise it will fail if trying
+# to execute in a different env with only spyder-kernel installed
 try:
-    _ = get_translation('spyder_unittest')
-except KeyError:  # pragma: no cover
-    import gettext
-    _ = gettext.gettext
-
+    # this line is needed for the tests to succeed
+    from .zmqstream import ZmqStreamWriter
+except:
+    # this line is needed for the plugin to work
+    from zmqstream import ZmqStreamWriter
 
 class FileStub():
     """Stub for ZmqStreamWriter which instead writes to a file."""
@@ -104,8 +102,8 @@ def pytest_runtest_logreport(self, report):
                 self.was_skipped = True
         if hasattr(report, 'wasxfail'):
             self.was_xfail = True
-            self.longrepr.append(report.wasxfail if report.wasxfail else _(
-                'WAS EXPECTED TO FAIL'))
+            self.longrepr.append(report.wasxfail if report.wasxfail else 
+                'WAS EXPECTED TO FAIL')
         self.sections = report.sections  # already accumulated over phases
         if report.longrepr:
             first_msg_idx = len(self.longrepr)
@@ -120,7 +118,7 @@ def pytest_runtest_logreport(self, report):
             if report.outcome == 'failed' and report.when in (
                     'setup', 'teardown'):
                 self.longrepr[first_msg_idx] = '{} {}: {}'.format(
-                    _('ERROR at'), report.when, self.longrepr[first_msg_idx])
+                    'ERROR at', report.when, self.longrepr[first_msg_idx])
 
     def pytest_runtest_logfinish(self, nodeid, location):
         """Called by pytest when the entire test is completed."""
diff --git a/spyder_unittest/backend/runnerbase.py b/spyder_unittest/backend/runnerbase.py
index 25e1abc8..22fff189 100644
--- a/spyder_unittest/backend/runnerbase.py
+++ b/spyder_unittest/backend/runnerbase.py
@@ -13,7 +13,6 @@
 from qtpy.QtCore import (QObject, QProcess, QProcessEnvironment, QTextCodec,
                          Signal)
 from spyder.py3compat import to_text_string
-from spyder.utils.misc import get_python_executable
 
 try:
     from importlib.util import find_spec as find_spec_or_loader
@@ -189,7 +188,7 @@ def _prepare_process(self, config, pythonpath):
             process.setProcessEnvironment(env)
         return process
 
-    def start(self, config, pythonpath):
+    def start(self, config, executable, pythonpath):
         """
         Start process which will run the unit test suite.
 
@@ -203,6 +202,8 @@ def start(self, config, pythonpath):
         ----------
         config : TestConfig
             Unit test configuration.
+        executable : str
+            Path to Python executable
         pythonpath : list of str
             List of directories to be added to the Python path
 
@@ -212,7 +213,6 @@ def start(self, config, pythonpath):
             If process failed to start.
         """
         self.process = self._prepare_process(config, pythonpath)
-        executable = get_python_executable()
         p_args = self.create_argument_list()
         try:
             os.remove(self.resultfilename)
diff --git a/spyder_unittest/backend/tests/test_pytestrunner.py b/spyder_unittest/backend/tests/test_pytestrunner.py
index 2c063e12..e3e9191f 100644
--- a/spyder_unittest/backend/tests/test_pytestrunner.py
+++ b/spyder_unittest/backend/tests/test_pytestrunner.py
@@ -8,10 +8,10 @@
 # Standard library imports
 import os
 import os.path as osp
+import sys
 
 # Third party imports
 import pytest
-from qtpy.QtCore import QByteArray
 
 # Local imports
 from spyder_unittest.backend.pytestrunner import (PyTestRunner,
@@ -58,12 +58,13 @@ def test_pytestrunner_start(monkeypatch):
 
     runner = PyTestRunner(None, 'results')
     config = Config()
-    runner.start(config, ['pythondir'])
+    runner.start(config, sys.executable, ['pythondir'])
     assert runner.config is config
     assert runner.reader is mock_reader
     runner.reader.sig_received.connect.assert_called_once_with(
         runner.process_output)
-    MockRunnerBase.start.assert_called_once_with(runner, config, ['pythondir'])
+    MockRunnerBase.start.assert_called_once_with(
+        runner, config, sys.executable, ['pythondir'])
 
 
 def test_pytestrunner_process_output_with_collected(qtbot):
@@ -75,6 +76,7 @@ def test_pytestrunner_process_output_with_collected(qtbot):
     expected = ['spam.ham', 'eggs.bacon']
     assert blocker.args == [expected]
 
+
 def test_pytestrunner_process_output_with_collecterror(qtbot):
     runner = PyTestRunner(None)
     output = [{
@@ -87,6 +89,7 @@ def test_pytestrunner_process_output_with_collecterror(qtbot):
     expected = [('ham.spam', 'msg')]
     assert blocker.args == [expected]
 
+
 def test_pytestrunner_process_output_with_starttest(qtbot):
     runner = PyTestRunner(None)
     output = [{'event': 'starttest', 'nodeid': 'ham/spam.py::ham'},
diff --git a/spyder_unittest/backend/tests/test_runnerbase.py b/spyder_unittest/backend/tests/test_runnerbase.py
index 728fe84c..0fc18a40 100644
--- a/spyder_unittest/backend/tests/test_runnerbase.py
+++ b/spyder_unittest/backend/tests/test_runnerbase.py
@@ -76,17 +76,13 @@ def test_runnerbase_start(monkeypatch):
     monkeypatch.setattr('spyder_unittest.backend.runnerbase.os.remove',
                         mock_remove)
 
-    monkeypatch.setattr(
-        'spyder_unittest.backend.runnerbase.get_python_executable',
-        lambda: 'python')
-
     runner = RunnerBase(None, 'results')
     runner._prepare_process = lambda c, p: mock_process
     runner.create_argument_list = lambda: ['arg1', 'arg2']
     config = Config('pytest', 'wdir')
     mock_process.waitForStarted = lambda: False
     with pytest.raises(RuntimeError):
-        runner.start(config, ['pythondir'])
+        runner.start(config, 'python_exec', ['pythondir'])
 
-    mock_process.start.assert_called_once_with('python', ['arg1', 'arg2'])
+    mock_process.start.assert_called_once_with('python_exec', ['arg1', 'arg2'])
     mock_remove.assert_called_once_with('results')
diff --git a/spyder_unittest/widgets/tests/test_unittestgui.py b/spyder_unittest/widgets/tests/test_unittestgui.py
index 292ba68d..764d2a39 100644
--- a/spyder_unittest/widgets/tests/test_unittestgui.py
+++ b/spyder_unittest/widgets/tests/test_unittestgui.py
@@ -7,6 +7,7 @@
 
 # Standard library imports
 import os
+import sys
 
 # Third party imports
 from qtpy.QtCore import Qt, QProcess
@@ -26,6 +27,10 @@
 @pytest.fixture
 def widget(qtbot):
     unittest_widget = UnitTestWidget('testwidget', None, None)
+    unittest_widget.get_conf(
+        'executable',
+        section='main_interpreter',
+        default=sys.executable)
     unittest_widget.setup()
     qtbot.addWidget(unittest_widget)
     return unittest_widget
diff --git a/spyder_unittest/widgets/unittestgui.py b/spyder_unittest/widgets/unittestgui.py
index 70f9d3c4..28614666 100644
--- a/spyder_unittest/widgets/unittestgui.py
+++ b/spyder_unittest/widgets/unittestgui.py
@@ -340,9 +340,9 @@ def run_tests(self, config=None):
         self.testrunner.sig_starttest.connect(self.tests_started)
         self.testrunner.sig_testresult.connect(self.tests_yield_result)
         self.testrunner.sig_stop.connect(self.tests_stopped)
-
+        executable = self.get_conf('executable', section='main_interpreter')
         try:
-            self.testrunner.start(config, pythonpath)
+            self.testrunner.start(config, executable, pythonpath)
         except RuntimeError:
             QMessageBox.critical(self,
                                  _("Error"), _("Process failed to start"))