From a87fe61992003e7e7e541f41194c5f0b3e458346 Mon Sep 17 00:00:00 2001 From: Matthew Treinish Date: Fri, 15 May 2020 19:26:34 -0400 Subject: [PATCH] Use FakeProvider for jupyter and monitor tests (#4296) * Use FakeProvider for jupyter and monitor tests Right now the jupyter widget tests and backend monitor tests are the only place in the qiskit-terra unittests that require talking to IQX. This commit removes that dependency by leveraging the FakeProvider and FakeBackends which contain snapshots of the responses from the IBMQ api. To accomplish mocking out the qiskit-ibmq-provider usage the backend_overview module had to be renamed because it conflicted with the backend_overview function and was not importable via an absolute import. * Fix tests and lint * Remove online tests from contributing documentation There are no longer any online tests in the suite now that the provider interactions have been mocked out. This commit removes the documentation about how to configure the online tests. * Fixes from review comments --- CONTRIBUTING.md | 14 ---- qiskit/test/mock/__init__.py | 1 + qiskit/test/mock/fake_provider.py | 42 +++++++++- qiskit/tools/jupyter/backend_overview.py | 2 +- qiskit/tools/jupyter/job_watcher.py | 10 ++- qiskit/tools/monitor/__init__.py | 2 +- .../{backend_overview.py => overview.py} | 0 test/python/tools/jupyter/test_notebooks.py | 44 ++++++----- .../tools/monitor/test_backend_monitor.py | 77 ++++++++++++++----- 9 files changed, 136 insertions(+), 56 deletions(-) rename qiskit/tools/monitor/{backend_overview.py => overview.py} (100%) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 944b5c5b8a..689cdd6b8f 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -280,19 +280,6 @@ C:\..\> set LOG_LEVEL="INFO" C:\..\> python -m unittest test/python/circuit/test_circuit_operations.py ``` -##### Online Tests - -Some tests require that you an IBMQ account configured. By default these -tests are always skipped. If you want to run these tests locally please -go to this -[page](https://quantumexperience.ng.bluemix.net/qx/account/advanced) and -register an account. Then you can either set the credentials explicitly -with the `IBMQ_TOKEN` and `IBMQ_URL` environment variables to specify -the token and url respectively for the IBMQ service. Alternatively, if -you already have a single set of credentials configured in your -environment (using a `.qiskitrc`) then you can just set -`QISKIT_TESTS_USE_CREDENTIALS_FILE` to `1` and it will use that. - ##### Test Skip Options How and which tests are executed is controlled by an environment @@ -300,7 +287,6 @@ variable, `QISKIT_TESTS`: Option | Description | Default ------ | ----------- | ------- -`skip_online` | Skips tests that require remote requests. Does not require user credentials. | `False` `run_slow` | It runs tests tagged as *slow*. | `False` It is possible to provide more than one option separated with commas. diff --git a/qiskit/test/mock/__init__.py b/qiskit/test/mock/__init__.py index ff9e0e18d3..42bc727517 100644 --- a/qiskit/test/mock/__init__.py +++ b/qiskit/test/mock/__init__.py @@ -23,6 +23,7 @@ The mock devices are mainly for testing the compiler. """ from .fake_provider import FakeProvider +from .fake_provider import FakeProviderFactory from .fake_backend import FakeBackend from .fake_job import FakeJob from .fake_qobj import FakeQobj diff --git a/qiskit/test/mock/fake_provider.py b/qiskit/test/mock/fake_provider.py index 851c18019a..7eb7572e86 100644 --- a/qiskit/test/mock/fake_provider.py +++ b/qiskit/test/mock/fake_provider.py @@ -12,7 +12,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -# pylint: disable=wildcard-import +# pylint: disable=wildcard-import,unused-argument """ Fake provider class that provides access to fake backends. @@ -76,3 +76,43 @@ class FakeProvider(BaseProvider): FakeAthens()] super().__init__() + + +class FakeProviderFactory: + """Fake provider factory class.""" + + def __init__(self): + self.fake_provider = FakeProvider() + + def load_account(self): + """Fake load_account method to mirror the IBMQ provider.""" + pass + + def enable_account(self, *args, **kwargs): + """Fake enable_account method to mirror the IBMQ provider factory.""" + pass + + def disable_account(self): + """Fake disable_account method to mirror the IBMQ provider factory.""" + pass + + def save_account(self, *args, **kwargs): + """Fake save_account method to mirror the IBMQ provider factory.""" + pass + + @staticmethod + def delete_account(): + """Fake delete_account method to mirror the IBMQ provider factory.""" + pass + + def update_account(self, force=False): + """Fake update_account method to mirror the IBMQ provider factory.""" + pass + + def providers(self): + """Fake providers method to mirror the IBMQ provider.""" + return [self.fake_provider] + + def get_provider(self, hub=None, group=None, project=None): + """Fake get_provider method to mirror the IBMQ provider.""" + return self.fake_provider diff --git a/qiskit/tools/jupyter/backend_overview.py b/qiskit/tools/jupyter/backend_overview.py index 2049b4ad4c..2792e37e5c 100644 --- a/qiskit/tools/jupyter/backend_overview.py +++ b/qiskit/tools/jupyter/backend_overview.py @@ -22,7 +22,7 @@ from IPython.core.magic import line_magic, Magics, magics_class # pylint: disab from IPython.core import magic_arguments # pylint: disable=import-error import matplotlib.pyplot as plt # pylint: disable=import-error import ipywidgets as widgets # pylint: disable=import-error -from qiskit.tools.monitor.backend_overview import get_unique_backends +from qiskit.tools.monitor.overview import get_unique_backends from qiskit.visualization.gate_map import plot_gate_map diff --git a/qiskit/tools/jupyter/job_watcher.py b/qiskit/tools/jupyter/job_watcher.py index 9fc59d8196..568b97675b 100644 --- a/qiskit/tools/jupyter/job_watcher.py +++ b/qiskit/tools/jupyter/job_watcher.py @@ -18,7 +18,11 @@ from IPython.core.magic import (line_magic, # pylint: disable=import-error Magics, magics_class) from qiskit.tools.events.pubsub import Subscriber -from qiskit.providers.ibmq.job.exceptions import IBMQJobApiError +try: + from qiskit.providers.ibmq.job.exceptions import IBMQJobApiError + HAS_IBMQ = True +except ImportError: + HAS_IBMQ = False from .job_widgets import (build_job_viewer, make_clear_button, make_labels, create_job_widget) from .watcher_monitor import _job_monitor @@ -29,6 +33,10 @@ class JobWatcher(Subscriber): """ def __init__(self): super().__init__() + if not HAS_IBMQ: + raise ImportError("qiskit-ibmq-provider is required to use the " + "job watcher. To install it run 'pip install " + "qiskit-ibmq-provider'") self.jobs = [] self._init_subscriber() self.job_viewer = None diff --git a/qiskit/tools/monitor/__init__.py b/qiskit/tools/monitor/__init__.py index b259623191..bcd041534d 100644 --- a/qiskit/tools/monitor/__init__.py +++ b/qiskit/tools/monitor/__init__.py @@ -16,4 +16,4 @@ """ from .job_monitor import job_monitor -from .backend_overview import backend_monitor, backend_overview +from .overview import backend_monitor, backend_overview diff --git a/qiskit/tools/monitor/backend_overview.py b/qiskit/tools/monitor/overview.py similarity index 100% rename from qiskit/tools/monitor/backend_overview.py rename to qiskit/tools/monitor/overview.py diff --git a/test/python/tools/jupyter/test_notebooks.py b/test/python/tools/jupyter/test_notebooks.py index 070e408c34..22b72a5c06 100644 --- a/test/python/tools/jupyter/test_notebooks.py +++ b/test/python/tools/jupyter/test_notebooks.py @@ -22,9 +22,8 @@ import unittest import nbformat from nbconvert.preprocessors import ExecutePreprocessor -import qiskit from qiskit.tools.visualization import HAS_MATPLOTLIB -from qiskit.test import (Path, QiskitTestCase, online_test, slow_test) +from qiskit.test import (Path, QiskitTestCase, slow_test) # Timeout (in seconds) for a single notebook. @@ -33,14 +32,12 @@ TIMEOUT = 1000 JUPYTER_KERNEL = 'python3' -@unittest.skipUnless(hasattr(qiskit, 'IBMQ'), - 'qiskit-ibmq-provider is required for these tests') class TestJupyter(QiskitTestCase): """Notebooks test case.""" def setUp(self): self.execution_path = os.path.join(Path.SDK.value, '..') - def _execute_notebook(self, filename, qe_token=None, qe_url=None): + def _execute_notebook(self, filename): # Create the preprocessor. execute_preprocessor = ExecutePreprocessor(timeout=TIMEOUT, kernel_name=JUPYTER_KERNEL) @@ -49,16 +46,26 @@ class TestJupyter(QiskitTestCase): with open(filename) as file_: notebook = nbformat.read(file_, as_version=4) - if qe_token and qe_url: - top_str = "from qiskit import IBMQ\n" - top_str += "IBMQ.enable_account('{token}', '{url}')".format(token=qe_token, - url=qe_url) - top = nbformat.notebooknode.NotebookNode({'cell_type': 'code', - 'execution_count': 0, - 'metadata': {}, - 'outputs': [], - 'source': top_str}) - notebook.cells = [top] + notebook.cells + top_str = """ + import qiskit + import sys + from unittest.mock import create_autospec, MagicMock + from qiskit.test.mock import FakeProviderFactory + from qiskit.providers import basicaer + fake_prov = FakeProviderFactory() + qiskit.IBMQ = fake_prov + ibmq_mock = create_autospec(basicaer) + ibmq_mock.IBMQJobApiError = MagicMock() + sys.modules['qiskit.providers.ibmq'] = ibmq_mock + sys.modules['qiskit.providers.ibmq.job'] = ibmq_mock + sys.modules['qiskit.providers.ibmq.job.exceptions'] = ibmq_mock + """ + top = nbformat.notebooknode.NotebookNode({'cell_type': 'code', + 'execution_count': 0, + 'metadata': {}, + 'outputs': [], + 'source': top_str}) + notebook.cells = [top] + notebook.cells # Run the notebook into the folder containing the `qiskit/` module. execute_preprocessor.preprocess( @@ -73,14 +80,11 @@ class TestJupyter(QiskitTestCase): 'notebooks/test_pbar_status.ipynb')) @unittest.skipIf(not HAS_MATPLOTLIB, 'matplotlib not available.') - @online_test @slow_test - def test_backend_tools(self, qe_token, qe_url): + def test_backend_tools(self): """Test Jupyter backend tools.""" self._execute_notebook(self._get_resource_path( - 'notebooks/test_backend_tools.ipynb'), - qe_token=qe_token, - qe_url=qe_url) + 'notebooks/test_backend_tools.ipynb')) if __name__ == '__main__': diff --git a/test/python/tools/monitor/test_backend_monitor.py b/test/python/tools/monitor/test_backend_monitor.py index 2b275a0a47..a14059a518 100644 --- a/test/python/tools/monitor/test_backend_monitor.py +++ b/test/python/tools/monitor/test_backend_monitor.py @@ -14,24 +14,69 @@ """Tests for the wrapper functionality.""" +import sys import unittest from unittest.mock import patch +from unittest.mock import MagicMock from io import StringIO +import qiskit +from qiskit import providers from qiskit.tools.monitor import backend_overview, backend_monitor -from qiskit.test import QiskitTestCase, online_test +from qiskit.test import QiskitTestCase +from qiskit.test.mock import FakeProviderFactory +from qiskit.test.mock import FakeBackend +from qiskit.test.mock import FakeVigo class TestBackendOverview(QiskitTestCase): """Tools test case.""" - @online_test - def test_backend_overview(self, qe_token, qe_url): - """Test backend_overview""" - from qiskit import IBMQ # pylint: disable: import-error - IBMQ.enable_account(qe_token, qe_url) - self.addCleanup(IBMQ.disable_account) + def _restore_ibmq(self): + if not self.import_error: + qiskit.IBMQ = self.ibmq_back + else: + del qiskit.IBMQ + if self.prov_backup: + providers.ibmq = self.prov_backup + else: + del providers.ibmq + def _restore_ibmq_mod(self): + if self.ibmq_module_backup is not None: + sys.modules['qiskit.providers.ibmq'] = self.ibmq_module_backup + else: + sys.modules.pop('qiskit.providers.ibmq') + + def setUp(self): + super().setUp() + ibmq_mock = MagicMock() + ibmq_mock.IBMQBackend = FakeBackend + if 'qiskit.providers.ibmq' in sys.modules: + self.ibmq_module_backup = sys.modules['qiskit.providers.ibmq'] + else: + self.ibmq_module_backup = None + sys.modules['qiskit.providers.ibmq'] = ibmq_mock + self.addCleanup(self._restore_ibmq_mod) + + if hasattr(qiskit, 'IBMQ'): + self.import_error = False + else: + self.import_error = True + qiskit.IBMQ = None + self.ibmq_back = qiskit.IBMQ + qiskit.IBMQ = FakeProviderFactory() + self.addCleanup(self._restore_ibmq) + if hasattr(providers, 'ibmq'): + self.prov_backup = providers.ibmq + else: + self.prov_backup = None + providers.ibmq = MagicMock() + + @patch('qiskit.tools.monitor.overview.get_unique_backends', + return_value=[FakeVigo()]) + def test_backend_overview(self, _): + """Test backend_overview""" with patch('sys.stdout', new=StringIO()) as fake_stdout: backend_overview() stdout = fake_stdout.getvalue() @@ -39,18 +84,14 @@ class TestBackendOverview(QiskitTestCase): self.assertIn('Avg. T1:', stdout) self.assertIn('Num. Qubits:', stdout) - @online_test - def test_backend_monitor(self, qe_token, qe_url): + @patch('qiskit.tools.monitor.overview.get_unique_backends', + return_value=[FakeVigo()]) + def test_backend_monitor(self, _): """Test backend_monitor""" - from qiskit import IBMQ # pylint: disable: import-error - IBMQ.enable_account(qe_token, qe_url) - self.addCleanup(IBMQ.disable_account) - - for provider in IBMQ.providers(): - for back in provider.backends(): - if not back.configuration().simulator: - backend = back - break + for back in [FakeVigo()]: + if not back.configuration().simulator: + backend = back + break with patch('sys.stdout', new=StringIO()) as fake_stdout: backend_monitor(backend)