diff --git a/.github/workflows/cron-other.yml b/.github/workflows/cron-other.yml deleted file mode 100644 index 035b72c881..0000000000 --- a/.github/workflows/cron-other.yml +++ /dev/null @@ -1,102 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -name: Cron-test -on: - schedule: - - cron: '0 5 * * *' -jobs: - test1: - name: tests1-python${{ matrix.python-version }}-${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - python-version: [3.7, 3.8, 3.9] - os: ["windows-latest", "ubuntu-latest"] - env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} - LOG_LEVEL: DEBUG - STREAM_LOG: True - QISKIT_IN_PARALLEL: True - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install Deps - run: | - python -m pip install --upgrade pip - pip install -U git+https://github.com/Qiskit/qiskit-aer.git - pip install -c constraints.txt -e . - pip install -U -c constraints.txt -r requirements-dev.txt - - name: Run Tests - run: make test1 - test2: - name: tests2-python${{ matrix.python-version }}-${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - python-version: [3.7, 3.8, 3.9] - os: ["windows-latest", "ubuntu-latest"] - env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} - LOG_LEVEL: DEBUG - STREAM_LOG: True - QISKIT_IN_PARALLEL: True - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install Deps - run: | - python -m pip install --upgrade pip - pip install -c constraints.txt -e . - pip install -U -c constraints.txt -r requirements-dev.txt - - name: Run Tests - run: make test2 - test3: - name: tests3-python${{ matrix.python-version }}-${{ matrix.os }} - runs-on: ${{ matrix.os }} - strategy: - matrix: - python-version: [3.7, 3.8, 3.9] - os: ["windows-latest", "ubuntu-latest"] - env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} - LOG_LEVEL: DEBUG - STREAM_LOG: True - QISKIT_IN_PARALLEL: True - steps: - - uses: actions/checkout@v2 - - name: Set up Python ${{ matrix.python-version }} - uses: actions/setup-python@v2 - with: - python-version: ${{ matrix.python-version }} - - name: Install Deps - run: | - python -m pip install --upgrade pip - pip install -c constraints.txt -e . - pip install -U -c constraints.txt -r requirements-dev.txt - - name: Run Tests - run: make test3 diff --git a/.github/workflows/cron-prod.yml b/.github/workflows/cron-prod.yml index 5878dc54c6..85f10c1c40 100644 --- a/.github/workflows/cron-prod.yml +++ b/.github/workflows/cron-prod.yml @@ -1,28 +1,77 @@ -name: Cron-prod +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +name: Cron-test on: schedule: - cron: '0 4 * * *' jobs: - runtime-integration: - name: runtime-integration - runs-on: macOS-latest + test1: + name: tests1-python${{ matrix.python-version }}-${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: ["windows-latest", "ubuntu-latest"] env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_DEVICE: ${{ secrets.QISKIT_IBM_RUNTIME_DEVICE }} + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} LOG_LEVEL: DEBUG STREAM_LOG: True QISKIT_IN_PARALLEL: True steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: ${{ matrix.python-version }} - name: Install Deps run: | python -m pip install --upgrade pip pip install -c constraints.txt -e . pip install -U -c constraints.txt -r requirements-dev.txt - name: Run Tests - run: make runtime_integration \ No newline at end of file + run: make test1 + test2: + name: tests2-python${{ matrix.python-version }}-${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: ["windows-latest", "ubuntu-latest"] + env: + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} + LOG_LEVEL: DEBUG + STREAM_LOG: True + QISKIT_IN_PARALLEL: True + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Deps + run: | + python -m pip install --upgrade pip + pip install -c constraints.txt -e . + pip install -U -c constraints.txt -r requirements-dev.txt + - name: Run Tests + run: make test2 diff --git a/.github/workflows/cron-slow.yml b/.github/workflows/cron-slow.yml index a5c3203dee..a690028b93 100644 --- a/.github/workflows/cron-slow.yml +++ b/.github/workflows/cron-slow.yml @@ -19,10 +19,12 @@ jobs: name: slow-test runs-on: macOS-latest env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} LOG_LEVEL: DEBUG STREAM_LOG: True QISKIT_TESTS: run_slow @@ -44,10 +46,12 @@ jobs: name: terra-main runs-on: macOS-latest env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} LOG_LEVEL: DEBUG STREAM_LOG: True QISKIT_TESTS: run_slow @@ -64,6 +68,5 @@ jobs: pip install -c constraints.txt -e . pip install -U -c constraints.txt -r requirements-dev.txt pip install -U git+https://github.com/Qiskit/qiskit-terra.git - pip install -U git+https://github.com/Qiskit/qiskit-aer.git - name: Run Tests run: make test diff --git a/.github/workflows/cron-staging.yml b/.github/workflows/cron-staging.yml index 29a6538f0e..cdec65d410 100644 --- a/.github/workflows/cron-staging.yml +++ b/.github/workflows/cron-staging.yml @@ -10,32 +10,70 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -name: Cron-staging +name: Cron-test on: schedule: - - cron: '0 4 * * *' + - cron: '0 5 * * *' jobs: - runtime-integration: - name: runtime-integration - runs-on: macOS-latest + test1: + name: tests1-python${{ matrix.python-version }}-${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: ["windows-latest", "ubuntu-latest"] env: - QISKIT_IBM_RUNTIME_STAGING_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_STAGING_API_TOKEN }} - QISKIT_IBM_RUNTIME_STAGING_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_STAGING_API_URL }} - QISKIT_IBM_RUNTIME_STAGING_DEVICE: ${{ secrets.QISKIT_IBM_RUNTIME_STAGING_DEVICE }} - QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS: True + QISKIT_IBM_STAGING_API_TOKEN: ${{ secrets.QISKIT_IBM_STAGING_API_TOKEN }} + QISKIT_IBM_STAGING_API_URL: ${{ secrets.QISKIT_IBM_STAGING_API_URL }} + QISKIT_IBM_STAGING_HGP: ${{ secrets.QISKIT_IBM_STAGING_HGP }} + QISKIT_IBM_STAGING_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_TOKEN }} + QISKIT_IBM_STAGING_CLOUD_URL: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_URL }} + QISKIT_IBM_STAGING_CLOUD_CRN: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_CRN }} + USE_STAGING_CREDENTIALS: True LOG_LEVEL: DEBUG STREAM_LOG: True QISKIT_IN_PARALLEL: True steps: - uses: actions/checkout@v2 - - name: Set up Python 3.9 + - name: Set up Python ${{ matrix.python-version }} uses: actions/setup-python@v2 with: - python-version: 3.9 + python-version: ${{ matrix.python-version }} - name: Install Deps run: | python -m pip install --upgrade pip pip install -c constraints.txt -e . pip install -U -c constraints.txt -r requirements-dev.txt - name: Run Tests - run: make runtime_integration + run: make test1 + test2: + name: tests2-python${{ matrix.python-version }}-${{ matrix.os }} + runs-on: ${{ matrix.os }} + strategy: + matrix: + python-version: [3.7, 3.8, 3.9] + os: ["windows-latest", "ubuntu-latest"] + env: + QISKIT_IBM_STAGING_API_TOKEN: ${{ secrets.QISKIT_IBM_STAGING_API_TOKEN }} + QISKIT_IBM_STAGING_API_URL: ${{ secrets.QISKIT_IBM_STAGING_API_URL }} + QISKIT_IBM_STAGING_HGP: ${{ secrets.QISKIT_IBM_STAGING_HGP }} + QISKIT_IBM_STAGING_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_TOKEN }} + QISKIT_IBM_STAGING_CLOUD_URL: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_URL }} + QISKIT_IBM_STAGING_CLOUD_CRN: ${{ secrets.QISKIT_IBM_STAGING_CLOUD_CRN }} + USE_STAGING_CREDENTIALS: True + LOG_LEVEL: DEBUG + STREAM_LOG: True + QISKIT_IN_PARALLEL: True + steps: + - uses: actions/checkout@v2 + - name: Set up Python ${{ matrix.python-version }} + uses: actions/setup-python@v2 + with: + python-version: ${{ matrix.python-version }} + - name: Install Deps + run: | + python -m pip install --upgrade pip + pip install -c constraints.txt -e . + pip install -U -c constraints.txt -r requirements-dev.txt + - name: Run Tests + run: make test2 diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 9b8c179ff5..10cbed8907 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -63,10 +63,12 @@ jobs: matrix: python-version: [3.7, 3.8, 3.9] env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} LOG_LEVEL: DEBUG STREAM_LOG: True QISKIT_IN_PARALLEL: True diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml index c5bd027614..ad7c722ee7 100644 --- a/.github/workflows/pr.yml +++ b/.github/workflows/pr.yml @@ -21,10 +21,12 @@ jobs: python-version: [3.7, 3.8, 3.9] os: ["macOS-latest", "ubuntu-latest", "windows-latest"] env: - QISKIT_IBM_RUNTIME_API_TOKEN: ${{ secrets.QISKIT_IBM_RUNTIME_API_TOKEN }} - QISKIT_IBM_RUNTIME_API_URL: ${{ secrets.QISKIT_IBM_RUNTIME_API_URL }} - QISKIT_IBM_RUNTIME_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_HGP }} - QISKIT_IBM_RUNTIME_PRIVATE_HGP: ${{ secrets.QISKIT_IBM_RUNTIME_PRIVATE_HGP }} + QISKIT_IBM_API_TOKEN: ${{ secrets.QISKIT_IBM_API_TOKEN }} + QISKIT_IBM_API_URL: ${{ secrets.QISKIT_IBM_API_URL }} + QISKIT_IBM_HGP: ${{ secrets.QISKIT_IBM_HGP }} + QISKIT_IBM_CLOUD_TOKEN: ${{ secrets.QISKIT_IBM_CLOUD_TOKEN }} + QISKIT_IBM_CLOUD_URL: ${{ secrets.QISKIT_IBM_CLOUD_URL }} + QISKIT_IBM_CLOUD_CRN: ${{ secrets.QISKIT_IBM_CLOUD_CRN }} LOG_LEVEL: DEBUG STREAM_LOG: True QISKIT_TESTS: skip_online diff --git a/.pylintrc b/.pylintrc index d3b190571f..6e21b3c068 100644 --- a/.pylintrc +++ b/.pylintrc @@ -68,6 +68,7 @@ confidence= # --disable=W". disable=arguments-renamed, # TODO: investigate / re-enable bad-mcs-classmethod-argument, # TODO: investigate / re-enable + bad-continuation, bad-whitespace # differences of opinion with black consider-iterating-dictionary, # TODO: investigate / re-enable consider-using-dict-items, # TODO: investigate / re-enable consider-using-f-string, # TODO: investigate / re-enable diff --git a/Makefile b/Makefile index dd6fe7052e..93f05b7139 100644 --- a/Makefile +++ b/Makefile @@ -27,13 +27,10 @@ test: python -m unittest -v test1: - python -m unittest -v test/ibm/test_ibm_backend.py test/ibm/test_account_client.py test/ibm/test_tutorials.py test/ibm/test_basic_server_paths.py + python -m unittest -v test/test_integration_backend.py test/test_integration_program.py test2: - python -m unittest -v test/ibm/test_proxies.py test/ibm/test_ibm_logger.py test/ibm/test_filter_backends.py test/ibm/test_registration.py - -test3: - python -m unittest -v test/ibm/test_serialization.py test/ibm/test_jupyter.py test/ibm/test_ibm_provider.py + python -m unittest -v test/test_integration_job.py runtime_integration: python -m unittest -v test/ibm/runtime/test_runtime_integration.py diff --git a/qiskit_ibm_runtime/__init__.py b/qiskit_ibm_runtime/__init__.py index b501ab5980..006d1aaa7b 100644 --- a/qiskit_ibm_runtime/__init__.py +++ b/qiskit_ibm_runtime/__init__.py @@ -279,7 +279,6 @@ def interim_result_callback(job_id, interim_result): from .ibm_backend import IBMBackend from .exceptions import * from .utils.utils import setup_logger -from .runner_result import RunnerResult from .version import __version__ from .ibm_runtime_service import IBMRuntimeService @@ -301,61 +300,3 @@ def interim_result_callback(job_id, interim_result): """The environment variable name that is used to set the level for the IBM Quantum logger.""" QISKIT_IBM_RUNTIME_LOG_FILE = "QISKIT_IBM_RUNTIME_LOG_FILE" """The environment variable name that is used to set the file for the IBM Quantum logger.""" - - -def least_busy( - backends: List[Union[Backend, BaseBackend]], - reservation_lookahead: Optional[int] = 60, -) -> Union[Backend, BaseBackend]: - """Return the least busy backend from a list. - - Return the least busy available backend for those that - have a ``pending_jobs`` in their ``status``. Note that local - backends may not have this attribute. - - Args: - backends: The backends to choose from. - reservation_lookahead: A backend is considered unavailable if it - has reservations in the next ``n`` minutes, where ``n`` is - the value of ``reservation_lookahead``. - If ``None``, reservations are not taken into consideration. - - Returns: - The backend with the fewest number of pending jobs. - - Raises: - IBMError: If the backends list is empty, or if none of the backends - is available, or if a backend in the list - does not have the ``pending_jobs`` attribute in its status. - """ - if not backends: - raise IBMError( - "Unable to find the least_busy backend from an empty list." - ) from None - try: - candidates = [] - now = datetime.now() - for back in backends: - backend_status = back.status() - if not backend_status.operational or backend_status.status_msg != "active": - continue - if reservation_lookahead and isinstance(back, IBMBackend): - end_time = now + timedelta(minutes=reservation_lookahead) - try: - if back.reservations(now, end_time): - continue - except Exception as err: # pylint: disable=broad-except - logger.warning( - "Unable to find backend reservation information. " - "It will not be taken into consideration. %s", - str(err), - ) - candidates.append(back) - if not candidates: - raise IBMError("No backend matches the criteria.") - return min(candidates, key=lambda b: b.status().pending_jobs) - except AttributeError as ex: - raise IBMError( - "A backend in the list does not have the `pending_jobs` " - "attribute in its status." - ) from ex diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py index 021b0702bb..607134c141 100644 --- a/qiskit_ibm_runtime/accounts/account.py +++ b/qiskit_ibm_runtime/accounts/account.py @@ -17,11 +17,14 @@ from urllib.parse import urlparse from typing_extensions import Literal +from requests.auth import AuthBase + +from ..api.auth import LegacyAuth, CloudAuth AccountType = Optional[Literal["cloud", "legacy"]] LEGACY_API_URL = "https://auth.quantum-computing.ibm.com/api" -CLOUD_API_URL = "https://cloud.ibm.com" +CLOUD_API_URL = "https://us-east.quantum-computing.cloud.ibm.com" def _assert_valid_auth(auth: AccountType) -> None: @@ -71,18 +74,26 @@ def __init__( proxies: Optional[dict] = None, verify: Optional[bool] = True, ): - """Account constructor.""" + """Account constructor. + + Args: + auth: Authentication type, ``cloud`` or ``legacy``. + token: Account token to use. + url: Authentication URL. + instance: Service instance to use. + proxies: Proxies to use. + verify: Whether to verify server's TLS certificate. + """ _assert_valid_auth(auth) self.auth = auth _assert_valid_token(token) self.token = token - resolved_url = url or LEGACY_API_URL if auth == "legacy" else CLOUD_API_URL + resolved_url = url or (LEGACY_API_URL if auth == "legacy" else CLOUD_API_URL) _assert_valid_url(resolved_url) self.url = resolved_url - _assert_valid_instance(auth, instance) self.instance = instance self.proxies = proxies self.verify = verify @@ -100,5 +111,12 @@ def from_saved_format(cls, data: dict) -> "Account": token=data.get("token"), instance=data.get("instance"), proxies=data.get("proxies"), - verify=data.get("verify"), + verify=data.get("verify", True), ) + + def get_auth_handler(self) -> AuthBase: + """Returns the respective authentication handler.""" + if self.auth == "cloud": + return CloudAuth(api_key=self.token, crn=self.instance) + + return LegacyAuth(access_token=self.token) diff --git a/qiskit_ibm_runtime/accounts/management.py b/qiskit_ibm_runtime/accounts/management.py index ca7cdd63ce..0ecaf24e91 100644 --- a/qiskit_ibm_runtime/accounts/management.py +++ b/qiskit_ibm_runtime/accounts/management.py @@ -22,6 +22,10 @@ os.path.expanduser("~"), ".qiskit", "qiskit-ibm.json" ) _DEFAULT_ACCOUNT_NAME = "default" +_DEFAULT_ACCOUNT_NAME_LEGACY = "default-legacy" +_DEFAULT_ACCOUNT_NAME_CLOUD = "default-cloud" +_DEFAULT_ACCOUNT_TYPE: AccountType = "cloud" +_ACCOUNT_TYPES = [_DEFAULT_ACCOUNT_TYPE, "legacy"] class AccountManager: @@ -58,14 +62,74 @@ def list() -> Union[dict, None]: return read_config(filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE) - @staticmethod - def get(name: Optional[str] = _DEFAULT_ACCOUNT_NAME) -> Account: - """Read account from disk.""" - return Account.from_saved_format( - read_config(filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE, name=name) - ) + @classmethod + def get( + cls, name: Optional[str] = None, auth: Optional[AccountType] = None + ) -> Optional[Account]: + """Read account from disk. + + Args: + name: Account name. Takes precedence if `auth` is also specified. + auth: Account auth type. + + Returns: + Account information. + + Raises: + ValueError: If the input value cannot be found on disk. + """ + if name: + saved_account = read_config( + filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE, name=name + ) + if not saved_account: + raise ValueError( + f"Account with the name {name} does not exist on disk." + ) + return Account.from_saved_format(saved_account) + + auth_ = auth or _DEFAULT_ACCOUNT_TYPE + env_account = cls._from_env_variables(auth_) + if env_account is not None: + return env_account + + if auth: + saved_account = read_config( + filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE, + name=cls._get_default_account_name(auth), + ) + if saved_account is None: + raise ValueError(f"No default {auth} account saved.") + return Account.from_saved_format(saved_account) + + all_config = read_config(filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE) + for account_type in _ACCOUNT_TYPES: + account_name = cls._get_default_account_name(account_type) + if account_name in all_config: + return Account.from_saved_format(all_config[account_name]) + + return None @staticmethod def delete(name: Optional[str] = _DEFAULT_ACCOUNT_NAME) -> bool: """Delete account from disk.""" return delete_config(name=name, filename=_DEFAULT_ACCOUNG_CONFIG_JSON_FILE) + + @classmethod + def _from_env_variables(cls, auth: Optional[AccountType]) -> Optional[Account]: + """Read account from environment variable.""" + token = os.getenv("QISKIT_IBM_API_TOKEN") + url = os.getenv("QISKIT_IBM_API_URL") + if not (token and url): + return None + return Account( + token=token, url=url, instance=os.getenv("QISKIT_IBM_INSTANCE"), auth=auth + ) + + @classmethod + def _get_default_account_name(cls, auth: AccountType) -> str: + return ( + _DEFAULT_ACCOUNT_NAME_CLOUD + if auth == "cloud" + else _DEFAULT_ACCOUNT_NAME_LEGACY + ) diff --git a/qiskit_ibm_runtime/accounts/storage.py b/qiskit_ibm_runtime/accounts/storage.py index aa827b7230..79cc3c4d6f 100644 --- a/qiskit_ibm_runtime/accounts/storage.py +++ b/qiskit_ibm_runtime/accounts/storage.py @@ -14,7 +14,7 @@ import json import os -from typing import Optional, Union +from typing import Optional, Dict def save_config( @@ -37,7 +37,7 @@ def save_config( def read_config( filename: str, name: Optional[str] = None, -) -> Union[dict, None]: +) -> Optional[Dict]: """Save configuration data from a JSON file.""" _ensure_file_exists(filename) diff --git a/qiskit_ibm_runtime/api/auth.py b/qiskit_ibm_runtime/api/auth.py index 33b5794c14..43a5ac955d 100644 --- a/qiskit_ibm_runtime/api/auth.py +++ b/qiskit_ibm_runtime/api/auth.py @@ -12,6 +12,8 @@ """Authentication helpers.""" +from typing import Dict + from requests import PreparedRequest from requests.auth import AuthBase @@ -35,10 +37,13 @@ def __eq__(self, other: object) -> bool: return False def __call__(self, r: PreparedRequest) -> PreparedRequest: - r.headers["Service-CRN"] = self.crn - r.headers["Authorization"] = f"apikey {self.api_key}" + r.headers.update(self.get_headers()) return r + def get_headers(self) -> Dict: + """Return authorization information to be stored in header.""" + return {"Service-CRN": self.crn, "Authorization": f"apikey {self.api_key}"} + class LegacyAuth(AuthBase): """Attaches Legacy Authentication to the given Request object.""" @@ -53,5 +58,9 @@ def __eq__(self, other: object) -> bool: return False def __call__(self, r: PreparedRequest) -> PreparedRequest: - r.headers["X-Access-Token"] = self.access_token + r.headers.update(self.get_headers()) return r + + def get_headers(self) -> Dict: + """Return authorization information to be stored in header.""" + return {"X-Access-Token": self.access_token} diff --git a/qiskit_ibm_runtime/api/client_parameters.py b/qiskit_ibm_runtime/api/client_parameters.py new file mode 100644 index 0000000000..29912427e5 --- /dev/null +++ b/qiskit_ibm_runtime/api/client_parameters.py @@ -0,0 +1,83 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Represent IBM Quantum account credentials.""" + +from typing import Dict, Optional, Any, Union + +from requests_ntlm import HttpNtlmAuth + +from ..api.auth import LegacyAuth, CloudAuth + +TEMPLATE_IBM_HUBS = "{prefix}/Network/{hub}/Groups/{group}/Projects/{project}" +"""str: Template for creating an IBM Quantum URL with hub/group/project information.""" + + +class ClientParameters: + """IBM Quantum account credentials and preferences. + + Note: + By convention, two credentials that have the same hub, group, + and project are considered equivalent, regardless of other attributes. + """ + + def __init__( + self, + auth_type: str, + token: str, + url: str = None, + instance: Optional[str] = None, + proxies: Optional[Dict] = None, + verify: bool = True, + ) -> None: + """Credentials constructor. + + Args: + token: IBM Quantum API token. + url: IBM Quantum URL (gets replaced with a new-style URL with hub, group, project). + proxies: Proxy configuration. + verify: If ``False``, ignores SSL certificates errors. + """ + self.token = token + self.instance = instance + self.auth_type = auth_type + self.url = url + self.proxies = proxies + self.verify = verify + + def get_auth_handler(self) -> Union[CloudAuth, LegacyAuth]: + """Returns the respective authentication handler.""" + if self.auth_type == "cloud": + return CloudAuth(api_key=self.token, crn=self.instance) + + return LegacyAuth(access_token=self.token) + + def connection_parameters(self) -> Dict[str, Any]: + """Construct connection related parameters. + + Returns: + A dictionary with connection-related parameters in the format + expected by ``requests``. The following keys can be present: + ``proxies``, ``verify``, and ``auth``. + """ + request_kwargs = {"verify": self.verify} + + if self.proxies: + if "urls" in self.proxies: + request_kwargs["proxies"] = self.proxies["urls"] + + if "username_ntlm" in self.proxies and "password_ntlm" in self.proxies: + request_kwargs["auth"] = HttpNtlmAuth( + self.proxies["username_ntlm"], self.proxies["password_ntlm"] + ) + + return request_kwargs diff --git a/qiskit_ibm_runtime/api/clients/account.py b/qiskit_ibm_runtime/api/clients/account.py index 2b842928fe..e374e968ba 100644 --- a/qiskit_ibm_runtime/api/clients/account.py +++ b/qiskit_ibm_runtime/api/clients/account.py @@ -1,6 +1,6 @@ # This code is part of Qiskit. # -# (C) Copyright IBM 2018, 2020. +# (C) Copyright IBM 2018, 2021. # # This code is licensed under the Apache License, Version 2.0. You may # obtain a copy of this license in the LICENSE.txt file in the root directory @@ -15,54 +15,45 @@ import logging from typing import List, Dict, Any, Optional -from datetime import datetime +from datetime import datetime as python_datetime -from qiskit_ibm_runtime.credentials import Credentials +from qiskit_ibm_runtime.utils.hgp import from_instance_format -from ..rest import Api, Account -from ..rest.backend import Backend +from .backend import BaseBackendClient +from ..rest import Account from ..session import RetrySession -from .base import BaseClient +from ..client_parameters import ClientParameters logger = logging.getLogger(__name__) -class AccountClient(BaseClient): +class AccountClient(BaseBackendClient): """Client for accessing an individual IBM Quantum account.""" - def __init__(self, credentials: Credentials, **request_kwargs: Any) -> None: + def __init__(self, params: ClientParameters) -> None: """AccountClient constructor. Args: - credentials: Account credentials. - **request_kwargs: Arguments for the request ``Session``. + params: Parameters used for server connection. """ self._session = RetrySession( - credentials.base_url, auth=credentials.get_auth_handler(), **request_kwargs + params.url, auth=params.get_auth_handler(), **params.connection_parameters() ) - # base_api is used to handle endpoints that don't include h/g/p. - # account_api is for h/g/p. - self.base_api = Api(self._session) + hub, group, project = from_instance_format(params.instance) self.account_api = Account( session=self._session, - hub=credentials.hub, - group=credentials.group, - project=credentials.project, + hub=hub, + group=group, + project=project, ) - self._credentials = credentials - # Backend-related public functions. - - def list_backends(self, timeout: Optional[float] = None) -> List[Dict[str, Any]]: + def list_backends(self) -> List[Dict[str, Any]]: """Return backends available for this provider. - Args: - timeout: Number of seconds to wait for the request. - Returns: Backends available for this provider. """ - return self.account_api.backends(timeout=timeout) + return self.account_api.backends() def backend_status(self, backend_name: str) -> Dict[str, Any]: """Return the status of the backend. @@ -76,7 +67,7 @@ def backend_status(self, backend_name: str) -> Dict[str, Any]: return self.account_api.backend(backend_name).status() def backend_properties( - self, backend_name: str, datetime: Optional[datetime] = None + self, backend_name: str, datetime: Optional[python_datetime] = None ) -> Dict[str, Any]: """Return the properties of the backend. @@ -87,7 +78,6 @@ def backend_properties( Returns: Backend properties. """ - # pylint: disable=redefined-outer-name return self.account_api.backend(backend_name).properties(datetime=datetime) def backend_pulse_defaults(self, backend_name: str) -> Dict: @@ -100,41 +90,3 @@ def backend_pulse_defaults(self, backend_name: str) -> Dict: Backend pulse defaults. """ return self.account_api.backend(backend_name).pulse_defaults() - - def backend_job_limit(self, backend_name: str) -> Dict[str, Any]: - """Return the job limit for the backend. - - Args: - backend_name: The name of the backend. - - Returns: - Backend job limit. - """ - return self.account_api.backend(backend_name).job_limit() - - def backend_reservations( - self, - backend_name: str, - start_datetime: Optional[datetime] = None, - end_datetime: Optional[datetime] = None, - ) -> List: - """Return backend reservation information. - - Args: - backend_name: Name of the backend. - start_datetime: Starting datetime in UTC. - end_datetime: Ending datetime in UTC. - - Returns: - Backend reservation information. - """ - backend_api = Backend(self._session, backend_name, "/Network") - return backend_api.reservations(start_datetime, end_datetime) - - def my_reservations(self) -> List: - """Return backend reservations made by the caller. - - Returns: - Backend reservation information. - """ - return self.base_api.reservations() diff --git a/qiskit_ibm_runtime/api/clients/auth.py b/qiskit_ibm_runtime/api/clients/auth.py index 1d9ba17f97..fd679b827e 100644 --- a/qiskit_ibm_runtime/api/clients/auth.py +++ b/qiskit_ibm_runtime/api/clients/auth.py @@ -19,6 +19,7 @@ from ..exceptions import AuthenticationLicenseError, RequestsApiError from ..rest import Api from ..session import RetrySession +from ..client_parameters import ClientParameters from .base import BaseClient @@ -26,20 +27,22 @@ class AuthClient(BaseClient): """Client for accessing IBM Quantum authentication services.""" - def __init__(self, api_token: str, auth_url: str, **request_kwargs: Any) -> None: + def __init__(self, client_params: ClientParameters) -> None: """AuthClient constructor. Args: - api_token: IBM Quantum API token. - auth_url: URL for the authentication service. - **request_kwargs: Arguments for the request ``Session``. + client_params: Parameters used for server connection. """ - self.api_token = api_token - self.auth_url = auth_url + self.api_token = client_params.token + self.auth_url = client_params.url self._service_urls = {} # type: ignore[var-annotated] - self.auth_api = Api(RetrySession(auth_url, **request_kwargs)) - self.base_api = self._init_service_clients(**request_kwargs) + self.auth_api = Api( + RetrySession(self.auth_url, **client_params.connection_parameters()) + ) + self.base_api = self._init_service_clients( + **client_params.connection_parameters() + ) def _init_service_clients(self, **request_kwargs: Any) -> Api: """Initialize the clients used for communicating with the API. @@ -165,7 +168,7 @@ def current_access_token(self) -> Optional[str]: """ return self.access_token - def current_service_urls(self) -> Dict[str, str]: + def current_service_urls(self) -> Dict: """Return the current service URLs. Returns: diff --git a/qiskit_ibm_runtime/api/clients/backend.py b/qiskit_ibm_runtime/api/clients/backend.py new file mode 100644 index 0000000000..a043f982fa --- /dev/null +++ b/qiskit_ibm_runtime/api/clients/backend.py @@ -0,0 +1,65 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2018, 2020. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Client for accessing backend information.""" + +import logging +from typing import Dict, Any, Optional +from datetime import datetime as python_datetime +from abc import ABC, abstractmethod + +from .base import BaseClient + +logger = logging.getLogger(__name__) + + +class BaseBackendClient(BaseClient, ABC): + """Client for accessing backend information.""" + + @abstractmethod + def backend_status(self, backend_name: str) -> Dict[str, Any]: + """Return the status of the backend. + + Args: + backend_name: The name of the backend. + + Returns: + Backend status. + """ + pass + + @abstractmethod + def backend_properties( + self, backend_name: str, datetime: Optional[python_datetime] = None + ) -> Dict[str, Any]: + """Return the properties of the backend. + + Args: + backend_name: The name of the backend. + datetime: Date and time for additional filtering of backend properties. + + Returns: + Backend properties. + """ + pass + + @abstractmethod + def backend_pulse_defaults(self, backend_name: str) -> Dict: + """Return the pulse defaults of the backend. + + Args: + backend_name: The name of the backend. + + Returns: + Backend pulse defaults. + """ + pass diff --git a/qiskit_ibm_runtime/api/clients/base.py b/qiskit_ibm_runtime/api/clients/base.py index 05b96d085d..2ada1850d6 100644 --- a/qiskit_ibm_runtime/api/clients/base.py +++ b/qiskit_ibm_runtime/api/clients/base.py @@ -24,7 +24,7 @@ from websocket import WebSocketApp, STATUS_NORMAL, STATUS_ABNORMAL_CLOSED -from ...credentials import Credentials +from ..client_parameters import ClientParameters from ..exceptions import WebsocketError, WebsocketTimeoutError from .utils import ws_proxy_params @@ -55,7 +55,7 @@ class BaseWebsocketClient(BaseClient, ABC): def __init__( self, websocket_url: str, - credentials: Credentials, + client_params: ClientParameters, job_id: str, message_queue: Optional[Queue] = None, ) -> None: @@ -63,15 +63,15 @@ def __init__( Args: websocket_url: URL for websocket communication with IBM Quantum. - credentials: Account credentials. + client_params: Parameters used for server connection. job_id: Job ID. message_queue: Queue used to hold received messages. """ self._websocket_url = websocket_url.rstrip("/") self._proxy_params = ws_proxy_params( - credentials=credentials, ws_url=self._websocket_url + client_params=client_params, ws_url=self._websocket_url ) - self._access_token = credentials.access_token + self._access_token = client_params.token self._job_id = job_id self._message_queue = message_queue self._header: Optional[Dict] = None diff --git a/qiskit_ibm_runtime/api/clients/runtime.py b/qiskit_ibm_runtime/api/clients/runtime.py index 3dd42a2b72..32fbc0a12e 100644 --- a/qiskit_ibm_runtime/api/clients/runtime.py +++ b/qiskit_ibm_runtime/api/clients/runtime.py @@ -14,33 +14,36 @@ import logging from typing import Any, Dict, List, Optional +from datetime import datetime as python_datetime -from qiskit_ibm_runtime.credentials import Credentials from qiskit_ibm_runtime.api.session import RetrySession +from .backend import BaseBackendClient from ..rest.runtime import Runtime +from ..client_parameters import ClientParameters +from ...utils.hgp import from_instance_format logger = logging.getLogger(__name__) -class RuntimeClient: +class RuntimeClient(BaseBackendClient): """Client for accessing runtime service.""" def __init__( self, - credentials: Credentials, + params: ClientParameters, ) -> None: - """RandomClient constructor. + """RuntimeClient constructor. Args: - credentials: Account credentials. + params: Connection parameters. """ self._session = RetrySession( - base_url=credentials.runtime_url or credentials.base_url, - auth=credentials.get_auth_handler(), - **credentials.connection_parameters() + base_url=params.url, + auth=params.get_auth_handler(), + **params.connection_parameters() ) - self.api = Runtime(self._session) + self._api = Runtime(self._session) def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: """Return a list of runtime programs. @@ -52,7 +55,7 @@ def list_programs(self, limit: int = None, skip: int = None) -> Dict[str, Any]: Returns: A list of runtime programs. """ - return self.api.list_programs(limit, skip) + return self._api.list_programs(limit, skip) def program_create( self, @@ -76,7 +79,7 @@ def program_create( Returns: Server response. """ - return self.api.create_program( + return self._api.create_program( program_data=program_data, name=name, description=description, @@ -94,7 +97,7 @@ def program_get(self, program_id: str) -> Dict: Returns: Program information. """ - return self.api.program(program_id).get() + return self._api.program(program_id).get() def set_program_visibility(self, program_id: str, public: bool) -> None: """Sets a program's visibility. @@ -106,38 +109,40 @@ def set_program_visibility(self, program_id: str, public: bool) -> None: """ if public: - self.api.program(program_id).make_public() + self._api.program(program_id).make_public() else: - self.api.program(program_id).make_private() + self._api.program(program_id).make_private() def program_run( self, program_id: str, - credentials: Credentials, backend_name: str, params: Dict, image: str, + hgp: Optional[str], ) -> Dict: """Run the specified program. Args: program_id: Program ID. - credentials: Credentials used to run the program. backend_name: Name of the backend to run the program. params: Parameters to use. image: The runtime image to use. + hgp: Hub/group/project to use. Returns: JSON response. """ - return self.api.program_run( + hgp_dict = {} + if hgp: + hub, group, project = from_instance_format(hgp) + hgp_dict = {"hub": hub, "group": group, "project": project} + return self._api.program_run( program_id=program_id, - hub=credentials.hub, - group=credentials.group, - project=credentials.project, backend_name=backend_name, params=params, image=image, + **hgp_dict ) def program_delete(self, program_id: str) -> None: @@ -146,7 +151,7 @@ def program_delete(self, program_id: str) -> None: Args: program_id: Program ID. """ - self.api.program(program_id).delete() + self._api.program(program_id).delete() def program_update( self, @@ -168,10 +173,10 @@ def program_update( spec: Backend requirements, parameters, interim results, return values, etc. """ if program_data: - self.api.program(program_id).update_data(program_data) + self._api.program(program_id).update_data(program_data) if any([name, description, max_execution_time, spec]): - self.api.program(program_id).update_metadata( + self._api.program(program_id).update_metadata( name=name, description=description, max_execution_time=max_execution_time, @@ -187,7 +192,7 @@ def job_get(self, job_id: str) -> Dict: Returns: JSON response. """ - response = self.api.program_job(job_id).get() + response = self._api.program_job(job_id).get() logger.debug("Runtime job get response: %s", response) return response @@ -216,7 +221,7 @@ def jobs_get( Returns: JSON response. """ - return self.api.jobs_get( + return self._api.jobs_get( limit=limit, skip=skip, pending=pending, @@ -235,7 +240,7 @@ def job_results(self, job_id: str) -> str: Returns: Job result. """ - return self.api.program_job(job_id).results() + return self._api.program_job(job_id).results() def job_interim_results(self, job_id: str) -> str: """Get the interim results of a program job. @@ -246,7 +251,7 @@ def job_interim_results(self, job_id: str) -> str: Returns: Job interim results. """ - return self.api.program_job(job_id).interim_results() + return self._api.program_job(job_id).interim_results() def job_cancel(self, job_id: str) -> None: """Cancel a job. @@ -254,7 +259,7 @@ def job_cancel(self, job_id: str) -> None: Args: job_id: Runtime job ID. """ - self.api.program_job(job_id).cancel() + self._api.program_job(job_id).cancel() def job_delete(self, job_id: str) -> None: """Delete a job. @@ -262,7 +267,7 @@ def job_delete(self, job_id: str) -> None: Args: job_id: Runtime job ID. """ - self.api.program_job(job_id).delete() + self._api.program_job(job_id).delete() def job_logs(self, job_id: str) -> str: """Get the job logs. @@ -273,11 +278,11 @@ def job_logs(self, job_id: str) -> str: Returns: Job logs. """ - return self.api.program_job(job_id).logs() + return self._api.program_job(job_id).logs() def logout(self) -> None: """Clear authorization cache.""" - self.api.logout() + self._api.logout() # IBM Cloud only functions @@ -287,7 +292,7 @@ def list_backends(self) -> List[str]: Returns: IBM Cloud backends available for this service instance. """ - return self.api.backends()["devices"] + return self._api.backends()["devices"] def backend_configuration(self, backend_name: str) -> Dict[str, Any]: """Return the configuration of the IBM Cloud backend. @@ -298,7 +303,7 @@ def backend_configuration(self, backend_name: str) -> Dict[str, Any]: Returns: Backend configuration. """ - return self.api.backend(backend_name).configuration() + return self._api.backend(backend_name).configuration() def backend_status(self, backend_name: str) -> Dict[str, Any]: """Return the status of the IBM Cloud backend. @@ -309,18 +314,26 @@ def backend_status(self, backend_name: str) -> Dict[str, Any]: Returns: Backend status. """ - return self.api.backend(backend_name).status() + return self._api.backend(backend_name).status() - def backend_properties(self, backend_name: str) -> Dict[str, Any]: + def backend_properties( + self, backend_name: str, datetime: Optional[python_datetime] = None + ) -> Dict[str, Any]: """Return the properties of the IBM Cloud backend. Args: backend_name: The name of the IBM Cloud backend. + datetime: Date and time for additional filtering of backend properties. Returns: Backend properties. + + Raises: + NotImplementedError: If `datetime` is specified. """ - return self.api.backend(backend_name).properties() + if datetime: + raise NotImplementedError("'datetime' is not supported with cloud runtime.") + return self._api.backend(backend_name).properties() def backend_pulse_defaults(self, backend_name: str) -> Dict: """Return the pulse defaults of the IBM Cloud backend. @@ -331,4 +344,4 @@ def backend_pulse_defaults(self, backend_name: str) -> Dict: Returns: Backend pulse defaults. """ - return self.api.backend(backend_name).pulse_defaults() + return self._api.backend(backend_name).pulse_defaults() diff --git a/qiskit_ibm_runtime/api/clients/runtime_ws.py b/qiskit_ibm_runtime/api/clients/runtime_ws.py index 5883d27221..51742f10ca 100644 --- a/qiskit_ibm_runtime/api/clients/runtime_ws.py +++ b/qiskit_ibm_runtime/api/clients/runtime_ws.py @@ -16,8 +16,8 @@ from typing import Optional from queue import Queue -from ...credentials import Credentials from .base import BaseWebsocketClient +from ..client_parameters import ClientParameters logger = logging.getLogger(__name__) @@ -28,7 +28,7 @@ class RuntimeWebsocketClient(BaseWebsocketClient): def __init__( self, websocket_url: str, - credentials: Credentials, + client_params: ClientParameters, job_id: str, message_queue: Optional[Queue] = None, ) -> None: @@ -36,12 +36,12 @@ def __init__( Args: websocket_url: URL for websocket communication with IBM Quantum. - credentials: Account credentials. + client_params: Parameters used for server connection. job_id: Job ID. message_queue: Queue used to hold received messages. """ - super().__init__(websocket_url, credentials, job_id, message_queue) - self._header = {"X-Access-Token": credentials.access_token} + super().__init__(websocket_url, client_params, job_id, message_queue) + self._header = client_params.get_auth_handler().get_headers() def _handle_message(self, message: str) -> None: """Handle received message. diff --git a/qiskit_ibm_runtime/api/clients/utils.py b/qiskit_ibm_runtime/api/clients/utils.py index 2152f5f103..b285c52ab7 100644 --- a/qiskit_ibm_runtime/api/clients/utils.py +++ b/qiskit_ibm_runtime/api/clients/utils.py @@ -15,20 +15,20 @@ from typing import Dict from urllib.parse import urlparse -from ...credentials import Credentials +from ..client_parameters import ClientParameters -def ws_proxy_params(credentials: Credentials, ws_url: str) -> Dict: +def ws_proxy_params(client_params: ClientParameters, ws_url: str) -> Dict: """Extract proxy information for websocket. Args: - credentials: Account credentials. + client_params: Parameters used for server connection. ws_url: Websocket URL. Returns: Proxy information to be used by the websocket client. """ - conn_data = credentials.connection_parameters() + conn_data = client_params.connection_parameters() out = {} if "proxies" in conn_data: @@ -61,8 +61,8 @@ def ws_proxy_params(credentials: Credentials, ws_url: str) -> Dict: if "auth" in conn_data: out["http_proxy_auth"] = ( - credentials.proxies["username_ntlm"], - credentials.proxies["password_ntlm"], + client_params.proxies["username_ntlm"], + client_params.proxies["password_ntlm"], ) return out diff --git a/qiskit_ibm_runtime/api/rest/runtime.py b/qiskit_ibm_runtime/api/rest/runtime.py index 4fec6d6a69..9e11988d87 100644 --- a/qiskit_ibm_runtime/api/rest/runtime.py +++ b/qiskit_ibm_runtime/api/rest/runtime.py @@ -113,23 +113,23 @@ def create_program( def program_run( self, program_id: str, - hub: str, - group: str, - project: str, backend_name: str, params: Dict, image: str, + hub: Optional[str] = None, + group: Optional[str] = None, + project: Optional[str] = None, ) -> Dict: """Execute the program. Args: program_id: Program ID. - hub: Hub to be used. - group: Group to be used. - project: Project to be used. backend_name: Name of the backend. params: Program parameters. image: Runtime image. + hub: Hub to be used. + group: Group to be used. + project: Project to be used. Returns: JSON response. @@ -137,13 +137,14 @@ def program_run( url = self.get_url("jobs") payload = { "program_id": program_id, - "hub": hub, - "group": group, - "project": project, "backend": backend_name, "params": params, "runtime": image, } + if all([hub, group, project]): + payload["hub"] = hub + payload["group"] = group + payload["project"] = project data = json.dumps(payload, cls=RuntimeEncoder) return self.session.post(url, data=data).json() diff --git a/qiskit_ibm_runtime/api/session.py b/qiskit_ibm_runtime/api/session.py index 07f7dc6ca9..20f390ba7e 100644 --- a/qiskit_ibm_runtime/api/session.py +++ b/qiskit_ibm_runtime/api/session.py @@ -255,7 +255,7 @@ def request( # type: ignore[override] headers.update(kwargs.pop("headers", {})) try: - self._log_request_info(url, method, kwargs) + self._log_request_info(final_url, method, kwargs) response = super().request(method, final_url, headers=headers, **kwargs) response.raise_for_status() except RequestException as ex: diff --git a/qiskit_ibm_runtime/credentials/credentials.py b/qiskit_ibm_runtime/credentials/credentials.py index 5c009b05ef..e1e8ab967a 100644 --- a/qiskit_ibm_runtime/credentials/credentials.py +++ b/qiskit_ibm_runtime/credentials/credentials.py @@ -21,7 +21,6 @@ from .hub_group_project_id import HubGroupProjectID from ..accounts import AccountType from ..api.auth import LegacyAuth, CloudAuth -from ..utils import crn_to_api_host REGEX_IBM_HUBS = ( "(?Phttp[s]://.+/api)" @@ -56,7 +55,6 @@ def __init__( verify: bool = True, services: Optional[Dict] = None, access_token: Optional[str] = None, - preferences: Optional[Dict] = None, default_provider: Optional[HubGroupProjectID] = None, ) -> None: """Credentials constructor. @@ -73,8 +71,6 @@ def __init__( verify: If ``False``, ignores SSL certificates errors. services: Additional services for this account. access_token: IBM Quantum access token. - preferences: Application preferences. Used for dictating preferred - action in services like the `ExperimentService`. default_provider: Default provider to use. """ self.auth = auth @@ -87,18 +83,15 @@ def __init__( self.hub, self.group, self.project, - ) = _unify_ibm_quantum_url(auth, url, instance, hub, group, project) + ) = _unify_ibm_quantum_url(auth, url, hub, group, project) self.auth_url = auth_url or url self.websockets_url = websockets_url self.proxies = proxies or {} self.verify = verify - self.preferences = preferences or {} self.default_provider = default_provider # Initialize additional service URLs. services = services or {} - self.extractor_url = services.get("extractorsService", None) - self.experiment_url = services.get("resultsDB", None) self.runtime_url = services.get("runtime", None) def get_auth_handler(self) -> AuthBase: @@ -153,7 +146,6 @@ def connection_parameters(self) -> Dict[str, Any]: def _unify_ibm_quantum_url( auth: AccountType, url: Optional[str] = None, - instance: Optional[str] = None, hub: Optional[str] = None, group: Optional[str] = None, project: Optional[str] = None, @@ -183,7 +175,7 @@ def _unify_ibm_quantum_url( base_url = url if auth == "cloud": - base_url = crn_to_api_host(instance) + base_url = url elif regex_match: base_url, hub, group, project = regex_match.groups() else: diff --git a/qiskit_ibm_runtime/exceptions.py b/qiskit_ibm_runtime/exceptions.py index 621f89cd0b..60a4e8d89f 100644 --- a/qiskit_ibm_runtime/exceptions.py +++ b/qiskit_ibm_runtime/exceptions.py @@ -105,37 +105,37 @@ class IBMApiError(IBMError): pass -class QiskitRuntimeError(IBMError): +class IBMRuntimeError(IBMError): """Base class for errors raised by the runtime service modules.""" pass -class RuntimeDuplicateProgramError(QiskitRuntimeError): +class RuntimeDuplicateProgramError(IBMRuntimeError): """Error raised when a program being uploaded already exists.""" pass -class RuntimeProgramNotFound(QiskitRuntimeError): +class RuntimeProgramNotFound(IBMRuntimeError): """Error raised when a program is not found.""" pass -class RuntimeJobFailureError(QiskitRuntimeError): +class RuntimeJobFailureError(IBMRuntimeError): """Error raised when a runtime job failed.""" pass -class RuntimeJobNotFound(QiskitRuntimeError): +class RuntimeJobNotFound(IBMRuntimeError): """Error raised when a job is not found.""" pass -class RuntimeInvalidStateError(QiskitRuntimeError): +class RuntimeInvalidStateError(IBMRuntimeError): """Errors raised when the state is not valid for the operation.""" pass diff --git a/qiskit_ibm_runtime/hub_group_project.py b/qiskit_ibm_runtime/hub_group_project.py index 34b27c66fe..52be41fb13 100644 --- a/qiskit_ibm_runtime/hub_group_project.py +++ b/qiskit_ibm_runtime/hub_group_project.py @@ -14,20 +14,16 @@ import logging from collections import OrderedDict -import traceback from typing import Any, Dict, Optional -from qiskit.providers.backend import BackendV1 as Backend -from qiskit.providers.models import PulseBackendConfiguration, QasmBackendConfiguration from qiskit_ibm_runtime import ( # pylint: disable=unused-import - ibm_runtime_service, ibm_backend, ) -from .utils.backend import decode_backend_configuration from .api.clients import AccountClient -from .credentials import Credentials -from .exceptions import IBMInputValueError +from .utils.backend_decoder import configuration_from_server_data +from .api.client_parameters import ClientParameters +from .utils.hgp import from_instance_format logger = logging.getLogger(__name__) @@ -37,31 +33,19 @@ class HubGroupProject: def __init__( self, - credentials: Credentials, - service: "ibm_runtime_service.IBMRuntimeService", - is_open: bool, + client_params: ClientParameters, + instance: str, ) -> None: """HubGroupProject constructor Args: - credentials: IBM Quantum credentials. - service: IBM Quantum account provider. - is_open: True means open access, False means premium + client_params: Parameters used for server connection. + instance: Hub/group/project. """ - self.credentials = credentials - self._service = service - self.is_open = is_open - self._api_client = AccountClient( - self.credentials, **self.credentials.connection_parameters() - ) + self._api_client = AccountClient(client_params) # Initialize the internal list of backends. self._backends: Dict[str, "ibm_backend.IBMBackend"] = {} - self._service_urls = { - "backend": self.credentials.url, - "experiment": self.credentials.experiment_url, - "random": self.credentials.extractor_url, - "runtime": self.credentials.runtime_url, - } + self._hub, self._group, self._project = from_instance_format(instance) @property def backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: @@ -83,72 +67,49 @@ def backends(self, value: Dict[str, "ibm_backend.IBMBackend"]) -> None: """ self._backends = value - def _discover_remote_backends( - self, timeout: Optional[float] = None - ) -> Dict[str, "ibm_backend.IBMBackend"]: + def _discover_remote_backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: """Return the remote backends available for this hub/group/project. - Args: - timeout: Maximum number of seconds to wait for the discovery of - remote backends. - Returns: A dict of the remote backend instances, keyed by backend name. """ - ret = OrderedDict() # type: ignore[var-annotated] - configs_list = self._api_client.list_backends(timeout=timeout) + ret = OrderedDict() + configs_list = self._api_client.list_backends() for raw_config in configs_list: - # Make sure the raw_config is of proper type - if not isinstance(raw_config, dict): - logger.warning( - "An error occurred when retrieving backend " - "information. Some backends might not be available." - ) + config = configuration_from_server_data( + raw_config=raw_config, instance=self.name + ) + if not config: continue - try: - decode_backend_configuration(raw_config) - try: - config = PulseBackendConfiguration.from_dict(raw_config) - except (KeyError, TypeError): - config = QasmBackendConfiguration.from_dict(raw_config) - backend_cls = ( - ibm_backend.IBMSimulator - if config.simulator - else ibm_backend.IBMBackend - ) - ret[config.backend_name] = backend_cls( - configuration=config, - service=self._service, - credentials=self.credentials, - account_client=self._api_client, - ) - except Exception: # pylint: disable=broad-except - logger.warning( - 'Remote backend "%s" for provider %s could not be instantiated due to an ' - "invalid config: %s", - raw_config.get("backend_name", raw_config.get("name", "unknown")), - repr(self), - traceback.format_exc(), - ) + backend_cls = ( + ibm_backend.IBMSimulator if config.simulator else ibm_backend.IBMBackend + ) + ret[config.backend_name] = backend_cls( + configuration=config, + api_client=self._api_client, + ) return ret - def get_backend(self, name: str) -> Optional[Backend]: + def get_backend(self, name: str) -> Optional["ibm_backend.IBMBackend"]: """Get backend by name.""" return self._backends.get(name, None) - def has_service(self, name: str) -> bool: - """Check if hgp has service by name.""" - if name not in self._service_urls: - raise IBMInputValueError(f"Unknown service {name} specified.") - return self._service_urls[name] is not None + @property + def name(self) -> str: + """Returns the unique id. + + Returns: + An ID uniquely represents this h/g/p. + """ + return f"{self._hub}/{self._group}/{self._project}" def __repr__(self) -> str: credentials_info = "hub='{}', group='{}', project='{}'".format( - self.credentials.hub, self.credentials.group, self.credentials.project + self._hub, self._group, self._project ) return "<{}({})>".format(self.__class__.__name__, credentials_info) def __eq__(self, other: Any) -> bool: if not isinstance(other, HubGroupProject): return False - return self.credentials == other.credentials + return self.name == other.name diff --git a/qiskit_ibm_runtime/ibm_backend.py b/qiskit_ibm_runtime/ibm_backend.py index 294155be0f..659680e9f8 100644 --- a/qiskit_ibm_runtime/ibm_backend.py +++ b/qiskit_ibm_runtime/ibm_backend.py @@ -14,7 +14,7 @@ import logging -from typing import List, Union, Optional, Any +from typing import Union, Optional, Any from datetime import datetime as python_datetime from qiskit.qobj.utils import MeasLevel, MeasReturnType @@ -28,16 +28,14 @@ ) from qiskit.providers.models import QasmBackendConfiguration, PulseBackendConfiguration -# pylint: disable=unused-import, cyclic-import -from qiskit_ibm_runtime import ibm_runtime_service - from .api.clients import AccountClient, RuntimeClient -from .backendreservation import BackendReservation -from .credentials import Credentials +from .api.clients.backend import BaseBackendClient from .exceptions import IBMBackendApiProtocolError -from .utils.converters import utc_to_local_all, local_to_utc -from .utils.backend import decode_pulse_defaults, decode_backend_properties -from .utils.backend import convert_reservation_data +from .utils.converters import local_to_utc +from .utils.backend_decoder import ( + defaults_from_server_data, + properties_from_server_data, +) logger = logging.getLogger(__name__) @@ -67,27 +65,17 @@ class IBMBackend(Backend): def __init__( self, configuration: Union[QasmBackendConfiguration, PulseBackendConfiguration], - service: "ibm_runtime_service.IBMRuntimeService", - credentials: Credentials, - account_client: Optional[AccountClient] = None, - runtime_client: Optional[RuntimeClient] = None, + api_client: BaseBackendClient, ) -> None: """IBMBackend constructor. Args: configuration: Backend configuration. - service: IBM Quantum account provider. - credentials: IBM Quantum credentials. api_client: IBM Quantum client used to communicate with the server. """ - super().__init__(provider=service, configuration=configuration) + super().__init__(configuration=configuration) - self._account_client = account_client - self._runtime_client = runtime_client - self._credentials = credentials - self.hub = credentials.hub - self.group = credentials.group - self.project = credentials.project + self._api_client = api_client # Attributes used by caching functions. self._properties = None @@ -131,6 +119,7 @@ def properties( datetime: By specifying `datetime`, this function returns an instance of the :class:`BackendProperties` whose timestamp is closest to, but older than, the specified `datetime`. + Note that this is only supported using legacy runtime. Returns: The backend properties or ``None`` if the backend properties are not @@ -138,6 +127,7 @@ def properties( Raises: TypeError: If an input argument is not of the correct type. + NotImplementedError: If `datetime` is specified when cloud rutime is used. """ # pylint: disable=arguments-differ if not isinstance(refresh, bool): @@ -145,24 +135,23 @@ def properties( "The 'refresh' argument needs to be a boolean. " "{} is of type {}".format(refresh, type(refresh)) ) - if datetime and not isinstance(datetime, python_datetime): - raise TypeError("'{}' is not of type 'datetime'.") if datetime: + if not isinstance(datetime, python_datetime): + raise TypeError("'{}' is not of type 'datetime'.") + if isinstance(self._api_client, RuntimeClient): + raise NotImplementedError( + "'datetime' is not supported by cloud runtime." + ) datetime = local_to_utc(datetime) if datetime or refresh or self._properties is None: - if self._account_client: - api_properties = self._account_client.backend_properties( - self.name(), datetime=datetime - ) - elif self._runtime_client: - api_properties = self._runtime_client.backend_properties(self.name()) + api_properties = self._api_client.backend_properties( + self.name(), datetime=datetime + ) if not api_properties: return None - decode_backend_properties(api_properties) - api_properties = utc_to_local_all(api_properties) - backend_properties = BackendProperties.from_dict(api_properties) + backend_properties = properties_from_server_data(api_properties) if datetime: # Don't cache result. return backend_properties self._properties = backend_properties @@ -182,10 +171,7 @@ def status(self) -> BackendStatus: Raises: IBMBackendApiProtocolError: If the status for the backend cannot be formatted properly. """ - if self._account_client: - api_status = self._account_client.backend_status(self.name()) - elif self._runtime_client: - api_status = self._runtime_client.backend_status(self.name()) + api_status = self._api_client.backend_status(self.name()) try: return BackendStatus.from_dict(api_status) @@ -210,45 +196,14 @@ def defaults(self, refresh: bool = False) -> Optional[PulseDefaults]: The backend pulse defaults or ``None`` if the backend does not support pulse. """ if refresh or self._defaults is None: - if self._account_client: - api_defaults = self._api_client.backend_pulse_defaults(self.name()) - elif self._runtime_client: - api_defaults = self._runtime_client.backend_pulse_defaults(self.name()) + api_defaults = self._api_client.backend_pulse_defaults(self.name()) if api_defaults: - decode_pulse_defaults(api_defaults) - self._defaults = PulseDefaults.from_dict(api_defaults) + self._defaults = defaults_from_server_data(api_defaults) else: self._defaults = None return self._defaults - def reservations( - self, - start_datetime: Optional[python_datetime] = None, - end_datetime: Optional[python_datetime] = None, - ) -> List[BackendReservation]: - """Return backend reservations. - - If start_datetime and/or end_datetime is specified, reservations with - time slots that overlap with the specified time window will be returned. - - Some of the reservation information is only available if you are the - owner of the reservation. - - Args: - start_datetime: Filter by the given start date/time, in local timezone. - end_datetime: Filter by the given end date/time, in local timezone. - - Returns: - A list of reservations that match the criteria. - """ - start_datetime = local_to_utc(start_datetime) if start_datetime else None - end_datetime = local_to_utc(end_datetime) if end_datetime else None - raw_response = self._account_client.backend_reservations( - self.name(), start_datetime, end_datetime - ) - return convert_reservation_data(raw_response, self.name()) - def configuration( self, ) -> Union[QasmBackendConfiguration, PulseBackendConfiguration]: @@ -300,19 +255,15 @@ class IBMRetiredBackend(IBMBackend): def __init__( self, configuration: Union[QasmBackendConfiguration, PulseBackendConfiguration], - service: "ibm_runtime_service.IBMRuntimeService", - credentials: Credentials, - api_client: AccountClient, + api_client: Optional[AccountClient] = None, ) -> None: """IBMRetiredBackend constructor. Args: configuration: Backend configuration. - service: IBM Quantum account provider. - credentials: IBM Quantum credentials. api_client: IBM Quantum client used to communicate with the server. """ - super().__init__(configuration, service, credentials, api_client) + super().__init__(configuration, api_client) self._status = BackendStatus( backend_name=self.name(), backend_version=self.configuration().backend_version, @@ -340,20 +291,11 @@ def status(self) -> BackendStatus: """Return the backend status.""" return self._status - def reservations( - self, - start_datetime: Optional[python_datetime] = None, - end_datetime: Optional[python_datetime] = None, - ) -> List[BackendReservation]: - return [] - @classmethod def from_name( cls, backend_name: str, - service: "ibm_runtime_service.IBMRuntimeService", - credentials: Credentials, - api: AccountClient, + api: Optional[AccountClient] = None, ) -> "IBMRetiredBackend": """Return a retired backend from its name.""" configuration = QasmBackendConfiguration( @@ -370,4 +312,4 @@ def from_name( gates=[GateConfig(name="TODO", parameters=[], qasm_def="TODO")], coupling_map=[[0, 1]], ) - return cls(configuration, service, credentials, api) + return cls(configuration, api) diff --git a/qiskit_ibm_runtime/ibm_runtime_service.py b/qiskit_ibm_runtime/ibm_runtime_service.py index 16c24336e4..c8100250f8 100644 --- a/qiskit_ibm_runtime/ibm_runtime_service.py +++ b/qiskit_ibm_runtime/ibm_runtime_service.py @@ -12,7 +12,6 @@ """Qiskit runtime service.""" -import copy import json import logging import re @@ -21,24 +20,20 @@ from collections import OrderedDict from typing import Dict, Callable, Optional, Union, List, Any, Type -from qiskit.circuit import QuantumCircuit from qiskit.providers.backend import BackendV1 as Backend from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit.providers.models import PulseBackendConfiguration, QasmBackendConfiguration from qiskit.providers.providerutils import filter_backends -from qiskit.transpiler import Layout -from qiskit_ibm_runtime import runtime_job, ibm_backend # pylint: disable=unused-import +from qiskit_ibm_runtime import ibm_backend # pylint: disable=unused-import from .accounts import AccountManager, Account, AccountType +from .accounts.exceptions import AccountsError from .api.clients import AuthClient, VersionClient from .api.clients.runtime import RuntimeClient from .api.exceptions import RequestsApiError -from .backendreservation import BackendReservation from .constants import QISKIT_IBM_RUNTIME_API_URL -from .credentials import Credentials, HubGroupProjectID from .exceptions import IBMNotAuthorizedError, IBMInputValueError, IBMProviderError from .exceptions import ( - QiskitRuntimeError, + IBMRuntimeError, RuntimeDuplicateProgramError, RuntimeProgramNotFound, RuntimeJobNotFound, @@ -46,11 +41,12 @@ ) from .hub_group_project import HubGroupProject # pylint: disable=cyclic-import from .program.result_decoder import ResultDecoder -from .runner_result import RunnerResult from .runtime_job import RuntimeJob from .runtime_program import RuntimeProgram, ParameterNamespace from .utils import RuntimeDecoder, to_base64_string, to_python_identifier -from .utils.backend import convert_reservation_data, decode_backend_configuration +from .utils.backend_decoder import configuration_from_server_data +from .utils.hgp import to_instance_format, from_instance_format +from .api.client_parameters import ClientParameters logger = logging.getLogger(__name__) @@ -117,81 +113,156 @@ class IBMRuntimeService: def __init__( self, + auth: Optional[AccountType] = None, token: Optional[str] = None, url: Optional[str] = None, - instance: Optional[str] = None, - auth: Optional[AccountType] = None, name: Optional[str] = None, + instance: Optional[str] = None, proxies: Optional[dict] = None, verify: Optional[bool] = None, ) -> None: """IBMRuntimeService constructor + An account is selected in the following order: + + - Account with the input `name`, if specified. + - Default account for the `auth` type, if `auth` is specified but `token` is not. + - Account defined by the input `auth` and `token`, if specified. + - Account defined by the environment variables, if defined. + - Default account for the cloud account, if one is available. + - Default account for the legacy account, if one is available. + + `instance`, `proxies`, and `verify` can be used to overwrite corresponding + values in the loaded account. + Args: + auth: Authentication type. ``cloud`` or ``legacy``. token: IBM Cloud API key or IBM Quantum API token. url: The API URL. Defaults to https://cloud.ibm.com (cloud) or https://auth.quantum-computing.ibm.com/api (legacy). - instance: The CRN (cloud) or hub/group/project (legacy). - auth: Authentication type. `cloud` or `legacy`. name: Name of the account to load. + instance: The service instance to use. For cloud runtime, this is the Cloud Resource + Name (CRN). For legacy runtime, this is the hub/group/project in that format. proxies: Proxy configuration for the server. - verify: Verify the server's TLS certificate. + verify: Whether to verify the server's TLS certificate. Returns: An instance of IBMRuntimeService. + + Raises: + IBMInputValueError: If an input is invalid. """ super().__init__() - # TODO: add support for loading default account when optional parameters are not set - # i.e. fallback to environment variables - # i.e. fallback to default account saved on disk - self.account = ( - AccountManager.get(name=name) - if name - else Account( - auth=auth, - token=token, - url=url, - instance=instance, - proxies=proxies, - verify=verify, - ) + self._account = self._discover_credentials( + token=token, + url=url, + instance=instance, + auth=auth, + name=name, + proxies=proxies, + verify=verify, ) - self.account_credentials = Credentials( - auth=self.account.auth, - token=self.account.token, - url=self.account.url, - instance=self.account.instance, - proxies=self.account.proxies, - verify=self.account.verify, + if self._account.auth == "cloud" and not self._account.instance: + raise IBMInputValueError( + f"Cloud account must have a service instance (CRN)." + ) + + self._client_params = ClientParameters( + auth_type=self._account.auth, + token=self._account.token, + url=self._account.url, + instance=self._account.instance, + proxies=self._account.proxies, + verify=self._account.verify, ) + + self._auth = self._account.auth self._programs: Dict[str, RuntimeProgram] = {} self._backends: Dict[str, "ibm_backend.IBMBackend"] = {} - if auth == "cloud": - self._api_client = RuntimeClient(credentials=self.account_credentials) - self._backends = self._discover_remote_backends() + if self._auth == "cloud": + self._api_client = RuntimeClient(self._client_params) + # TODO: We can make the backend discovery lazy + self._backends = self._discover_cloud_backends() + return else: - self._initialize_hgps(credentials=self.account_credentials) - self._api_client = None - hgps = self._get_hgps() - for hgp in hgps: + auth_client = self._authenticate_legacy_account(self._client_params) + # Update client parameters to use authenticated values. + self._client_params.url = auth_client.current_service_urls()["services"][ + "runtime" + ] + self._client_params.token = auth_client.current_access_token() + self._api_client = RuntimeClient(self._client_params) + self._hgps = self._initialize_hgps(auth_client) + for hgp in self._hgps.values(): for backend_name, backend in hgp.backends.items(): if backend_name not in self._backends: self._backends[backend_name] = backend - if not self._api_client and hgp.has_service("runtime"): - self._default_hgp = hgp - self._api_client = RuntimeClient(self._default_hgp.credentials) - self._access_token = self._default_hgp.credentials.access_token - self._ws_url = self._default_hgp.credentials.runtime_url.replace( - "https", "wss" - ) - self._programs = {} - - self._discover_backends() - - def _discover_remote_backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: + + # TODO - it'd be nice to allow some kind of autocomplete, but `service.ibmq_foo` + # just seems wrong since backends are not runtime service instances. + # self._discover_backends() + + def _discover_credentials( + self, + token: Optional[str] = None, + url: Optional[str] = None, + instance: Optional[str] = None, + auth: Optional[AccountType] = None, + name: Optional[str] = None, + proxies: Optional[dict] = None, + verify: Optional[bool] = None, + ) -> Account: + """Discover account credentials.""" + account = None + verify_ = verify or True + if name: + if any([auth, token, url]): + logger.warning( + "Loading account with name %s. Any input 'auth', 'token', 'url' are ignored.", + name, + ) + account = AccountManager.get(name=name) + elif auth: + if auth not in ["legacy", "cloud"]: + raise ValueError("'auth' can only be 'cloud' or 'legacy'") + if token: + return Account( + auth=auth, + token=token, + url=url, + instance=instance, + proxies=proxies, + verify=verify_, + ) + if url: + logger.warning( + "Loading default %s account. Input 'url' is ignored.", auth + ) + account = AccountManager.get(auth=auth) + elif any([token, url]): + # Let's not infer based on these attributes as they may change in the future. + raise ValueError( + "'auth' is required if 'token', or 'url' is specified but 'name' is not." + ) + + if account is None: + account = AccountManager.get() + if account is None: + raise AccountsError("Unable to find account.") + + if instance: + account.instance = instance + if proxies: + account.proxies = proxies + if verify is not None: + account.verify = verify + + return account + + def _discover_cloud_backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: """Return the remote backends available for this service instance. Returns: @@ -203,99 +274,94 @@ def _discover_remote_backends(self) -> Dict[str, "ibm_backend.IBMBackend"]: raw_config = self._api_client.backend_configuration( backend_name=backend_name ) - # Make sure the raw_config is of proper type - if not isinstance(raw_config, dict): - logger.warning( - "An error occurred when retrieving backend " - "information. Some backends might not be available." - ) + config = configuration_from_server_data( + raw_config=raw_config, instance=self._account.instance + ) + if not config: continue - try: - decode_backend_configuration(raw_config) - try: - config = PulseBackendConfiguration.from_dict(raw_config) - except (KeyError, TypeError): - config = QasmBackendConfiguration.from_dict(raw_config) - backend_cls = ( - ibm_backend.IBMSimulator - if config.simulator - else ibm_backend.IBMBackend - ) - ret[config.backend_name] = backend_cls( - configuration=config, - service=self, - credentials=self.account_credentials, - runtime_client=self._api_client, - ) - except Exception: # pylint: disable=broad-except - logger.warning( - 'Remote backend "%s" for service instance %s could not be instantiated due to an ' - "invalid config: %s", - raw_config.get("backend_name", raw_config.get("name", "unknown")), - repr(self), - traceback.format_exc(), - ) + backend_cls = ( + ibm_backend.IBMSimulator if config.simulator else ibm_backend.IBMBackend + ) + ret[config.backend_name] = backend_cls( + configuration=config, + api_client=self._api_client, + ) + return ret - def _initialize_hgps( - self, credentials: Credentials, preferences: Optional[Dict] = None - ) -> None: + def _authenticate_legacy_account( + self, client_params: ClientParameters + ) -> AuthClient: """Authenticate against IBM Quantum and populate the hub/group/projects. Args: - credentials: Credentials for IBM Quantum. - preferences: Account preferences. + client_params: Parameters used for server connection. Raises: IBMProviderCredentialsInvalidUrl: If the URL specified is not a valid IBM Quantum authentication URL. - IBMProviderError: If no hub/group/project could be found for this account. + IBMNotAuthorizedError: If the account is not authorized to use runtime. + + Returns: + Authentication client. """ - self._hgps: Dict[HubGroupProjectID, HubGroupProject] = OrderedDict() - version_info = self._check_api_version(credentials) + version_info = self._check_api_version(client_params) # Check the URL is a valid authentication URL. if not version_info["new_api"] or "api-auth" not in version_info: raise IBMProviderCredentialsInvalidUrl( "The URL specified ({}) is not an IBM Quantum authentication URL. " "Valid authentication URL: {}.".format( - credentials.url, QISKIT_IBM_RUNTIME_API_URL + client_params.url, QISKIT_IBM_RUNTIME_API_URL ) ) - auth_client = AuthClient( - credentials.token, - credentials.base_url, - **credentials.connection_parameters(), - ) + auth_client = AuthClient(client_params) + service_urls = auth_client.current_service_urls() + if not service_urls.get("services", {}).get(SERVICE_NAME): + raise IBMNotAuthorizedError( + "This account is not authorized to use legacy runtime service." + ) + return auth_client + + def _initialize_hgps( + self, + auth_client: AuthClient, + ) -> Dict: + """Authenticate against IBM Quantum and populate the hub/group/projects. + + Args: + auth_client: Authentication data. + + Raises: + IBMProviderCredentialsInvalidUrl: If the URL specified is not + a valid IBM Quantum authentication URL. + IBMProviderError: If no hub/group/project could be found for this account. + + Returns: + The hub/group/projects for this account. + """ + # pylint: disable=unsubscriptable-object + hgps: OrderedDict[str, HubGroupProject] = OrderedDict() service_urls = auth_client.current_service_urls() user_hubs = auth_client.user_hubs() - preferences = preferences or {} - is_open = True # First hgp is open access for hub_info in user_hubs: # Build credentials. - hgp_credentials = Credentials( - auth=credentials.auth, - token=credentials.token, - access_token=auth_client.current_access_token(), - instance=credentials.instance, + hgp_params = ClientParameters( + auth_type=self._account.auth, + token=auth_client.current_access_token(), url=service_urls["http"], - auth_url=credentials.auth_url, - websockets_url=service_urls["ws"], - proxies=credentials.proxies, - verify=credentials.verify, - services=service_urls.get("services", {}), - default_provider=credentials.default_provider, - **hub_info, - ) - hgp_credentials.preferences = preferences.get( - hgp_credentials.unique_id(), {} + instance=to_instance_format( + hub_info["hub"], hub_info["group"], hub_info["project"] + ), + proxies=self._account.proxies, + verify=self._account.verify, ) + # Build the hgp. try: hgp = HubGroupProject( - credentials=hgp_credentials, service=self, is_open=is_open + client_params=hgp_params, instance=hgp_params.instance ) - self._hgps[hgp.credentials.unique_id()] = hgp - is_open = False # hgps after first are premium and not open access + hgps[hgp.name] = hgp except Exception: # pylint: disable=broad-except # Catch-all for errors instantiating the hgp. logger.warning( @@ -303,122 +369,88 @@ def _initialize_hgps( hub_info, traceback.format_exc(), ) - if not self._hgps: + if not hgps: raise IBMProviderError( - "No hub/group/project could be found for this account." + "No hub/group/project that supports Qiskit Runtime could " + "be found for this account." ) # Move open hgp to end of the list - if len(self._hgps) > 1: - open_hgp = self._get_hgp() - self._hgps.move_to_end(open_hgp.credentials.unique_id()) - if credentials.default_provider: - # Move user selected hgp to front of the list - hub, group, project = credentials.default_provider.to_tuple() - default_hgp = self._get_hgp(hub=hub, group=group, project=project) - self._hgps.move_to_end(default_hgp.credentials.unique_id(), last=False) + if len(hgps) > 1: + open_key, open_val = hgps.popitem(last=False) + hgps[open_key] = open_val + + default_hgp = self._account.instance + if default_hgp: + if default_hgp in hgps: + # Move user selected hgp to front of the list + hgps.move_to_end(default_hgp, last=False) + else: + warnings.warn( + f"Default hub/group/project {default_hgp} not " + "found for the account and is ignored." + ) + return hgps @staticmethod - def _check_api_version(credentials: Credentials) -> Dict[str, Union[bool, str]]: + def _check_api_version(params: ClientParameters) -> Dict[str, Union[bool, str]]: """Check the version of the remote server in a set of credentials. Args: - credentials: IBM Quantum Credentials + params: Parameters used for server connection. Returns: A dictionary with version information. """ - version_finder = VersionClient( - credentials.base_url, **credentials.connection_parameters() - ) + version_finder = VersionClient(url=params.url, **params.connection_parameters()) return version_finder.version() def _get_hgp( self, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, + instance: Optional[str] = None, backend_name: Optional[str] = None, - service_name: Optional[str] = None, ) -> HubGroupProject: - """Return an instance of `HubGroupProject` for a single hub/group/project combination. + """Return an instance of `HubGroupProject`. This function also allows to find the `HubGroupProject` that contains a backend - `backend_name` providing service `service_name`. + `backend_name`. Args: - hub: Name of the hub. - group: Name of the group. - project: Name of the project. + instance: The hub/group/project to use. backend_name: Name of the IBM Quantum backend. - service_name: Name of the IBM Quantum service. Returns: An instance of `HubGroupProject` that matches the specified criteria or the default. Raises: - IBMProviderError: If no hub/group/project matches the specified criteria, - if more than one hub/group/project matches the specified criteria, if - no hub/group/project could be found for this account or if no backend matches the - criteria. + IBMInputValueError: If no hub/group/project matches the specified criteria, + or if the input value is in an incorrect format. + QiskitBackendNotFoundError: If backend cannot be found. """ - # If any `hub`, `group`, or `project` is specified, make sure all parameters are set. - if any([hub, group, project]) and not all([hub, group, project]): - raise IBMProviderError( - "The hub, group, and project parameters must all be " - "specified. " - 'hub = "{}", group = "{}", project = "{}"'.format(hub, group, project) - ) - hgps = self._get_hgps(hub=hub, group=group, project=project) - if any([hub, group, project]): - if not hgps: - raise IBMProviderError( - "No hub/group/project matches the specified criteria: " - "hub = {}, group = {}, project = {}".format(hub, group, project) + if instance: + _ = from_instance_format(instance) # Verify format + if instance not in self._hgps: + raise IBMInputValueError( + f"Hub/group/project {instance} " + "could not be found for this account." ) - if len(hgps) > 1: - raise IBMProviderError( - "More than one hub/group/project matches the " - "specified criteria. hub = {}, group = {}, project = {}".format( - hub, group, project - ) + if backend_name and not self._hgps[instance].get_backend(backend_name): + raise QiskitBackendNotFoundError( + f"Backend {backend_name} cannot be found in " + f"hub/group/project {instance}" ) - elif not hgps: - # Prevent edge case where no hub/group/project is available. - raise IBMProviderError( - "No hub/group/project could be found for this account." - ) - elif backend_name and service_name: - for hgp in hgps: - if hgp.has_service(service_name) and hgp.get_backend(backend_name): - return hgp - raise IBMProviderError("No backend matches the criteria.") - return hgps[0] - - def _get_hgps( - self, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - ) -> List[HubGroupProject]: - """Return a list of `HubGroupProject` instances, subject to optional filtering. + return self._hgps[instance] - Args: - hub: Name of the hub. - group: Name of the group. - project: Name of the project. + if not backend_name: + return list(self._hgps.values())[0] - Returns: - A list of `HubGroupProject` instances that match the specified criteria. - """ - filters: List[Callable[[HubGroupProjectID], bool]] = [] - if hub: - filters.append(lambda hgp: hgp.hub == hub) - if group: - filters.append(lambda hgp: hgp.group == group) - if project: - filters.append(lambda hgp: hgp.project == project) - hgps = [hgp for key, hgp in self._hgps.items() if all(f(key) for f in filters)] - return hgps + for hgp in self._hgps.values(): + if hgp.get_backend(backend_name): + return hgp + + raise QiskitBackendNotFoundError( + f"Backend {backend_name} cannot be found in any" + f"hub/group/project for this account." + ) def _discover_backends(self) -> None: """Discovers the remote backends for this account, if not already known.""" @@ -432,32 +464,23 @@ def _discover_backends(self) -> None: def backends( self, name: Optional[str] = None, - filters: Optional[Callable[[List["ibm_backend.IBMBackend"]], bool]] = None, min_num_qubits: Optional[int] = None, - input_allowed: Optional[Union[str, List[str]]] = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, + instance: Optional[str] = None, + filters: Optional[Callable[[List["ibm_backend.IBMBackend"]], bool]] = None, **kwargs: Any, ) -> List["ibm_backend.IBMBackend"]: """Return all backends accessible via this account, subject to optional filtering. Args: name: Backend name to filter by. + min_num_qubits: Minimum number of qubits the backend has to have. + instance: The service instance to use. For cloud runtime, this is the Cloud Resource + Name (CRN). For legacy runtime, this is the hub/group/project in that format. filters: More complex filters, such as lambda functions. For example:: IBMRuntimeService.backends( filters=lambda b: b.configuration().quantum_volume > 16) - min_num_qubits: Minimum number of qubits the backend has to have. - input_allowed: Filter by the types of input the backend supports. - Valid input types are ``job`` (circuit job) and ``runtime`` (Qiskit Runtime). - For example, ``inputs_allowed='runtime'`` will return all backends - that support Qiskit Runtime. If a list is given, the backend must - support all types specified in the list. - hub: Name of the hub. - group: Name of the group. - project: Name of the project. kwargs: Simple filters that specify a ``True``/``False`` criteria in the backend configuration, backends status, or provider credentials. An example to get the operational backends with 5 qubits:: @@ -466,74 +489,32 @@ def backends( Returns: The list of available backends that match the filter. - - Raises: - IBMBackendValueError: If only one or two parameters from `hub`, `group`, - `project` are specified. """ - backends: List["ibm_backend.IBMBackend"] = list() - if all([hub, group, project]): - hgp = self._get_hgp(hub, group, project) - backends = list(hgp.backends.values()) + # TODO filter out input_allowed not having runtime + if self._auth == "legacy": + if instance: + backends = list(self._get_hgp(instance=instance).backends.values()) + else: + backends = list(self._backends.values()) else: + # TODO filtering by instance for cloud backends = list(self._backends.values()) - # Special handling of the `name` parameter, to support alias resolution. + if name: - aliases = self._aliased_backend_names() - aliases.update(self._deprecated_backend_names()) - name = aliases.get(name, name) kwargs["backend_name"] = name if min_num_qubits: backends = list( filter(lambda b: b.configuration().n_qubits >= min_num_qubits, backends) ) - if input_allowed: - if not isinstance(input_allowed, list): - input_allowed = [input_allowed] - backends = list( - filter( - lambda b: set(input_allowed) - <= set(b.configuration().input_allowed), - backends, - ) - ) return filter_backends(backends, filters=filters, **kwargs) - def my_reservations(self) -> List[BackendReservation]: - """Return your upcoming reservations. - - Returns: - A list of your upcoming reservations. - """ - raw_response = self._default_hgp._api_client.my_reservations() - return convert_reservation_data(raw_response) - - @staticmethod - def _deprecated_backend_names() -> Dict[str, str]: - """Returns deprecated backend names.""" - return { - "ibmqx_qasm_simulator": "ibmq_qasm_simulator", - "ibmqx_hpc_qasm_simulator": "ibmq_qasm_simulator", - "real": "ibmqx1", - } - - @staticmethod - def _aliased_backend_names() -> Dict[str, str]: - """Returns aliased backend names.""" - return { - "ibmq_5_yorktown": "ibmqx2", - "ibmq_5_tenerife": "ibmqx4", - "ibmq_16_rueschlikon": "ibmqx5", - "ibmq_20_austin": "QS1_1", - } - - def active_account(self) -> dict: + def active_account(self) -> Optional[Dict[str, str]]: """Return the IBM Quantum account currently in use for the session. Returns: A dictionary with information about the account currently in the session. """ - return self.account.to_saved_format() + return self._account.to_saved_format() @staticmethod def delete_account(name: Optional[str]) -> bool: @@ -573,7 +554,7 @@ def save_account( verify: Verify the server's TLS certificate. """ - return AccountManager.save( + AccountManager.save( token=token, url=url, instance=instance, @@ -599,156 +580,27 @@ def saved_accounts() -> dict: def get_backend( self, name: str = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - **kwargs: Any, + instance: Optional[str] = None, ) -> Backend: """Return a single backend matching the specified filtering. Args: - name (str): name of the backend. - hub: Name of the hub. - group: Name of the group. - project: Name of the project. - **kwargs: dict used for filtering. + name: Name of the backend. + instance: The service instance to use. For cloud runtime, this is the Cloud Resource + Name (CRN). For legacy runtime, this is the hub/group/project in that format. Returns: - Backend: a backend matching the filtering. + Backend: A backend matching the filtering. Raises: - QiskitBackendNotFoundError: if no backend could be found or - more than one backend matches the filtering criteria. - IBMProviderValueError: If only one or two parameters from `hub`, `group`, - `project` are specified. + QiskitBackendNotFoundError: if no backend could be found. """ # pylint: disable=arguments-differ - backends = self.backends(name, hub=hub, group=group, project=project, **kwargs) - if len(backends) > 1: - raise QiskitBackendNotFoundError( - "More than one backend matches the criteria" - ) + backends = self.backends(name, instance=instance) if not backends: raise QiskitBackendNotFoundError("No backend matches the criteria") return backends[0] - def run_circuits( - self, - circuits: Union[QuantumCircuit, List[QuantumCircuit]], - backend_name: str, - shots: Optional[int] = None, - initial_layout: Optional[Union[Layout, Dict, List]] = None, - layout_method: Optional[str] = None, - routing_method: Optional[str] = None, - translation_method: Optional[str] = None, - seed_transpiler: Optional[int] = None, - optimization_level: int = 1, - init_qubits: bool = True, - rep_delay: Optional[float] = None, - transpiler_options: Optional[dict] = None, - measurement_error_mitigation: bool = False, - use_measure_esp: Optional[bool] = None, - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, - **run_config: Dict, - ) -> "runtime_job.RuntimeJob": - """Execute the input circuit(s) on a backend using the runtime service. - - Note: - This method uses the IBM Quantum runtime service which is not - available to all accounts. - - Args: - circuits: Circuit(s) to execute. - - backend_name: Name of the backend to execute circuits on. - Transpiler options are automatically grabbed from backend configuration - and properties unless otherwise specified. - - shots: Number of repetitions of each circuit, for sampling. If not specified, - the backend default is used. - - initial_layout: Initial position of virtual qubits on physical qubits. - - layout_method: Name of layout selection pass ('trivial', 'dense', - 'noise_adaptive', 'sabre'). - Sometimes a perfect layout can be available in which case the layout_method - may not run. - - routing_method: Name of routing pass ('basic', 'lookahead', 'stochastic', 'sabre') - - translation_method: Name of translation pass ('unroller', 'translator', 'synthesis') - - seed_transpiler: Sets random seed for the stochastic parts of the transpiler. - - optimization_level: How much optimization to perform on the circuits. - Higher levels generate more optimized circuits, at the expense of longer - transpilation time. - If None, level 1 will be chosen as default. - - init_qubits: Whether to reset the qubits to the ground state for each shot. - - rep_delay: Delay between programs in seconds. Only supported on certain - backends (``backend.configuration().dynamic_reprate_enabled`` ). If supported, - ``rep_delay`` will be used instead of ``rep_time`` and must be from the - range supplied by the backend (``backend.configuration().rep_delay_range``). - Default is given by ``backend.configuration().default_rep_delay``. - - transpiler_options: Additional transpiler options. - - measurement_error_mitigation: Whether to apply measurement error mitigation. - - use_measure_esp: Whether to use excited state promoted (ESP) readout for measurements - which are the final instruction on a qubit. ESP readout can offer higher fidelity - than standard measurement sequences. See - `here `_. - - hub: Name of the hub. - - group: Name of the group. - - project: Name of the project. - - **run_config: Extra arguments used to configure the circuit execution. - - Returns: - Runtime job. - """ - inputs = copy.deepcopy(run_config) # type: Dict[str, Any] - inputs["circuits"] = circuits - inputs["optimization_level"] = optimization_level - inputs["init_qubits"] = init_qubits - inputs["measurement_error_mitigation"] = measurement_error_mitigation - if shots: - inputs["shots"] = shots - if initial_layout: - inputs["initial_layout"] = initial_layout - if layout_method: - inputs["layout_method"] = layout_method - if routing_method: - inputs["routing_method"] = routing_method - if translation_method: - inputs["translation_method"] = translation_method - if seed_transpiler: - inputs["seed_transpiler"] = seed_transpiler - if rep_delay: - inputs["rep_delay"] = rep_delay - if transpiler_options: - inputs["transpiler_options"] = transpiler_options - if use_measure_esp is not None: - inputs["use_measure_esp"] = use_measure_esp - options = {"backend_name": backend_name} - return self.run( - "circuit-runner", - options=options, - inputs=inputs, - result_decoder=RunnerResult, - hub=hub, - group=group, - project=project, - ) - def pprint_programs( self, refresh: bool = False, @@ -834,7 +686,7 @@ def program(self, program_id: str, refresh: bool = False) -> RuntimeProgram: Raises: RuntimeProgramNotFound: If the program does not exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ if program_id not in self._programs or refresh: try: @@ -844,7 +696,7 @@ def program(self, program_id: str, refresh: bool = False) -> RuntimeProgram: raise RuntimeProgramNotFound( f"Program not found: {ex.message}" ) from None - raise QiskitRuntimeError(f"Failed to get program: {ex}") from None + raise IBMRuntimeError(f"Failed to get program: {ex}") from None self._programs[program_id] = self._to_program(response) @@ -892,10 +744,8 @@ def run( inputs: Union[Dict, ParameterNamespace], callback: Optional[Callable] = None, result_decoder: Optional[Type[ResultDecoder]] = None, - image: Optional[str] = "", - hub: Optional[str] = None, - group: Optional[str] = None, - project: Optional[str] = None, + image: str = "", + instance: Optional[str] = None, ) -> RuntimeJob: """Execute the runtime program. @@ -915,18 +765,17 @@ def run( ``ResultDecoder`` is used if not specified. image: The runtime image used to execute the program, specified in the form of image_name:tag. Not all accounts are authorized to select a different image. - hub: Name of the hub. - group: Name of the group. - project: Name of the project. + instance: The service instance to use. For cloud runtime, this is the Cloud Resource + Name (CRN). For legacy runtime, this is the hub/group/project in that format. Returns: A ``RuntimeJob`` instance representing the execution. Raises: IBMInputValueError: If input is invalid. + RuntimeProgramNotFound: If the program cannot be found. + IBMRuntimeError: An error occurred running the program. """ - if "backend_name" not in options: - raise IBMInputValueError('"backend_name" is required field in "options"') # If using params object, extract as dictionary if isinstance(inputs, ParameterNamespace): inputs.validate() @@ -937,37 +786,44 @@ def run( image, ): raise IBMInputValueError('"image" needs to be in form of image_name:tag') - backend_name = options["backend_name"] - if not all([hub, group, project]) and self._default_hgp.get_backend( - backend_name - ): - hgp = self._default_hgp + + backend_name = options.get("backend_name", "") + + hgp_name = None + if self._auth == "legacy": + if not backend_name: + raise IBMInputValueError( + '"backend_name" is required field in "options" for legacy runtime.' + ) + # Find the right hgp + hgp = self._get_hgp(instance=instance, backend_name=backend_name) + backend = hgp.get_backend(backend_name) + hgp_name = hgp.name else: - hgp = self._get_hgp( - hub=hub, - group=group, - project=project, + # TODO Support instance for cloud + # TODO Support optional backend name when fully supported by server + backend = self.get_backend(backend_name) + + result_decoder = result_decoder or ResultDecoder + try: + response = self._api_client.program_run( + program_id=program_id, backend_name=backend_name, - service_name=SERVICE_NAME, + params=inputs, + image=image, + hgp=hgp_name, ) - credentials = hgp.credentials - api_client = ( - self._api_client if hgp == self._default_hgp else RuntimeClient(credentials) - ) - result_decoder = result_decoder or ResultDecoder - response = api_client.program_run( - program_id=program_id, - credentials=credentials, - backend_name=backend_name, - params=inputs, - image=image, - ) + except RequestsApiError as ex: + if ex.status_code == 404: + raise RuntimeProgramNotFound( + f"Program not found: {ex.message}" + ) from None + raise IBMRuntimeError(f"Failed to run program: {ex}") from None - backend = self.get_backend(backend_name) job = RuntimeJob( backend=backend, - api_client=api_client, - credentials=credentials, + api_client=self._api_client, + client_params=self._client_params, job_id=response["id"], program_id=program_id, params=inputs, @@ -1025,7 +881,7 @@ def upload_program( IBMInputValueError: If required metadata is missing. RuntimeDuplicateProgramError: If a program with the same name already exists. IBMNotAuthorizedError: If you are not authorized to upload programs. - QiskitRuntimeError: If the upload failed. + IBMRuntimeError: If the upload failed. """ program_metadata = self._read_metadata(metadata=metadata) @@ -1052,7 +908,7 @@ def upload_program( raise IBMNotAuthorizedError( "You are not authorized to upload programs." ) from None - raise QiskitRuntimeError(f"Failed to create program: {ex}") from None + raise IBMRuntimeError(f"Failed to create program: {ex}") from None return response["id"] def _read_metadata(self, metadata: Optional[Union[Dict, str]] = None) -> Dict: @@ -1110,7 +966,7 @@ def update_program( Raises: RuntimeProgramNotFound: If the program doesn't exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ if not any([data, metadata, name, description, max_execution_time, spec]): warnings.warn( @@ -1146,7 +1002,7 @@ def update_program( raise RuntimeProgramNotFound( f"Program not found: {ex.message}" ) from None - raise QiskitRuntimeError(f"Failed to update program: {ex}") from None + raise IBMRuntimeError(f"Failed to update program: {ex}") from None if program_id in self._programs: program = self._programs[program_id] @@ -1178,7 +1034,7 @@ def delete_program(self, program_id: str) -> None: Raises: RuntimeProgramNotFound: If the program doesn't exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ try: self._api_client.program_delete(program_id=program_id) @@ -1187,7 +1043,7 @@ def delete_program(self, program_id: str) -> None: raise RuntimeProgramNotFound( f"Program not found: {ex.message}" ) from None - raise QiskitRuntimeError(f"Failed to delete program: {ex}") from None + raise IBMRuntimeError(f"Failed to delete program: {ex}") from None if program_id in self._programs: del self._programs[program_id] @@ -1201,17 +1057,17 @@ def set_program_visibility(self, program_id: str, public: bool) -> None: If ``False``, make the program visible to just your account. Raises: - RuntimeJobNotFound: if program not found (404) - QiskitRuntimeError: if update failed (401, 403) + RuntimeProgramNotFound: if program not found (404) + IBMRuntimeError: if update failed (401, 403) """ try: self._api_client.set_program_visibility(program_id, public) except RequestsApiError as ex: if ex.status_code == 404: - raise RuntimeJobNotFound(f"Program not found: {ex.message}") from None - raise QiskitRuntimeError( - f"Failed to set program visibility: {ex}" - ) from None + raise RuntimeProgramNotFound( + f"Program not found: {ex.message}" + ) from None + raise IBMRuntimeError(f"Failed to set program visibility: {ex}") from None if program_id in self._programs: program = self._programs[program_id] @@ -1228,14 +1084,14 @@ def job(self, job_id: str) -> RuntimeJob: Raises: RuntimeJobNotFound: If the job doesn't exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ try: response = self._api_client.job_get(job_id) except RequestsApiError as ex: if ex.status_code == 404: raise RuntimeJobNotFound(f"Job not found: {ex.message}") from None - raise QiskitRuntimeError(f"Failed to delete job: {ex}") from None + raise IBMRuntimeError(f"Failed to delete job: {ex}") from None return self._decode_job(response) def jobs( @@ -1244,9 +1100,7 @@ def jobs( skip: int = 0, pending: bool = None, program_id: str = None, - hub: str = None, - group: str = None, - project: str = None, + instance: Optional[str] = None, ) -> List[RuntimeJob]: """Retrieve all runtime jobs, subject to optional filtering. @@ -1257,23 +1111,23 @@ def jobs( jobs are included. If ``False``, 'DONE', 'CANCELLED' and 'ERROR' jobs are included. program_id: Filter by Program ID. - hub: Filter by hub - hub, group, and project must all be specified. - group: Filter by group - hub, group, and project must all be specified. - project: Filter by project - hub, group, and project must all be specified. + instance: The service instance to use. Currently only supported for legacy runtime, + and should be in the hub/group/project. Returns: A list of runtime jobs. Raises: - IBMInputValueError: If any but not all of the parameters ``hub``, ``group`` - and ``project`` are given. + IBMInputValueError: If an input value is invalid. """ - if any([hub, group, project]) and not all([hub, group, project]): - raise IBMInputValueError( - "Hub, group and project " - "parameters must all be specified. " - 'hub = "{}", group = "{}", project = "{}"'.format(hub, group, project) - ) + hub = group = project = None + if instance: + if self._auth == "cloud": + raise IBMInputValueError( + "'instance' is not supported by cloud runtime." + ) + hub, group, project = from_instance_format(instance) + job_responses = [] # type: List[Dict[str, Any]] current_page_limit = limit or 20 offset = skip @@ -1321,14 +1175,14 @@ def delete_job(self, job_id: str) -> None: Raises: RuntimeJobNotFound: If the job doesn't exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ try: self._api_client.job_delete(job_id) except RequestsApiError as ex: if ex.status_code == 404: raise RuntimeJobNotFound(f"Job not found: {ex.message}") from None - raise QiskitRuntimeError(f"Failed to delete job: {ex}") from None + raise IBMRuntimeError(f"Failed to delete job: {ex}") from None def _decode_job(self, raw_data: Dict) -> RuntimeJob: """Decode job data received from the server. @@ -1339,26 +1193,20 @@ def _decode_job(self, raw_data: Dict) -> RuntimeJob: Returns: Decoded job data. """ - hub = raw_data["hub"] - group = raw_data["group"] - project = raw_data["project"] + hub = raw_data.get("hub") + group = raw_data.get("group") + project = raw_data.get("project") + instance = ( + to_instance_format(hub, group, project) + if all([hub, group, project]) + else None + ) # Try to find the right backend try: - backend = self.get_backend( - raw_data["backend"], hub=hub, group=group, project=project - ) + backend = self.get_backend(raw_data["backend"], instance=instance) except (IBMProviderError, QiskitBackendNotFoundError): backend = ibm_backend.IBMRetiredBackend.from_name( backend_name=raw_data["backend"], - service=self, - credentials=Credentials( - auth="legacy", - token="", - url="", - hub=hub, - group=group, - project=project, - ), api=None, ) @@ -1375,7 +1223,7 @@ def _decode_job(self, raw_data: Dict) -> RuntimeJob: return RuntimeJob( backend=backend, api_client=self._api_client, - credentials=self._default_hgp.credentials, + client_params=self._client_params, job_id=raw_data["id"], program_id=raw_data.get("program", {}).get("id", ""), params=decoded, @@ -1396,5 +1244,42 @@ def logout(self) -> None: """ self._api_client.logout() + def least_busy( + self, + min_num_qubits: Optional[int] = None, + instance: Optional[str] = None, + filters: Optional[Callable[[List["ibm_backend.IBMBackend"]], bool]] = None, + **kwargs: Any, + ) -> ibm_backend.IBMBackend: + """Return the least busy available backend. + + Returns: + The backend with the fewest number of pending jobs. + + Raises: + QiskitBackendNotFoundError: If no backend matches the criteria. + """ + backends = self.backends( + min_num_qubits=min_num_qubits, instance=instance, filters=filters, **kwargs + ) + candidates = [] + for back in backends: + backend_status = back.status() + if not backend_status.operational or backend_status.status_msg != "active": + continue + candidates.append(back) + if not candidates: + raise QiskitBackendNotFoundError("No backend matches the criteria.") + return min(candidates, key=lambda b: b.status().pending_jobs) + + @property + def auth(self) -> str: + """Return the authentication type used. + + Returns: + The authentication type used. + """ + return self._auth + def __repr__(self) -> str: return "<{}>".format(self.__class__.__name__) diff --git a/qiskit_ibm_runtime/jupyter/qubits_widget.py b/qiskit_ibm_runtime/jupyter/qubits_widget.py index 2eddd2f69c..558c23d0bf 100644 --- a/qiskit_ibm_runtime/jupyter/qubits_widget.py +++ b/qiskit_ibm_runtime/jupyter/qubits_widget.py @@ -10,6 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. # pylint: disable=invalid-name +# pylint: disable=consider-iterating-dictionary """Widget for qubit properties tab.""" diff --git a/qiskit_ibm_runtime/runner_result.py b/qiskit_ibm_runtime/runner_result.py deleted file mode 100644 index bf609ba6de..0000000000 --- a/qiskit_ibm_runtime/runner_result.py +++ /dev/null @@ -1,74 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Circuit-runner result class""" - -from typing import List, Union -import json - -from qiskit.result import Result, QuasiDistribution -from qiskit.result.postprocess import _hex_to_bin -from qiskit.exceptions import QiskitError - -from .program.result_decoder import ResultDecoder - - -class RunnerResult(Result, ResultDecoder): - """Result class for Qiskit Runtime program circuit-runner.""" - - @classmethod - def decode(cls, data: str) -> "RunnerResult": - """Decoding for results from Qiskit runtime jobs.""" - return cls.from_dict(json.loads(data)) - - def get_quasiprobabilities( - self, experiment: Union[int, List] = None - ) -> Union[QuasiDistribution, List[QuasiDistribution]]: - """Get quasiprobabilites associated with one or more experiments. - - Parameters: - experiment: Indices of experiments to grab quasiprobabilities from. - - Returns: - A single distribution or a list of distributions. - - Raises: - QiskitError: If experiment result doesn't contain quasiprobabilities. - """ - if experiment is None: - exp_keys = range(len(self.results)) - else: - exp_keys = [experiment] # type: ignore[assignment] - - dict_list = [] - for key in exp_keys: - if "quasiprobabilities" in self.data(key).keys(): - shots = self.results[key].shots - hex_quasi = self.results[key].data.quasiprobabilities - bit_lenth = len(self.results[key].header.final_measurement_mapping) - quasi = {} - for hkey, val in hex_quasi.items(): - quasi[_hex_to_bin(hkey).zfill(bit_lenth)] = val - - out = QuasiDistribution(quasi, shots) - out.shots = shots - dict_list.append(out) - else: - raise QiskitError( - 'No quasiprobabilities for experiment "{}"'.format(repr(key)) - ) - - # Return first item of dict_list if size is 1 - if len(dict_list) == 1: - return dict_list[0] - else: - return dict_list diff --git a/qiskit_ibm_runtime/runtime_job.py b/qiskit_ibm_runtime/runtime_job.py index 75d439ead9..f42b95dc4e 100644 --- a/qiskit_ibm_runtime/runtime_job.py +++ b/qiskit_ibm_runtime/runtime_job.py @@ -28,14 +28,14 @@ from .exceptions import ( RuntimeJobFailureError, RuntimeInvalidStateError, - QiskitRuntimeError, + IBMRuntimeError, ) from .program.result_decoder import ResultDecoder from .api.clients import RuntimeClient, RuntimeWebsocketClient, WebsocketClientCloseCode from .exceptions import IBMError from .api.exceptions import RequestsApiError from .utils.converters import utc_to_local -from .credentials import Credentials +from .api.client_parameters import ClientParameters logger = logging.getLogger(__name__) @@ -82,7 +82,7 @@ def __init__( self, backend: Backend, api_client: RuntimeClient, - credentials: Credentials, + client_params: ClientParameters, job_id: str, program_id: str, params: Optional[Dict] = None, @@ -96,7 +96,7 @@ def __init__( Args: backend: The backend instance used to run this job. api_client: Object for connecting to the server. - credentials: Account credentials. + client_params: Parameters used for server connection. job_id: Job ID. program_id: ID of the program this job is for. params: Job parameters. @@ -123,8 +123,8 @@ def __init__( self._ws_client_future = None # type: Optional[futures.Future] self._result_queue = queue.Queue() # type: queue.Queue self._ws_client = RuntimeWebsocketClient( - websocket_url=credentials.runtime_url.replace("https", "wss"), - credentials=credentials, + websocket_url=client_params.url.replace("https", "wss"), + client_params=client_params, job_id=job_id, message_queue=self._result_queue, ) @@ -189,7 +189,7 @@ def cancel(self) -> None: Raises: RuntimeInvalidStateError: If the job is in a state that cannot be cancelled. - QiskitRuntimeError: If unable to cancel job. + IBMRuntimeError: If unable to cancel job. """ try: self._api_client.job_cancel(self.job_id) @@ -198,7 +198,7 @@ def cancel(self) -> None: raise RuntimeInvalidStateError( f"Job cannot be cancelled: {ex}" ) from None - raise QiskitRuntimeError(f"Failed to cancel job: {ex}") from None + raise IBMRuntimeError(f"Failed to cancel job: {ex}") from None self.cancel_result_streaming() self._status = JobStatus.CANCELLED @@ -294,7 +294,7 @@ def logs(self) -> str: Job logs, including standard output and error. Raises: - QiskitRuntimeError: If a network error occurred. + IBMRuntimeError: If a network error occurred. """ if self.status() not in JOB_FINAL_STATES: logger.warning("Job logs are only available after the job finishes.") @@ -303,7 +303,7 @@ def logs(self) -> str: except RequestsApiError as err: if err.status_code == 404: return "" - raise QiskitRuntimeError(f"Failed to get job logs: {err}") from None + raise IBMRuntimeError(f"Failed to get job logs: {err}") from None def _set_status_and_error_message(self) -> None: """Fetch and set status and error message.""" diff --git a/qiskit_ibm_runtime/runtime_program.py b/qiskit_ibm_runtime/runtime_program.py index 30ed8bdc3f..6dc5bf653e 100644 --- a/qiskit_ibm_runtime/runtime_program.py +++ b/qiskit_ibm_runtime/runtime_program.py @@ -17,7 +17,7 @@ from typing import Optional, Dict from types import SimpleNamespace from qiskit_ibm_runtime.exceptions import IBMInputValueError, IBMNotAuthorizedError -from .exceptions import QiskitRuntimeError, RuntimeProgramNotFound +from .exceptions import IBMRuntimeError, RuntimeProgramNotFound from .api.clients.runtime import RuntimeClient from .api.exceptions import RequestsApiError @@ -298,7 +298,7 @@ def _refresh(self) -> None: Raises: RuntimeProgramNotFound: If the program does not exist. - QiskitRuntimeError: If the request failed. + IBMRuntimeError: If the request failed. """ try: response = self._api_client.program_get(self._id) @@ -307,7 +307,7 @@ def _refresh(self) -> None: raise RuntimeProgramNotFound( f"Program not found: {ex.message}" ) from None - raise QiskitRuntimeError(f"Failed to get program: {ex}") from None + raise IBMRuntimeError(f"Failed to get program: {ex}") from None self._backend_requirements = {} self._parameters = {} self._return_values = {} diff --git a/qiskit_ibm_runtime/utils/backend.py b/qiskit_ibm_runtime/utils/backend_decoder.py similarity index 61% rename from qiskit_ibm_runtime/utils/backend.py rename to qiskit_ibm_runtime/utils/backend_decoder.py index a16344fbef..800b440f43 100644 --- a/qiskit_ibm_runtime/utils/backend.py +++ b/qiskit_ibm_runtime/utils/backend_decoder.py @@ -12,52 +12,70 @@ """Utilities for working with IBM Quantum backends.""" -from typing import List, Optional, Dict, Union +from typing import List, Dict, Union, Optional +import logging +import traceback import dateutil.parser +from qiskit.providers.models import ( + BackendProperties, + PulseDefaults, +) +from qiskit.providers.models import ( + PulseBackendConfiguration, + QasmBackendConfiguration, +) -from ..backendreservation import BackendReservation -from ..utils.converters import utc_to_local +from .converters import utc_to_local_all +logger = logging.getLogger(__name__) -def convert_reservation_data( - raw_reservations: List, backend_name: Optional[str] = None -) -> List[BackendReservation]: - """Convert a list of raw reservation data to ``BackendReservation`` objects. + +def configuration_from_server_data( + raw_config: Dict, + instance: str = "", +) -> Optional[Union[QasmBackendConfiguration, PulseBackendConfiguration]]: + """Create an IBMBackend instance from raw server data. Args: - raw_reservations: Raw reservation data. - backend_name: Name of the backend. + raw_config: Raw configuration. + instance: Service instance. Returns: - A list of ``BackendReservation`` objects. + Backend configuration. """ - reservations = [] - for raw_res in raw_reservations: - creation_datetime = raw_res.get("creationDate", None) - creation_datetime = ( - utc_to_local(creation_datetime) if creation_datetime else None + # Make sure the raw_config is of proper type + if not isinstance(raw_config, dict): + logger.warning( # type: ignore[unreachable] + "An error occurred when retrieving backend " + "information. Some backends might not be available." ) - backend_name = backend_name or raw_res.get("backendName", None) - reservations.append( - BackendReservation( - backend_name=backend_name, - start_datetime=utc_to_local(raw_res["initialDate"]), - end_datetime=utc_to_local(raw_res["endDate"]), - mode=raw_res.get("mode", None), - reservation_id=raw_res.get("id", None), - creation_datetime=creation_datetime, - hub_info=raw_res.get("hubInfo", None), - ) + return None + try: + _decode_backend_configuration(raw_config) + try: + return PulseBackendConfiguration.from_dict(raw_config) + except (KeyError, TypeError): + return QasmBackendConfiguration.from_dict(raw_config) + except Exception: # pylint: disable=broad-except + logger.warning( + 'Remote backend "%s" for service instance %s could not be instantiated due to an ' + "invalid config: %s", + raw_config.get("backend_name", raw_config.get("name", "unknown")), + repr(instance), + traceback.format_exc(), ) - return reservations + return None -def decode_pulse_defaults(defaults: Dict) -> None: +def defaults_from_server_data(defaults: Dict) -> PulseDefaults: """Decode pulse defaults data. Args: - defaults: A ``PulseDefaults`` in dictionary format. + defaults: Raw pulse defaults data. + + Returns: + A ``PulseDefaults`` instance. """ for item in defaults["pulse_library"]: _decode_pulse_library_item(item) @@ -67,12 +85,17 @@ def decode_pulse_defaults(defaults: Dict) -> None: for instr in cmd["sequence"]: _decode_pulse_qobj_instr(instr) + return PulseDefaults.from_dict(defaults) + -def decode_backend_properties(properties: Dict) -> None: +def properties_from_server_data(properties: Dict) -> BackendProperties: """Decode backend properties. Args: - properties: A ``BackendProperties`` in dictionary format. + properties: Raw properties data. + + Returns: + A ``BackendProperties`` instance. """ properties["last_update_date"] = dateutil.parser.isoparse( properties["last_update_date"] @@ -86,8 +109,11 @@ def decode_backend_properties(properties: Dict) -> None: for gen in properties["general"]: gen["date"] = dateutil.parser.isoparse(gen["date"]) + properties = utc_to_local_all(properties) + return BackendProperties.from_dict(properties) + -def decode_backend_configuration(config: Dict) -> None: +def _decode_backend_configuration(config: Dict) -> None: """Decode backend configuration. Args: diff --git a/qiskit_ibm_runtime/utils/hgp.py b/qiskit_ibm_runtime/utils/hgp.py new file mode 100644 index 0000000000..46d11928e4 --- /dev/null +++ b/qiskit_ibm_runtime/utils/hgp.py @@ -0,0 +1,43 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Hub/group/project utility functions.""" + +from typing import Tuple +from ..exceptions import IBMInputValueError + + +def from_instance_format(instance: str) -> Tuple[str, str, str]: + """Convert the input instance to [hub, group, project]. + + Args: + instance: Service instance in hub/group/project format. + + Returns: + Hub, group, and project. + + Raises: + IBMInputValueError: If input is not in the correct format. + """ + try: + hub, group, project = instance.split("/") + return hub, group, project + except (ValueError, AttributeError): + raise IBMInputValueError( + f"Input instance value {instance} is not in the" + f"correct hub/group/project format." + ) + + +def to_instance_format(hub: str, group: str, project: str) -> str: + """Convert input to hub/group/project format.""" + return f"{hub}/{group}/{project}" diff --git a/qiskit_ibm_runtime/utils/utils.py b/qiskit_ibm_runtime/utils/utils.py index 54a5564c81..afdf86e01d 100644 --- a/qiskit_ibm_runtime/utils/utils.py +++ b/qiskit_ibm_runtime/utils/utils.py @@ -28,6 +28,9 @@ def is_crn(locator: str) -> bool: Args: locator: The value to check. + + Returns: + Whether the input is a CRN. """ return isinstance(locator, str) and locator.startswith("crn:") @@ -38,6 +41,9 @@ def crn_to_api_host(crn: str) -> str: Args: crn: The CRN. + Returns: + API host. + Raises: CannotMapCrnToApiHostError: If the corresponding API host cannot be determined. """ diff --git a/qiskit_ibm_runtime/visualization/interactive/error_map.py b/qiskit_ibm_runtime/visualization/interactive/error_map.py index 00a80d39f8..a8be5119b6 100644 --- a/qiskit_ibm_runtime/visualization/interactive/error_map.py +++ b/qiskit_ibm_runtime/visualization/interactive/error_map.py @@ -10,6 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. # pylint: disable=invalid-name +# pylint: disable=consider-iterating-dictionary """Interactive error map for IBM Quantum devices.""" diff --git a/qiskit_ibm_runtime/visualization/interactive/gate_map.py b/qiskit_ibm_runtime/visualization/interactive/gate_map.py index d89b5d2a43..bca94b96f7 100644 --- a/qiskit_ibm_runtime/visualization/interactive/gate_map.py +++ b/qiskit_ibm_runtime/visualization/interactive/gate_map.py @@ -9,6 +9,7 @@ # Any modifications or derivative works of this code must retain this # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. +# pylint: disable=consider-iterating-dictionary """Interactive gate map for IBM Quantum devices.""" diff --git a/test/decorators.py b/test/decorators.py deleted file mode 100644 index ba9f1ef28c..0000000000 --- a/test/decorators.py +++ /dev/null @@ -1,363 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Decorators for using with IBM Provider unit tests. - - Environment variables used by the decorators: - * QISKIT_IBM_RUNTIME_API_TOKEN: default API token to use. - * QISKIT_IBM_RUNTIME_API_URL: default API url to use. - * QISKIT_IBM_RUNTIME_HGP: default hub/group/project to use. - * QISKIT_IBM_RUNTIME_PRIVATE_HGP: hub/group/project to use for private jobs. - * QISKIT_IBM_RUNTIME_DEVICE: default device to use. - * QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS: True if use staging credentials. - * QISKIT_IBM_RUNTIME_STAGING_API_TOKEN: staging API token to use. - * QISKIT_IBM_RUNTIME_STAGING_API_URL: staging API url to use. - * QISKIT_IBM_RUNTIME_STAGING_HGP: staging hub/group/project to use. - * QISKIT_IBM_RUNTIME_STAGING_DEVICE: staging device to use. - * QISKIT_IBM_RUNTIME_STAGING_PRIVATE_HGP: staging hub/group/project to use for private jobs. -""" - -import os -from functools import wraps -from unittest import SkipTest -from typing import Tuple, Optional - -from qiskit.test.testing_options import get_test_options -from qiskit_ibm_runtime import least_busy -from qiskit_ibm_runtime import IBMRuntimeService -from qiskit_ibm_runtime.credentials import Credentials, discover_credentials -from qiskit_ibm_runtime.hub_group_project import HubGroupProject - - -def requires_qe_access(func): - """Decorator that signals that the test uses the online API. - - It involves: - * determines if the test should be skipped by checking environment - variables. - * if the `QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS` environment variable is - set, it reads the credentials from an alternative set of environment - variables. - * if the test is not skipped, it reads `qe_token` and `qe_url` from - environment variables or qiskitrc. - * if the test is not skipped, it appends `qe_token` and `qe_url` as - arguments to the test function. - - Args: - func (callable): test function to be decorated. - - Returns: - callable: the decorated function. - """ - - @wraps(func) - def _wrapper(obj, *args, **kwargs): - if get_test_options()["skip_online"]: - raise SkipTest("Skipping online tests") - credentials = _get_credentials() - kwargs.update({"qe_token": credentials.token, "qe_url": credentials.url}) - return func(obj, *args, **kwargs) - - return _wrapper - - -def requires_providers(func): - """Decorator that signals the test uses the online API, via a public and premium hgp. - - This decorator delegates into the `requires_qe_access` decorator and appends a provider, - an open access hub/group/project and a premium hub/group/project to the decorated function. - - Args: - func (callable): Test function to be decorated. - - Returns: - callable: The decorated function. - """ - - @wraps(func) - @requires_qe_access - def _wrapper(*args, **kwargs): - qe_token = kwargs.pop("qe_token") - qe_url = kwargs.pop("qe_url") - service = IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - # Get open access hgp - open_hgp = _get_open_hgp(service) - if not open_hgp: - raise SkipTest("Requires open access hub/group/project.") - # Get a premium hgp - premium_hub, premium_group, premium_project = _get_custom_hgp() - if not all([premium_hub, premium_group, premium_project]): - raise SkipTest( - "Requires both the open access and premium hub/group/project." - ) - kwargs.update( - { - "service": service, - "hgps": { - "open_hgp": { - "hub": open_hgp.credentials.hub, - "group": open_hgp.credentials.group, - "project": open_hgp.credentials.project, - }, - "premium_hgp": { - "hub": premium_hub, - "group": premium_group, - "project": premium_project, - }, - }, - } - ) - return func(*args, **kwargs) - - return _wrapper - - -def requires_provider(func): - """Decorator that signals the test uses the online API, via a custom hub/group/project. - - This decorator delegates into the `requires_qe_access` decorator, but - instead of the credentials it appends a `provider` argument to the decorated - function. It also appends the custom `hub`, `group` and `project` arguments. - - Args: - func (callable): test function to be decorated. - - Returns: - callable: the decorated function. - """ - - @wraps(func) - @requires_qe_access - def _wrapper(*args, **kwargs): - token = kwargs.pop("qe_token") - url = kwargs.pop("qe_url") - service = IBMRuntimeService(auth="legacy", token=token, url=url) - hub, group, project = _get_custom_hgp() - kwargs.update( - {"service": service, "hub": hub, "group": group, "project": project} - ) - return func(*args, **kwargs) - - return _wrapper - - -def requires_private_provider(func): - """Decorator that signals the test requires a hub/group/project for private jobs. - - This decorator appends `provider`, `hub`, `group` and `project` arguments to the decorated - function. - - Args: - func (callable): test function to be decorated. - - Returns: - callable: the decorated function. - """ - - @wraps(func) - @requires_qe_access - def _wrapper(*args, **kwargs): - token = kwargs.pop("qe_token") - url = kwargs.pop("qe_url") - service = IBMRuntimeService(auth="legacy", token=token, url=url) - hub, group, project = _get_private_hgp() - kwargs.update( - {"service": service, "hub": hub, "group": group, "project": project} - ) - return func(*args, **kwargs) - - return _wrapper - - -def requires_device(func): - """Decorator that retrieves the appropriate backend to use for testing. - - It involves: - * Enable the account using credentials obtained from the - `requires_qe_access` decorator. - * Use the backend specified by `QISKIT_IBM_RUNTIME_STAGING_DEVICE` if - `QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS` is set, otherwise use the backend - specified by `QISKIT_IBM_RUNTIME_DEVICE`. - * if device environment variable is not set, use the least busy - real backend. - * appends arguments `backend` to the decorated function. - - Args: - func (callable): test function to be decorated. - - Returns: - callable: the decorated function. - """ - - @wraps(func) - @requires_qe_access - def _wrapper(obj, *args, **kwargs): - backend_name = ( - os.getenv("QISKIT_IBM_RUNTIME_STAGING_DEVICE", None) - if os.getenv("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", "") - else os.getenv("QISKIT_IBM_RUNTIME_DEVICE", None) - ) - _backend = _get_backend( - qe_token=kwargs.pop("qe_token"), - qe_url=kwargs.pop("qe_url"), - backend_name=backend_name, - ) - kwargs.update({"backend": _backend}) - return func(obj, *args, **kwargs) - - return _wrapper - - -def requires_runtime_device(func): - """Decorator that retrieves the appropriate backend to use for testing. - - Args: - func (callable): test function to be decorated. - - Returns: - callable: the decorated function. - """ - - @wraps(func) - @requires_qe_access - def _wrapper(obj, *args, **kwargs): - backend_name = ( - os.getenv("QISKIT_IBM_RUNTIME_STAGING_DEVICE", None) - if os.getenv("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", "") - else os.getenv("QISKIT_IBM_RUNTIME_DEVICE", None) - ) - if not backend_name: - raise SkipTest("Runtime device not specified") - _backend = _get_backend( - qe_token=kwargs.pop("qe_token"), - qe_url=kwargs.pop("qe_url"), - backend_name=backend_name, - ) - kwargs.update({"backend": _backend}) - return func(obj, *args, **kwargs) - - return _wrapper - - -def _get_backend(qe_token, qe_url, backend_name): - """Get the specified backend.""" - service = IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - _backend = None - hub, group, project = _get_custom_hgp() - if backend_name: - _backend = service.get_backend( - name=backend_name, hub=hub, group=group, project=project - ) - else: - _backend = least_busy( - service.backends( - simulator=False, min_num_qubits=5, hub=hub, group=group, project=project - ) - ) - if not _backend: - raise Exception("Unable to find a suitable backend.") - return _backend - - -def _get_credentials(): - """Finds the credentials for a specific test and options. - - Returns: - Credentials: set of credentials - - Raises: - Exception: When the credential could not be set and they are needed - for that set of options. - """ - if os.getenv("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", ""): - # Special case: instead of using the standard credentials mechanism, - # load them from different environment variables. This assumes they - # will always be in place, as is used by the CI setup. - return Credentials( - token=os.getenv("QISKIT_IBM_RUNTIME_STAGING_API_TOKEN"), - url=os.getenv("QISKIT_IBM_RUNTIME_STAGING_API_URL"), - auth_url=os.getenv("QISKIT_IBM_RUNTIME_STAGING_API_URL"), - ) - # Attempt to read the standard credentials. - discovered_credentials, _ = discover_credentials() - if discovered_credentials: - # Decide which credentials to use for testing. - if len(discovered_credentials) > 1: - try: - # Attempt to use IBM Quantum credentials. - return discovered_credentials[(None, None, None)] - except KeyError: - pass - # Use the first available credentials. - return list(discovered_credentials.values())[0] - raise Exception("Unable to locate valid credentials.") - - -def _get_open_hgp(service: IBMRuntimeService) -> Optional[HubGroupProject]: - """Get open hub/group/project - - Returns: - Open hub/group/project or ``None``. - """ - hgps = service._get_hgps() - for hgp in hgps: - if hgp.is_open: - return hgp - return None - - -def _get_custom_hgp() -> Tuple[str, str, str]: - """Get a custom hub/group/project - - Gets the hub/group/project set in QISKIT_IBM_RUNTIME_STAGING_HGP for staging env or - QISKIT_IBM_RUNTIME_HGP for production env. - - Returns: - Tuple of custom hub/group/project or ``None`` if not set. - """ - hub = None - group = None - project = None - hgp = ( - os.getenv("QISKIT_IBM_RUNTIME_STAGING_HGP", None) - if os.getenv("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", "") - else os.getenv("QISKIT_IBM_RUNTIME_HGP", None) - ) - if hgp: - hub, group, project = hgp.split("/") - return hub, group, project - - -def _get_private_hgp() -> Tuple[str, str, str]: - """Get a private hub/group/project - - Gets the hub/group/project set in QISKIT_IBM_RUNTIME_STAGING_PRIVATE_HGP for staging env or - QISKIT_IBM_RUNTIME_PRIVATE_HGP for production env. - - Returns: - Tuple of custom hub/group/project or ``None`` if not set. - - Raises: - SkipTest: requires private provider - """ - hub = None - group = None - project = None - hgp = ( - os.getenv("QISKIT_IBM_RUNTIME_STAGING_PRIVATE_HGP", None) - if os.getenv("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", "") - else os.getenv("QISKIT_IBM_RUNTIME_PRIVATE_HGP", None) - ) - if not hgp: - raise SkipTest("Requires private provider.") - hub, group, project = hgp.split("/") - return hub, group, project diff --git a/test/ibm/runtime/test_runtime.py b/test/ibm/runtime/test_runtime.py deleted file mode 100644 index e135d60441..0000000000 --- a/test/ibm/runtime/test_runtime.py +++ /dev/null @@ -1,907 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for runtime service.""" - -import copy -import json -import os -from io import StringIO -from unittest.mock import patch -from unittest import mock, skipIf -import uuid -import time -import random -import subprocess -import tempfile -import warnings -from datetime import datetime -import numpy as np -import scipy.sparse - -from qiskit.algorithms.optimizers import ( - ADAM, - GSLS, - IMFIL, - SPSA, - QNSPSA, - SNOBFIT, - L_BFGS_B, - NELDER_MEAD, -) -from qiskit.result import Result -from qiskit.circuit import Parameter, QuantumCircuit -from qiskit.test.reference_circuits import ReferenceCircuits -from qiskit.circuit.library import EfficientSU2 -from qiskit.opflow import ( - PauliSumOp, - MatrixOp, - PauliOp, - CircuitOp, - EvolvedOp, - TaperedPauliSumOp, - Z2Symmetries, - I, - X, - Y, - Z, - StateFn, - CircuitStateFn, - DictStateFn, - VectorStateFn, - OperatorStateFn, - SparseVectorStateFn, - CVaRMeasurement, - ComposedOp, - SummedOp, - TensoredOp, -) -from qiskit.quantum_info import SparsePauliOp, Pauli, PauliTable, Statevector -from qiskit.providers.jobstatus import JobStatus - -from qiskit_ibm_runtime.exceptions import IBMInputValueError -from qiskit_ibm_runtime import IBMRuntimeService, RuntimeJob, IBMBackend -from qiskit_ibm_runtime.credentials import Credentials -from qiskit_ibm_runtime.hub_group_project import HubGroupProject -from qiskit_ibm_runtime.utils import RuntimeEncoder, RuntimeDecoder -from qiskit_ibm_runtime.constants import API_TO_JOB_ERROR_MESSAGE -from qiskit_ibm_runtime.exceptions import RuntimeProgramNotFound, RuntimeJobFailureError -from qiskit_ibm_runtime.runtime_program import ParameterNamespace - -from ...ibm_test_case import IBMTestCase -from .fake_runtime_client import ( - BaseFakeRuntimeClient, - FailedRanTooLongRuntimeJob, - FailedRuntimeJob, - CancelableRuntimeJob, - CustomResultRuntimeJob, -) -from .utils import SerializableClass, SerializableClassDecoder, get_complex_types -from ...contextmanagers import mock_ibm_provider - - -class TestRuntime(IBMTestCase): - """Class for testing runtime modules.""" - - DEFAULT_DATA = "def main() {}" - DEFAULT_METADATA = { - "name": "qiskit-test", - "description": "Test program.", - "max_execution_time": 300, - "spec": { - "backend_requirements": {"min_num_qubits": 5}, - "parameters": { - "properties": { - "param1": { - "description": "Desc 1", - "type": "string", - "enum": ["a", "b", "c"], - }, - "param2": {"description": "Desc 2", "type": "integer", "min": 0}, - }, - "required": ["param1"], - }, - "return_values": { - "type": "object", - "description": "Return values", - "properties": { - "ret_val": {"description": "Some return value.", "type": "string"} - }, - }, - "interim_results": { - "properties": { - "int_res": {"description": "Some interim result", "type": "string"} - } - }, - }, - } - - def setUp(self): - """Initial test setup.""" - super().setUp() - with mock_ibm_provider(): - self.service = IBMRuntimeService(auth="legacy", token="abc") - self.service._programs = {} - self.service._default_hgp = mock.MagicMock(spec=HubGroupProject) - self.service._default_hgp.credentials = Credentials( - token="", url="", services={"runtime": "https://quantum-computing.ibm.com"} - ) - - def get_backend(backend_name, hub=None, group=None, project=None): - # pylint: disable=unused-argument - return mock.MagicMock(spec=IBMBackend) - - self.service.get_backend = get_backend - self.service._api_client = BaseFakeRuntimeClient() - - def test_coder(self): - """Test runtime encoder and decoder.""" - result = Result( - backend_name="ibmqx2", - backend_version="1.1", - qobj_id="12345", - job_id="67890", - success=False, - results=[], - ) - - data = { - "string": "foo", - "float": 1.5, - "complex": 2 + 3j, - "array": np.array([[1, 2, 3], [4, 5, 6]]), - "result": result, - "sclass": SerializableClass("foo"), - } - encoded = json.dumps(data, cls=RuntimeEncoder) - decoded = json.loads(encoded, cls=RuntimeDecoder) - decoded["sclass"] = SerializableClass.from_json(decoded["sclass"]) - - decoded_result = decoded.pop("result") - data.pop("result") - - decoded_array = decoded.pop("array") - orig_array = data.pop("array") - - self.assertEqual(decoded, data) - self.assertIsInstance(decoded_result, Result) - self.assertTrue((decoded_array == orig_array).all()) - - def test_coder_qc(self): - """Test runtime encoder and decoder for circuits.""" - bell = ReferenceCircuits.bell() - unbound = EfficientSU2(num_qubits=4, reps=1, entanglement="linear") - subtests = (bell, unbound, [bell, unbound]) - for circ in subtests: - with self.subTest(circ=circ): - encoded = json.dumps(circ, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - if not isinstance(circ, list): - decoded = [decoded] - self.assertTrue( - all(isinstance(item, QuantumCircuit) for item in decoded) - ) - - def test_coder_operators(self): - """Test runtime encoder and decoder for operators.""" - x = Parameter("x") - y = x + 1 - qc = QuantumCircuit(1) - qc.h(0) - coeffs = np.array([1, 2, 3, 4, 5, 6]) - table = PauliTable.from_labels(["III", "IXI", "IYY", "YIZ", "XYZ", "III"]) - op = 2.0 * I ^ I - z2_symmetries = Z2Symmetries( - [Pauli("IIZI"), Pauli("ZIII")], - [Pauli("IIXI"), Pauli("XIII")], - [1, 3], - [-1, 1], - ) - isqrt2 = 1 / np.sqrt(2) - sparse = scipy.sparse.csr_matrix([[0, isqrt2, 0, isqrt2]]) - - subtests = ( - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1]), coeff=y), - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1 + 2j]), coeff=3 - 2j), - PauliSumOp.from_list( - [("II", -1.052373245772859), ("IZ", 0.39793742484318045)] - ), - PauliSumOp(SparsePauliOp(table, coeffs), coeff=10), - MatrixOp(primitive=np.array([[0, -1j], [1j, 0]]), coeff=x), - PauliOp(primitive=Pauli("Y"), coeff=x), - CircuitOp(qc, coeff=x), - EvolvedOp(op, coeff=x), - TaperedPauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), z2_symmetries), - StateFn(qc, coeff=x), - CircuitStateFn(qc, is_measurement=True), - DictStateFn("1" * 3, is_measurement=True), - VectorStateFn(np.ones(2 ** 3, dtype=complex)), - OperatorStateFn(CircuitOp(QuantumCircuit(1))), - SparseVectorStateFn(sparse), - Statevector([1, 0]), - CVaRMeasurement(Z, 0.2), - ComposedOp([(X ^ Y ^ Z), (Z ^ X ^ Y ^ Z).to_matrix_op()]), - SummedOp([X ^ X * 2, Y ^ Y], 2), - TensoredOp([(X ^ Y), (Z ^ I)]), - (Z ^ Z) ^ (I ^ 2), - ) - for op in subtests: - with self.subTest(op=op): - encoded = json.dumps(op, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertEqual(op, decoded) - - @skipIf(os.name == "nt", "Test not supported on Windows") - def test_coder_optimizers(self): - """Test runtime encoder and decoder for optimizers.""" - subtests = ( - (ADAM, {"maxiter": 100, "amsgrad": True}), - (GSLS, {"maxiter": 50, "min_step_size": 0.01}), - (IMFIL, {"maxiter": 20}), - (SPSA, {"maxiter": 10, "learning_rate": 0.01, "perturbation": 0.1}), - (SNOBFIT, {"maxiter": 200, "maxfail": 20}), - (QNSPSA, {"fidelity": 123, "maxiter": 25, "resamplings": {1: 100, 2: 50}}), - # some SciPy optimizers only work with default arguments due to Qiskit/qiskit-terra#6682 - (L_BFGS_B, {}), - (NELDER_MEAD, {}), - ) - for opt_cls, settings in subtests: - with self.subTest(opt_cls=opt_cls): - optimizer = opt_cls(**settings) - encoded = json.dumps(optimizer, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertTrue(isinstance(decoded, opt_cls)) - for key, value in settings.items(): - self.assertEqual(decoded.settings[key], value) - - def test_encoder_datetime(self): - """Test encoding a datetime.""" - subtests = ( - {"datetime": datetime.now()}, - {"datetime": datetime(2021, 8, 4)}, - {"datetime": datetime.fromtimestamp(1326244364)}, - ) - for obj in subtests: - encoded = json.dumps(obj, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertEqual(decoded, obj) - - def test_encoder_callable(self): - """Test encoding a callable.""" - with warnings.catch_warnings(record=True) as warn_cm: - encoded = json.dumps({"fidelity": lambda x: x}, cls=RuntimeEncoder) - decoded = json.loads(encoded, cls=RuntimeDecoder) - self.assertIsNone(decoded["fidelity"]) - self.assertEqual(len(warn_cm), 1) - - def test_decoder_import(self): - """Test runtime decoder importing modules.""" - script = """ -import sys -import json -from qiskit_ibm_runtime import RuntimeDecoder -if __name__ == '__main__': - obj = json.loads(sys.argv[1], cls=RuntimeDecoder) - print(obj.__class__.__name__) -""" - temp_fp = tempfile.NamedTemporaryFile(mode="w", delete=False) - self.addCleanup(os.remove, temp_fp.name) - temp_fp.write(script) - temp_fp.close() - - subtests = ( - PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), - DictStateFn("1" * 3, is_measurement=True), - Statevector([1, 0]), - ) - for op in subtests: - with self.subTest(op=op): - encoded = json.dumps(op, cls=RuntimeEncoder) - self.assertIsInstance(encoded, str) - cmd = ["python", temp_fp.name, encoded] - proc = subprocess.run( - cmd, - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - universal_newlines=True, - check=True, - ) - self.assertIn(op.__class__.__name__, proc.stdout) - - def test_list_programs(self): - """Test listing programs.""" - program_id = self._upload_program() - programs = self.service.programs() - all_ids = [prog.program_id for prog in programs] - self.assertIn(program_id, all_ids) - - def test_list_programs_with_limit_skip(self): - """Test listing programs with limit and skip.""" - program_1 = self._upload_program() - program_2 = self._upload_program() - program_3 = self._upload_program() - programs = self.service.programs(limit=2, skip=1) - all_ids = [prog.program_id for prog in programs] - self.assertNotIn(program_1, all_ids) - self.assertIn(program_2, all_ids) - self.assertIn(program_3, all_ids) - programs = self.service.programs(limit=3) - all_ids = [prog.program_id for prog in programs] - self.assertIn(program_1, all_ids) - - def test_list_program(self): - """Test listing a single program.""" - program_id = self._upload_program() - program = self.service.program(program_id) - self.assertEqual(program_id, program.program_id) - - def test_print_programs(self): - """Test printing programs.""" - ids = [] - for idx in range(3): - ids.append(self._upload_program(name=f"name_{idx}")) - - programs = self.service.programs() - with patch("sys.stdout", new=StringIO()) as mock_stdout: - self.service.pprint_programs() - stdout = mock_stdout.getvalue() - for prog in programs: - self.assertIn(prog.program_id, stdout) - self.assertIn(prog.name, stdout) - self.assertNotIn(str(prog.max_execution_time), stdout) - self.service.pprint_programs(detailed=True) - stdout_detailed = mock_stdout.getvalue() - for prog in programs: - self.assertIn(prog.program_id, stdout_detailed) - self.assertIn(prog.name, stdout_detailed) - self.assertIn(str(prog.max_execution_time), stdout_detailed) - - def test_upload_program(self): - """Test uploading a program.""" - max_execution_time = 3000 - is_public = True - program_id = self._upload_program( - max_execution_time=max_execution_time, is_public=is_public - ) - self.assertTrue(program_id) - program = self.service.program(program_id) - self.assertTrue(program) - self.assertEqual(max_execution_time, program.max_execution_time) - self.assertEqual(program.is_public, is_public) - - def test_update_program(self): - """Test updating program.""" - new_data = "def main() {foo=bar}" - new_metadata = copy.deepcopy(self.DEFAULT_METADATA) - new_metadata["name"] = "test_update_program" - new_name = "name2" - new_description = "some other description" - new_cost = self.DEFAULT_METADATA["max_execution_time"] + 100 - new_spec = copy.deepcopy(self.DEFAULT_METADATA["spec"]) - new_spec["backend_requirements"] = {"input_allowed": "runtime"} - - sub_tests = [ - {"data": new_data}, - {"metadata": new_metadata}, - {"data": new_data, "metadata": new_metadata}, - {"metadata": new_metadata, "name": new_name}, - { - "data": new_data, - "metadata": new_metadata, - "description": new_description, - }, - {"max_execution_time": new_cost, "spec": new_spec}, - ] - - for new_vals in sub_tests: - with self.subTest(new_vals=new_vals.keys()): - program_id = self._upload_program() - self.service.update_program(program_id=program_id, **new_vals) - updated = self.service.program(program_id, refresh=True) - if "data" in new_vals: - raw_program = self.service._api_client.program_get(program_id) - self.assertEqual(new_data, raw_program["data"]) - if "metadata" in new_vals and "name" not in new_vals: - self.assertEqual(new_metadata["name"], updated.name) - if "name" in new_vals: - self.assertEqual(new_name, updated.name) - if "description" in new_vals: - self.assertEqual(new_description, updated.description) - if "max_execution_time" in new_vals: - self.assertEqual(new_cost, updated.max_execution_time) - if "spec" in new_vals: - raw_program = self.service._api_client.program_get(program_id) - self.assertEqual(new_spec, raw_program["spec"]) - - def test_update_program_no_new_fields(self): - """Test updating a program without any new data.""" - program_id = self._upload_program() - with warnings.catch_warnings(record=True) as warn_cm: - self.service.update_program(program_id=program_id) - self.assertEqual(len(warn_cm), 1) - - def test_delete_program(self): - """Test deleting program.""" - program_id = self._upload_program() - self.service.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.service.program(program_id, refresh=True) - - def test_double_delete_program(self): - """Test deleting a deleted program.""" - program_id = self._upload_program() - self.service.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.service.delete_program(program_id) - - def test_run_program(self): - """Test running program.""" - params = {"param1": "foo"} - job = self._run_program(inputs=params) - self.assertTrue(job.job_id) - self.assertIsInstance(job, RuntimeJob) - self.assertIsInstance(job.status(), JobStatus) - self.assertEqual(job.inputs, params) - job.wait_for_final_state() - self.assertEqual(job.status(), JobStatus.DONE) - self.assertTrue(job.result()) - - def test_run_program_with_custom_runtime_image(self): - """Test running program.""" - params = {"param1": "foo"} - image = "name:tag" - job = self._run_program(inputs=params, image=image) - self.assertTrue(job.job_id) - self.assertIsInstance(job, RuntimeJob) - self.assertIsInstance(job.status(), JobStatus) - self.assertEqual(job.inputs, params) - job.wait_for_final_state() - self.assertEqual(job.status(), JobStatus.DONE) - self.assertTrue(job.result()) - self.assertEqual(job.image, image) - - def test_retrieve_program_data(self): - """Test retrieving program data""" - program_id = self._upload_program(name="qiskit-test") - self.service.programs() - program = self.service.program(program_id) - self.assertEqual(program.data, self.DEFAULT_DATA) - self._validate_program(program) - - def test_program_params_validation(self): - """Test program parameters validation process""" - program_id = self.service.upload_program( - data=self.DEFAULT_DATA, metadata=self.DEFAULT_METADATA - ) - program = self.service.program(program_id) - params: ParameterNamespace = program.parameters() - params.param1 = "Hello, World" - # Check OK params - params.validate() - # Check OK params - contains unnecessary param - params.param3 = "Hello, World" - params.validate() - # Check bad params - missing required param - params.param1 = None - with self.assertRaises(IBMInputValueError): - params.validate() - params.param1 = "foo" - - def test_program_params_namespace(self): - """Test running a program using parameter namespace.""" - program_id = self.service.upload_program( - data=self.DEFAULT_DATA, metadata=self.DEFAULT_METADATA - ) - params = self.service.program(program_id).parameters() - params.param1 = "Hello World" - self._run_program(program_id, inputs=params) - - def test_run_program_failed(self): - """Test a failed program execution.""" - job = self._run_program(job_classes=FailedRuntimeJob) - job.wait_for_final_state() - job_result_raw = self.service._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertEqual( - API_TO_JOB_ERROR_MESSAGE["FAILED"].format(job.job_id, job_result_raw), - job.error_message(), - ) - with self.assertRaises(RuntimeJobFailureError): - job.result() - - def test_run_program_failed_ran_too_long(self): - """Test a program that failed since it ran longer than maxiumum execution time.""" - job = self._run_program(job_classes=FailedRanTooLongRuntimeJob) - job.wait_for_final_state() - job_result_raw = self.service._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertEqual( - API_TO_JOB_ERROR_MESSAGE["CANCELLED - RAN TOO LONG"].format( - job.job_id, job_result_raw - ), - job.error_message(), - ) - with self.assertRaises(RuntimeJobFailureError): - job.result() - - def test_retrieve_job(self): - """Test retrieving a job.""" - program_id = self._upload_program() - params = {"param1": "foo"} - job = self._run_program(program_id, inputs=params) - rjob = self.service.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(program_id, rjob.program_id) - - def test_jobs_no_limit(self): - """Test retrieving jobs without limit.""" - jobs = [] - program_id = self._upload_program() - for _ in range(25): - jobs.append(self._run_program(program_id)) - rjobs = self.service.jobs(limit=None) - self.assertEqual(25, len(rjobs)) - - def test_jobs_limit(self): - """Test retrieving jobs with limit.""" - jobs = [] - job_count = 25 - program_id = self._upload_program() - for _ in range(job_count): - jobs.append(self._run_program(program_id)) - - limits = [21, 30] - for limit in limits: - with self.subTest(limit=limit): - rjobs = self.service.jobs(limit=limit) - self.assertEqual(min(limit, job_count), len(rjobs)) - - def test_jobs_skip(self): - """Test retrieving jobs with skip.""" - jobs = [] - program_id = self._upload_program() - for _ in range(5): - jobs.append(self._run_program(program_id)) - rjobs = self.service.jobs(skip=4) - self.assertEqual(1, len(rjobs)) - - def test_jobs_skip_limit(self): - """Test retrieving jobs with skip and limit.""" - jobs = [] - program_id = self._upload_program() - for _ in range(10): - jobs.append(self._run_program(program_id)) - rjobs = self.service.jobs(skip=4, limit=2) - self.assertEqual(2, len(rjobs)) - - def test_jobs_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING).""" - jobs = [] - program_id = self._upload_program() - (jobs, pending_jobs_count, _) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - rjobs = self.service.jobs(pending=True) - self.assertEqual(pending_jobs_count, len(rjobs)) - - def test_jobs_limit_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING) with limit.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - limit = 4 - rjobs = self.service.jobs(limit=limit, pending=True) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_skip_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING) with skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, pending_jobs_count, _) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - skip = 4 - rjobs = self.service.jobs(skip=skip, pending=True) - self.assertEqual(pending_jobs_count - skip, len(rjobs)) - - def test_jobs_limit_skip_pending(self): - """Test retrieving pending jobs (QUEUED, RUNNING) with limit and skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - limit = 2 - skip = 3 - rjobs = self.service.jobs(limit=limit, skip=skip, pending=True) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED).""" - jobs = [] - program_id = self._upload_program() - (jobs, _, returned_jobs_count) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - rjobs = self.service.jobs(pending=False) - self.assertEqual(returned_jobs_count, len(rjobs)) - - def test_jobs_limit_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with limit.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - limit = 6 - rjobs = self.service.jobs(limit=limit, pending=False) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_skip_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, _, returned_jobs_count) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - skip = 4 - rjobs = self.service.jobs(skip=skip, pending=False) - self.assertEqual(returned_jobs_count - skip, len(rjobs)) - - def test_jobs_limit_skip_returned(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with limit and skip.""" - jobs = [] - program_id = self._upload_program() - (jobs, *_) = self._populate_jobs_with_all_statuses( - jobs=jobs, program_id=program_id - ) - limit = 6 - skip = 2 - rjobs = self.service.jobs(limit=limit, skip=skip, pending=False) - self.assertEqual(limit, len(rjobs)) - - def test_jobs_filter_by_program_id(self): - """Test retrieving jobs by Program ID.""" - program_id = self._upload_program() - program_id_1 = self._upload_program() - job = self._run_program(program_id=program_id) - job_1 = self._run_program(program_id=program_id_1) - job.wait_for_final_state() - job_1.wait_for_final_state() - rjobs = self.service.jobs(program_id=program_id) - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - - def test_jobs_filter_by_provider(self): - """Test retrieving jobs by provider.""" - program_id = self._upload_program() - job = self._run_program( - program_id=program_id, - hub="defaultHub", - group="defaultGroup", - project="defaultProject", - ) - job.wait_for_final_state() - rjobs = self.service.jobs( - program_id=program_id, - hub="defaultHub", - group="defaultGroup", - project="defaultProject", - ) - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - rjobs = self.service.jobs( - program_id=program_id, hub="test", group="test", project="test" - ) - self.assertFalse(rjobs) - with self.assertRaises(IBMInputValueError): - self.service.jobs(hub="defaultHub") - - def test_cancel_job(self): - """Test canceling a job.""" - job = self._run_program(job_classes=CancelableRuntimeJob) - time.sleep(1) - job.cancel() - self.assertEqual(job.status(), JobStatus.CANCELLED) - rjob = self.service.job(job.job_id) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) - - def test_final_result(self): - """Test getting final result.""" - job = self._run_program() - result = job.result() - self.assertTrue(result) - - def test_interim_results(self): - """Test getting interim results.""" - job = self._run_program() - interim_results = job.interim_results() - self.assertTrue(interim_results) - - def test_job_status(self): - """Test job status.""" - job = self._run_program() - time.sleep(random.randint(1, 5)) - self.assertTrue(job.status()) - - def test_job_inputs(self): - """Test job inputs.""" - inputs = {"param1": "foo", "param2": "bar"} - job = self._run_program(inputs=inputs) - self.assertEqual(inputs, job.inputs) - - def test_job_program_id(self): - """Test job program ID.""" - program_id = self._upload_program() - job = self._run_program(program_id=program_id) - self.assertEqual(program_id, job.program_id) - - def test_wait_for_final_state(self): - """Test wait for final state.""" - job = self._run_program() - job.wait_for_final_state() - self.assertEqual(JobStatus.DONE, job.status()) - - def test_result_decoder(self): - """Test result decoder.""" - custom_result = get_complex_types() - job_cls = CustomResultRuntimeJob - job_cls.custom_result = custom_result - - sub_tests = [(SerializableClassDecoder, None), (None, SerializableClassDecoder)] - for result_decoder, decoder in sub_tests: - with self.subTest(decoder=decoder): - job = self._run_program(job_classes=job_cls, decoder=result_decoder) - result = job.result(decoder=decoder) - self.assertIsInstance(result["serializable_class"], SerializableClass) - - def test_get_result_twice(self): - """Test getting results multiple times.""" - custom_result = get_complex_types() - job_cls = CustomResultRuntimeJob - job_cls.custom_result = custom_result - - job = self._run_program(job_classes=job_cls) - _ = job.result() - _ = job.result() - - def test_program_metadata(self): - """Test program metadata.""" - file_name = "test_metadata.json" - with open(file_name, "w") as file: - json.dump(self.DEFAULT_METADATA, file) - self.addCleanup(os.remove, file_name) - - sub_tests = [file_name, self.DEFAULT_METADATA] - - for metadata in sub_tests: - with self.subTest(metadata_type=type(metadata)): - program_id = self.service.upload_program( - data=self.DEFAULT_DATA, metadata=metadata - ) - program = self.service.program(program_id) - self.service.delete_program(program_id) - self._validate_program(program) - - def test_different_providers(self): - """Test retrieving job submitted with different provider.""" - program_id = self._upload_program() - job = self._run_program(program_id) - cred = Credentials( - token="", - url="", - hub="hub2", - group="group2", - project="project2", - services={"runtime": "https://quantum-computing.ibm.com"}, - ) - self.service._default_hgp.credentials = cred - rjob = self.service.job(job.job_id) - self.assertIsNotNone(rjob.backend) - - def _upload_program( - self, name=None, max_execution_time=300, is_public: bool = False - ): - """Upload a new program.""" - name = name or uuid.uuid4().hex - data = self.DEFAULT_DATA - metadata = copy.deepcopy(self.DEFAULT_METADATA) - metadata.update(name=name) - metadata.update(is_public=is_public) - metadata.update(max_execution_time=max_execution_time) - program_id = self.service.upload_program(data=data, metadata=metadata) - return program_id - - def _run_program( - self, - program_id=None, - inputs=None, - job_classes=None, - final_status=None, - decoder=None, - image="", - hub=None, - group=None, - project=None, - ): - """Run a program.""" - options = {"backend_name": "some_backend"} - if final_status is not None: - self.service._api_client.set_final_status(final_status) - elif job_classes: - self.service._api_client.set_job_classes(job_classes) - elif all([hub, group, project]): - self.service._api_client.set_hgp(hub, group, project) - if program_id is None: - program_id = self._upload_program() - with patch( - "qiskit_ibm_runtime.ibm_runtime_service.RuntimeClient", - return_value=self.service._api_client, - ): - job = self.service.run( - program_id=program_id, - options=options, - inputs=inputs, - result_decoder=decoder, - image=image, - ) - return job - - def _populate_jobs_with_all_statuses(self, jobs, program_id): - pending_jobs_count = 0 - returned_jobs_count = 0 - for _ in range(3): - jobs.append(self._run_program(program_id, final_status="RUNNING")) - pending_jobs_count += 1 - for _ in range(4): - jobs.append(self._run_program(program_id, final_status="COMPLETED")) - returned_jobs_count += 1 - for _ in range(2): - jobs.append(self._run_program(program_id, final_status="QUEUED")) - pending_jobs_count += 1 - for _ in range(3): - jobs.append(self._run_program(program_id, final_status="FAILED")) - returned_jobs_count += 1 - for _ in range(2): - jobs.append(self._run_program(program_id, final_status="CANCELLED")) - returned_jobs_count += 1 - return (jobs, pending_jobs_count, returned_jobs_count) - - def _validate_program(self, program): - """Validate a program.""" - self.assertEqual(self.DEFAULT_METADATA["name"], program.name) - self.assertEqual(self.DEFAULT_METADATA["description"], program.description) - self.assertEqual( - self.DEFAULT_METADATA["max_execution_time"], program.max_execution_time - ) - self.assertTrue(program.creation_date) - self.assertTrue(program.update_date) - self.assertEqual( - self.DEFAULT_METADATA["spec"]["backend_requirements"], - program.backend_requirements, - ) - self.assertEqual( - self.DEFAULT_METADATA["spec"]["parameters"], program.parameters().metadata - ) - self.assertEqual( - self.DEFAULT_METADATA["spec"]["return_values"], program.return_values - ) - self.assertEqual( - self.DEFAULT_METADATA["spec"]["interim_results"], program.interim_results - ) diff --git a/test/ibm/runtime/test_runtime_integration.py b/test/ibm/runtime/test_runtime_integration.py deleted file mode 100644 index eb9ed6badd..0000000000 --- a/test/ibm/runtime/test_runtime_integration.py +++ /dev/null @@ -1,779 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for runtime service.""" - -import copy -import unittest -import os -import uuid -import time -import random -from contextlib import suppress -import tempfile - -from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES -from qiskit.test.reference_circuits import ReferenceCircuits - -from qiskit_ibm_runtime.constants import API_TO_JOB_ERROR_MESSAGE -from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError -from qiskit_ibm_runtime.runtime_program import RuntimeProgram -from qiskit_ibm_runtime.exceptions import ( - RuntimeDuplicateProgramError, - RuntimeProgramNotFound, - RuntimeJobFailureError, - RuntimeInvalidStateError, - RuntimeJobNotFound, -) - -from ...ibm_test_case import IBMTestCase -from ...decorators import requires_runtime_device -from ...proxy_server import MockProxyServer, use_proxies -from .utils import SerializableClass, SerializableClassDecoder, get_complex_types - - -class TestRuntimeIntegration(IBMTestCase): - """Integration tests for runtime modules.""" - - RUNTIME_PROGRAM = """ -import random -import time -import warnings - -from qiskit import transpile -from qiskit.circuit.random import random_circuit - -def prepare_circuits(backend): - circuit = random_circuit(num_qubits=5, depth=4, measure=True, - seed=random.randint(0, 1000)) - return transpile(circuit, backend) - -def main(backend, user_messenger, **kwargs): - iterations = kwargs['iterations'] - sleep_per_iteration = kwargs.pop('sleep_per_iteration', 0) - interim_results = kwargs.pop('interim_results', {}) - final_result = kwargs.pop("final_result", {}) - for it in range(iterations): - time.sleep(sleep_per_iteration) - qc = prepare_circuits(backend) - user_messenger.publish({"iteration": it, "interim_results": interim_results}) - backend.run(qc).result() - - user_messenger.publish(final_result, final=True) - print("this is a stdout message") - warnings.warn("this is a stderr message") - """ - - RUNTIME_PROGRAM_METADATA = { - "max_execution_time": 600, - "description": "Qiskit test program", - } - PROGRAM_PREFIX = "qiskit-test" - - @classmethod - @requires_runtime_device - def setUpClass(cls, backend): - """Initial class level setup.""" - # pylint: disable=arguments-differ - super().setUpClass() - cls.backend = backend - cls.poll_time = 1 if backend.configuration().simulator else 5 - cls.service = backend.provider() - metadata = copy.deepcopy(cls.RUNTIME_PROGRAM_METADATA) - metadata["name"] = cls._get_program_name() - try: - cls.program_id = cls.service.upload_program( - data=cls.RUNTIME_PROGRAM, metadata=metadata - ) - except RuntimeDuplicateProgramError: - pass - except IBMNotAuthorizedError: - raise unittest.SkipTest("No upload access.") - - @classmethod - def tearDownClass(cls) -> None: - """Class level teardown.""" - super().tearDownClass() - with suppress(Exception): - cls.service.delete_program(cls.program_id) - - def setUp(self) -> None: - """Test level setup.""" - super().setUp() - self.to_delete = [] - self.to_cancel = [] - self.proxy_process = None - - def tearDown(self) -> None: - """Test level teardown.""" - super().tearDown() - # Delete programs - for prog in self.to_delete: - with suppress(Exception): - self.service.delete_program(prog) - - # Cancel and delete jobs. - for job in self.to_cancel: - with suppress(Exception): - job.cancel() - with suppress(Exception): - self.service.delete_job(job.job_id) - - def test_list_programs(self): - """Test listing programs.""" - programs = self.service.programs() - self.assertTrue(programs) - found = False - for prog in programs: - self._validate_program(prog) - if prog.program_id == self.program_id: - found = True - self.assertTrue(found, f"Program {self.program_id} not found!") - - def test_list_programs_with_limit_skip(self): - """Test listing programs with limit and skip.""" - self._upload_program() - self._upload_program() - self._upload_program() - programs = self.service.programs(limit=3, refresh=True) - all_ids = [prog.program_id for prog in programs] - self.assertEqual(len(all_ids), 3) - programs = self.service.programs(limit=2, skip=1) - some_ids = [prog.program_id for prog in programs] - self.assertEqual(len(some_ids), 2) - self.assertNotIn(all_ids[0], some_ids) - self.assertIn(all_ids[1], some_ids) - self.assertIn(all_ids[2], some_ids) - - def test_list_program(self): - """Test listing a single program.""" - program = self.service.program(self.program_id) - self.assertEqual(self.program_id, program.program_id) - self._validate_program(program) - - def test_retrieve_program_data(self): - """Test retrieving program data""" - program = self.service.program(self.program_id) - self.assertEqual(self.RUNTIME_PROGRAM, program.data) - self._validate_program(program) - - def test_retrieve_unauthorized_program_data(self): - """Test retrieving program data when user is not the program author""" - program = self.service.program("sample-program") - self._validate_program(program) - with self.assertRaises(IBMNotAuthorizedError): - return program.data - - def test_upload_program(self): - """Test uploading a program.""" - max_execution_time = 3000 - program_id = self._upload_program(max_execution_time=max_execution_time) - self.assertTrue(program_id) - program = self.service.program(program_id) - self.assertTrue(program) - self.assertEqual(max_execution_time, program.max_execution_time) - - def test_upload_program_file(self): - """Test uploading a program using a file.""" - temp_fp = tempfile.NamedTemporaryFile(mode="w", delete=False) - self.addCleanup(os.remove, temp_fp.name) - temp_fp.write(self.RUNTIME_PROGRAM) - temp_fp.close() - - program_id = self._upload_program(data=temp_fp.name) - self.assertTrue(program_id) - program = self.service.program(program_id) - self.assertTrue(program) - - @unittest.skipIf( - not os.environ.get("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", ""), - "Only runs on staging", - ) - def test_upload_public_program(self): - """Test uploading a public program.""" - max_execution_time = 3000 - is_public = True - program_id = self._upload_program( - max_execution_time=max_execution_time, is_public=is_public - ) - self.assertTrue(program_id) - program = self.service.program(program_id) - self.assertTrue(program) - self.assertEqual(max_execution_time, program.max_execution_time) - self.assertEqual(program.is_public, is_public) - - @unittest.skipIf( - not os.environ.get("QISKIT_IBM_RUNTIME_USE_STAGING_CREDENTIALS", ""), - "Only runs on staging", - ) - def test_set_visibility(self): - """Test setting the visibility of a program.""" - program_id = self._upload_program() - # Get the initial visibility - prog: RuntimeProgram = self.service.program(program_id) - start_vis = prog.is_public - # Flip the original value - self.service.set_program_visibility(program_id, not start_vis) - # Get the new visibility - prog: RuntimeProgram = self.service.program(program_id, refresh=True) - end_vis = prog.is_public - # Verify changed - self.assertNotEqual(start_vis, end_vis) - - def test_delete_program(self): - """Test deleting program.""" - program_id = self._upload_program() - self.service.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.service.program(program_id, refresh=True) - - def test_double_delete_program(self): - """Test deleting a deleted program.""" - program_id = self._upload_program() - self.service.delete_program(program_id) - with self.assertRaises(RuntimeProgramNotFound): - self.service.delete_program(program_id) - - def test_update_program_data(self): - """Test updating program data.""" - program_v1 = """ -def main(backend, user_messenger, **kwargs): - return "version 1" - """ - program_v2 = """ -def main(backend, user_messenger, **kwargs): - return "version 2" - """ - program_id = self._upload_program(data=program_v1) - self.assertEqual(program_v1, self.service.program(program_id).data) - self.service.update_program(program_id=program_id, data=program_v2) - self.assertEqual(program_v2, self.service.program(program_id).data) - - def test_update_program_metadata(self): - """Test updating program metadata.""" - program_id = self._upload_program() - original = self.service.program(program_id) - new_metadata = { - "name": self._get_program_name(), - "description": "test_update_program_metadata", - "max_execution_time": original.max_execution_time + 100, - "spec": { - "return_values": {"type": "object", "description": "Some return value"} - }, - } - self.service.update_program(program_id=program_id, metadata=new_metadata) - updated = self.service.program(program_id, refresh=True) - self.assertEqual(new_metadata["name"], updated.name) - self.assertEqual(new_metadata["description"], updated.description) - self.assertEqual(new_metadata["max_execution_time"], updated.max_execution_time) - self.assertEqual(new_metadata["spec"]["return_values"], updated.return_values) - - def test_run_program(self): - """Test running a program.""" - job = self._run_program(final_result="foo") - result = job.result() - self.assertEqual(JobStatus.DONE, job.status()) - self.assertEqual("foo", result) - - def test_run_program_failed(self): - """Test a failed program execution.""" - options = {"backend_name": self.backend.name()} - job = self.service.run(program_id=self.program_id, inputs={}, options=options) - self.log.info("Runtime job %s submitted.", job.job_id) - - job.wait_for_final_state() - job_result_raw = self.service._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertIn( - API_TO_JOB_ERROR_MESSAGE["FAILED"].format(job.job_id, job_result_raw), - job.error_message(), - ) - with self.assertRaises(RuntimeJobFailureError) as err_cm: - job.result() - self.assertIn("KeyError", str(err_cm.exception)) - - def test_run_program_failed_ran_too_long(self): - """Test a program that failed since it ran longer than maxiumum execution time.""" - max_execution_time = 60 - inputs = {"iterations": 1, "sleep_per_iteration": 60} - program_id = self._upload_program(max_execution_time=max_execution_time) - options = {"backend_name": self.backend.name()} - job = self.service.run(program_id=program_id, inputs=inputs, options=options) - self.log.info("Runtime job %s submitted.", job.job_id) - - job.wait_for_final_state() - job_result_raw = self.service._api_client.job_results(job.job_id) - self.assertEqual(JobStatus.ERROR, job.status()) - self.assertIn( - API_TO_JOB_ERROR_MESSAGE["CANCELLED - RAN TOO LONG"].format( - job.job_id, job_result_raw - ), - job.error_message(), - ) - with self.assertRaises(RuntimeJobFailureError): - job.result() - - def test_retrieve_job_queued(self): - """Test retrieving a queued job.""" - _ = self._run_program(iterations=10) - job = self._run_program(iterations=2) - self._wait_for_status(job, JobStatus.QUEUED) - rjob = self.service.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(self.program_id, rjob.program_id) - - def test_retrieve_job_running(self): - """Test retrieving a running job.""" - job = self._run_program(iterations=10) - self._wait_for_status(job, JobStatus.RUNNING) - rjob = self.service.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(self.program_id, job.program_id) - - def test_retrieve_job_done(self): - """Test retrieving a finished job.""" - job = self._run_program() - job.wait_for_final_state() - rjob = self.service.job(job.job_id) - self.assertEqual(job.job_id, rjob.job_id) - self.assertEqual(self.program_id, job.program_id) - - def test_retrieve_all_jobs(self): - """Test retrieving all jobs.""" - job = self._run_program() - rjobs = self.service.jobs() - found = False - for rjob in rjobs: - if rjob.job_id == job.job_id: - self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.inputs, rjob.inputs) - found = True - break - self.assertTrue(found, f"Job {job.job_id} not returned.") - - def test_retrieve_jobs_limit(self): - """Test retrieving jobs with limit.""" - jobs = [] - for _ in range(3): - jobs.append(self._run_program()) - - rjobs = self.service.jobs(limit=2) - self.assertEqual(len(rjobs), 2) - job_ids = {job.job_id for job in jobs} - rjob_ids = {rjob.job_id for rjob in rjobs} - self.assertTrue(rjob_ids.issubset(job_ids)) - - def test_retrieve_pending_jobs(self): - """Test retrieving pending jobs (QUEUED, RUNNING).""" - job = self._run_program(iterations=10) - self._wait_for_status(job, JobStatus.RUNNING) - rjobs = self.service.jobs(pending=True) - found = False - for rjob in rjobs: - if rjob.job_id == job.job_id: - self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.inputs, rjob.inputs) - found = True - break - self.assertTrue(found, f"Pending job {job.job_id} not retrieved.") - - def test_retrieve_returned_jobs(self): - """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED).""" - job = self._run_program() - job.wait_for_final_state() - rjobs = self.service.jobs(pending=False) - found = False - for rjob in rjobs: - if rjob.job_id == job.job_id: - self.assertEqual(job.program_id, rjob.program_id) - self.assertEqual(job.inputs, rjob.inputs) - found = True - break - self.assertTrue(found, f"Returned job {job.job_id} not retrieved.") - - def test_retrieve_jobs_by_program_id(self): - """Test retrieving jobs by Program ID.""" - program_id = self._upload_program() - job = self._run_program(program_id=program_id) - job.wait_for_final_state() - rjobs = self.service.jobs(program_id=program_id) - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - - def test_jobs_filter_by_provider(self): - """Test retrieving jobs by provider.""" - default_hgp = self.service._default_hgp - hub = default_hgp.credentials.hub - group = default_hgp.credentials.group - project = default_hgp.credentials.project - program_id = self._upload_program() - job = self._run_program(program_id=program_id) - job.wait_for_final_state() - rjobs = self.service.jobs( - program_id=program_id, hub=hub, group=group, project=project - ) - self.assertEqual(program_id, rjobs[0].program_id) - self.assertEqual(1, len(rjobs)) - rjobs = self.service.jobs( - program_id=program_id, hub="test", group="test", project="test" - ) - self.assertFalse(rjobs) - - def test_cancel_job_queued(self): - """Test canceling a queued job.""" - _ = self._run_program(iterations=10) - job = self._run_program(iterations=2) - self._wait_for_status(job, JobStatus.QUEUED) - job.cancel() - self.assertEqual(job.status(), JobStatus.CANCELLED) - time.sleep(10) # Wait a bit for DB to update. - rjob = self.service.job(job.job_id) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) - - def test_cancel_job_running(self): - """Test canceling a running job.""" - job = self._run_program(iterations=3) - self._wait_for_status(job, JobStatus.RUNNING) - job.cancel() - self.assertEqual(job.status(), JobStatus.CANCELLED) - time.sleep(10) # Wait a bit for DB to update. - rjob = self.service.job(job.job_id) - self.assertEqual(rjob.status(), JobStatus.CANCELLED) - - def test_cancel_job_done(self): - """Test canceling a finished job.""" - job = self._run_program() - job.wait_for_final_state() - with self.assertRaises(RuntimeInvalidStateError): - job.cancel() - - def test_delete_job(self): - """Test deleting a job.""" - sub_tests = [JobStatus.QUEUED, JobStatus.RUNNING, JobStatus.DONE] - for status in sub_tests: - with self.subTest(status=status): - if status == JobStatus.QUEUED: - _ = self._run_program(iterations=10) - - job = self._run_program(iterations=2) - self._wait_for_status(job, status) - self.service.delete_job(job.job_id) - with self.assertRaises(RuntimeJobNotFound): - self.service.job(job.job_id) - - def test_interim_result_callback(self): - """Test interim result callback.""" - - def result_callback(job_id, interim_result): - nonlocal final_it - final_it = interim_result["iteration"] - nonlocal callback_err - if job_id != job.job_id: - callback_err.append(f"Unexpected job ID: {job_id}") - if interim_result["interim_results"] != int_res: - callback_err.append(f"Unexpected interim result: {interim_result}") - - int_res = "foo" - final_it = 0 - callback_err = [] - iterations = 3 - job = self._run_program( - iterations=iterations, interim_results=int_res, callback=result_callback - ) - job.wait_for_final_state() - self.assertEqual(iterations - 1, final_it) - self.assertFalse(callback_err) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_stream_results(self): - """Test stream_results method.""" - - def result_callback(job_id, interim_result): - nonlocal final_it - final_it = interim_result["iteration"] - nonlocal callback_err - if job_id != job.job_id: - callback_err.append(f"Unexpected job ID: {job_id}") - if interim_result["interim_results"] != int_res: - callback_err.append(f"Unexpected interim result: {interim_result}") - - int_res = "bar" - final_it = 0 - callback_err = [] - iterations = 3 - job = self._run_program(iterations=iterations, interim_results=int_res) - job.stream_results(result_callback) - job.wait_for_final_state() - self.assertEqual(iterations - 1, final_it) - self.assertFalse(callback_err) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_stream_results_done(self): - """Test streaming interim results after job is done.""" - - def result_callback(job_id, interim_result): - # pylint: disable=unused-argument - nonlocal called_back - called_back = True - - called_back = False - job = self._run_program(interim_results="foobar") - job.wait_for_final_state() - job._status = JobStatus.RUNNING # Allow stream_results() - job.stream_results(result_callback) - time.sleep(2) - self.assertFalse(called_back) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_retrieve_interim_results(self): - """Test retrieving interim results with API endpoint""" - int_res = "foo" - job = self._run_program(interim_results=int_res) - job.wait_for_final_state() - interim_results = job.interim_results() - self.assertIn(int_res, interim_results[0]) - - def test_callback_error(self): - """Test error in callback method.""" - - def result_callback(job_id, interim_result): - # pylint: disable=unused-argument - if interim_result["iteration"] == 0: - raise ValueError("Kaboom!") - nonlocal final_it - final_it = interim_result["iteration"] - - final_it = 0 - iterations = 3 - with self.assertLogs("qiskit_ibm_runtime", level="WARNING") as err_cm: - job = self._run_program( - iterations=iterations, interim_results="foo", callback=result_callback - ) - job.wait_for_final_state() - - self.assertIn("Kaboom", ", ".join(err_cm.output)) - self.assertEqual(iterations - 1, final_it) - self.assertIsNotNone(job._ws_client._server_close_code) - - def test_callback_cancel_job(self): - """Test canceling a running job while streaming results.""" - - def result_callback(job_id, interim_result): - # pylint: disable=unused-argument - nonlocal final_it - final_it = interim_result["iteration"] - - final_it = 0 - iterations = 3 - sub_tests = [JobStatus.QUEUED, JobStatus.RUNNING] - - for status in sub_tests: - with self.subTest(status=status): - if status == JobStatus.QUEUED: - _ = self._run_program(iterations=10) - - job = self._run_program( - iterations=iterations, - interim_results="foo", - callback=result_callback, - ) - self._wait_for_status(job, status) - job.cancel() - time.sleep(3) # Wait for cleanup - self.assertIsNotNone(job._ws_client._server_close_code) - self.assertLess(final_it, iterations) - - def test_final_result(self): - """Test getting final result.""" - final_result = get_complex_types() - job = self._run_program(final_result=final_result) - result = job.result(decoder=SerializableClassDecoder) - self.assertEqual(final_result, result) - - rresults = self.service.job(job.job_id).result(decoder=SerializableClassDecoder) - self.assertEqual(final_result, rresults) - - def test_job_status(self): - """Test job status.""" - job = self._run_program(iterations=1) - time.sleep(random.randint(1, 5)) - self.assertTrue(job.status()) - - def test_job_inputs(self): - """Test job inputs.""" - interim_results = get_complex_types() - inputs = {"iterations": 1, "interim_results": interim_results} - options = {"backend_name": self.backend.name()} - job = self.service.run( - program_id=self.program_id, inputs=inputs, options=options - ) - self.log.info("Runtime job %s submitted.", job.job_id) - self.to_cancel.append(job) - self.assertEqual(inputs, job.inputs) - rjob = self.service.job(job.job_id) - rinterim_results = rjob.inputs["interim_results"] - self._assert_complex_types_equal(interim_results, rinterim_results) - - def test_job_backend(self): - """Test job backend.""" - job = self._run_program() - self.assertEqual(self.backend, job.backend) - - def test_job_program_id(self): - """Test job program ID.""" - job = self._run_program() - self.assertEqual(self.program_id, job.program_id) - - def test_wait_for_final_state(self): - """Test wait for final state.""" - job = self._run_program() - job.wait_for_final_state() - self.assertEqual(JobStatus.DONE, job.status()) - - def test_logout(self): - """Test logout.""" - self.service.logout() - # Make sure we can still do things. - self._upload_program() - _ = self._run_program() - - def test_run_circuit(self): - """Test run_circuits""" - job = self.service.run_circuits( - ReferenceCircuits.bell(), backend_name=self.backend.name(), shots=100 - ) - counts = job.result().get_counts() - self.assertEqual(100, sum(counts.values())) - - def test_job_creation_date(self): - """Test job creation date.""" - job = self._run_program(iterations=1) - self.assertTrue(job.creation_date) - rjob = self.service.job(job.job_id) - self.assertTrue(rjob.creation_date) - rjobs = self.service.jobs(limit=2) - for rjob in rjobs: - self.assertTrue(rjob.creation_date) - - def test_websocket_proxy(self): - """Test connecting to websocket via proxy.""" - - def result_callback(job_id, interim_result): # pylint: disable=unused-argument - nonlocal callback_called - callback_called = True - - MockProxyServer(self, self.log).start() - callback_called = False - - with use_proxies(self.service._default_hgp, MockProxyServer.VALID_PROXIES): - job = self._run_program(iterations=1, callback=result_callback) - job.wait_for_final_state() - - self.assertTrue(callback_called) - - def test_websocket_proxy_invalid_port(self): - """Test connecting to websocket via invalid proxy port.""" - - def result_callback(job_id, interim_result): # pylint: disable=unused-argument - nonlocal callback_called - callback_called = True - - callback_called = False - invalid_proxy = { - "https": "http://{}:{}".format( - MockProxyServer.PROXY_IP_ADDRESS, MockProxyServer.INVALID_PROXY_PORT - ) - } - with use_proxies(self.service._default_hgp, invalid_proxy): - with self.assertLogs("qiskit_ibm_runtime", "WARNING") as log_cm: - job = self._run_program(iterations=1, callback=result_callback) - job.wait_for_final_state() - self.assertIn("WebsocketError", ",".join(log_cm.output)) - self.assertFalse(callback_called) - - def test_job_logs(self): - """Test job logs.""" - job = self._run_program(final_result="foo") - with self.assertLogs("qiskit_ibm_runtime", "WARN"): - job.logs() - job.wait_for_final_state() - job_logs = job.logs() - self.assertIn("this is a stdout message", job_logs) - self.assertIn("this is a stderr message", job_logs) - - def _validate_program(self, program): - """Validate a program.""" - self.assertTrue(program) - self.assertTrue(program.name) - self.assertTrue(program.program_id) - self.assertTrue(program.description) - self.assertTrue(program.max_execution_time) - self.assertTrue(program.creation_date) - self.assertTrue(program.update_date) - - def _upload_program( - self, name=None, max_execution_time=300, data=None, is_public: bool = False - ): - """Upload a new program.""" - name = name or self._get_program_name() - data = data or self.RUNTIME_PROGRAM - metadata = copy.deepcopy(self.RUNTIME_PROGRAM_METADATA) - metadata["name"] = name - metadata["max_execution_time"] = max_execution_time - metadata["is_public"] = is_public - program_id = self.service.upload_program(data=data, metadata=metadata) - self.to_delete.append(program_id) - return program_id - - @classmethod - def _get_program_name(cls): - """Return a unique program name.""" - return cls.PROGRAM_PREFIX + "_" + uuid.uuid4().hex - - def _assert_complex_types_equal(self, expected, received): - """Verify the received data in complex types is expected.""" - if "serializable_class" in received: - received["serializable_class"] = SerializableClass.from_json( - received["serializable_class"] - ) - self.assertEqual(expected, received) - - def _run_program( - self, - program_id=None, - iterations=1, - interim_results=None, - final_result=None, - callback=None, - ): - """Run a program.""" - inputs = { - "iterations": iterations, - "interim_results": interim_results or {}, - "final_result": final_result or {}, - } - pid = program_id or self.program_id - options = {"backend_name": self.backend.name()} - job = self.service.run( - program_id=pid, inputs=inputs, options=options, callback=callback - ) - self.log.info("Runtime job %s submitted.", job.job_id) - self.to_cancel.append(job) - return job - - def _wait_for_status(self, job, status): - """Wait for job to reach a certain status.""" - wait_time = 1 if status == JobStatus.QUEUED else self.poll_time - while job.status() not in JOB_FINAL_STATES + (status,): - time.sleep(wait_time) - if job.status() != status: - self.skipTest(f"Job {job.job_id} unable to reach status {status}.") diff --git a/test/ibm/test_basic_server_paths.py b/test/ibm/test_basic_server_paths.py deleted file mode 100644 index 0f49e30803..0000000000 --- a/test/ibm/test_basic_server_paths.py +++ /dev/null @@ -1,55 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests that hit all the basic server endpoints using both a public and premium provider.""" - -from datetime import datetime, timedelta - -from ..decorators import requires_providers -from ..ibm_test_case import IBMTestCase - - -class TestBasicServerPaths(IBMTestCase): - """Test the basic server endpoints using both a public and premium provider.""" - - @classmethod - @requires_providers - def setUpClass(cls, service, hgps): - # pylint: disable=arguments-differ - super().setUpClass() - cls.service = service # Dict[str, IBMRuntimeService] - cls.hgps = hgps - cls.last_week = datetime.now() - timedelta(days=7) - - def test_device_properties_and_defaults(self): - """Test the properties and defaults for an open pulse device.""" - for desc, hgp in self.hgps.items(): - pulse_backends = self.service.backends( - open_pulse=True, operational=True, **hgp - ) - if not pulse_backends: - raise self.skipTest( - "Skipping pulse test since no pulse backend " - 'found for "{}"'.format(desc) - ) - - pulse_backend = pulse_backends[0] - with self.subTest(desc=desc, backend=pulse_backend): - self.assertIsNotNone(pulse_backend.properties()) - self.assertIsNotNone(pulse_backend.defaults()) - - def test_device_status(self): - """Test device status.""" - for desc, hgp in self.hgps.items(): - backend = self.service.backends(simulator=False, operational=True, **hgp)[0] - with self.subTest(desc=desc, backend=backend): - self.assertTrue(backend.status()) diff --git a/test/ibm/test_filter_backends.py b/test/ibm/test_filter_backends.py deleted file mode 100644 index 0443fe8f5e..0000000000 --- a/test/ibm/test_filter_backends.py +++ /dev/null @@ -1,197 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Backends Filtering Test.""" - -from unittest import mock -from datetime import datetime - -from dateutil import tz -from qiskit_ibm_runtime import least_busy -from qiskit_ibm_runtime import IBMError - -from ..ibm_test_case import IBMTestCase -from ..decorators import requires_provider, requires_device - - -class TestBackendFilters(IBMTestCase): - """Qiskit Backend Filtering Tests.""" - - @classmethod - @requires_provider - def setUpClass(cls, service, hub, group, project): - """Initial class level setup.""" - # pylint: disable=arguments-differ - super().setUpClass() - cls.service = service - cls.hub = hub - cls.group = group - cls.project = project - - @requires_device - def test_filter_config_properties(self, backend): - """Test filtering by configuration properties.""" - # Use the default backend as a reference for the filter. - n_qubits = backend.configuration().n_qubits - - filtered_backends = self.service.backends( - n_qubits=n_qubits, - local=False, - hub=self.hub, - group=self.group, - project=self.project, - ) - - self.assertTrue(filtered_backends) - for filtered_backend in filtered_backends[:5]: - with self.subTest(filtered_backend=filtered_backend): - self.assertEqual(n_qubits, filtered_backend.configuration().n_qubits) - self.assertFalse(filtered_backend.configuration().local) - - def test_filter_status_dict(self): - """Test filtering by dictionary of mixed status/configuration properties.""" - filtered_backends = self.service.backends( - operational=True, # from status - local=False, - simulator=True, # from configuration - hub=self.hub, - group=self.group, - project=self.project, - ) - - self.assertTrue(filtered_backends) - for backend in filtered_backends[:5]: - with self.subTest(backend=backend): - self.assertTrue(backend.status().operational) - self.assertFalse(backend.configuration().local) - self.assertTrue(backend.configuration().simulator) - - def test_filter_config_callable(self): - """Test filtering by lambda function on configuration properties.""" - filtered_backends = self.service.backends( - filters=lambda x: ( - not x.configuration().simulator and x.configuration().n_qubits >= 5 - ), - hub=self.hub, - group=self.group, - project=self.project, - ) - - self.assertTrue(filtered_backends) - for backend in filtered_backends[:5]: - with self.subTest(backend=backend): - self.assertFalse(backend.configuration().simulator) - self.assertGreaterEqual(backend.configuration().n_qubits, 5) - - def test_filter_least_busy(self): - """Test filtering by least busy function.""" - backends = self.service.backends( - hub=self.hub, group=self.group, project=self.project - ) - least_busy_backend = least_busy(backends) - self.assertTrue(least_busy_backend) - - def test_filter_least_busy_reservation(self): - """Test filtering by least busy function, with reservations.""" - backend = reservations = None - for backend in self.service.backends( - simulator=False, - operational=True, - status_msg="active", - hub=self.hub, - group=self.group, - project=self.project, - ): - reservations = backend.reservations() - if reservations: - break - - if not reservations: - self.skipTest("Test case requires reservations.") - - reserv = reservations[0] - now = datetime.now(tz=tz.tzlocal()) - window = 60 - if reserv.start_datetime > now: - window = (reserv.start_datetime - now).seconds * 60 - self.assertRaises(IBMError, least_busy, [backend], window) - - self.assertEqual(least_busy([backend], None), backend) - - backs = [backend] - for back in self.service.backends( - simulator=False, - operational=True, - status_msg="active", - hub=self.hub, - group=self.group, - project=self.project, - ): - if back.name() != backend.name(): - backs.append(back) - break - self.assertTrue(least_busy(backs, window)) - - def test_filter_least_busy_paused(self): - """Test filtering by least busy function, with paused backend.""" - backends = self.service.backends( - hub=self.hub, group=self.group, project=self.project - ) - if len(backends) < 2: - self.skipTest("Test needs at least 2 backends.") - paused_backend = backends[0] - paused_status = paused_backend.status() - paused_status.status_msg = "internal" - paused_status.pending_jobs = 0 - paused_backend.status = mock.MagicMock(return_value=paused_status) - - least_busy_backend = least_busy(backends) - self.assertTrue(least_busy_backend) - self.assertNotEqual(least_busy_backend.name(), paused_backend.name()) - self.assertEqual(least_busy_backend.status().status_msg, "active") - - def test_filter_min_num_qubits(self): - """Test filtering by minimum number of qubits.""" - filtered_backends = self.service.backends( - min_num_qubits=5, - simulator=False, - filters=lambda b: b.configuration().quantum_volume >= 10, - hub=self.hub, - group=self.group, - project=self.project, - ) - - self.assertTrue(filtered_backends) - for backend in filtered_backends[:5]: - with self.subTest(backend=backend): - self.assertGreaterEqual(backend.configuration().n_qubits, 5) - self.assertTrue(backend.configuration().quantum_volume, 10) - - def test_filter_input_allowed(self): - """Test filtering by input allowed""" - subtests = ("job", ["job"], ["job", "runtime"]) - - for input_type in subtests: - with self.subTest(input_type=input_type): - filtered = self.service.backends( - input_allowed=input_type, - hub=self.hub, - group=self.group, - project=self.project, - ) - self.assertTrue(filtered) - if not isinstance(input_type, list): - input_type = [input_type] - for backend in filtered[:5]: - self.assertTrue( - set(input_type) <= set(backend.configuration().input_allowed) - ) diff --git a/test/ibm/test_ibm_backend.py b/test/ibm/test_ibm_backend.py deleted file mode 100644 index 9a98545f1e..0000000000 --- a/test/ibm/test_ibm_backend.py +++ /dev/null @@ -1,129 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""IBMBackend Test.""" - -from datetime import timedelta, datetime - -from ..ibm_test_case import IBMTestCase -from ..decorators import requires_device, requires_provider - - -class TestIBMBackend(IBMTestCase): - """Test ibm_backend module.""" - - @classmethod - @requires_device - def setUpClass(cls, backend): - """Initial class level setup.""" - # pylint: disable=arguments-differ - super().setUpClass() - cls.backend = backend - - def test_backend_status(self): - """Check the status of a real chip.""" - self.assertTrue(self.backend.status().operational) - - def test_backend_properties(self): - """Check the properties of calibration of a real chip.""" - self.assertIsNotNone(self.backend.properties()) - - def test_backend_pulse_defaults(self): - """Check the backend pulse defaults of each backend.""" - service = self.backend.provider() - for backend in service.backends(): - with self.subTest(backend_name=backend.name()): - defaults = backend.defaults() - if backend.configuration().open_pulse: - self.assertIsNotNone(defaults) - - def test_backend_reservations(self): - """Test backend reservations.""" - service = self.backend.provider() - backend = reservations = None - for backend in service.backends( - simulator=False, - operational=True, - hub=self.backend.hub, - group=self.backend.group, - project=self.backend.project, - ): - reservations = backend.reservations() - if reservations: - break - - if not reservations: - self.skipTest("Test case requires reservations.") - - reserv = reservations[0] - self.assertGreater(reserv.duration, 0) - self.assertTrue(reserv.mode) - before_start = reserv.start_datetime - timedelta(seconds=30) - after_start = reserv.start_datetime + timedelta(seconds=30) - before_end = reserv.end_datetime - timedelta(seconds=30) - after_end = reserv.end_datetime + timedelta(seconds=30) - - # Each tuple contains the start datetime, end datetime, whether a - # reservation should be found, and the description. - sub_tests = [ - (before_start, after_end, True, "before start, after end"), - (before_start, before_end, True, "before start, before end"), - (after_start, before_end, True, "after start, before end"), - (before_start, None, True, "before start, None"), - (None, after_end, True, "None, after end"), - (before_start, before_start, False, "before start, before start"), - (after_end, after_end, False, "after end, after end"), - ] - - for start_dt, end_dt, should_find, name in sub_tests: - with self.subTest(name=name): - f_reservs = backend.reservations( - start_datetime=start_dt, end_datetime=end_dt - ) - found = False - for f_reserv in f_reservs: - if f_reserv == reserv: - found = True - break - self.assertEqual( - found, - should_find, - "Reservation {} found={}, used start datetime {}, end datetime {}".format( - reserv, found, start_dt, end_dt - ), - ) - - -class TestIBMBackendService(IBMTestCase): - """Test ibm_backend_service module.""" - - @classmethod - @requires_provider - def setUpClass(cls, service, hub, group, project): - """Initial class level setup.""" - # pylint: disable=arguments-differ - super().setUpClass() - cls.service = service - cls.hub = hub - cls.group = group - cls.project = project - cls.last_week = datetime.now() - timedelta(days=7) - - def test_my_reservations(self): - """Test my_reservations method""" - reservations = self.service.my_reservations() - for reserv in reservations: - for attr in reserv.__dict__: - self.assertIsNotNone( - getattr(reserv, attr), - "Reservation {} is missing attribute {}".format(reserv, attr), - ) diff --git a/test/ibm/test_ibm_provider.py b/test/ibm/test_ibm_provider.py deleted file mode 100644 index 4ecf25c45b..0000000000 --- a/test/ibm/test_ibm_provider.py +++ /dev/null @@ -1,251 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for the IBMRuntimeService class.""" - -from datetime import datetime -from unittest import mock - -from qiskit import ClassicalRegister, QuantumCircuit, QuantumRegister -from qiskit.providers.exceptions import QiskitBackendNotFoundError -from qiskit.providers.models.backendproperties import BackendProperties - -from qiskit_ibm_runtime import IBMRuntimeService -from qiskit_ibm_runtime import hub_group_project -from qiskit_ibm_runtime.api.clients import AccountClient -from qiskit_ibm_runtime.api.exceptions import RequestsApiError -from qiskit_ibm_runtime.exceptions import ( - IBMProviderCredentialsInvalidUrl, -) -from qiskit_ibm_runtime.ibm_backend import IBMSimulator, IBMBackend -from ..decorators import requires_qe_access, requires_provider -from ..ibm_test_case import IBMTestCase - -API_URL = "https://api.quantum-computing.ibm.com/api" -AUTH_URL = "https://auth.quantum-computing.ibm.com/api" - - -class TestIBMProviderEnableAccount(IBMTestCase): - """Tests for IBMRuntimeService.""" - - # Enable Account Tests - - @requires_qe_access - def test_provider_init_token(self, qe_token, qe_url): - """Test initializing IBMRuntimeService with only API token.""" - # pylint: disable=unused-argument - service = IBMRuntimeService(auth="legacy", token=qe_token) - self.assertIsInstance(service, IBMRuntimeService) - self.assertEqual(service._default_hgp.credentials.token, qe_token) - - @requires_qe_access - def test_pass_unreachable_proxy(self, qe_token, qe_url): - """Test using an unreachable proxy while enabling an account.""" - proxies = { - "urls": { - "http": "http://user:password@127.0.0.1:5678", - "https": "https://user:password@127.0.0.1:5678", - } - } - with self.assertRaises(RequestsApiError) as context_manager: - IBMRuntimeService( - auth="legacy", token=qe_token, url=qe_url, proxies=proxies - ) - self.assertIn("ProxyError", str(context_manager.exception)) - - def test_provider_init_non_auth_url(self): - """Test initializing IBMRuntimeService with a non-auth URL.""" - qe_token = "invalid" - qe_url = API_URL - - with self.assertRaises(IBMProviderCredentialsInvalidUrl) as context_manager: - IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - - self.assertIn("authentication URL", str(context_manager.exception)) - - def test_provider_init_non_auth_url_with_hub(self): - """Test initializing IBMRuntimeService with a non-auth URL containing h/g/p.""" - qe_token = "invalid" - qe_url = API_URL + "/Hubs/X/Groups/Y/Projects/Z" - - with self.assertRaises(IBMProviderCredentialsInvalidUrl) as context_manager: - IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - - self.assertIn("authentication URL", str(context_manager.exception)) - - @requires_qe_access - def test_discover_backend_failed(self, qe_token, qe_url): - """Test discovering backends failed.""" - with mock.patch.object( - AccountClient, - "list_backends", - return_value=[{"backend_name": "bad_backend"}], - ): - with self.assertLogs( - hub_group_project.logger, level="WARNING" - ) as context_manager: - IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - self.assertIn("bad_backend", str(context_manager.output)) - - -class TestIBMProviderHubGroupProject(IBMTestCase): - """Tests for IBMRuntimeService HubGroupProject related methods.""" - - @requires_qe_access - def _initialize_provider(self, qe_token=None, qe_url=None): - """Initialize and return provider.""" - return IBMRuntimeService(auth="legacy", token=qe_token, url=qe_url) - - def setUp(self): - """Initial test setup.""" - super().setUp() - - self.service = self._initialize_provider() - self.credentials = self.service._default_hgp.credentials - - def test_get_hgp(self): - """Test get single hgp.""" - hgp = self.service._get_hgp( - hub=self.credentials.hub, - group=self.credentials.group, - project=self.credentials.project, - ) - self.assertEqual(self.service._default_hgp, hgp) - - def test_get_hgps_with_filter(self): - """Test get hgps with a filter.""" - hgp = self.service._get_hgps( - hub=self.credentials.hub, - group=self.credentials.group, - project=self.credentials.project, - )[0] - self.assertEqual(self.service._default_hgp, hgp) - - def test_get_hgps_no_filter(self): - """Test get hgps without a filter.""" - hgps = self.service._get_hgps() - self.assertIn(self.service._default_hgp, hgps) - - -class TestIBMProviderServices(IBMTestCase): - """Tests for services provided by the IBMRuntimeService class.""" - - @requires_provider - def setUp(self, service, hub, group, project): - """Initial test setup.""" - # pylint: disable=arguments-differ - super().setUp() - self.service = service - self.hub = hub - self.group = group - self.project = project - qr = QuantumRegister(1) - cr = ClassicalRegister(1) - self.qc1 = QuantumCircuit(qr, cr, name="circuit0") - self.qc1.h(qr[0]) - self.qc1.measure(qr, cr) - - def test_remote_backends_exist_real_device(self): - """Test if there are remote backends that are devices.""" - remotes = self.service.backends( - simulator=False, hub=self.hub, group=self.group, project=self.project - ) - self.assertTrue(remotes) - - def test_remote_backends_exist_simulator(self): - """Test if there are remote backends that are simulators.""" - remotes = self.service.backends( - simulator=True, hub=self.hub, group=self.group, project=self.project - ) - self.assertTrue(remotes) - - def test_remote_backends_instantiate_simulators(self): - """Test if remote backends that are simulators are an ``IBMSimulator`` instance.""" - remotes = self.service.backends( - simulator=True, hub=self.hub, group=self.group, project=self.project - ) - for backend in remotes: - with self.subTest(backend=backend): - self.assertIsInstance(backend, IBMSimulator) - - def test_remote_backend_status(self): - """Test backend_status.""" - remotes = self.service.backends( - hub=self.hub, group=self.group, project=self.project - ) - for backend in remotes: - _ = backend.status() - - def test_remote_backend_configuration(self): - """Test backend configuration.""" - remotes = self.service.backends( - hub=self.hub, group=self.group, project=self.project - ) - for backend in remotes: - _ = backend.configuration() - - def test_remote_backend_properties(self): - """Test backend properties.""" - remotes = self.service.backends( - simulator=False, hub=self.hub, group=self.group, project=self.project - ) - for backend in remotes: - properties = backend.properties() - if backend.configuration().simulator: - self.assertEqual(properties, None) - - def test_aliases(self): - """Test that display names of devices map the regular names.""" - aliased_names = self.service._aliased_backend_names() - - for display_name, backend_name in aliased_names.items(): - with self.subTest(display_name=display_name, backend_name=backend_name): - try: - backend_by_name = self.service.get_backend( - backend_name, - hub=self.hub, - group=self.group, - project=self.project, - ) - except QiskitBackendNotFoundError: - # The real name of the backend might not exist - pass - else: - backend_by_display_name = self.service.get_backend(display_name) - self.assertEqual(backend_by_name, backend_by_display_name) - self.assertEqual(backend_by_display_name.name(), backend_name) - - def test_remote_backend_properties_filter_date(self): - """Test backend properties filtered by date.""" - backends = self.service.backends( - simulator=False, hub=self.hub, group=self.group, project=self.project - ) - - datetime_filter = datetime(2019, 2, 1).replace(tzinfo=None) - for backend in backends: - with self.subTest(backend=backend): - properties = backend.properties(datetime=datetime_filter) - if isinstance(properties, BackendProperties): - last_update_date = properties.last_update_date.replace(tzinfo=None) - self.assertLessEqual(last_update_date, datetime_filter) - else: - self.assertEqual(properties, None) - - def test_provider_backends(self): - """Test provider_backends have correct attributes.""" - provider_backends = { - back - for back in dir(self.service) - if isinstance(getattr(self.service, back), IBMBackend) - } - backends = {back.name().lower() for back in self.service._backends.values()} - self.assertEqual(provider_backends, backends) diff --git a/test/ibm/test_utils.py b/test/ibm/test_utils.py deleted file mode 100644 index b83c820698..0000000000 --- a/test/ibm/test_utils.py +++ /dev/null @@ -1,47 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Tests for utils.""" - -from qiskit_ibm_runtime.utils import is_crn, crn_to_api_host -from qiskit_ibm_runtime import CannotMapCrnToApiHostError - -from ..ibm_test_case import IBMTestCase - - -CRN_HOST_TUPLES = [ - [ - "crn:v1:bluemix:public:quantum-computing:us-east:a/...::", - "https://us-east.quantum-computing.cloud.ibm.com", - ] -] - - -class TestUtils(IBMTestCase): - """Tests for utility functions.""" - - def test_is_crn(self): - """Tests detection of CRN values.""" - self.assertFalse(is_crn("abc")) - for entry in CRN_HOST_TUPLES: - self.assertTrue(is_crn(entry[0])) - - def test_map_to_api_host(self): - """Tests mapping of CRN values to API hosts.""" - for entry in CRN_HOST_TUPLES: - print(entry) - print(crn_to_api_host(entry[0])) - self.assertEqual(crn_to_api_host(entry[0]), entry[1]) - - self.assertRaises( - CannotMapCrnToApiHostError, lambda: crn_to_api_host("invalid") - ) diff --git a/test/ibm_test_case.py b/test/ibm_test_case.py index a0af410f91..399a0ba537 100644 --- a/test/ibm_test_case.py +++ b/test/ibm_test_case.py @@ -15,15 +15,14 @@ import os import logging import inspect - -from qiskit.test.base import BaseQiskitTestCase +from unittest import TestCase from qiskit_ibm_runtime import QISKIT_IBM_RUNTIME_LOGGER_NAME -from .utils import setup_test_logging +from .utils.utils import setup_test_logging -class IBMTestCase(BaseQiskitTestCase): +class IBMTestCase(TestCase): """Custom TestCase for use with the Qiskit IBM Runtime.""" @classmethod diff --git a/test/jobtestcase.py b/test/jobtestcase.py deleted file mode 100644 index 7b6d184eb3..0000000000 --- a/test/jobtestcase.py +++ /dev/null @@ -1,37 +0,0 @@ -# This code is part of Qiskit. -# -# (C) Copyright IBM 2021. -# -# This code is licensed under the Apache License, Version 2.0. You may -# obtain a copy of this license in the LICENSE.txt file in the root directory -# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. -# -# Any modifications or derivative works of this code must retain this -# copyright notice, and modified files need to carry a notice indicating -# that they have been altered from the originals. - -"""Custom TestCase for Jobs.""" - -import time - -from qiskit.providers import JobStatus - -from .ibm_test_case import IBMTestCase - - -class JobTestCase(IBMTestCase): - """Include common functionality when testing jobs.""" - - def wait_for_initialization(self, job, timeout=1): - """Waits until job progresses from `INITIALIZING` to other status.""" - waited = 0 - wait = 0.1 - while job.status() is JobStatus.INITIALIZING: - time.sleep(wait) - waited += wait - if waited > timeout: - self.fail( - msg="The JOB is still initializing after timeout ({}s)".format( - timeout - ) - ) diff --git a/test/ibm/runtime/__init__.py b/test/mock/__init__.py similarity index 94% rename from test/ibm/runtime/__init__.py rename to test/mock/__init__.py index 99de8ba789..86c34a4a3a 100644 --- a/test/ibm/runtime/__init__.py +++ b/test/mock/__init__.py @@ -10,4 +10,4 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Runtime related tests.""" +"""Test mock classes.""" diff --git a/test/mock/fake_account_client.py b/test/mock/fake_account_client.py new file mode 100644 index 0000000000..4fecc0bbf0 --- /dev/null +++ b/test/mock/fake_account_client.py @@ -0,0 +1,112 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2019. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Fake AccountClient.""" + +from typing import List, Dict, Any, Optional +from datetime import datetime as python_datetime + +from qiskit.test.mock.backends import FakeLima + + +class FakeApiBackend: + """Fake backend.""" + + def __init__(self, config_update=None, status_update=None): + fake_backend = FakeLima() + self.properties = fake_backend.properties().to_dict() + self.defaults = fake_backend.defaults().to_dict() + + self.configuration = fake_backend.configuration().to_dict() + self.configuration["online_date"] = python_datetime.now().isoformat() + if config_update: + self.configuration.update(**config_update) + self.name = self.configuration["backend_name"] + + self.status = fake_backend.status().to_dict() + if status_update: + self.status.update(**status_update) + + +class BaseFakeAccountClient: + """Base class for faking the AccountClient.""" + + def __init__( + self, + hgp: Optional[str] = None, + num_backends: int = 2, + specs: List[Dict] = None, + ): + """Initialize a fake account client. + + Args: + num_backends: Number of backends. Ignored if ``specs`` is specified. + specs: Backend specs. This is a dictionary of overwritten backend + configuration / status. For example:: + + specs = [ {"configuration": {"backend_name": "backend1"}, + "status": {"operational": False}} + ] + """ + self._hgp = hgp + self._fake_backend = FakeLima() + self._backends = [] + if not specs: + specs = [{}] * num_backends + + for idx, backend_spec in enumerate(specs): + config = backend_spec.get("configuration", {}) + status = backend_spec.get("status", {}) + if "backend_name" not in config: + config["backend_name"] = f"backend{idx}" + self._backends.append(FakeApiBackend(config, status)) + + def list_backends(self, *args, **kwargs) -> List[Dict[str, Any]]: + """Return backends available for this provider.""" + # pylint: disable=unused-argument + return [back.configuration.copy() for back in self._backends] + + def backend_status(self, backend_name: str) -> Dict[str, Any]: + """Return the status of the backend.""" + for back in self._backends: + if back.name == backend_name: + return back.status.copy() + raise ValueError(f"Backend {backend_name} not found") + + def backend_properties( + self, backend_name: str, datetime: Optional[python_datetime] = None + ) -> Dict[str, Any]: + """Return the properties of the backend.""" + # pylint: disable=unused-argument + for back in self._backends: + if back.name == backend_name: + return back.properties.copy() + raise ValueError(f"Backend {backend_name} not found") + + def backend_pulse_defaults(self, backend_name: str) -> Dict: + """Return the pulse defaults of the backend.""" + for back in self._backends: + if back.name == backend_name: + return back.defaults.copy() + raise ValueError(f"Backend {backend_name} not found") + + # Test-only methods. + + @property + def backend_names(self): + """Return names of the backends.""" + return [back.name for back in self._backends] + + @property + def hgp(self): + """Return hub/group/project.""" + return self._hgp diff --git a/test/mock/fake_legacy_auth_client.py b/test/mock/fake_legacy_auth_client.py new file mode 100644 index 0000000000..417636053a --- /dev/null +++ b/test/mock/fake_legacy_auth_client.py @@ -0,0 +1,75 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Fake legacy AuthClient.""" + +from typing import Dict, List, Union, Optional + + +class BaseFakeAuthClient: + """Base class for faking the runtime client.""" + + def __init__(self, *args, **kwargs): + """Initialize a auth runtime client.""" + pass + + def user_urls(self) -> Dict[str, str]: + """Retrieve the API URLs from the authentication service. + + Returns: + A dict with the base URLs for the services. Currently + supported keys are: + + * ``http``: The API URL for HTTP communication. + * ``ws``: The API URL for websocket communication. + * ``services`: The API URL for additional services. + """ + return { + "http": "http://127.0.0.1", + "ws": "ws://127.0.0.1", + "services": {"runtime": "http://127.0.0.1"}, + } + + def user_hubs(self) -> List[Dict[str, str]]: + """Retrieve the hub/group/project sets available to the user.""" + + hubs = [] + for idx in range(2): + hubs.append( + {"hub": f"hub{idx}", "group": f"group{idx}", "project": f"project{idx}"} + ) + return hubs + + def api_version(self) -> Dict[str, Union[str, bool]]: + """Return the version of the API. + + Returns: + API version. + """ + return {"new_api": True, "api-auth": "0.1"} + + def current_access_token(self) -> Optional[str]: + """Return the current access token. + + Returns: + The access token in use. + """ + return "123" + + def current_service_urls(self) -> Dict[str, str]: + """Return the current service URLs. + + Returns: + A dict with the base URLs for the services, in the same + format as :meth:`user_urls()`. + """ + return {"runtime": "http://127.0.0.1"} diff --git a/test/ibm/runtime/fake_runtime_client.py b/test/mock/fake_runtime_client.py similarity index 81% rename from test/ibm/runtime/fake_runtime_client.py rename to test/mock/fake_runtime_client.py index fa16c46dbc..880ef036ac 100644 --- a/test/ibm/runtime/fake_runtime_client.py +++ b/test/mock/fake_runtime_client.py @@ -18,10 +18,25 @@ import base64 from typing import Optional, Dict from concurrent.futures import ThreadPoolExecutor +from functools import wraps -from qiskit_ibm_runtime.credentials import Credentials from qiskit_ibm_runtime.api.exceptions import RequestsApiError from qiskit_ibm_runtime.utils import RuntimeEncoder +from qiskit_ibm_runtime.utils.hgp import from_instance_format + +from .fake_account_client import BaseFakeAccountClient + + +def cloud_only(func): + """Decorator that runs a test using both legacy and cloud services.""" + + @wraps(func) + def _wrapper(self, *args, **kwargs): + if self._auth_type != "cloud": + raise ValueError(f"Method {func} called by a legacy client!") + return func(self, *args, **kwargs) + + return _wrapper class BaseFakeProgram: @@ -230,30 +245,19 @@ def _auto_progress(self): class BaseFakeRuntimeClient: """Base class for faking the runtime client.""" - def __init__( - self, - job_classes=None, - final_status=None, - job_kwargs=None, - hub=None, - group=None, - project=None, - ): + def __init__(self, *args, **kwargs): """Initialize a fake runtime client.""" + # pylint: disable=unused-argument + test_options = kwargs.pop("test_options", {}) self._programs = {} self._jobs = {} - self._job_classes = job_classes or [] - self._final_status = final_status - self._job_kwargs = job_kwargs or {} - self._hub = hub - self._group = group - self._project = project - - def set_hgp(self, hub, group, project): - """Set hub, group and project""" - self._hub = hub - self._group = group - self._project = project + self._job_classes = test_options.get("job_classes", []) + self._final_status = test_options.get("final_status") + self._job_kwargs = test_options.get("job_kwargs", {}) + self._backend_client = test_options.get( + "backend_client", BaseFakeAccountClient() + ) + self._auth_type = test_options.get("auth_type", "legacy") def set_job_classes(self, classes): """Set job classes to use.""" @@ -313,9 +317,7 @@ def program_update( spec: Optional[Dict] = None, ) -> None: """Update a program.""" - if program_id not in self._programs: - raise RequestsApiError("Program not found", status_code=404) - program = self._programs[program_id] + program = self._get_program(program_id) program._data = program_data or program._data program._name = name or program._name program._description = description or program._description @@ -339,21 +341,23 @@ def program_get(self, program_id: str): def program_run( self, program_id: str, - credentials: Credentials, backend_name: str, - params: str, - image: Optional[str] = "", + params: Dict, + image: str, + hgp: Optional[str], ): """Run the specified program.""" + _ = self._get_program(program_id) job_id = uuid.uuid4().hex job_cls = ( self._job_classes.pop(0) if len(self._job_classes) > 0 else BaseFakeRuntimeJob ) - hub = self._hub or credentials.hub - group = self._group or credentials.group - project = self._project or credentials.project + if hgp: + hub, group, project = from_instance_format(hgp) + else: + hub = group = project = None job = job_cls( job_id=job_id, program_id=program_id, @@ -364,15 +368,14 @@ def program_run( params=params, final_status=self._final_status, image=image, - **self._job_kwargs + **self._job_kwargs, ) self._jobs[job_id] = job return {"id": job_id} def program_delete(self, program_id: str) -> None: """Delete the specified program.""" - if program_id not in self._programs: - raise RequestsApiError("Program not found", status_code=404) + self._get_program(program_id) del self._programs[program_id] def job_get(self, job_id): @@ -421,7 +424,8 @@ def set_program_visibility(self, program_id: str, public: bool) -> None: public: If ``True``, make the program visible to all. If ``False``, make the program visible to just your account. """ - self._programs[program_id]._is_public = public + program = self._get_program(program_id) + program._is_public = public def job_results(self, job_id): """Get the results of a program job.""" @@ -440,8 +444,51 @@ def job_delete(self, job_id): self._get_job(job_id) del self._jobs[job_id] + def _get_program(self, program_id): + """Get program.""" + if program_id not in self._programs: + raise RequestsApiError("Program not found", status_code=404) + return self._programs[program_id] + def _get_job(self, job_id): """Get job.""" if job_id not in self._jobs: raise RequestsApiError("Job not found", status_code=404) return self._jobs[job_id] + + @cloud_only + def list_backends(self): + """Return IBM Cloud backends""" + self._check_cloud_only() + return self._backend_client.backend_names + + @cloud_only + def backend_configuration(self, backend_name: str): + """Return the configuration of the IBM Cloud backend.""" + configs = self._backend_client.list_backends() + for conf in configs: + if conf["backend_name"] == backend_name: + return conf + raise ValueError(f"Backend {backend_name} not found.") + + @cloud_only + def backend_status(self, backend_name: str): + """Return the status of the IBM Cloud backend.""" + return self._backend_client.backend_status(backend_name) + + @cloud_only + def backend_properties(self, backend_name: str, datetime=None): + """Return the properties of the IBM Cloud backend.""" + if datetime: + raise NotImplementedError("'datetime' is not supported with cloud runtime.") + return self._backend_client.backend_properties(backend_name) + + @cloud_only + def backend_pulse_defaults(self, backend_name: str): + """Return the pulse defaults of the IBM Cloud backend.""" + return self._backend_client.backend_pulse_defaults(backend_name) + + @cloud_only + def _check_cloud_only(self): + if self._auth_type != "cloud": + raise ValueError("A backend method is called by a legacy client!") diff --git a/test/mock/fake_runtime_service.py b/test/mock/fake_runtime_service.py new file mode 100644 index 0000000000..cdd639b92b --- /dev/null +++ b/test/mock/fake_runtime_service.py @@ -0,0 +1,126 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Context managers for using with IBM Provider unit tests.""" + +from typing import Dict +from collections import OrderedDict +from unittest import mock + +from qiskit_ibm_runtime.ibm_runtime_service import IBMRuntimeService +from qiskit_ibm_runtime.hub_group_project import HubGroupProject +from qiskit_ibm_runtime.api.client_parameters import ClientParameters +from qiskit_ibm_runtime.api.clients import AuthClient + +from .fake_account_client import BaseFakeAccountClient +from .fake_runtime_client import BaseFakeRuntimeClient + + +class FakeRuntimeService(IBMRuntimeService): + """Creates an IBMRuntimeService instance with mocked hub/group/project. + + By default there are 2 h/g/p - `hub0/group0/project0` and `hub1/group1/project1`. + Each h/g/p has 2 backends - `common_backend` and `unique_backend_`. + + There are a few test options available, via the `test_options` input: + + - num_hgps: Number of hub/group/project. + - account_client: Custom account (backend) client to use. + """ + + DEFAULT_HGPS = ["hub0/group0/project0", "hub1/group1/project1"] + DEFAULT_COMMON_BACKEND = "common_backend" + DEFAULT_UNIQUE_BACKEND_PREFIX = "unique_backend_" + + def __init__(self, *args, **kwargs): + test_options = kwargs.pop("test_options", {}) + self._test_num_hgps = test_options.get("num_hgps", 2) + self._fake_account_client = test_options.get("account_client") + + with mock.patch( + "qiskit_ibm_runtime.ibm_runtime_service.RuntimeClient", + new=BaseFakeRuntimeClient, + ): + super().__init__(*args, **kwargs) + + def _authenticate_legacy_account(self, client_params: ClientParameters): + """Mock authentication.""" + return FakeAuthClient() + + def _initialize_hgps( + self, + auth_client: AuthClient, + ) -> Dict: + """Mock hgp initialization.""" + + hgps = OrderedDict() + + for idx in range(self._test_num_hgps): + hgp_name = self.DEFAULT_HGPS[idx] + + hgp_params = ClientParameters( + auth_type="legacy", + token="some_token", + url="some_url", + instance=hgp_name, + ) + hgp = HubGroupProject(client_params=hgp_params, instance=hgp_name) + fake_account_client = self._fake_account_client + if not fake_account_client: + specs = [ + {"configuration": {"backend_name": self.DEFAULT_COMMON_BACKEND}}, + { + "configuration": { + "backend_name": self.DEFAULT_UNIQUE_BACKEND_PREFIX + + str(idx) + } + }, + ] + fake_account_client = BaseFakeAccountClient(specs=specs, hgp=hgp_name) + hgp._api_client = fake_account_client + hgps[hgp_name] = hgp + + return hgps + + def _discover_cloud_backends(self): + """Mock discovery cloud backends.""" + if not self._fake_account_client: + specs = [{"configuration": {"backend_name": self.DEFAULT_COMMON_BACKEND}}] + for idx in range(self._test_num_hgps): + specs.append( + { + "configuration": { + "backend_name": self.DEFAULT_UNIQUE_BACKEND_PREFIX + + str(idx) + } + } + ) + self._fake_account_client = BaseFakeAccountClient(specs=specs) + + test_options = { + "backend_client": self._fake_account_client, + "auth_type": "cloud", + } + self._api_client = BaseFakeRuntimeClient(test_options=test_options) + return super()._discover_cloud_backends() + + +class FakeAuthClient: + """Fake auth client.""" + + def current_service_urls(self): + """Return service urls.""" + return {"http": "legacy_api_url", "services": {"runtime": "legacy_runtime_url"}} + + def current_access_token(self): + """Return access token.""" + return "some_token" diff --git a/test/http_server.py b/test/mock/http_server.py similarity index 100% rename from test/http_server.py rename to test/mock/http_server.py diff --git a/test/proxy_server.py b/test/mock/proxy_server.py similarity index 93% rename from test/proxy_server.py rename to test/mock/proxy_server.py index 93f304e433..3f95d5abd0 100644 --- a/test/proxy_server.py +++ b/test/mock/proxy_server.py @@ -52,10 +52,10 @@ def stop(self): @contextmanager -def use_proxies(hgp, proxies): +def use_proxies(service, proxies): """Context manager to set and restore proxies setting.""" try: - hgp.credentials.proxies = {"urls": proxies} + service._client_params.proxies = {"urls": proxies} yield finally: - hgp.credentials.proxies = None + service._client_params.proxies = None diff --git a/test/ibm/runtime/ws_handler.py b/test/mock/ws_handler.py similarity index 100% rename from test/ibm/runtime/ws_handler.py rename to test/mock/ws_handler.py diff --git a/test/ws_server.py b/test/mock/ws_server.py similarity index 100% rename from test/ws_server.py rename to test/mock/ws_server.py diff --git a/test/test_account.py b/test/test_account.py new file mode 100644 index 0000000000..b8966d10e5 --- /dev/null +++ b/test/test_account.py @@ -0,0 +1,295 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for the account functions.""" + +import uuid +import logging +import os +from unittest import skipIf + +from qiskit_ibm_runtime.accounts.account import CLOUD_API_URL, LEGACY_API_URL +from qiskit_ibm_runtime.exceptions import IBMInputValueError + +from .ibm_test_case import IBMTestCase +from .mock.fake_runtime_service import FakeRuntimeService +from .utils.account import get_qiskitrc_contents, custom_qiskitrc, no_envs, custom_envs + + +# NamedTemporaryFiles not support in Windows +@skipIf(os.name == "nt", "Test not supported in Windows") +class TestEnableAccount(IBMTestCase): + """Tests for IBMRuntimeService enable account.""" + + def test_enable_account_by_name(self): + """Test initializing account by name.""" + name = "foo" + token = uuid.uuid4().hex + with custom_qiskitrc(name=name, token=token): + service = FakeRuntimeService(name=name) + + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + + def test_enable_account_by_auth(self): + """Test initializing account by auth.""" + for auth in ["cloud", "legacy"]: + with self.subTest(auth=auth), no_envs(["QISKIT_IBM_API_TOKEN"]): + token = uuid.uuid4().hex + with custom_qiskitrc(auth=auth, token=token): + service = FakeRuntimeService(auth=auth) + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + + def test_enable_account_by_token_url(self): + """Test initializing account by token or url.""" + token = uuid.uuid4().hex + subtests = [ + {"token": token}, + {"url": "some_url"}, + {"token": token, "url": "some_url"}, + ] + for param in subtests: + with self.subTest(param=param): + with self.assertRaises(ValueError): + _ = FakeRuntimeService(**param) + + def test_enable_account_by_name_and_other(self): + """Test initializing account by name and other.""" + subtests = [ + {"auth": "cloud"}, + {"token": "some_token"}, + {"url": "some_url"}, + {"auth": "cloud", "token": "some_token", "url": "some_url"}, + ] + + name = "foo" + token = uuid.uuid4().hex + for param in subtests: + with self.subTest(param=param), custom_qiskitrc(name=name, token=token): + with self.assertLogs("qiskit_ibm_runtime", logging.WARNING) as logged: + service = FakeRuntimeService(name=name, **param) + + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + self.assertIn("are ignored", logged.output[0]) + + def test_enable_cloud_account_by_auth_token_url(self): + """Test initializing cloud account by auth, token, url.""" + # Enable account will fail due to missing CRN. + urls = [None, "some_url"] + for url in urls: + with self.subTest(url=url), no_envs(["QISKIT_IBM_API_TOKEN"]): + token = uuid.uuid4().hex + with self.assertRaises(IBMInputValueError) as err: + _ = FakeRuntimeService(auth="cloud", token=token, url=url) + self.assertIn("instance", str(err.exception)) + + def test_enable_legacy_account_by_auth_token_url(self): + """Test initializing legacy account by auth, token, url.""" + urls = [(None, LEGACY_API_URL), ("some_url", "some_url")] + for url, expected in urls: + with self.subTest(url=url), no_envs(["QISKIT_IBM_API_TOKEN"]): + token = uuid.uuid4().hex + service = FakeRuntimeService(auth="legacy", token=token, url=url) + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + self.assertEqual(service._account.url, expected) + + def test_enable_account_by_auth_url(self): + """Test initializing legacy account by auth, token, url.""" + subtests = ["legacy", "cloud"] + for auth in subtests: + with self.subTest(auth=auth): + token = uuid.uuid4().hex + with custom_qiskitrc(auth=auth, token=token), no_envs( + ["QISKIT_IBM_API_TOKEN"] + ): + with self.assertLogs( + "qiskit_ibm_runtime", logging.WARNING + ) as logged: + service = FakeRuntimeService(auth=auth, url="some_url") + + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + expected = CLOUD_API_URL if auth == "cloud" else LEGACY_API_URL + self.assertEqual(service._account.url, expected) + self.assertIn("url", logged.output[0]) + + def test_enable_account_by_only_auth(self): + """Test initializing account with single saved account.""" + subtests = ["legacy", "cloud"] + for auth in subtests: + with self.subTest(auth=auth): + token = uuid.uuid4().hex + with custom_qiskitrc(auth=auth, token=token), no_envs( + ["QISKIT_IBM_API_TOKEN"] + ): + service = FakeRuntimeService() + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + expected = CLOUD_API_URL if auth == "cloud" else LEGACY_API_URL + self.assertEqual(service._account.url, expected) + self.assertEqual(service._account.auth, auth) + + def test_enable_account_both_auth(self): + """Test initializing account with both saved types.""" + token = uuid.uuid4().hex + contents = get_qiskitrc_contents(auth="cloud", token=token) + contents.update(get_qiskitrc_contents(auth="legacy", token=uuid.uuid4().hex)) + with custom_qiskitrc(contents=contents), no_envs(["QISKIT_IBM_API_TOKEN"]): + service = FakeRuntimeService() + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + self.assertEqual(service._account.url, CLOUD_API_URL) + self.assertEqual(service._account.auth, "cloud") + + def test_enable_account_by_env_auth(self): + """Test initializing account by environment variable and auth.""" + subtests = ["legacy", "cloud", None] + for auth in subtests: + with self.subTest(auth=auth): + token = uuid.uuid4().hex + url = uuid.uuid4().hex + envs = { + "QISKIT_IBM_API_TOKEN": token, + "QISKIT_IBM_API_URL": url, + "QISKIT_IBM_INSTANCE": "my_crn", + } + with custom_envs(envs): + service = FakeRuntimeService(auth=auth) + + self.assertTrue(service._account) + self.assertEqual(service._account.token, token) + self.assertEqual(service._account.url, url) + auth = auth or "cloud" + self.assertEqual(service._account.auth, auth) + + def test_enable_account_by_env_token_url(self): + """Test initializing account by environment variable and extra.""" + token = uuid.uuid4().hex + url = uuid.uuid4().hex + envs = { + "QISKIT_IBM_API_TOKEN": token, + "QISKIT_IBM_API_URL": url, + "QISKIT_IBM_INSTANCE": "my_crn", + } + subtests = [{"token": token}, {"url": url}, {"token": token, "url": url}] + for extra in subtests: + with self.subTest(extra=extra): + with custom_envs(envs) as _, self.assertRaises(ValueError) as err: + _ = FakeRuntimeService(**extra) + self.assertIn("token", str(err.exception)) + + def test_enable_account_bad_name(self): + """Test initializing account by bad name.""" + name = "phantom" + with custom_qiskitrc() as _, self.assertRaises(ValueError) as err: + _ = FakeRuntimeService(name=name) + self.assertIn(name, str(err.exception)) + + def test_enable_account_bad_auth(self): + """Test initializing account by bad name.""" + auth = "phantom" + with custom_qiskitrc() as _, self.assertRaises(ValueError) as err: + _ = FakeRuntimeService(auth=auth) + self.assertIn("auth", str(err.exception)) + + def test_enable_account_by_name_pref(self): + """Test initializing account by name and preferences.""" + name = "foo" + subtests = [ + {"proxies": "foo"}, + {"verify": False}, + {"instance": "bar"}, + {"proxies": "foo", "verify": False, "instance": "bar"}, + ] + for extra in subtests: + with self.subTest(extra=extra): + with custom_qiskitrc(name=name, verify=True, proxies="some proxies"): + service = FakeRuntimeService(name=name, **extra) + self.assertTrue(service._account) + self._verify_prefs(extra, service._account) + + def test_enable_account_by_auth_pref(self): + """Test initializing account by auth and preferences.""" + subtests = [ + {"proxies": "foo"}, + {"verify": False}, + {"instance": "bar"}, + {"proxies": "foo", "verify": False, "instance": "bar"}, + ] + for auth in ["cloud", "legacy"]: + for extra in subtests: + with self.subTest(auth=auth, extra=extra), custom_qiskitrc( + auth=auth, verify=True, proxies="some proxies" + ), no_envs(["QISKIT_IBM_API_TOKEN"]): + service = FakeRuntimeService(auth=auth, **extra) + self.assertTrue(service._account) + self._verify_prefs(extra, service._account) + + def test_enable_account_by_env_pref(self): + """Test initializing account by environment variable and preferences.""" + subtests = [ + {"proxies": "foo"}, + {"verify": False}, + {"instance": "bar"}, + {"proxies": "foo", "verify": False, "instance": "bar"}, + ] + for extra in subtests: + with self.subTest(extra=extra): + token = uuid.uuid4().hex + url = uuid.uuid4().hex + envs = { + "QISKIT_IBM_API_TOKEN": token, + "QISKIT_IBM_API_URL": url, + "QISKIT_IBM_INSTANCE": "my_crn", + } + with custom_envs(envs): + service = FakeRuntimeService(**extra) + + self.assertTrue(service._account) + self._verify_prefs(extra, service._account) + + def test_enable_account_by_name_input_instance(self): + """Test initializing account by name and input instance.""" + name = "foo" + instance = uuid.uuid4().hex + with custom_qiskitrc(name=name, instance=""): + service = FakeRuntimeService(name=name, instance=instance) + self.assertTrue(service._account) + self.assertEqual(service._account.instance, instance) + + def test_enable_account_by_auth_input_instance(self): + """Test initializing account by auth and input instance.""" + instance = uuid.uuid4().hex + with custom_qiskitrc(auth="cloud", instance=""): + service = FakeRuntimeService(auth="cloud", instance=instance) + self.assertTrue(service._account) + self.assertEqual(service._account.instance, instance) + + def test_enable_account_by_env_input_instance(self): + """Test initializing account by env and input instance.""" + instance = uuid.uuid4().hex + envs = {"QISKIT_IBM_API_TOKEN": "some_token", "QISKIT_IBM_API_URL": "some_url"} + with custom_envs(envs): + service = FakeRuntimeService(auth="cloud", instance=instance) + self.assertTrue(service._account) + self.assertEqual(service._account.instance, instance) + + def _verify_prefs(self, prefs, account): + if "proxies" in prefs: + self.assertEqual(account.proxies, prefs["proxies"]) + if "verify" in prefs: + self.assertEqual(account.verify, prefs["verify"]) + if "instance" in prefs: + self.assertEqual(account.instance, prefs["instance"]) diff --git a/test/ibm/test_account_client.py b/test/test_account_client.py similarity index 74% rename from test/ibm/test_account_client.py rename to test/test_account_client.py index a1d381d9ed..97399c7719 100644 --- a/test/ibm/test_account_client.py +++ b/test/test_account_client.py @@ -14,46 +14,22 @@ import re -from qiskit.circuit import ClassicalRegister, QuantumCircuit, QuantumRegister from qiskit_ibm_runtime.api.clients import AccountClient, AuthClient from qiskit_ibm_runtime.api.exceptions import ApiError, RequestsApiError +from qiskit_ibm_runtime.api.client_parameters import ClientParameters -from ..ibm_test_case import IBMTestCase -from ..decorators import requires_qe_access, requires_provider -from ..contextmanagers import custom_envs, no_envs -from ..http_server import SimpleServer, ClientErrorHandler +from .ibm_test_case import IBMTestCase +from .mock.http_server import SimpleServer, ClientErrorHandler +from .utils.account import custom_envs, no_envs +from .utils.decorators import requires_qe_access class TestAccountClient(IBMTestCase): """Tests for AccountClient.""" - @classmethod - @requires_provider - def setUpClass(cls, service, hub, group, project): - """Initial class level setup.""" - # pylint: disable=arguments-differ - super().setUpClass() - cls.service = service - cls.hub = hub - cls.group = group - cls.project = project - def setUp(self): """Initial test setup.""" super().setUp() - qr = QuantumRegister(2) - cr = ClassicalRegister(2) - self.qc1 = QuantumCircuit(qr, cr, name="qc1") - self.qc2 = QuantumCircuit(qr, cr, name="qc2") - self.qc1.h(qr) - self.qc2.h(qr[0]) - self.qc2.cx(qr[0], qr[1]) - self.qc1.measure(qr[0], cr[0]) - self.qc1.measure(qr[1], cr[1]) - self.qc2.measure(qr[0], cr[0]) - self.qc2.measure(qr[1], cr[1]) - self.seed = 73846087 - self.fake_server = None def tearDown(self) -> None: @@ -65,7 +41,10 @@ def tearDown(self) -> None: def _get_client(self): """Helper for instantiating an AccountClient.""" # pylint: disable=no-value-for-parameter - return AccountClient(self.service._default_hgp.credentials) + params = ClientParameters( + auth_type="legacy", url=SimpleServer.URL, token="foo", instance="h/g/p" + ) + return AccountClient(params) def test_custom_client_app_header(self): """Check custom client application header.""" @@ -90,7 +69,7 @@ def test_client_error(self): client = self._get_client() self.fake_server = SimpleServer(handler_class=ClientErrorHandler) self.fake_server.start() - client.account_api.session.base_url = SimpleServer.URL + # client.account_api.session.base_url = SimpleServer.URL sub_tests = [ {"error": "Bad client input"}, @@ -114,7 +93,7 @@ class TestAuthClient(IBMTestCase): @requires_qe_access def test_valid_login(self, qe_token, qe_url): """Test valid authentication.""" - client = AuthClient(qe_token, qe_url) + client = self._init_auth_client(qe_token, qe_url) self.assertTrue(client.access_token) @requires_qe_access @@ -122,33 +101,33 @@ def test_url_404(self, qe_token, qe_url): """Test login against a 404 URL""" url_404 = re.sub(r"/api.*$", "/api/TEST_404", qe_url) with self.assertRaises(ApiError): - _ = AuthClient(qe_token, url_404) + _ = self._init_auth_client(qe_token, url_404) @requires_qe_access def test_invalid_token(self, qe_token, qe_url): """Test login using invalid token.""" qe_token = "INVALID_TOKEN" with self.assertRaises(ApiError): - _ = AuthClient(qe_token, qe_url) + _ = self._init_auth_client(qe_token, qe_url) @requires_qe_access def test_url_unreachable(self, qe_token, qe_url): """Test login against an invalid (malformed) URL.""" qe_url = "INVALID_URL" with self.assertRaises(ApiError): - _ = AuthClient(qe_token, qe_url) + _ = self._init_auth_client(qe_token, qe_url) @requires_qe_access def test_api_version(self, qe_token, qe_url): """Check the version of the QX API.""" - client = AuthClient(qe_token, qe_url) + client = self._init_auth_client(qe_token, qe_url) version = client.api_version() self.assertIsNotNone(version) @requires_qe_access def test_user_urls(self, qe_token, qe_url): """Check the user urls of the QX API.""" - client = AuthClient(qe_token, qe_url) + client = self._init_auth_client(qe_token, qe_url) user_urls = client.user_urls() self.assertIsNotNone(user_urls) self.assertTrue("http" in user_urls and "ws" in user_urls) @@ -156,7 +135,7 @@ def test_user_urls(self, qe_token, qe_url): @requires_qe_access def test_user_hubs(self, qe_token, qe_url): """Check the user hubs of the QX API.""" - client = AuthClient(qe_token, qe_url) + client = self._init_auth_client(qe_token, qe_url) user_hubs = client.user_hubs() self.assertIsNotNone(user_hubs) for user_hub in user_hubs: @@ -164,3 +143,8 @@ def test_user_hubs(self, qe_token, qe_url): self.assertTrue( "hub" in user_hub and "group" in user_hub and "project" in user_hub ) + + def _init_auth_client(self, token, url): + """Return an AuthClient.""" + params = ClientParameters(auth_type="legacy", token=token, url=url) + return AuthClient(params) diff --git a/test/test_backend_retrieval.py b/test/test_backend_retrieval.py new file mode 100644 index 0000000000..29f03bba49 --- /dev/null +++ b/test/test_backend_retrieval.py @@ -0,0 +1,240 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Backends Filtering Test.""" + +from qiskit.test.mock.backends import FakeLima +from qiskit.providers.exceptions import QiskitBackendNotFoundError + +from .ibm_test_case import IBMTestCase +from .mock.fake_account_client import BaseFakeAccountClient +from .mock.fake_runtime_service import FakeRuntimeService +from .utils.decorators import run_legacy_and_cloud_fake + + +class TestBackendFilters(IBMTestCase): + """Qiskit Backend Filtering Tests.""" + + @run_legacy_and_cloud_fake + def test_no_filter(self, service): + """Test no filtering.""" + # FakeRuntimeService by default creates 3 backends. + backend_name = [back.name() for back in service.backends()] + self.assertEqual(len(backend_name), 3) + + @run_legacy_and_cloud_fake + def test_filter_by_name(self, service): + """Test filtering by name.""" + for name in [ + FakeRuntimeService.DEFAULT_COMMON_BACKEND, + FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "0", + ]: + with self.subTest(name=name): + backend_name = [back.name() for back in service.backends(name=name)] + self.assertEqual(len(backend_name), 1) + + def test_filter_by_instance_legacy(self): + """Test filtering by instance.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + for hgp in FakeRuntimeService.DEFAULT_HGPS: + with self.subTest(hgp=hgp): + backends = service.backends(instance=hgp) + backend_name = [back.name() for back in backends] + self.assertEqual(len(backend_name), 2) + for back in backends: + self.assertEqual(back._api_client.hgp, hgp) + + def test_filter_config_properties(self): + """Test filtering by configuration properties.""" + n_qubits = 5 + fake_backends = [ + self._get_specs(n_qubits=n_qubits, local=False), + self._get_specs(n_qubits=n_qubits * 2, local=False), + self._get_specs(n_qubits=n_qubits, local=True), + ] + + services = self._get_services(fake_backends) + for service in services: + with self.subTest(service=service.auth): + filtered_backends = service.backends(n_qubits=n_qubits, local=False) + self.assertTrue(len(filtered_backends), 1) + self.assertEqual( + n_qubits, filtered_backends[0].configuration().n_qubits + ) + self.assertFalse(filtered_backends[0].configuration().local) + + def test_filter_status_dict(self): + """Test filtering by dictionary of mixed status/configuration properties.""" + fake_backends = [ + self._get_specs(operational=True, simulator=True), + self._get_specs(operational=True, simulator=True), + self._get_specs(operational=True, simulator=False), + self._get_specs(operational=False, simulator=False), + ] + + services = self._get_services(fake_backends) + for service in services: + with self.subTest(service=service.auth): + filtered_backends = service.backends( + operational=True, # from status + simulator=True, # from configuration + ) + self.assertTrue(len(filtered_backends), 2) + for backend in filtered_backends: + self.assertTrue(backend.status().operational) + self.assertTrue(backend.configuration().simulator) + + def test_filter_config_callable(self): + """Test filtering by lambda function on configuration properties.""" + n_qubits = 5 + fake_backends = [ + self._get_specs(n_qubits=n_qubits), + self._get_specs(n_qubits=n_qubits * 2), + self._get_specs(n_qubits=n_qubits - 1), + ] + + services = self._get_services(fake_backends) + for service in services: + with self.subTest(service=service.auth): + filtered_backends = service.backends( + filters=lambda x: (x.configuration().n_qubits >= 5) + ) + self.assertTrue(len(filtered_backends), 2) + for backend in filtered_backends: + self.assertGreaterEqual(backend.configuration().n_qubits, n_qubits) + + def test_filter_least_busy(self): + """Test filtering by least busy function.""" + default_stat = {"pending_jobs": 1, "operational": True, "status_msg": "active"} + fake_backends = [ + self._get_specs( + **{**default_stat, "backend_name": "bingo", "pending_jobs": 5} + ), + self._get_specs(**{**default_stat, "pending_jobs": 7}), + self._get_specs(**{**default_stat, "operational": False}), + self._get_specs(**{**default_stat, "status_msg": "internal"}), + ] + + services = self._get_services(fake_backends) + for service in services: + with self.subTest(service=service.auth): + backend = service.least_busy() + self.assertEqual(backend.name(), "bingo") + + def test_filter_min_num_qubits(self): + """Test filtering by minimum number of qubits.""" + n_qubits = 5 + fake_backends = [ + self._get_specs(n_qubits=n_qubits), + self._get_specs(n_qubits=n_qubits * 2), + self._get_specs(n_qubits=n_qubits - 1), + ] + + services = self._get_services(fake_backends) + for service in services: + with self.subTest(service=service.auth): + filtered_backends = service.backends(min_num_qubits=n_qubits) + self.assertTrue(len(filtered_backends), 2) + for backend in filtered_backends: + self.assertGreaterEqual(backend.configuration().n_qubits, n_qubits) + + def test_filter_by_hgp(self): + """Test filtering by hub/group/project.""" + num_backends = 3 + test_options = { + "account_client": BaseFakeAccountClient(num_backends=num_backends), + "num_hgps": 2, + } + legacy_service = FakeRuntimeService( + auth="legacy", + token="my_token", + instance="my_instance", + test_options=test_options, + ) + backends = legacy_service.backends(instance="hub0/group0/project0") + self.assertEqual(len(backends), num_backends) + + def _get_specs(self, **kwargs): + """Get the backend specs to pass to the fake account client.""" + specs = {"configuration": {}, "status": {}} + status_keys = FakeLima().status().to_dict() + status_keys.pop("backend_name") # name is in both config and status + status_keys = list(status_keys.keys()) + for key, val in kwargs.items(): + if key in status_keys: + specs["status"][key] = val + else: + specs["configuration"][key] = val + return specs + + def _get_services(self, fake_backends): + """Get both cloud and legacy services initialized with fake backends.""" + test_options = {"account_client": BaseFakeAccountClient(specs=fake_backends)} + legacy_service = FakeRuntimeService( + auth="legacy", + token="my_token", + instance="my_instance", + test_options=test_options, + ) + cloud_service = FakeRuntimeService( + auth="cloud", + token="my_token", + instance="my_instance", + test_options=test_options, + ) + return [legacy_service, cloud_service] + + +class TestGetBackend(IBMTestCase): + """Test getting a backend via legacy api.""" + + def test_get_common_backend(self): + """Test getting a backend that is in default and non-default hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = service.get_backend(FakeRuntimeService.DEFAULT_COMMON_BACKEND) + self.assertEqual(backend._api_client.hgp, list(service._hgps.keys())[0]) + + def test_get_unique_backend_default_hgp(self): + """Test getting a backend in the default hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend_name = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "0" + backend = service.get_backend(backend_name) + self.assertEqual(backend._api_client.hgp, list(service._hgps.keys())[0]) + + def test_get_unique_backend_non_default_hgp(self): + """Test getting a backend in the non default hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend_name = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "1" + backend = service.get_backend(backend_name) + self.assertEqual(backend._api_client.hgp, list(service._hgps.keys())[1]) + + def test_get_phantom_backend(self): + """Test getting a phantom backend.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + with self.assertRaises(QiskitBackendNotFoundError): + service.get_backend("phantom") + + def test_get_backend_by_hgp(self): + """Test getting a backend by hgp.""" + hgp = FakeRuntimeService.DEFAULT_HGPS[1] + backend_name = FakeRuntimeService.DEFAULT_COMMON_BACKEND + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = service.get_backend(backend_name, instance=hgp) + self.assertEqual(backend._api_client.hgp, hgp) + + def test_get_backend_by_bad_hgp(self): + """Test getting a backend not in hgp.""" + hgp = FakeRuntimeService.DEFAULT_HGPS[1] + backend_name = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "0" + service = FakeRuntimeService(auth="legacy", token="my_token") + with self.assertRaises(QiskitBackendNotFoundError): + _ = service.get_backend(backend_name, instance=hgp) diff --git a/test/ibm/test_serialization.py b/test/test_backend_serialization.py similarity index 81% rename from test/ibm/test_serialization.py rename to test/test_backend_serialization.py index 865f7c4a0a..4c7a7cb8c6 100644 --- a/test/ibm/test_serialization.py +++ b/test/test_backend_serialization.py @@ -16,35 +16,29 @@ import dateutil.parser -from ..decorators import requires_provider -from ..ibm_test_case import IBMTestCase +from .ibm_test_case import IBMTestCase +from .utils.decorators import requires_cloud_legacy_services, run_cloud_legacy_real class TestSerialization(IBMTestCase): """Test data serialization.""" @classmethod - @requires_provider - def setUpClass(cls, service, hub, group, project): + @requires_cloud_legacy_services + def setUpClass(cls, services): """Initial class level setup.""" # pylint: disable=arguments-differ super().setUpClass() - cls.service = service - cls.hub = hub - cls.group = group - cls.project = project - cls.sim_backend = service.get_backend( - "ibmq_qasm_simulator", hub=cls.hub, group=cls.group, project=cls.project - ) + cls.services = services + cls.instances = {} + for serv in services: + cls.instances[serv.auth] = serv._account.instance - def test_backend_configuration(self): + @run_cloud_legacy_real + def test_backend_configuration(self, service): """Test deserializing backend configuration.""" - backends = self.service.backends( - operational=True, - simulator=False, - hub=self.hub, - group=self.group, - project=self.project, + backends = service.backends( + operational=True, simulator=False, instance=self.instances[service.auth] ) # Known keys that look like a serialized complex number. @@ -67,14 +61,11 @@ def test_backend_configuration(self): backend.configuration().to_dict(), good_keys, good_keys_prefixes ) - def test_pulse_defaults(self): + @run_cloud_legacy_real + def test_pulse_defaults(self, service): """Test deserializing backend configuration.""" - backends = self.service.backends( - operational=True, - open_pulse=True, - hub=self.hub, - group=self.group, - project=self.project, + backends = service.backends( + operational=True, open_pulse=True, instance=self.instances[service.auth] ) if not backends: self.skipTest("Need pulse backends.") @@ -86,14 +77,11 @@ def test_pulse_defaults(self): with self.subTest(backend=backend): self._verify_data(backend.defaults().to_dict(), good_keys) - def test_backend_properties(self): + @run_cloud_legacy_real + def test_backend_properties(self, service): """Test deserializing backend properties.""" - backends = self.service.backends( - operational=True, - simulator=False, - hub=self.hub, - group=self.group, - project=self.project, + backends = service.backends( + operational=True, simulator=False, instance=self.instances[service.auth] ) # Known keys that look like a serialized object. diff --git a/test/test_basic_server_paths.py b/test/test_basic_server_paths.py new file mode 100644 index 0000000000..52be3c2e1a --- /dev/null +++ b/test/test_basic_server_paths.py @@ -0,0 +1,53 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests that hit all the basic server endpoints using both a public and premium h/g/p.""" + +from .ibm_test_case import IBMTestCase +from .utils.decorators import requires_multiple_hgps + + +class TestBasicServerPaths(IBMTestCase): + """Test the basic server endpoints using both a public and premium provider.""" + + @classmethod + @requires_multiple_hgps + def setUpClass(cls, service, open_hgp, premium_hgp): + # pylint: disable=arguments-differ + super().setUpClass() + cls.service = service # Dict[str, IBMRuntimeService] + cls.hgps = [open_hgp, premium_hgp] + + def test_device_properties_and_defaults(self): + """Test device properties and defaults.""" + for hgp in self.hgps: + with self.subTest(hgp=hgp): + pulse_backends = self.service.backends( + simulator=False, operational=True, instance=hgp + ) + if not pulse_backends: + raise self.skipTest( + "Skipping pulse test since no pulse backend " + 'found for "{}"'.format(hgp) + ) + + self.assertIsNotNone(pulse_backends[0].properties()) + self.assertIsNotNone(pulse_backends[0].defaults()) + + def test_device_status(self): + """Test device status.""" + for hgp in self.hgps: + with self.subTest(hgp=hgp): + backend = self.service.backends( + simulator=False, operational=True, instance=hgp + )[0] + self.assertTrue(backend.status()) diff --git a/test/ibm/test_registration.py b/test/test_client_parameters.py similarity index 61% rename from test/ibm/test_registration.py rename to test/test_client_parameters.py index 294dcc6139..1b294e7a8e 100644 --- a/test/ibm/test_registration.py +++ b/test/test_client_parameters.py @@ -10,41 +10,32 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Test the registration and credentials modules.""" +"""Tests for ClientParameters.""" -from requests_ntlm import HttpNtlmAuth +import uuid -from qiskit_ibm_runtime.credentials import ( - Credentials, -) -from ..ibm_test_case import IBMTestCase +from requests_ntlm import HttpNtlmAuth -IBM_TEMPLATE = "https://localhost/api/Hubs/{}/Groups/{}/Projects/{}" +from qiskit_ibm_runtime.api.client_parameters import ClientParameters +from qiskit_ibm_runtime.api.auth import CloudAuth, LegacyAuth -PROXIES = { - "urls": { - "http": "http://user:password@127.0.0.1:5678", - "https": "https://user:password@127.0.0.1:5678", - } -} +from .ibm_test_case import IBMTestCase -class TestCredentialsKwargs(IBMTestCase): - """Test for ``Credentials.connection_parameters()``.""" +class TestClientParameters(IBMTestCase): + """Test for ``ClientParameters``.""" def test_no_proxy_params(self) -> None: """Test when no proxy parameters are passed.""" no_params_expected_result = {"verify": True} - no_params_credentials = Credentials("dummy_token", "https://dummy_url") + no_params_credentials = self._get_client_params() result = no_params_credentials.connection_parameters() self.assertDictEqual(no_params_expected_result, result) def test_verify_param(self) -> None: """Test 'verify' arg is acknowledged.""" false_verify_expected_result = {"verify": False} - false_verify_credentials = Credentials( - "dummy_token", "https://dummy_url", verify=False - ) + false_verify_credentials = self._get_client_params(verify=False) result = false_verify_credentials.connection_parameters() self.assertDictEqual(false_verify_expected_result, result) @@ -52,9 +43,7 @@ def test_proxy_param(self) -> None: """Test using only proxy urls (no NTLM credentials).""" urls = {"http": "localhost:8080", "https": "localhost:8080"} proxies_only_expected_result = {"verify": True, "proxies": urls} - proxies_only_credentials = Credentials( - "dummy_token", "https://dummy_url", proxies={"urls": urls} - ) + proxies_only_credentials = self._get_client_params(proxies={"urls": urls}) result = proxies_only_credentials.connection_parameters() self.assertDictEqual(proxies_only_expected_result, result) @@ -71,8 +60,8 @@ def test_proxies_param_with_ntlm(self) -> None: "proxies": urls, "auth": HttpNtlmAuth("domain\\username", "password"), } - proxies_with_ntlm_credentials = Credentials( - "dummy_token", "https://dummy_url", proxies=proxies_with_ntlm_dict + proxies_with_ntlm_credentials = self._get_client_params( + proxies=proxies_with_ntlm_dict ) result = proxies_with_ntlm_credentials.connection_parameters() @@ -89,8 +78,8 @@ def test_malformed_proxy_param(self) -> None: """Test input with malformed nesting of the proxies dictionary.""" urls = {"http": "localhost:8080", "https": "localhost:8080"} malformed_nested_proxies_dict = {"proxies": urls} - malformed_nested_credentials = Credentials( - "dummy_token", "https://dummy_url", proxies=malformed_nested_proxies_dict + malformed_nested_credentials = self._get_client_params( + proxies=malformed_nested_proxies_dict ) # Malformed proxy entries should be ignored. @@ -106,10 +95,51 @@ def test_malformed_ntlm_params(self) -> None: "username_ntlm": 1234, "password_ntlm": 5678, } - malformed_ntlm_credentials = Credentials( - "dummy_token", "https://dummy_url", proxies=malformed_ntlm_credentials_dict + malformed_ntlm_credentials = self._get_client_params( + proxies=malformed_ntlm_credentials_dict ) # Should raise when trying to do username.split('\\', ) # in NTLM credentials due to int not facilitating 'split'. with self.assertRaises(AttributeError): _ = malformed_ntlm_credentials.connection_parameters() + + def test_auth_handler_legacy(self): + """Test getting legacy auth handler.""" + token = uuid.uuid4().hex + params = self._get_client_params(auth_type="legacy", token=token) + handler = params.get_auth_handler() + self.assertIsInstance(handler, LegacyAuth) + self.assertIn(token, handler.get_headers().values()) + + def test_auth_handler_cloud(self): + """Test getting cloud auth handler.""" + token = uuid.uuid4().hex + instance = uuid.uuid4().hex + params = self._get_client_params( + auth_type="cloud", token=token, instance=instance + ) + handler = params.get_auth_handler() + self.assertIsInstance(handler, CloudAuth) + self.assertIn(f"apikey {token}", handler.get_headers().values()) + self.assertIn(instance, handler.get_headers().values()) + + def _get_client_params( + self, + auth_type="legacy", + token="dummy_token", + url="https://dummy_url", + instance=None, + proxies=None, + verify=None, + ): + """Return a custom ClientParameters.""" + if verify is None: + verify = True + return ClientParameters( + auth_type=auth_type, + token=token, + url=url, + instance=instance, + proxies=proxies, + verify=verify, + ) diff --git a/test/test_data_serialization.py b/test/test_data_serialization.py new file mode 100644 index 0000000000..49a273d276 --- /dev/null +++ b/test/test_data_serialization.py @@ -0,0 +1,273 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for runtime data serialization.""" + +import json +import os +from unittest import skipIf +import subprocess +import tempfile +import warnings +from datetime import datetime +import numpy as np +import scipy.sparse + +from qiskit.algorithms.optimizers import ( + ADAM, + GSLS, + IMFIL, + SPSA, + QNSPSA, + SNOBFIT, + L_BFGS_B, + NELDER_MEAD, +) +from qiskit.result import Result +from qiskit.circuit import Parameter, QuantumCircuit +from qiskit.test.reference_circuits import ReferenceCircuits +from qiskit.circuit.library import EfficientSU2 +from qiskit.opflow import ( + PauliSumOp, + MatrixOp, + PauliOp, + CircuitOp, + EvolvedOp, + TaperedPauliSumOp, + Z2Symmetries, + I, + X, + Y, + Z, + StateFn, + CircuitStateFn, + DictStateFn, + VectorStateFn, + OperatorStateFn, + SparseVectorStateFn, + CVaRMeasurement, + ComposedOp, + SummedOp, + TensoredOp, +) +from qiskit.quantum_info import SparsePauliOp, Pauli, PauliTable, Statevector + +from qiskit_ibm_runtime.utils import RuntimeEncoder, RuntimeDecoder + +from .ibm_test_case import IBMTestCase +from .utils.serialization import ( + SerializableClass, + SerializableClassDecoder, + get_complex_types, +) +from .utils.program import run_program +from .mock.fake_runtime_service import FakeRuntimeService +from .mock.fake_runtime_client import CustomResultRuntimeJob + + +class TestDataSerialization(IBMTestCase): + """Class for testing runtime data serialization.""" + + def test_coder(self): + """Test runtime encoder and decoder.""" + result = Result( + backend_name="ibmqx2", + backend_version="1.1", + qobj_id="12345", + job_id="67890", + success=False, + results=[], + ) + + data = { + "string": "foo", + "float": 1.5, + "complex": 2 + 3j, + "array": np.array([[1, 2, 3], [4, 5, 6]]), + "result": result, + "sclass": SerializableClass("foo"), + } + encoded = json.dumps(data, cls=RuntimeEncoder) + decoded = json.loads(encoded, cls=RuntimeDecoder) + decoded["sclass"] = SerializableClass.from_json(decoded["sclass"]) + + decoded_result = decoded.pop("result") + data.pop("result") + + decoded_array = decoded.pop("array") + orig_array = data.pop("array") + + self.assertEqual(decoded, data) + self.assertIsInstance(decoded_result, Result) + self.assertTrue((decoded_array == orig_array).all()) + + def test_coder_qc(self): + """Test runtime encoder and decoder for circuits.""" + bell = ReferenceCircuits.bell() + unbound = EfficientSU2(num_qubits=4, reps=1, entanglement="linear") + subtests = (bell, unbound, [bell, unbound]) + for circ in subtests: + with self.subTest(circ=circ): + encoded = json.dumps(circ, cls=RuntimeEncoder) + self.assertIsInstance(encoded, str) + decoded = json.loads(encoded, cls=RuntimeDecoder) + if not isinstance(circ, list): + decoded = [decoded] + self.assertTrue( + all(isinstance(item, QuantumCircuit) for item in decoded) + ) + + def test_coder_operators(self): + """Test runtime encoder and decoder for operators.""" + x = Parameter("x") + y = x + 1 + qc = QuantumCircuit(1) + qc.h(0) + coeffs = np.array([1, 2, 3, 4, 5, 6]) + table = PauliTable.from_labels(["III", "IXI", "IYY", "YIZ", "XYZ", "III"]) + op = 2.0 * I ^ I + z2_symmetries = Z2Symmetries( + [Pauli("IIZI"), Pauli("ZIII")], + [Pauli("IIXI"), Pauli("XIII")], + [1, 3], + [-1, 1], + ) + isqrt2 = 1 / np.sqrt(2) + sparse = scipy.sparse.csr_matrix([[0, isqrt2, 0, isqrt2]]) + + subtests = ( + PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), + PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1]), coeff=y), + PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[1 + 2j]), coeff=3 - 2j), + PauliSumOp.from_list( + [("II", -1.052373245772859), ("IZ", 0.39793742484318045)] + ), + PauliSumOp(SparsePauliOp(table, coeffs), coeff=10), + MatrixOp(primitive=np.array([[0, -1j], [1j, 0]]), coeff=x), + PauliOp(primitive=Pauli("Y"), coeff=x), + CircuitOp(qc, coeff=x), + EvolvedOp(op, coeff=x), + TaperedPauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), z2_symmetries), + StateFn(qc, coeff=x), + CircuitStateFn(qc, is_measurement=True), + DictStateFn("1" * 3, is_measurement=True), + VectorStateFn(np.ones(2 ** 3, dtype=complex)), + OperatorStateFn(CircuitOp(QuantumCircuit(1))), + SparseVectorStateFn(sparse), + Statevector([1, 0]), + CVaRMeasurement(Z, 0.2), + ComposedOp([(X ^ Y ^ Z), (Z ^ X ^ Y ^ Z).to_matrix_op()]), + SummedOp([X ^ X * 2, Y ^ Y], 2), + TensoredOp([(X ^ Y), (Z ^ I)]), + (Z ^ Z) ^ (I ^ 2), + ) + for op in subtests: + with self.subTest(op=op): + encoded = json.dumps(op, cls=RuntimeEncoder) + self.assertIsInstance(encoded, str) + decoded = json.loads(encoded, cls=RuntimeDecoder) + self.assertEqual(op, decoded) + + @skipIf(os.name == "nt", "Test not supported on Windows") + def test_coder_optimizers(self): + """Test runtime encoder and decoder for optimizers.""" + subtests = ( + (ADAM, {"maxiter": 100, "amsgrad": True}), + (GSLS, {"maxiter": 50, "min_step_size": 0.01}), + (IMFIL, {"maxiter": 20}), + (SPSA, {"maxiter": 10, "learning_rate": 0.01, "perturbation": 0.1}), + (SNOBFIT, {"maxiter": 200, "maxfail": 20}), + (QNSPSA, {"fidelity": 123, "maxiter": 25, "resamplings": {1: 100, 2: 50}}), + # some SciPy optimizers only work with default arguments due to Qiskit/qiskit-terra#6682 + (L_BFGS_B, {}), + (NELDER_MEAD, {}), + ) + for opt_cls, settings in subtests: + with self.subTest(opt_cls=opt_cls): + optimizer = opt_cls(**settings) + encoded = json.dumps(optimizer, cls=RuntimeEncoder) + self.assertIsInstance(encoded, str) + decoded = json.loads(encoded, cls=RuntimeDecoder) + self.assertTrue(isinstance(decoded, opt_cls)) + for key, value in settings.items(): + self.assertEqual(decoded.settings[key], value) + + def test_encoder_datetime(self): + """Test encoding a datetime.""" + subtests = ( + {"datetime": datetime.now()}, + {"datetime": datetime(2021, 8, 4)}, + {"datetime": datetime.fromtimestamp(1326244364)}, + ) + for obj in subtests: + encoded = json.dumps(obj, cls=RuntimeEncoder) + self.assertIsInstance(encoded, str) + decoded = json.loads(encoded, cls=RuntimeDecoder) + self.assertEqual(decoded, obj) + + def test_encoder_callable(self): + """Test encoding a callable.""" + with warnings.catch_warnings(record=True) as warn_cm: + encoded = json.dumps({"fidelity": lambda x: x}, cls=RuntimeEncoder) + decoded = json.loads(encoded, cls=RuntimeDecoder) + self.assertIsNone(decoded["fidelity"]) + self.assertEqual(len(warn_cm), 1) + + def test_decoder_import(self): + """Test runtime decoder importing modules.""" + script = """ +import sys +import json +from qiskit_ibm_runtime import RuntimeDecoder +if __name__ == '__main__': + obj = json.loads(sys.argv[1], cls=RuntimeDecoder) + print(obj.__class__.__name__) +""" + temp_fp = tempfile.NamedTemporaryFile(mode="w", delete=False) + self.addCleanup(os.remove, temp_fp.name) + temp_fp.write(script) + temp_fp.close() + + subtests = ( + PauliSumOp(SparsePauliOp(Pauli("XYZX"), coeffs=[2]), coeff=3), + DictStateFn("1" * 3, is_measurement=True), + Statevector([1, 0]), + ) + for op in subtests: + with self.subTest(op=op): + encoded = json.dumps(op, cls=RuntimeEncoder) + self.assertIsInstance(encoded, str) + cmd = ["python", temp_fp.name, encoded] + proc = subprocess.run( + cmd, + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + universal_newlines=True, + check=True, + ) + self.assertIn(op.__class__.__name__, proc.stdout) + + def test_result_decoder(self): + """Test result decoder.""" + custom_result = get_complex_types() + job_cls = CustomResultRuntimeJob + job_cls.custom_result = custom_result + legacy_service = FakeRuntimeService(auth="legacy", token="some_token") + + sub_tests = [(SerializableClassDecoder, None), (None, SerializableClassDecoder)] + for result_decoder, decoder in sub_tests: + with self.subTest(decoder=decoder): + job = run_program( + service=legacy_service, job_classes=job_cls, decoder=result_decoder + ) + result = job.result(decoder=decoder) + self.assertIsInstance(result["serializable_class"], SerializableClass) diff --git a/test/test_integration_backend.py b/test/test_integration_backend.py new file mode 100644 index 0000000000..51a71b2176 --- /dev/null +++ b/test/test_integration_backend.py @@ -0,0 +1,96 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for backend functions using real runtime service.""" + +from unittest import SkipTest + +from .ibm_test_case import IBMTestCase +from .utils.decorators import ( + requires_cloud_legacy_services, + run_cloud_legacy_real, + requires_cloud_legacy_devices, +) + + +class TestIntegrationBackend(IBMTestCase): + """Integration tests for backend functions.""" + + @classmethod + @requires_cloud_legacy_services + def setUpClass(cls, services): + """Initial class level setup.""" + # pylint: disable=arguments-differ + super().setUpClass() + cls.services = services + + @run_cloud_legacy_real + def test_backends(self, service): + """Test getting all backends.""" + backends = service.backends() + self.assertTrue(backends) + backend_names = [back.name() for back in backends] + self.assertEqual(len(backend_names), len(set(backend_names))) + + @run_cloud_legacy_real + def test_get_backend(self, service): + """Test getting a backend.""" + backends = service.backends() + backend = service.get_backend(backends[0].name()) + self.assertTrue(backend) + + +class TestIBMBackend(IBMTestCase): + """Test ibm_backend module.""" + + @classmethod + @requires_cloud_legacy_devices + def setUpClass(cls, devices): + """Initial class level setup.""" + # pylint: disable=arguments-differ + super().setUpClass() + cls.devices = devices + + def test_backend_status(self): + """Check the status of a real chip.""" + for backend in self.devices: + with self.subTest(backend=backend.name()): + self.assertTrue(backend.status().operational) + + def test_backend_properties(self): + """Check the properties of calibration of a real chip.""" + for backend in self.devices: + with self.subTest(backend=backend.name()): + if backend.configuration().simulator: + raise SkipTest("Skip since simulator does not have properties.") + self.assertIsNotNone(backend.properties()) + + def test_backend_pulse_defaults(self): + """Check the backend pulse defaults of each backend.""" + for backend in self.devices: + with self.subTest(backend=backend.name()): + if backend.configuration().simulator: + raise SkipTest("Skip since simulator does not have defaults.") + self.assertIsNotNone(backend.defaults()) + + def test_backend_configuration(self): + """Check the backend configuration of each backend.""" + for backend in self.devices: + with self.subTest(backend=backend.name()): + self.assertIsNotNone(backend.configuration()) + + def test_backend_run(self): + """Check one cannot do backend.run""" + for backend in self.devices: + with self.subTest(backend=backend.name()): + with self.assertRaises(RuntimeError): + backend.run() diff --git a/test/test_integration_job.py b/test/test_integration_job.py new file mode 100644 index 0000000000..671e9e6e95 --- /dev/null +++ b/test/test_integration_job.py @@ -0,0 +1,663 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for job functions using real runtime service.""" + +import copy +import unittest +import uuid +import time +import random +from contextlib import suppress +from collections import defaultdict + +from qiskit.providers.jobstatus import JobStatus, JOB_FINAL_STATES +from qiskit.providers.exceptions import QiskitBackendNotFoundError +from qiskit.test.decorators import slow_test + +from qiskit_ibm_runtime.constants import API_TO_JOB_ERROR_MESSAGE +from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError +from qiskit_ibm_runtime.exceptions import ( + RuntimeDuplicateProgramError, + RuntimeJobFailureError, + RuntimeInvalidStateError, + RuntimeJobNotFound, +) + +from .ibm_test_case import IBMTestCase +from .utils.decorators import requires_cloud_legacy_services, run_cloud_legacy_real +from .utils.templates import RUNTIME_PROGRAM, RUNTIME_PROGRAM_METADATA, PROGRAM_PREFIX +from .utils.serialization import ( + get_complex_types, + SerializableClassDecoder, + SerializableClass, +) +from .mock.proxy_server import MockProxyServer, use_proxies + + +class TestIntegrationJob(IBMTestCase): + """Integration tests for job functions.""" + + @classmethod + @requires_cloud_legacy_services + def setUpClass(cls, services): + """Initial class level setup.""" + # pylint: disable=arguments-differ + super().setUpClass() + cls.services = services + metadata = copy.deepcopy(RUNTIME_PROGRAM_METADATA) + metadata["name"] = cls._get_program_name() + cls.program_ids = {} + cls.sim_backends = {} + cls.real_backends = {} + for service in services: + try: + prog_id = service.upload_program( + data=RUNTIME_PROGRAM, metadata=metadata + ) + cls.log.debug("Uploaded %s program %s", service.auth, prog_id) + cls.program_ids[service.auth] = prog_id + except RuntimeDuplicateProgramError: + pass + except IBMNotAuthorizedError: + raise unittest.SkipTest("No upload access.") + + cls.sim_backends[service.auth] = service.backends(simulator=True)[0].name() + + @classmethod + def tearDownClass(cls) -> None: + """Class level teardown.""" + super().tearDownClass() + with suppress(Exception): + for service in cls.services: + service.delete_program(cls.program_ids[service.auth]) + cls.log.debug( + "Deleted %s program %s", service.auth, cls.program_ids[service.auth] + ) + + def setUp(self) -> None: + """Test level setup.""" + super().setUp() + self.poll_time = 1 + self.to_delete = defaultdict(list) + self.to_cancel = defaultdict(list) + + def tearDown(self) -> None: + """Test level teardown.""" + super().tearDown() + # Delete programs + for service in self.services: + for prog in self.to_delete[service.auth]: + with suppress(Exception): + service.delete_program(prog) + + # Cancel and delete jobs. + for service in self.services: + for job in self.to_cancel[service.auth]: + with suppress(Exception): + job.cancel() + with suppress(Exception): + service.delete_job(job.job_id) + + @run_cloud_legacy_real + def test_run_program(self, service): + """Test running a program.""" + job = self._run_program(service, final_result="foo") + result = job.result() + self.assertEqual(JobStatus.DONE, job.status()) + self.assertEqual("foo", result) + + @slow_test + @run_cloud_legacy_real + def test_run_program_real_device(self, service): + """Test running a program.""" + device = self._get_real_device(service) + job = self._run_program(service, final_result="foo", backend=device) + result = job.result() + self.assertEqual(JobStatus.DONE, job.status()) + self.assertEqual("foo", result) + + @run_cloud_legacy_real + def test_run_program_failed(self, service): + """Test a failed program execution.""" + job = self._run_program(service, inputs={}) + job.wait_for_final_state() + job_result_raw = service._api_client.job_results(job.job_id) + self.assertEqual(JobStatus.ERROR, job.status()) + self.assertIn( + API_TO_JOB_ERROR_MESSAGE["FAILED"].format(job.job_id, job_result_raw), + job.error_message(), + ) + with self.assertRaises(RuntimeJobFailureError) as err_cm: + job.result() + self.assertIn("KeyError", str(err_cm.exception)) + + @run_cloud_legacy_real + def test_run_program_failed_ran_too_long(self, service): + """Test a program that failed since it ran longer than maximum execution time.""" + max_execution_time = 60 + inputs = {"iterations": 1, "sleep_per_iteration": 60} + program_id = self._upload_program( + service, max_execution_time=max_execution_time + ) + job = self._run_program(service, program_id=program_id, inputs=inputs) + + job.wait_for_final_state() + job_result_raw = service._api_client.job_results(job.job_id) + self.assertEqual(JobStatus.ERROR, job.status()) + self.assertIn( + API_TO_JOB_ERROR_MESSAGE["CANCELLED - RAN TOO LONG"].format( + job.job_id, job_result_raw + ), + job.error_message(), + ) + with self.assertRaises(RuntimeJobFailureError): + job.result() + + @run_cloud_legacy_real + def test_retrieve_job_queued(self, service): + """Test retrieving a queued job.""" + real_device = self._get_real_device(service) + _ = self._run_program(service, iterations=10, backend=real_device) + job = self._run_program(service, iterations=2, backend=real_device) + self._wait_for_status(job, JobStatus.QUEUED) + rjob = service.job(job.job_id) + self.assertEqual(job.job_id, rjob.job_id) + self.assertEqual(self.program_ids[service.auth], rjob.program_id) + + @run_cloud_legacy_real + def test_retrieve_job_running(self, service): + """Test retrieving a running job.""" + job = self._run_program(service, iterations=10) + self._wait_for_status(job, JobStatus.RUNNING) + rjob = service.job(job.job_id) + self.assertEqual(job.job_id, rjob.job_id) + self.assertEqual(self.program_ids[service.auth], rjob.program_id) + + @run_cloud_legacy_real + def test_retrieve_job_done(self, service): + """Test retrieving a finished job.""" + job = self._run_program(service) + job.wait_for_final_state() + rjob = service.job(job.job_id) + self.assertEqual(job.job_id, rjob.job_id) + self.assertEqual(self.program_ids[service.auth], rjob.program_id) + + @run_cloud_legacy_real + def test_retrieve_all_jobs(self, service): + """Test retrieving all jobs.""" + job = self._run_program(service) + rjobs = service.jobs() + found = False + for rjob in rjobs: + if rjob.job_id == job.job_id: + self.assertEqual(job.program_id, rjob.program_id) + self.assertEqual(job.inputs, rjob.inputs) + found = True + break + self.assertTrue(found, f"Job {job.job_id} not returned.") + + @run_cloud_legacy_real + def test_retrieve_jobs_limit(self, service): + """Test retrieving jobs with limit.""" + jobs = [] + for _ in range(3): + jobs.append(self._run_program(service)) + + rjobs = service.jobs(limit=2) + self.assertEqual(len(rjobs), 2) + job_ids = {job.job_id for job in jobs} + rjob_ids = {rjob.job_id for rjob in rjobs} + self.assertTrue(rjob_ids.issubset(job_ids)) + + @run_cloud_legacy_real + def test_retrieve_pending_jobs(self, service): + """Test retrieving pending jobs (QUEUED, RUNNING).""" + job = self._run_program(service, iterations=10) + self._wait_for_status(job, JobStatus.RUNNING) + rjobs = service.jobs(pending=True) + found = False + for rjob in rjobs: + if rjob.job_id == job.job_id: + self.assertEqual(job.program_id, rjob.program_id) + self.assertEqual(job.inputs, rjob.inputs) + found = True + break + self.assertTrue(found, f"Pending job {job.job_id} not retrieved.") + + @run_cloud_legacy_real + def test_retrieve_returned_jobs(self, service): + """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED).""" + job = self._run_program(service) + job.wait_for_final_state() + rjobs = service.jobs(pending=False) + found = False + for rjob in rjobs: + if rjob.job_id == job.job_id: + self.assertEqual(job.program_id, rjob.program_id) + self.assertEqual(job.inputs, rjob.inputs) + found = True + break + self.assertTrue(found, f"Returned job {job.job_id} not retrieved.") + + @run_cloud_legacy_real + def test_retrieve_jobs_by_program_id(self, service): + """Test retrieving jobs by Program ID.""" + program_id = self._upload_program(service) + job = self._run_program(service, program_id=program_id) + job.wait_for_final_state() + rjobs = service.jobs(program_id=program_id) + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs)) + + def test_jobs_filter_by_hgp(self): + """Test retrieving jobs by hgp.""" + service = [serv for serv in self.services if serv.auth == "legacy"][0] + default_hgp = list(service._hgps.keys())[0] + program_id = self._upload_program(service) + job = self._run_program(service, program_id=program_id) + job.wait_for_final_state() + rjobs = service.jobs(program_id=program_id, instance=default_hgp) + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs)) + + uuid_ = uuid.uuid4().hex + fake_hgp = f"{uuid_}/{uuid_}/{uuid_}" + rjobs = service.jobs(program_id=program_id, instance=fake_hgp) + self.assertFalse(rjobs) + + @run_cloud_legacy_real + def test_cancel_job_queued(self, service): + """Test canceling a queued job.""" + real_device = self._get_real_device(service) + _ = self._run_program(service, iterations=10, backend=real_device) + job = self._run_program(service, iterations=2, backend=real_device) + self._wait_for_status(job, JobStatus.QUEUED) + job.cancel() + self.assertEqual(job.status(), JobStatus.CANCELLED) + time.sleep(10) # Wait a bit for DB to update. + rjob = service.job(job.job_id) + self.assertEqual(rjob.status(), JobStatus.CANCELLED) + + @run_cloud_legacy_real + def test_cancel_job_running(self, service): + """Test canceling a running job.""" + job = self._run_program(service, iterations=3) + self._wait_for_status(job, JobStatus.RUNNING) + job.cancel() + self.assertEqual(job.status(), JobStatus.CANCELLED) + time.sleep(10) # Wait a bit for DB to update. + rjob = service.job(job.job_id) + self.assertEqual(rjob.status(), JobStatus.CANCELLED) + + @run_cloud_legacy_real + def test_cancel_job_done(self, service): + """Test canceling a finished job.""" + job = self._run_program(service) + job.wait_for_final_state() + with self.assertRaises(RuntimeInvalidStateError): + job.cancel() + + @run_cloud_legacy_real + def test_delete_job(self, service): + """Test deleting a job.""" + sub_tests = [JobStatus.RUNNING, JobStatus.DONE] + for status in sub_tests: + with self.subTest(status=status): + job = self._run_program(service, iterations=2) + self._wait_for_status(job, status) + service.delete_job(job.job_id) + with self.assertRaises(RuntimeJobNotFound): + service.job(job.job_id) + + @run_cloud_legacy_real + def test_delete_job_queued(self, service): + """Test deleting a queued job.""" + real_device = self._get_real_device(service) + _ = self._run_program(service, iterations=10, backend=real_device) + job = self._run_program(service, iterations=2, backend=real_device) + self._wait_for_status(job, JobStatus.QUEUED) + service.delete_job(job.job_id) + with self.assertRaises(RuntimeJobNotFound): + service.job(job.job_id) + + @run_cloud_legacy_real + def test_interim_result_callback(self, service): + """Test interim result callback.""" + + def result_callback(job_id, interim_result): + nonlocal final_it + final_it = interim_result["iteration"] + nonlocal callback_err + if job_id != job.job_id: + callback_err.append(f"Unexpected job ID: {job_id}") + if interim_result["interim_results"] != int_res: + callback_err.append(f"Unexpected interim result: {interim_result}") + + int_res = "foo" + final_it = 0 + callback_err = [] + iterations = 3 + job = self._run_program( + service, + iterations=iterations, + interim_results=int_res, + callback=result_callback, + ) + job.wait_for_final_state() + self.assertEqual(iterations - 1, final_it) + self.assertFalse(callback_err) + self.assertIsNotNone(job._ws_client._server_close_code) + + @run_cloud_legacy_real + def test_stream_results(self, service): + """Test stream_results method.""" + + def result_callback(job_id, interim_result): + nonlocal final_it + final_it = interim_result["iteration"] + nonlocal callback_err + if job_id != job.job_id: + callback_err.append(f"Unexpected job ID: {job_id}") + if interim_result["interim_results"] != int_res: + callback_err.append(f"Unexpected interim result: {interim_result}") + + int_res = "bar" + final_it = 0 + callback_err = [] + iterations = 3 + job = self._run_program(service, iterations=iterations, interim_results=int_res) + job.stream_results(result_callback) + job.wait_for_final_state() + self.assertEqual(iterations - 1, final_it) + self.assertFalse(callback_err) + self.assertIsNotNone(job._ws_client._server_close_code) + + @run_cloud_legacy_real + def test_stream_results_done(self, service): + """Test streaming interim results after job is done.""" + + def result_callback(job_id, interim_result): + # pylint: disable=unused-argument + nonlocal called_back + called_back = True + + called_back = False + job = self._run_program(service, interim_results="foobar") + job.wait_for_final_state() + job._status = JobStatus.RUNNING # Allow stream_results() + job.stream_results(result_callback) + time.sleep(2) + self.assertFalse(called_back) + self.assertIsNotNone(job._ws_client._server_close_code) + + @run_cloud_legacy_real + def test_retrieve_interim_results(self, service): + """Test retrieving interim results with API endpoint""" + int_res = "foo" + job = self._run_program(service, interim_results=int_res) + job.wait_for_final_state() + interim_results = job.interim_results() + self.assertIn(int_res, interim_results[0]) + + @run_cloud_legacy_real + def test_callback_error(self, service): + """Test error in callback method.""" + + def result_callback(job_id, interim_result): + # pylint: disable=unused-argument + if interim_result["iteration"] == 0: + raise ValueError("Kaboom!") + nonlocal final_it + final_it = interim_result["iteration"] + + final_it = 0 + iterations = 3 + with self.assertLogs("qiskit_ibm_runtime", level="WARNING") as err_cm: + job = self._run_program( + service, + iterations=iterations, + interim_results="foo", + callback=result_callback, + ) + job.wait_for_final_state() + + self.assertIn("Kaboom", ", ".join(err_cm.output)) + self.assertEqual(iterations - 1, final_it) + self.assertIsNotNone(job._ws_client._server_close_code) + + @run_cloud_legacy_real + def test_callback_cancel_job(self, service): + """Test canceling a running job while streaming results.""" + + def result_callback(job_id, interim_result): + # pylint: disable=unused-argument + nonlocal final_it + final_it = interim_result["iteration"] + + final_it = 0 + iterations = 3 + sub_tests = [JobStatus.QUEUED, JobStatus.RUNNING] + + for status in sub_tests: + with self.subTest(status=status): + if status == JobStatus.QUEUED: + _ = self._run_program(service, iterations=10) + + job = self._run_program( + service=service, + iterations=iterations, + interim_results="foo", + callback=result_callback, + ) + self._wait_for_status(job, status) + job.cancel() + time.sleep(3) # Wait for cleanup + self.assertIsNotNone(job._ws_client._server_close_code) + self.assertLess(final_it, iterations) + + @run_cloud_legacy_real + def test_final_result(self, service): + """Test getting final result.""" + final_result = get_complex_types() + job = self._run_program(service, final_result=final_result) + result = job.result(decoder=SerializableClassDecoder) + self.assertEqual(final_result, result) + + rresults = service.job(job.job_id).result(decoder=SerializableClassDecoder) + self.assertEqual(final_result, rresults) + + @run_cloud_legacy_real + def test_job_status(self, service): + """Test job status.""" + job = self._run_program(service, iterations=1) + time.sleep(random.randint(1, 5)) + self.assertTrue(job.status()) + + @run_cloud_legacy_real + def test_job_inputs(self, service): + """Test job inputs.""" + interim_results = get_complex_types() + inputs = {"iterations": 1, "interim_results": interim_results} + job = self._run_program(service, inputs=inputs) + self.assertEqual(inputs, job.inputs) + rjob = service.job(job.job_id) + rinterim_results = rjob.inputs["interim_results"] + self._assert_complex_types_equal(interim_results, rinterim_results) + + @run_cloud_legacy_real + def test_job_backend(self, service): + """Test job backend.""" + job = self._run_program(service) + self.assertEqual(self.sim_backends[service.auth], job.backend.name()) + + @run_cloud_legacy_real + def test_job_program_id(self, service): + """Test job program ID.""" + job = self._run_program(service) + self.assertEqual(self.program_ids[service.auth], job.program_id) + + @run_cloud_legacy_real + def test_wait_for_final_state(self, service): + """Test wait for final state.""" + job = self._run_program(service) + job.wait_for_final_state() + self.assertEqual(JobStatus.DONE, job.status()) + + @run_cloud_legacy_real + def test_logout(self, service): + """Test logout.""" + if service.auth == "cloud": + # TODO - re-enable when fixed + self.skipTest("Logout does not work for cloud") + service.logout() + # Make sure we can still do things. + self._upload_program(service) + _ = self._run_program(service) + + @run_cloud_legacy_real + def test_job_creation_date(self, service): + """Test job creation date.""" + job = self._run_program(service, iterations=1) + self.assertTrue(job.creation_date) + rjob = service.job(job.job_id) + self.assertTrue(rjob.creation_date) + rjobs = service.jobs(limit=2) + for rjob in rjobs: + self.assertTrue(rjob.creation_date) + + @run_cloud_legacy_real + def test_websocket_proxy(self, service): + """Test connecting to websocket via proxy.""" + + def result_callback(job_id, interim_result): # pylint: disable=unused-argument + nonlocal callback_called + callback_called = True + + MockProxyServer(self, self.log).start() + callback_called = False + + with use_proxies(service, MockProxyServer.VALID_PROXIES): + job = self._run_program(service, iterations=1, callback=result_callback) + job.wait_for_final_state() + + self.assertTrue(callback_called) + + @run_cloud_legacy_real + def test_websocket_proxy_invalid_port(self, service): + """Test connecting to websocket via invalid proxy port.""" + + def result_callback(job_id, interim_result): # pylint: disable=unused-argument + nonlocal callback_called + callback_called = True + + callback_called = False + invalid_proxy = { + "https": "http://{}:{}".format( + MockProxyServer.PROXY_IP_ADDRESS, MockProxyServer.INVALID_PROXY_PORT + ) + } + # TODO - verify WebsocketError in output log. For some reason self.assertLogs + # doesn't always work even when the error is clearly logged. + with use_proxies(service, invalid_proxy): + job = self._run_program(service, iterations=2, callback=result_callback) + job.wait_for_final_state() + self.assertFalse(callback_called) + + @run_cloud_legacy_real + def test_job_logs(self, service): + """Test job logs.""" + job = self._run_program(service, final_result="foo") + with self.assertLogs("qiskit_ibm_runtime", "WARN"): + job.logs() + job.wait_for_final_state() + job_logs = job.logs() + self.assertIn("this is a stdout message", job_logs) + self.assertIn("this is a stderr message", job_logs) + + def _upload_program( + self, + service, + name=None, + max_execution_time=300, + data=None, + is_public: bool = False, + ): + """Upload a new program.""" + name = name or self._get_program_name() + data = data or RUNTIME_PROGRAM + metadata = copy.deepcopy(RUNTIME_PROGRAM_METADATA) + metadata["name"] = name + metadata["max_execution_time"] = max_execution_time + metadata["is_public"] = is_public + program_id = service.upload_program(data=data, metadata=metadata) + self.to_delete[service.auth].append(program_id) + return program_id + + @classmethod + def _get_program_name(cls): + """Return a unique program name.""" + return PROGRAM_PREFIX + "_" + uuid.uuid4().hex + + def _assert_complex_types_equal(self, expected, received): + """Verify the received data in complex types is expected.""" + if "serializable_class" in received: + received["serializable_class"] = SerializableClass.from_json( + received["serializable_class"] + ) + self.assertEqual(expected, received) + + def _run_program( + self, + service, + program_id=None, + iterations=1, + inputs=None, + interim_results=None, + final_result=None, + callback=None, + backend=None, + ): + """Run a program.""" + self.log.debug("Running program on %s", service.auth) + inputs = ( + inputs + if inputs is not None + else { + "iterations": iterations, + "interim_results": interim_results or {}, + "final_result": final_result or {}, + } + ) + pid = program_id or self.program_ids[service.auth] + backend_name = backend or self.sim_backends[service.auth] + options = {"backend_name": backend_name} + job = service.run( + program_id=pid, inputs=inputs, options=options, callback=callback + ) + self.log.info("Runtime job %s submitted.", job.job_id) + self.to_cancel[service.auth].append(job) + return job + + def _wait_for_status(self, job, status): + """Wait for job to reach a certain status.""" + wait_time = 1 if status == JobStatus.QUEUED else self.poll_time + while job.status() not in JOB_FINAL_STATES + (status,): + time.sleep(wait_time) + if job.status() != status: + self.skipTest(f"Job {job.job_id} unable to reach status {status}.") + + def _get_real_device(self, service): + try: + return service.least_busy(simulator=False).name() + except QiskitBackendNotFoundError: + raise unittest.SkipTest("No real device") # cloud has no real device diff --git a/test/test_integration_program.py b/test/test_integration_program.py new file mode 100644 index 0000000000..5d3acb6646 --- /dev/null +++ b/test/test_integration_program.py @@ -0,0 +1,259 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for runtime service.""" + +import copy +import unittest +import os +import uuid +from contextlib import suppress +import tempfile +from collections import defaultdict + +from qiskit_ibm_runtime.exceptions import IBMNotAuthorizedError +from qiskit_ibm_runtime.runtime_program import RuntimeProgram +from qiskit_ibm_runtime.exceptions import ( + RuntimeProgramNotFound, +) + +from .ibm_test_case import IBMTestCase +from .utils.decorators import requires_cloud_legacy_services, run_cloud_legacy_real +from .utils.templates import RUNTIME_PROGRAM, RUNTIME_PROGRAM_METADATA, PROGRAM_PREFIX + + +class TestIntegrationProgram(IBMTestCase): + """Integration tests for runtime modules.""" + + @classmethod + @requires_cloud_legacy_services + def setUpClass(cls, services): + """Initial class level setup.""" + # pylint: disable=arguments-differ + super().setUpClass() + cls.services = services + + def setUp(self) -> None: + """Test level setup.""" + super().setUp() + self.to_delete = defaultdict(list) + + def tearDown(self) -> None: + """Test level teardown.""" + super().tearDown() + # Delete programs + for service in self.services: + for prog in self.to_delete[service.auth]: + with suppress(Exception): + service.delete_program(prog) + + @run_cloud_legacy_real + def test_list_programs(self, service): + """Test listing programs.""" + program_id = self._upload_program(service) + programs = service.programs() + self.assertTrue(programs) + found = False + for prog in programs: + self._validate_program(prog) + if prog.program_id == program_id: + found = True + self.assertTrue(found, f"Program {program_id} not found!") + + @run_cloud_legacy_real + def test_list_programs_with_limit_skip(self, service): + """Test listing programs with limit and skip.""" + for _ in range(4): + self._upload_program(service) + programs = service.programs(limit=3, refresh=True) + all_ids = [prog.program_id for prog in programs] + self.assertEqual(len(all_ids), 3) + programs = service.programs(limit=2, skip=1) + some_ids = [prog.program_id for prog in programs] + self.assertEqual(len(some_ids), 2) + self.assertNotIn(all_ids[0], some_ids) + self.assertIn(all_ids[1], some_ids) + self.assertIn(all_ids[2], some_ids) + + @run_cloud_legacy_real + def test_list_program(self, service): + """Test listing a single program.""" + program_id = self._upload_program(service) + program = service.program(program_id) + self.assertEqual(program_id, program.program_id) + self._validate_program(program) + + @run_cloud_legacy_real + def test_retrieve_program_data(self, service): + """Test retrieving program data""" + program_id = self._upload_program(service) + program = service.program(program_id) + self.assertEqual(RUNTIME_PROGRAM, program.data) + self._validate_program(program) + + @run_cloud_legacy_real + def test_retrieve_unauthorized_program_data(self, service): + """Test retrieving program data when user is not the program author""" + program = service.program("sample-program") + self._validate_program(program) + with self.assertRaises(IBMNotAuthorizedError): + return program.data + + @run_cloud_legacy_real + def test_upload_program(self, service): + """Test uploading a program.""" + max_execution_time = 3000 + program_id = self._upload_program( + service, max_execution_time=max_execution_time + ) + self.assertTrue(program_id) + program = service.program(program_id) + self.assertTrue(program) + self.assertEqual(max_execution_time, program.max_execution_time) + + @run_cloud_legacy_real + def test_upload_program_file(self, service): + """Test uploading a program using a file.""" + temp_fp = tempfile.NamedTemporaryFile(mode="w", delete=False) + self.addCleanup(os.remove, temp_fp.name) + temp_fp.write(RUNTIME_PROGRAM) + temp_fp.close() + + program_id = self._upload_program(service, data=temp_fp.name) + self.assertTrue(program_id) + program = service.program(program_id) + self.assertTrue(program) + + @unittest.skip("Skip until authorized to upload public on cloud") + @unittest.skipIf( + not os.environ.get("QISKIT_IBM_USE_STAGING_CREDENTIALS", ""), + "Only runs on staging", + ) + @run_cloud_legacy_real + def test_upload_public_program(self, service): + """Test uploading a public program.""" + max_execution_time = 3000 + is_public = True + program_id = self._upload_program( + service, max_execution_time=max_execution_time, is_public=is_public + ) + self.assertTrue(program_id) + program = service.program(program_id) + self.assertTrue(program) + self.assertEqual(max_execution_time, program.max_execution_time) + self.assertEqual(program.is_public, is_public) + + @unittest.skip("Skip until authorized to upload public on cloud") + @unittest.skipIf( + not os.environ.get("QISKIT_IBM_USE_STAGING_CREDENTIALS", ""), + "Only runs on staging", + ) + @run_cloud_legacy_real + def test_set_visibility(self, service): + """Test setting the visibility of a program.""" + program_id = self._upload_program(service) + # Get the initial visibility + prog: RuntimeProgram = service.program(program_id) + start_vis = prog.is_public + # Flip the original value + service.set_program_visibility(program_id, not start_vis) + # Get the new visibility + prog: RuntimeProgram = service.program(program_id, refresh=True) + end_vis = prog.is_public + # Verify changed + self.assertNotEqual(start_vis, end_vis) + + @run_cloud_legacy_real + def test_delete_program(self, service): + """Test deleting program.""" + program_id = self._upload_program(service) + service.delete_program(program_id) + with self.assertRaises(RuntimeProgramNotFound): + service.program(program_id, refresh=True) + + @run_cloud_legacy_real + def test_double_delete_program(self, service): + """Test deleting a deleted program.""" + program_id = self._upload_program(service) + service.delete_program(program_id) + with self.assertRaises(RuntimeProgramNotFound): + service.delete_program(program_id) + + @run_cloud_legacy_real + def test_update_program_data(self, service): + """Test updating program data.""" + program_v1 = """ +def main(backend, user_messenger, **kwargs): + return "version 1" + """ + program_v2 = """ +def main(backend, user_messenger, **kwargs): + return "version 2" + """ + program_id = self._upload_program(service, data=program_v1) + self.assertEqual(program_v1, service.program(program_id).data) + service.update_program(program_id=program_id, data=program_v2) + self.assertEqual(program_v2, service.program(program_id).data) + + @run_cloud_legacy_real + def test_update_program_metadata(self, service): + """Test updating program metadata.""" + program_id = self._upload_program(service) + original = service.program(program_id) + new_metadata = { + "name": self._get_program_name(), + "description": "test_update_program_metadata", + "max_execution_time": original.max_execution_time + 100, + "spec": { + "return_values": {"type": "object", "description": "Some return value"} + }, + } + service.update_program(program_id=program_id, metadata=new_metadata) + updated = service.program(program_id, refresh=True) + self.assertEqual(new_metadata["name"], updated.name) + self.assertEqual(new_metadata["description"], updated.description) + self.assertEqual(new_metadata["max_execution_time"], updated.max_execution_time) + self.assertEqual(new_metadata["spec"]["return_values"], updated.return_values) + + def _validate_program(self, program): + """Validate a program.""" + self.assertTrue(program) + self.assertTrue(program.name) + self.assertTrue(program.program_id) + self.assertTrue(program.description) + self.assertTrue(program.max_execution_time) + self.assertTrue(program.creation_date) + self.assertTrue(program.update_date) + + def _upload_program( + self, + service, + name=None, + max_execution_time=300, + data=None, + is_public: bool = False, + ): + """Upload a new program.""" + name = name or self._get_program_name() + data = data or RUNTIME_PROGRAM + metadata = copy.deepcopy(RUNTIME_PROGRAM_METADATA) + metadata["name"] = name + metadata["max_execution_time"] = max_execution_time + metadata["is_public"] = is_public + program_id = service.upload_program(data=data, metadata=metadata) + self.to_delete[service.auth].append(program_id) + return program_id + + @classmethod + def _get_program_name(cls): + """Return a unique program name.""" + return PROGRAM_PREFIX + "_" + uuid.uuid4().hex diff --git a/test/test_job_retrieval.py b/test/test_job_retrieval.py new file mode 100644 index 0000000000..c7c96bfa70 --- /dev/null +++ b/test/test_job_retrieval.py @@ -0,0 +1,256 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for runtime job retrieval.""" + +from qiskit_ibm_runtime.exceptions import IBMInputValueError + +from .ibm_test_case import IBMTestCase +from .mock.fake_runtime_service import FakeRuntimeService +from .utils.program import run_program, upload_program +from .utils.decorators import run_legacy_and_cloud_fake + + +class TestRetrieveJobs(IBMTestCase): + """Class for testing job retrieval.""" + + def setUp(self): + """Initial test setup.""" + super().setUp() + self._legacy_service = FakeRuntimeService(auth="legacy", token="my_token") + + @run_legacy_and_cloud_fake + def test_retrieve_job(self, service): + """Test retrieving a job.""" + program_id = upload_program(service) + params = {"param1": "foo"} + job = run_program(service=service, program_id=program_id, inputs=params) + rjob = service.job(job.job_id) + self.assertEqual(job.job_id, rjob.job_id) + self.assertEqual(program_id, rjob.program_id) + + @run_legacy_and_cloud_fake + def test_jobs_no_limit(self, service): + """Test retrieving jobs without limit.""" + program_id = upload_program(service) + + jobs = [] + for _ in range(25): + jobs.append(run_program(service, program_id)) + rjobs = service.jobs(limit=None) + self.assertEqual(25, len(rjobs)) + + @run_legacy_and_cloud_fake + def test_jobs_limit(self, service): + """Test retrieving jobs with limit.""" + program_id = upload_program(service) + + jobs = [] + job_count = 25 + for _ in range(job_count): + jobs.append(run_program(service, program_id)) + + limits = [21, 30] + for limit in limits: + with self.subTest(limit=limit): + rjobs = service.jobs(limit=limit) + self.assertEqual(min(limit, job_count), len(rjobs)) + + @run_legacy_and_cloud_fake + def test_jobs_skip(self, service): + """Test retrieving jobs with skip.""" + program_id = upload_program(service) + + jobs = [] + for _ in range(5): + jobs.append(run_program(service, program_id)) + rjobs = service.jobs(skip=4) + self.assertEqual(1, len(rjobs)) + + def test_jobs_skip_limit(self): + """Test retrieving jobs with skip and limit.""" + service = self._legacy_service + program_id = upload_program(service) + + jobs = [] + for _ in range(10): + jobs.append(run_program(service, program_id)) + rjobs = service.jobs(skip=4, limit=2) + self.assertEqual(2, len(rjobs)) + + @run_legacy_and_cloud_fake + def test_jobs_pending(self, service): + """Test retrieving pending jobs (QUEUED, RUNNING).""" + program_id = upload_program(service) + + _, pending_jobs_count, _ = self._populate_jobs_with_all_statuses( + service, program_id=program_id + ) + rjobs = service.jobs(pending=True) + self.assertEqual(pending_jobs_count, len(rjobs)) + + def test_jobs_limit_pending(self): + """Test retrieving pending jobs (QUEUED, RUNNING) with limit.""" + service = self._legacy_service + program_id = upload_program(service) + + self._populate_jobs_with_all_statuses(service, program_id=program_id) + limit = 4 + rjobs = service.jobs(limit=limit, pending=True) + self.assertEqual(limit, len(rjobs)) + + def test_jobs_skip_pending(self): + """Test retrieving pending jobs (QUEUED, RUNNING) with skip.""" + service = self._legacy_service + program_id = upload_program(service) + + _, pending_jobs_count, _ = self._populate_jobs_with_all_statuses( + service, program_id=program_id + ) + skip = 4 + rjobs = service.jobs(skip=skip, pending=True) + self.assertEqual(pending_jobs_count - skip, len(rjobs)) + + def test_jobs_limit_skip_pending(self): + """Test retrieving pending jobs (QUEUED, RUNNING) with limit and skip.""" + service = self._legacy_service + program_id = upload_program(service) + + self._populate_jobs_with_all_statuses(service, program_id=program_id) + limit = 2 + skip = 3 + rjobs = service.jobs(limit=limit, skip=skip, pending=True) + self.assertEqual(limit, len(rjobs)) + + def test_jobs_returned(self): + """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED).""" + service = self._legacy_service + program_id = upload_program(service) + + _, _, returned_jobs_count = self._populate_jobs_with_all_statuses( + service, program_id=program_id + ) + rjobs = service.jobs(pending=False) + self.assertEqual(returned_jobs_count, len(rjobs)) + + def test_jobs_limit_returned(self): + """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with limit.""" + service = self._legacy_service + program_id = upload_program(service) + + self._populate_jobs_with_all_statuses(service, program_id=program_id) + limit = 6 + rjobs = service.jobs(limit=limit, pending=False) + self.assertEqual(limit, len(rjobs)) + + def test_jobs_skip_returned(self): + """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with skip.""" + service = self._legacy_service + program_id = upload_program(service) + + _, _, returned_jobs_count = self._populate_jobs_with_all_statuses( + service, program_id=program_id + ) + skip = 4 + rjobs = service.jobs(skip=skip, pending=False) + self.assertEqual(returned_jobs_count - skip, len(rjobs)) + + def test_jobs_limit_skip_returned(self): + """Test retrieving returned jobs (COMPLETED, FAILED, CANCELLED) with limit and skip.""" + service = self._legacy_service + program_id = upload_program(service) + + self._populate_jobs_with_all_statuses(service, program_id=program_id) + limit = 6 + skip = 2 + rjobs = service.jobs(limit=limit, skip=skip, pending=False) + self.assertEqual(limit, len(rjobs)) + + @run_legacy_and_cloud_fake + def test_jobs_filter_by_program_id(self, service): + """Test retrieving jobs by Program ID.""" + program_id = upload_program(service) + program_id_1 = upload_program(service) + + job = run_program(service=service, program_id=program_id) + job_1 = run_program(service=service, program_id=program_id_1) + job.wait_for_final_state() + job_1.wait_for_final_state() + rjobs = service.jobs(program_id=program_id) + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs)) + + def test_jobs_filter_by_instance(self): + """Test retrieving jobs by instance.""" + service = self._legacy_service + program_id = upload_program(service) + instance = FakeRuntimeService.DEFAULT_HGPS[1] + + job = run_program(service=service, program_id=program_id, instance=instance) + job.wait_for_final_state() + rjobs = service.jobs(program_id=program_id, instance=instance) + self.assertTrue(rjobs) + self.assertEqual(program_id, rjobs[0].program_id) + self.assertEqual(1, len(rjobs)) + rjobs = service.jobs( + program_id=program_id, instance="nohub1/nogroup1/noproject1" + ) + self.assertFalse(rjobs) + + def test_jobs_bad_instance(self): + """Test retrieving jobs with bad instance values.""" + service = self._legacy_service + with self.assertRaises(IBMInputValueError): + _ = service.jobs(instance="foo") + + def test_different_hgps(self): + """Test retrieving job submitted with different hgp.""" + # Initialize with hgp0 + service = FakeRuntimeService( + auth="legacy", + token="some_token", + instance=FakeRuntimeService.DEFAULT_HGPS[0], + ) + program_id = upload_program(service) + + # Run with hgp1 backend. + backend_name = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "1" + job = run_program(service, program_id=program_id, backend_name=backend_name) + + rjob = service.job(job.job_id) + self.assertIsNotNone(rjob.backend) + + def _populate_jobs_with_all_statuses(self, service, program_id): + """Populate the database with jobs of all statuses.""" + jobs = [] + pending_jobs_count = 0 + returned_jobs_count = 0 + status_count = { + "RUNNING": 3, + "COMPLETED": 4, + "QUEUED": 2, + "FAILED": 3, + "CANCELLED": 2, + } + pending_status = ["RUNNING", "QUEUED"] + for stat, count in status_count.items(): + for _ in range(count): + jobs.append( + run_program( + service=service, program_id=program_id, final_status=stat + ) + ) + if stat in pending_status: + pending_jobs_count += 1 + else: + returned_jobs_count += 1 + return jobs, pending_jobs_count, returned_jobs_count diff --git a/test/test_jobs.py b/test/test_jobs.py new file mode 100644 index 0000000000..360a960a49 --- /dev/null +++ b/test/test_jobs.py @@ -0,0 +1,257 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for job related runtime functions.""" + +import time +import random + +from qiskit.providers.jobstatus import JobStatus +from qiskit.providers.exceptions import QiskitBackendNotFoundError + +from qiskit_ibm_runtime import RuntimeJob +from qiskit_ibm_runtime.constants import API_TO_JOB_ERROR_MESSAGE +from qiskit_ibm_runtime.exceptions import ( + RuntimeJobFailureError, + RuntimeJobNotFound, + RuntimeProgramNotFound, + IBMInputValueError, +) + +from .ibm_test_case import IBMTestCase +from .mock.fake_runtime_client import ( + FailedRuntimeJob, + FailedRanTooLongRuntimeJob, + CancelableRuntimeJob, + CustomResultRuntimeJob, +) +from .mock.fake_runtime_service import FakeRuntimeService +from .utils.program import run_program, upload_program +from .utils.serialization import get_complex_types +from .utils.decorators import run_legacy_and_cloud_fake + + +class TestRuntimeJob(IBMTestCase): + """Class for testing runtime jobs.""" + + @run_legacy_and_cloud_fake + def test_run_program(self, service): + """Test running program.""" + params = {"param1": "foo"} + job = run_program(service=service, inputs=params) + self.assertTrue(job.job_id) + self.assertIsInstance(job, RuntimeJob) + self.assertIsInstance(job.status(), JobStatus) + self.assertEqual(job.inputs, params) + job.wait_for_final_state() + self.assertEqual(job.status(), JobStatus.DONE) + self.assertTrue(job.result()) + + @run_legacy_and_cloud_fake + def test_run_phantom_program(self, service): + """Test running a phantom program.""" + with self.assertRaises(RuntimeProgramNotFound): + _ = run_program(service=service, program_id="phantom_program") + + @run_legacy_and_cloud_fake + def test_run_program_phantom_backend(self, service): + """Test running on a phantom backend.""" + with self.assertRaises(QiskitBackendNotFoundError): + _ = run_program(service=service, backend_name="phantom_backend") + + def test_run_program_missing_backend_legacy(self): + """Test running a legacy program with no backend.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + with self.assertRaises(IBMInputValueError): + _ = run_program(service=service, backend_name="") + + def test_run_program_default_hgp_backend(self): + """Test running a program with a backend in default hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = FakeRuntimeService.DEFAULT_COMMON_BACKEND + default_hgp = list(service._hgps.values())[0] + self.assertIn(backend, default_hgp.backends.keys()) + job = run_program(service=service, backend_name=backend) + self.assertEqual(job.backend.name(), backend) + self.assertEqual( + job.backend._api_client.hgp, FakeRuntimeService.DEFAULT_HGPS[0] + ) + + def test_run_program_non_default_hgp_backend(self): + """Test running a program with a backend in non-default hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "1" + default_hgp = list(service._hgps.values())[0] + self.assertNotIn(backend, default_hgp.backends.keys()) + job = run_program(service=service, backend_name=backend) + self.assertEqual(job.backend.name(), backend) + + def test_run_program_by_hgp_backend(self): + """Test running a program with both backend and hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = FakeRuntimeService.DEFAULT_COMMON_BACKEND + non_default_hgp = list(service._hgps.keys())[1] + job = run_program( + service=service, backend_name=backend, instance=non_default_hgp + ) + self.assertEqual(job.backend.name(), backend) + self.assertEqual(job.backend._api_client.hgp, non_default_hgp) + + def test_run_program_by_hgp_bad_backend(self): + """Test running a program with backend not in hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + backend = FakeRuntimeService.DEFAULT_UNIQUE_BACKEND_PREFIX + "1" + default_hgp = list(service._hgps.values())[0] + self.assertNotIn(backend, default_hgp.backends.keys()) + with self.assertRaises(QiskitBackendNotFoundError): + _ = run_program( + service=service, backend_name=backend, instance=default_hgp.name + ) + + def test_run_program_by_phantom_hgp(self): + """Test running a program with a phantom hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + with self.assertRaises(IBMInputValueError): + _ = run_program(service=service, instance="h/g/p") + + def test_run_program_by_bad_hgp(self): + """Test running a program with a bad hgp.""" + service = FakeRuntimeService(auth="legacy", token="my_token") + with self.assertRaises(IBMInputValueError): + _ = run_program(service=service, instance="foo") + + @run_legacy_and_cloud_fake + def test_run_program_with_custom_runtime_image(self, service): + """Test running program with a custom image.""" + params = {"param1": "foo"} + image = "name:tag" + job = run_program(service=service, inputs=params, image=image) + self.assertTrue(job.job_id) + self.assertIsInstance(job, RuntimeJob) + self.assertIsInstance(job.status(), JobStatus) + self.assertEqual(job.inputs, params) + job.wait_for_final_state() + self.assertEqual(job.status(), JobStatus.DONE) + self.assertTrue(job.result()) + self.assertEqual(job.image, image) + + @run_legacy_and_cloud_fake + def test_run_program_failed(self, service): + """Test a failed program execution.""" + job = run_program(service=service, job_classes=FailedRuntimeJob) + job.wait_for_final_state() + job_result_raw = service._api_client.job_results(job.job_id) + self.assertEqual(JobStatus.ERROR, job.status()) + self.assertEqual( + API_TO_JOB_ERROR_MESSAGE["FAILED"].format(job.job_id, job_result_raw), + job.error_message(), + ) + with self.assertRaises(RuntimeJobFailureError): + job.result() + + @run_legacy_and_cloud_fake + def test_run_program_failed_ran_too_long(self, service): + """Test a program that failed since it ran longer than maximum execution time.""" + job = run_program(service=service, job_classes=FailedRanTooLongRuntimeJob) + job.wait_for_final_state() + job_result_raw = service._api_client.job_results(job.job_id) + self.assertEqual(JobStatus.ERROR, job.status()) + self.assertEqual( + API_TO_JOB_ERROR_MESSAGE["CANCELLED - RAN TOO LONG"].format( + job.job_id, job_result_raw + ), + job.error_message(), + ) + with self.assertRaises(RuntimeJobFailureError): + job.result() + + @run_legacy_and_cloud_fake + def test_program_params_namespace(self, service): + """Test running a program using parameter namespace.""" + program_id = upload_program(service) + params = service.program(program_id).parameters() + params.param1 = "Hello World" + run_program(service, program_id, inputs=params) + + @run_legacy_and_cloud_fake + def test_cancel_job(self, service): + """Test canceling a job.""" + job = run_program(service, job_classes=CancelableRuntimeJob) + time.sleep(1) + job.cancel() + self.assertEqual(job.status(), JobStatus.CANCELLED) + rjob = service.job(job.job_id) + self.assertEqual(rjob.status(), JobStatus.CANCELLED) + + @run_legacy_and_cloud_fake + def test_final_result(self, service): + """Test getting final result.""" + job = run_program(service) + result = job.result() + self.assertTrue(result) + + @run_legacy_and_cloud_fake + def test_interim_results(self, service): + """Test getting interim results.""" + job = run_program(service) + # TODO maybe a bit more validation on the returned interim results + interim_results = job.interim_results() + self.assertTrue(interim_results) + + @run_legacy_and_cloud_fake + def test_job_status(self, service): + """Test job status.""" + job = run_program(service) + time.sleep(random.randint(1, 5)) + self.assertTrue(job.status()) + + @run_legacy_and_cloud_fake + def test_job_inputs(self, service): + """Test job inputs.""" + inputs = {"param1": "foo", "param2": "bar"} + job = run_program(service, inputs=inputs) + self.assertEqual(inputs, job.inputs) + + @run_legacy_and_cloud_fake + def test_job_program_id(self, service): + """Test job program ID.""" + program_id = upload_program(service) + job = run_program(service, program_id=program_id) + self.assertEqual(program_id, job.program_id) + + @run_legacy_and_cloud_fake + def test_wait_for_final_state(self, service): + """Test wait for final state.""" + job = run_program(service) + job.wait_for_final_state() + self.assertEqual(JobStatus.DONE, job.status()) + + @run_legacy_and_cloud_fake + def test_get_result_twice(self, service): + """Test getting results multiple times.""" + custom_result = get_complex_types() + job_cls = CustomResultRuntimeJob + job_cls.custom_result = custom_result + + job = run_program(service=service, job_classes=job_cls) + _ = job.result() + _ = job.result() + + @run_legacy_and_cloud_fake + def test_delete_job(self, service): + """Test deleting a job.""" + params = {"param1": "foo"} + job = run_program(service=service, inputs=params) + self.assertTrue(job.job_id) + service.delete_job(job.job_id) + with self.assertRaises(RuntimeJobNotFound): + service.job(job.job_id) diff --git a/test/ibm/test_jupyter.py b/test/test_jupyter.py similarity index 95% rename from test/ibm/test_jupyter.py rename to test/test_jupyter.py index c65b4a62b3..bc18263584 100644 --- a/test/ibm/test_jupyter.py +++ b/test/test_jupyter.py @@ -12,6 +12,8 @@ """Tests for Jupyter tools.""" +import unittest + from qiskit_ibm_runtime.jupyter.qubits_widget import qubits_tab from qiskit_ibm_runtime.jupyter.config_widget import config_tab from qiskit_ibm_runtime.jupyter.gates_widget import gates_tab @@ -19,10 +21,11 @@ from qiskit_ibm_runtime.jupyter.dashboard.backend_widget import make_backend_widget from qiskit_ibm_runtime.jupyter.dashboard.utils import BackendWithProviders -from ..decorators import requires_provider -from ..ibm_test_case import IBMTestCase +from .ibm_test_case import IBMTestCase +from .utils.decorators import requires_provider +@unittest.skip("Skip until jupyter is done") class TestBackendInfo(IBMTestCase): """Test backend information Jupyter widget.""" @@ -70,6 +73,7 @@ def test_error_map_tab(self): iplot_error_map(backend) +@unittest.skip("Skip until jupyter is done") class TestIBMDashboard(IBMTestCase): """Test backend information Jupyter widget.""" diff --git a/test/ibm/test_ibm_logger.py b/test/test_logger.py similarity index 99% rename from test/ibm/test_ibm_logger.py rename to test/test_logger.py index 8a44b1775d..c4810b0ab6 100644 --- a/test/ibm/test_ibm_logger.py +++ b/test/test_logger.py @@ -20,7 +20,7 @@ from qiskit_ibm_runtime import QISKIT_IBM_RUNTIME_LOG_LEVEL, QISKIT_IBM_RUNTIME_LOG_FILE from qiskit_ibm_runtime.utils.utils import setup_logger -from ..ibm_test_case import IBMTestCase +from .ibm_test_case import IBMTestCase class TestLogger(IBMTestCase): diff --git a/test/test_programs.py b/test/test_programs.py new file mode 100644 index 0000000000..bcbfba9907 --- /dev/null +++ b/test/test_programs.py @@ -0,0 +1,258 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Tests for program related runtime functions.""" + +import copy +import json +import os +from io import StringIO +from unittest.mock import patch +import warnings +import tempfile + +from qiskit_ibm_runtime.exceptions import IBMInputValueError +from qiskit_ibm_runtime.exceptions import RuntimeProgramNotFound +from qiskit_ibm_runtime.runtime_program import ParameterNamespace + +from .ibm_test_case import IBMTestCase +from .utils.program import upload_program, DEFAULT_DATA, DEFAULT_METADATA +from .utils.decorators import run_legacy_and_cloud_fake + + +class TestPrograms(IBMTestCase): + """Class for testing runtime modules.""" + + @run_legacy_and_cloud_fake + def test_list_programs(self, service): + """Test listing programs.""" + program_id = upload_program(service) + programs = service.programs() + all_ids = [prog.program_id for prog in programs] + self.assertIn(program_id, all_ids) + + @run_legacy_and_cloud_fake + def test_list_programs_with_limit_skip(self, service): + """Test listing programs with limit and skip.""" + program_ids = [] + for _ in range(3): + program_ids.append(upload_program(service)) + programs = service.programs(limit=2, skip=1) + all_ids = [prog.program_id for prog in programs] + self.assertNotIn(program_ids[0], all_ids) + self.assertIn(program_ids[1], all_ids) + self.assertIn(program_ids[2], all_ids) + programs = service.programs(limit=3) + all_ids = [prog.program_id for prog in programs] + self.assertIn(program_ids[0], all_ids) + + @run_legacy_and_cloud_fake + def test_list_program(self, service): + """Test listing a single program.""" + program_id = upload_program(service) + program = service.program(program_id) + self.assertEqual(program_id, program.program_id) + + @run_legacy_and_cloud_fake + def test_print_programs(self, service): + """Test printing programs.""" + ids = [] + for idx in range(3): + ids.append(upload_program(service, name=f"name_{idx}")) + + programs = service.programs() + with patch("sys.stdout", new=StringIO()) as mock_stdout: + service.pprint_programs() + stdout = mock_stdout.getvalue() + for prog in programs: + self.assertIn(prog.program_id, stdout) + self.assertIn(prog.name, stdout) + self.assertNotIn(str(prog.max_execution_time), stdout) + service.pprint_programs(detailed=True) + stdout_detailed = mock_stdout.getvalue() + for prog in programs: + self.assertIn(prog.program_id, stdout_detailed) + self.assertIn(prog.name, stdout_detailed) + self.assertIn(str(prog.max_execution_time), stdout_detailed) + + @run_legacy_and_cloud_fake + def test_upload_program(self, service): + """Test uploading a program.""" + max_execution_time = 3000 + is_public = True + program_id = upload_program( + service=service, max_execution_time=max_execution_time, is_public=is_public + ) + self.assertTrue(program_id) + program = service.program(program_id) + self.assertTrue(program) + self.assertEqual(max_execution_time, program.max_execution_time) + self.assertEqual(program.is_public, is_public) + + @run_legacy_and_cloud_fake + def test_update_program(self, service): + """Test updating program.""" + new_data = "def main() {foo=bar}" + new_metadata = copy.deepcopy(DEFAULT_METADATA) + new_metadata["name"] = "test_update_program" + new_name = "name2" + new_description = "some other description" + new_cost = DEFAULT_METADATA["max_execution_time"] + 100 + new_spec = copy.deepcopy(DEFAULT_METADATA["spec"]) + new_spec["backend_requirements"] = {"input_allowed": "runtime"} + + sub_tests = [ + {"data": new_data}, + {"metadata": new_metadata}, + {"data": new_data, "metadata": new_metadata}, + {"metadata": new_metadata, "name": new_name}, + { + "data": new_data, + "metadata": new_metadata, + "description": new_description, + }, + {"max_execution_time": new_cost, "spec": new_spec}, + ] + + for new_vals in sub_tests: + with self.subTest(new_vals=new_vals.keys()): + program_id = upload_program(service) + service.update_program(program_id=program_id, **new_vals) + updated = service.program(program_id, refresh=True) + if "data" in new_vals: + raw_program = service._api_client.program_get(program_id) + self.assertEqual(new_data, raw_program["data"]) + if "metadata" in new_vals and "name" not in new_vals: + self.assertEqual(new_metadata["name"], updated.name) + if "name" in new_vals: + self.assertEqual(new_name, updated.name) + if "description" in new_vals: + self.assertEqual(new_description, updated.description) + if "max_execution_time" in new_vals: + self.assertEqual(new_cost, updated.max_execution_time) + if "spec" in new_vals: + raw_program = service._api_client.program_get(program_id) + self.assertEqual(new_spec, raw_program["spec"]) + + @run_legacy_and_cloud_fake + def test_update_program_no_new_fields(self, service): + """Test updating a program without any new data.""" + program_id = upload_program(service) + with warnings.catch_warnings(record=True) as warn_cm: + service.update_program(program_id=program_id) + self.assertEqual(len(warn_cm), 1) + + @run_legacy_and_cloud_fake + def test_update_phantom_program(self, service): + """Test updating a phantom program.""" + with self.assertRaises(RuntimeProgramNotFound): + service.update_program("phantom_program", name="foo") + + @run_legacy_and_cloud_fake + def test_delete_program(self, service): + """Test deleting program.""" + program_id = upload_program(service) + service.delete_program(program_id) + with self.assertRaises(RuntimeProgramNotFound): + service.program(program_id, refresh=True) + + @run_legacy_and_cloud_fake + def test_double_delete_program(self, service): + """Test deleting a deleted program.""" + program_id = upload_program(service) + service.delete_program(program_id) + with self.assertRaises(RuntimeProgramNotFound): + service.delete_program(program_id) + + @run_legacy_and_cloud_fake + def test_retrieve_program_data(self, service): + """Test retrieving program data""" + program_id = upload_program(service, name="qiskit-test") + service.programs() + program = service.program(program_id) + self.assertEqual(program.data, DEFAULT_DATA) + self._validate_program(program) + + @run_legacy_and_cloud_fake + def test_program_params_validation(self, service): + """Test program parameters validation process""" + program_id = upload_program(service) + program = service.program(program_id) + params: ParameterNamespace = program.parameters() + params.param1 = "Hello, World" + # Check OK params + params.validate() + # Check OK params - contains unnecessary param + params.param3 = "Hello, World" + params.validate() + # Check bad params - missing required param + params.param1 = None + with self.assertRaises(IBMInputValueError): + params.validate() + params.param1 = "foo" + + @run_legacy_and_cloud_fake + def test_program_metadata(self, service): + """Test program metadata.""" + temp_fp = tempfile.NamedTemporaryFile(mode="w+", delete=False) + json.dump(DEFAULT_METADATA, temp_fp) + temp_fp.close() + + sub_tests = [temp_fp.name, DEFAULT_METADATA] + try: + for metadata in sub_tests: + with self.subTest(metadata_type=type(metadata)): + program_id = service.upload_program( + data=DEFAULT_DATA, metadata=metadata + ) + program = service.program(program_id) + service.delete_program(program_id) + self._validate_program(program) + finally: + os.remove(temp_fp.name) + + @run_legacy_and_cloud_fake + def test_set_program_visibility(self, service): + """Test setting program visibility.""" + program_id = upload_program(service, is_public=False) + service.set_program_visibility(program_id, True) + program = service.program(program_id) + self.assertTrue(program.is_public) + + @run_legacy_and_cloud_fake + def test_set_program_visibility_phantom_program(self, service): + """Test setting program visibility for a phantom program.""" + with self.assertRaises(RuntimeProgramNotFound): + service.set_program_visibility("foo", True) + + def _validate_program(self, program): + """Validate a program.""" + self.assertEqual(DEFAULT_METADATA["name"], program.name) + self.assertEqual(DEFAULT_METADATA["description"], program.description) + self.assertEqual( + DEFAULT_METADATA["max_execution_time"], program.max_execution_time + ) + self.assertTrue(program.creation_date) + self.assertTrue(program.update_date) + self.assertEqual( + DEFAULT_METADATA["spec"]["backend_requirements"], + program.backend_requirements, + ) + self.assertEqual( + DEFAULT_METADATA["spec"]["parameters"], program.parameters().metadata + ) + self.assertEqual( + DEFAULT_METADATA["spec"]["return_values"], program.return_values + ) + self.assertEqual( + DEFAULT_METADATA["spec"]["interim_results"], program.interim_results + ) diff --git a/test/ibm/test_proxies.py b/test/test_proxies.py similarity index 58% rename from test/ibm/test_proxies.py rename to test/test_proxies.py index 36bd72d336..fb687f3732 100644 --- a/test/ibm/test_proxies.py +++ b/test/test_proxies.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Tests for the AuthClient and VersionClient proxy support.""" +"""Tests for the proxy support.""" import urllib import subprocess @@ -20,9 +20,11 @@ from qiskit_ibm_runtime import IBMRuntimeService from qiskit_ibm_runtime.api.clients import AuthClient, VersionClient from qiskit_ibm_runtime.api.exceptions import RequestsApiError -from qiskit_ibm_runtime.credentials import Credentials -from ..ibm_test_case import IBMTestCase -from ..decorators import requires_qe_access +from qiskit_ibm_runtime.api.client_parameters import ClientParameters +from qiskit_ibm_runtime.api.clients.runtime import RuntimeClient + +from .ibm_test_case import IBMTestCase +from .utils.decorators import requires_qe_access, requires_cloud_service ADDRESS = "127.0.0.1" PORT = 8085 @@ -53,9 +55,44 @@ def tearDown(self): # wait for the process to terminate self.proxy_process.wait() + @requires_cloud_service + def test_proxies_cloud_runtime_client(self, service, instance): + """Should reach the proxy using RuntimeClient.""" + # pylint: disable=unused-argument + params = service._client_params + params.proxies = {"urls": VALID_PROXIES} + client = RuntimeClient(params) + client.list_programs(limit=1) + api_line = pproxy_desired_access_log_line(params.url) + self.proxy_process.terminate() # kill to be able of reading the output + proxy_output = self.proxy_process.stdout.read().decode("utf-8") + self.assertIn(api_line, proxy_output) + + @requires_qe_access + def test_proxies_legacy_runtime_client(self, qe_token, qe_url): + """Should reach the proxy using RuntimeClient.""" + service = IBMRuntimeService( + auth="legacy", + token=qe_token, + url=qe_url, + proxies={"urls": VALID_PROXIES}, + ) + service.programs(limit=1) + + auth_line = pproxy_desired_access_log_line(qe_url) + api_line = list(service._hgps.values())[0]._api_client._session.base_url + api_line = pproxy_desired_access_log_line(api_line) + self.proxy_process.terminate() # kill to be able of reading the output + proxy_output = self.proxy_process.stdout.read().decode("utf-8") + + # Check if the authentication call went through proxy. + self.assertIn(auth_line, proxy_output) + # Check if the API call (querying providers list) went through proxy. + self.assertIn(api_line, proxy_output) + @requires_qe_access - def test_proxies_ibm_account(self, qe_token, qe_url): - """Should reach the proxy using account.enable.""" + def test_proxies_account_client(self, qe_token, qe_url): + """Should reach the proxy using AccountClient.""" service = IBMRuntimeService( auth="legacy", token=qe_token, @@ -66,7 +103,8 @@ def test_proxies_ibm_account(self, qe_token, qe_url): self.proxy_process.terminate() # kill to be able of reading the output auth_line = pproxy_desired_access_log_line(qe_url) - api_line = pproxy_desired_access_log_line(service._default_hgp.credentials.url) + api_line = list(service._hgps.values())[0]._api_client._session.base_url + api_line = pproxy_desired_access_log_line(api_line) proxy_output = self.proxy_process.stdout.read().decode("utf-8") # Check if the authentication call went through proxy. @@ -78,8 +116,14 @@ def test_proxies_ibm_account(self, qe_token, qe_url): def test_proxies_authclient(self, qe_token, qe_url): """Should reach the proxy using AuthClient.""" pproxy_desired_access_log_line_ = pproxy_desired_access_log_line(qe_url) + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies={"urls": VALID_PROXIES}, + ) - _ = AuthClient(qe_token, qe_url, proxies=VALID_PROXIES) + _ = AuthClient(params) self.proxy_process.terminate() # kill to be able of reading the output self.assertIn( @@ -102,11 +146,31 @@ def test_proxies_versionclient(self, qe_token, qe_url): self.proxy_process.stdout.read().decode("utf-8"), ) + @requires_qe_access + def test_invalid_proxy_port_runtime_client(self, qe_token, qe_url): + """Should raise RequestApiError with ProxyError using RuntimeClient.""" + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies={"urls": INVALID_PORT_PROXIES}, + ) + with self.assertRaises(RequestsApiError) as context_manager: + client = RuntimeClient(params) + client.list_programs(limit=1) + self.assertIsInstance(context_manager.exception.__cause__, ProxyError) + @requires_qe_access def test_invalid_proxy_port_authclient(self, qe_token, qe_url): """Should raise RequestApiError with ProxyError using AuthClient.""" + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies={"urls": INVALID_PORT_PROXIES}, + ) with self.assertRaises(RequestsApiError) as context_manager: - _ = AuthClient(qe_token, qe_url, proxies=INVALID_PORT_PROXIES) + _ = AuthClient(params) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -120,11 +184,32 @@ def test_invalid_proxy_port_versionclient(self, qe_token, qe_url): self.assertIsInstance(context_manager.exception.__cause__, ProxyError) + @requires_qe_access + def test_invalid_proxy_address_runtime_client(self, qe_token, qe_url): + """Should raise RequestApiError with ProxyError using RuntimeClient.""" + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies={"urls": INVALID_ADDRESS_PROXIES}, + ) + with self.assertRaises(RequestsApiError) as context_manager: + client = RuntimeClient(params) + client.list_programs(limit=1) + + self.assertIsInstance(context_manager.exception.__cause__, ProxyError) + @requires_qe_access def test_invalid_proxy_address_authclient(self, qe_token, qe_url): """Should raise RequestApiError with ProxyError using AuthClient.""" + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies={"urls": INVALID_ADDRESS_PROXIES}, + ) with self.assertRaises(RequestsApiError) as context_manager: - _ = AuthClient(qe_token, qe_url, proxies=INVALID_ADDRESS_PROXIES) + _ = AuthClient(params) self.assertIsInstance(context_manager.exception.__cause__, ProxyError) @@ -149,11 +234,14 @@ def test_proxy_urls(self, qe_token, qe_url): ] for proxy_url in test_urls: with self.subTest(proxy_url=proxy_url): - credentials = Credentials( - qe_token, qe_url, proxies={"urls": {"https": proxy_url}} + params = ClientParameters( + auth_type="legacy", + token=qe_token, + url=qe_url, + proxies={"urls": {"https": proxy_url}}, ) version_finder = VersionClient( - credentials.base_url, **credentials.connection_parameters() + params.url, **params.connection_parameters() ) version_finder.version() diff --git a/test/ibm/runtime/test_runtime_ws.py b/test/test_runtime_ws.py similarity index 94% rename from test/ibm/runtime/test_runtime_ws.py rename to test/test_runtime_ws.py index 6c0499f1f5..b819800f85 100644 --- a/test/ibm/runtime/test_runtime_ws.py +++ b/test/test_runtime_ws.py @@ -16,13 +16,13 @@ from qiskit.test.mock.fake_qasm_simulator import FakeQasmSimulator -from qiskit_ibm_runtime.credentials import Credentials from qiskit_ibm_runtime import RuntimeJob from qiskit_ibm_runtime.exceptions import RuntimeInvalidStateError +from qiskit_ibm_runtime.api.client_parameters import ClientParameters -from ...ibm_test_case import IBMTestCase -from ...ws_server import MockWsServer -from .ws_handler import ( +from .ibm_test_case import IBMTestCase +from .mock.ws_server import MockWsServer +from .mock.ws_handler import ( websocket_handler, JOB_ID_PROGRESS_DONE, JOB_ID_ALREADY_DONE, @@ -30,7 +30,7 @@ JOB_ID_RETRY_FAILURE, JOB_PROGRESS_RESULT_COUNT, ) -from .fake_runtime_client import BaseFakeRuntimeClient +from .mock.fake_runtime_client import BaseFakeRuntimeClient class TestRuntimeWebsocketClient(IBMTestCase): @@ -187,13 +187,13 @@ def result_callback(job_id, interim_result): def _get_job(self, callback=None, job_id=JOB_ID_PROGRESS_DONE): """Get a runtime job.""" - cred = Credentials( - token="my_token", url="", services={"runtime": MockWsServer.VALID_WS_URL} + params = ClientParameters( + auth_type="legacy", token="my_token", url=MockWsServer.VALID_WS_URL ) job = RuntimeJob( backend=FakeQasmSimulator(), api_client=BaseFakeRuntimeClient(), - credentials=cred, + client_params=params, job_id=job_id, program_id="my-program", user_callback=callback, diff --git a/test/ibm/test_tutorials.py b/test/test_tutorials.py similarity index 95% rename from test/ibm/test_tutorials.py rename to test/test_tutorials.py index 44c938a5e0..49f460a69d 100644 --- a/test/ibm/test_tutorials.py +++ b/test/test_tutorials.py @@ -12,7 +12,7 @@ """Tests for the tutorials, copied from ``qiskit-iqx-tutorials``.""" -from unittest import skipIf +from unittest import skipIf, skip import os import glob import warnings @@ -24,7 +24,7 @@ from qiskit_ibm_runtime.utils.utils import to_python_identifier -from ..ibm_test_case import IBMTestCase +from .ibm_test_case import IBMTestCase TUTORIAL_PATH = "docs/tutorials/**/*.ipynb" @@ -51,6 +51,7 @@ def test_function(self): return type.__new__(mcs, name, bases, dict_) +@skip("Skip until we have tutorials") @skipIf(not TEST_OPTIONS["run_slow"], "Skipping slow tests.") class TestTutorials(IBMTestCase, metaclass=TutorialsTestCaseMeta): """Tests for tutorials.""" diff --git a/test/ibm/__init__.py b/test/utils/__init__.py similarity index 92% rename from test/ibm/__init__.py rename to test/utils/__init__.py index 1fa91ead63..c576b47d5b 100644 --- a/test/ibm/__init__.py +++ b/test/utils/__init__.py @@ -10,4 +10,4 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Tests for IBM Quantum Provider.""" +"""Test utility functions.""" diff --git a/test/contextmanagers.py b/test/utils/account.py similarity index 57% rename from test/contextmanagers.py rename to test/utils/account.py index c58f7bb542..ccebd8a883 100644 --- a/test/contextmanagers.py +++ b/test/utils/account.py @@ -13,14 +13,17 @@ """Context managers for using with IBM Provider unit tests.""" import os -from contextlib import ContextDecorator, contextmanager -from typing import Optional, Dict +import json +import uuid +from contextlib import ContextDecorator +from tempfile import NamedTemporaryFile from unittest.mock import patch -from qiskit_ibm_runtime import IBMRuntimeService -from qiskit_ibm_runtime.credentials import Credentials +from qiskit_ibm_runtime.accounts import management +from qiskit_ibm_runtime.accounts.account import CLOUD_API_URL, LEGACY_API_URL from qiskit_ibm_runtime.credentials.environ import VARIABLES_MAP + CREDENTIAL_ENV_VARS = VARIABLES_MAP.keys() @@ -99,34 +102,65 @@ def side_effect(self, filename_): return self.isfile_original(filename_) -def _mock_initialize_hgps( - self, credentials: Credentials, preferences: Optional[Dict] = None -) -> None: - """Mock ``_initialize_hgps()``, just storing the credentials.""" - hgp = dict() - hgp["credentials"] = credentials - self._hgp = hgp - self._hgps = {} - if preferences: - credentials.preferences = preferences.get(credentials.unique_id(), {}) - - -@contextmanager -def mock_ibm_provider(): - """Mock the initialization of ``IBMRuntimeService``, so it does not query the API.""" - patcher = patch.object( - IBMRuntimeService, - "_initialize_hgps", - side_effect=_mock_initialize_hgps, - autospec=True, - ) - patcher2 = patch.object( - IBMRuntimeService, - "_check_api_version", - return_value={"new_api": True, "api-auth": "0.1"}, - ) - patcher.start() - patcher2.start() - yield - patcher2.stop() - patcher.stop() +class custom_qiskitrc(ContextDecorator): + """Context manager that uses a temporary qiskitrc.""" + + # pylint: disable=invalid-name + + def __init__(self, contents=None, **kwargs): + # Create a temporary file with the contents. + contents = contents or get_qiskitrc_contents(**kwargs) + self.tmp_file = NamedTemporaryFile(mode="w+") + json.dump(contents, self.tmp_file) + self.tmp_file.flush() + self.default_qiskitrc_file_original = ( + management._DEFAULT_ACCOUNG_CONFIG_JSON_FILE + ) + + def __enter__(self): + # Temporarily modify the default location of the qiskitrc file. + management._DEFAULT_ACCOUNG_CONFIG_JSON_FILE = self.tmp_file.name + return self + + def __exit__(self, *exc): + # Delete the temporary file and restore the default location. + self.tmp_file.close() + management._DEFAULT_ACCOUNG_CONFIG_JSON_FILE = ( + self.default_qiskitrc_file_original + ) + + +def get_qiskitrc_contents( + name=None, + auth="cloud", + token=None, + url=None, + instance=None, + verify=None, + proxies=None, +): + """Generate qiskitrc content""" + if instance is None: + instance = "some_instance" if auth == "cloud" else "hub/group/project" + token = token or uuid.uuid4().hex + if name is None: + name = ( + management._DEFAULT_ACCOUNT_NAME_CLOUD + if auth == "cloud" + else management._DEFAULT_ACCOUNT_NAME_LEGACY + ) + if url is None: + url = CLOUD_API_URL if auth == "cloud" else LEGACY_API_URL + out = { + name: { + "auth": auth, + "url": url, + "token": token, + "instance": instance, + } + } + if verify is not None: + out["verify"] = verify + if proxies is not None: + out["proxies"] = proxies + return out diff --git a/test/utils/decorators.py b/test/utils/decorators.py new file mode 100644 index 0000000000..1cc48f761a --- /dev/null +++ b/test/utils/decorators.py @@ -0,0 +1,288 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Decorators for using with IBM Provider unit tests. + + Environment variables used by the decorators: + * QISKIT_IBM_API_TOKEN: default API token to use. + * QISKIT_IBM_API_URL: default API url to use. + * QISKIT_IBM_HGP: default hub/group/project to use. + * QISKIT_IBM_PRIVATE_HGP: hub/group/project to use for private jobs. + * QISKIT_IBM_DEVICE: default device to use. + * QISKIT_IBM_USE_STAGING_CREDENTIALS: True if use staging credentials. + * QISKIT_IBM_STAGING_API_TOKEN: staging API token to use. + * QISKIT_IBM_STAGING_API_URL: staging API url to use. + * QISKIT_IBM_STAGING_HGP: staging hub/group/project to use. + * QISKIT_IBM_STAGING_DEVICE: staging device to use. + * QISKIT_IBM_STAGING_PRIVATE_HGP: staging hub/group/project to use for private jobs. +""" + +import os +from functools import wraps +from unittest import SkipTest +from typing import Optional, List, Union + +from qiskit.test.testing_options import get_test_options +from qiskit_ibm_runtime import IBMRuntimeService + +from ..mock.fake_runtime_service import FakeRuntimeService + + +def requires_online_access(func): + """Decorator that signals whether online access is needed.""" + + @wraps(func) + def _wrapper(*args, **kwargs): + if get_test_options()["skip_online"]: + raise SkipTest("Skipping online tests") + return func(*args, **kwargs) + + return _wrapper + + +def requires_qe_access(func): + """Test requires legacy access.""" + + @wraps(func) + def _wrapper(obj, *args, **kwargs): + token, url, _ = _get_token_url_instance("legacy") + kwargs.update({"qe_token": token, "qe_url": url}) + return func(obj, *args, **kwargs) + + return _wrapper + + +def requires_multiple_hgps(func): + """Test requires a public and premium hgp.""" + + @wraps(func) + def _wrapper(*args, **kwargs): + service = _get_service("legacy") + hgps = list(service._hgps.keys()) + if len(hgps) < 2: + raise SkipTest("Test require at least 2 hub/group/project.") + + # Get open access hgp + open_hgp = hgps[-1] + premium_hgp = hgps[0] + kwargs.update( + { + "service": service, + "open_hgp": open_hgp, + "premium_hgp": premium_hgp, + } + ) + return func(*args, **kwargs) + + return _wrapper + + +def requires_legacy_service(func): + """Test requires legacy online API.""" + + @wraps(func) + def _wrapper(*args, **kwargs): + token, url, instance = _get_token_url_instance("legacy") + service = IBMRuntimeService( + auth="legacy", token=token, url=url, instance=instance + ) + kwargs.update({"service": service, "instance": instance}) + return func(*args, **kwargs) + + return _wrapper + + +def requires_cloud_service(func): + """Test requires cloud online API.""" + + @wraps(func) + def _wrapper(*args, **kwargs): + token, url, instance = _get_token_url_instance("cloud") + service = IBMRuntimeService( + auth="cloud", token=token, url=url, instance=instance + ) + kwargs.update({"service": service, "instance": instance}) + return func(*args, **kwargs) + + return _wrapper + + +def requires_cloud_legacy_services(func): + """Test requires cloud online API.""" + + @wraps(func) + def _wrapper(*args, **kwargs): + cloud_token, cloud_url, cloud_instance = _get_token_url_instance("cloud") + cloud_service = IBMRuntimeService( + auth="cloud", token=cloud_token, url=cloud_url, instance=cloud_instance + ) + legacy_token, legacy_url, legacy_instance = _get_token_url_instance("legacy") + legacy_service = IBMRuntimeService( + auth="legacy", token=legacy_token, url=legacy_url, instance=legacy_instance + ) + + kwargs.update({"services": [cloud_service, legacy_service]}) + return func(*args, **kwargs) + + return _wrapper + + +def requires_provider(func): + """Decorator that signals the test uses the online API, via a custom hub/group/project. + + This decorator delegates into the `requires_qe_access` decorator, but + instead of the credentials it appends a `provider` argument to the decorated + function. It also appends the custom `hub`, `group` and `project` arguments. + + Args: + func (callable): test function to be decorated. + + Returns: + callable: the decorated function. + """ + + @wraps(func) + @requires_qe_access + def _wrapper(*args, **kwargs): + token = kwargs.pop("qe_token") + url = kwargs.pop("qe_url") + service = IBMRuntimeService(auth="legacy", token=token, url=url) + hub, group, project = _get_custom_hgp() + kwargs.update( + {"service": service, "hub": hub, "group": group, "project": project} + ) + return func(*args, **kwargs) + + return _wrapper + + +def requires_cloud_legacy_devices(func): + """Test requires both cloud and legacy devices.""" + + @wraps(func) + def _wrapper(obj, *args, **kwargs): + legacy = _get_service("legacy") + cloud = _get_service("cloud") + legacy_backend = legacy.least_busy(simulator=False, min_num_qubits=5) + # TODO use real device when cloud supports it + cloud_backend = cloud.least_busy(min_num_qubits=5) + + kwargs.update({"devices": [cloud_backend, legacy_backend]}) + return func(obj, *args, **kwargs) + + return _wrapper + + +@requires_online_access +def _get_token_url_instance(auth): + # TODO: Change this once we start using different environments + if auth == "cloud": + if os.getenv("QISKIT_IBM_USE_STAGING_CREDENTIALS", ""): + return ( + os.getenv("QISKIT_IBM_STAGING_CLOUD_TOKEN"), + os.getenv("QISKIT_IBM_STAGING_CLOUD_URL"), + os.getenv("QISKIT_IBM_STAGING_CLOUD_CRN"), + ) + + return ( + os.getenv("QISKIT_IBM_CLOUD_TOKEN"), + os.getenv("QISKIT_IBM_CLOUD_URL"), + os.getenv("QISKIT_IBM_CLOUD_CRN"), + ) + + if os.getenv("QISKIT_IBM_USE_STAGING_CREDENTIALS", ""): + # Special case: instead of using the standard credentials mechanism, + # load them from different environment variables. This assumes they + # will always be in place, as is used by the CI setup. + return ( + os.getenv("QISKIT_IBM_STAGING_API_TOKEN"), + os.getenv("QISKIT_IBM_STAGING_API_URL"), + os.getenv("QISKIT_IBM_STAGING_HGP"), + ) + + return ( + os.getenv("QISKIT_IBM_API_TOKEN"), + os.getenv("QISKIT_IBM_API_URL"), + os.getenv("QISKIT_IBM_HGP"), + ) + + +def _get_service(auth: str) -> Union[List, IBMRuntimeService]: + """Return service(s). + + Args: + auth: Service type, ``cloud``, ``legacy``, or ``both``. + + Returns: + Runtime service(s) + """ + if auth in ["cloud", "legacy"]: + token, url, instance = _get_token_url_instance(auth) + return IBMRuntimeService(auth=auth, token=token, url=url, instance=instance) + + services = [] + for auth_ in ["cloud", "legacy"]: + token, url, instance = _get_token_url_instance(auth_) + services.append( + IBMRuntimeService(auth=auth_, token=token, url=url, instance=instance) + ) + return services + + +def _get_custom_hgp() -> Optional[str]: + """Get a custom hub/group/project + + Gets the hub/group/project set in QISKIT_IBM_STAGING_HGP for staging env or + QISKIT_IBM_HGP for production env. + + Returns: + Custom hub/group/project or ``None`` if not set. + """ + hgp = ( + os.getenv("QISKIT_IBM_STAGING_HGP", None) + if os.getenv("QISKIT_IBM_USE_STAGING_CREDENTIALS", "") + else os.getenv("QISKIT_IBM_HGP", None) + ) + return hgp + + +def run_legacy_and_cloud_fake(func): + """Decorator that runs a test using both legacy and cloud fake services.""" + + @wraps(func) + def _wrapper(self, *args, **kwargs): + legacy_service = FakeRuntimeService( + auth="legacy", token="my_token", instance="my_instance" + ) + cloud_service = FakeRuntimeService( + auth="cloud", token="my_token", instance="my_instance" + ) + for service in [legacy_service, cloud_service]: + with self.subTest(service=service.auth): + kwargs["service"] = service + func(self, *args, **kwargs) + + return _wrapper + + +def run_cloud_legacy_real(func): + """Decorator that runs a test using both legacy and cloud real services.""" + + @wraps(func) + def _wrapper(self, *args, **kwargs): + for service in self.services: + # for service, instance in [(legacy_service, legacy_instance)]: + with self.subTest(service=service.auth): + kwargs["service"] = service + func(self, *args, **kwargs) + + return _wrapper diff --git a/test/utils/program.py b/test/utils/program.py new file mode 100644 index 0000000000..eeffe31103 --- /dev/null +++ b/test/utils/program.py @@ -0,0 +1,93 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Utility functions for runtime testing.""" + +import uuid +import copy + + +DEFAULT_DATA = "def main() {}" +DEFAULT_METADATA = { + "name": "qiskit-test", + "description": "Test program.", + "max_execution_time": 300, + "spec": { + "backend_requirements": {"min_num_qubits": 5}, + "parameters": { + "properties": { + "param1": { + "description": "Desc 1", + "type": "string", + "enum": ["a", "b", "c"], + }, + "param2": {"description": "Desc 2", "type": "integer", "min": 0}, + }, + "required": ["param1"], + }, + "return_values": { + "type": "object", + "description": "Return values", + "properties": { + "ret_val": {"description": "Some return value.", "type": "string"} + }, + }, + "interim_results": { + "properties": { + "int_res": {"description": "Some interim result", "type": "string"} + } + }, + }, +} + + +def upload_program(service, name=None, max_execution_time=300, is_public: bool = False): + """Upload a new program.""" + name = name or uuid.uuid4().hex + data = DEFAULT_DATA + metadata = copy.deepcopy(DEFAULT_METADATA) + metadata.update(name=name) + metadata.update(is_public=is_public) + metadata.update(max_execution_time=max_execution_time) + program_id = service.upload_program(data=data, metadata=metadata) + return program_id + + +def run_program( + service, + program_id=None, + inputs=None, + job_classes=None, + final_status=None, + decoder=None, + image="", + instance=None, + backend_name=None, +): + """Run a program.""" + backend_name = backend_name if backend_name is not None else "common_backend" + options = {"backend_name": backend_name} + if final_status is not None: + service._api_client.set_final_status(final_status) + elif job_classes: + service._api_client.set_job_classes(job_classes) + if program_id is None: + program_id = upload_program(service) + job = service.run( + program_id=program_id, + options=options, + inputs=inputs, + result_decoder=decoder, + image=image, + instance=instance, + ) + return job diff --git a/test/ibm/runtime/utils.py b/test/utils/serialization.py similarity index 96% rename from test/ibm/runtime/utils.py rename to test/utils/serialization.py index 2f164d5f19..2d43b3a342 100644 --- a/test/ibm/runtime/utils.py +++ b/test/utils/serialization.py @@ -10,7 +10,7 @@ # copyright notice, and modified files need to carry a notice indicating # that they have been altered from the originals. -"""Utility functions for runtime testing.""" +"""Serialization utility functions for runtime testing.""" import json diff --git a/test/utils/templates.py b/test/utils/templates.py new file mode 100644 index 0000000000..173c16c9e5 --- /dev/null +++ b/test/utils/templates.py @@ -0,0 +1,48 @@ +# This code is part of Qiskit. +# +# (C) Copyright IBM 2021. +# +# This code is licensed under the Apache License, Version 2.0. You may +# obtain a copy of this license in the LICENSE.txt file in the root directory +# of this source tree or at http://www.apache.org/licenses/LICENSE-2.0. +# +# Any modifications or derivative works of this code must retain this +# copyright notice, and modified files need to carry a notice indicating +# that they have been altered from the originals. + +"""Templates for use with unit tests.""" + +RUNTIME_PROGRAM = """ +import random +import time +import warnings + +from qiskit import transpile +from qiskit.circuit.random import random_circuit + +def prepare_circuits(backend): + circuit = random_circuit(num_qubits=5, depth=4, measure=True, + seed=random.randint(0, 1000)) + return transpile(circuit, backend) + +def main(backend, user_messenger, **kwargs): + iterations = kwargs['iterations'] + sleep_per_iteration = kwargs.pop('sleep_per_iteration', 0) + interim_results = kwargs.pop('interim_results', {}) + final_result = kwargs.pop("final_result", {}) + for it in range(iterations): + time.sleep(sleep_per_iteration) + qc = prepare_circuits(backend) + user_messenger.publish({"iteration": it, "interim_results": interim_results}) + backend.run(qc).result() + + user_messenger.publish(final_result, final=True) + print("this is a stdout message") + warnings.warn("this is a stderr message") + """ + +RUNTIME_PROGRAM_METADATA = { + "max_execution_time": 600, + "description": "Qiskit test program", +} +PROGRAM_PREFIX = "qiskit-test" diff --git a/test/utils.py b/test/utils/utils.py similarity index 100% rename from test/utils.py rename to test/utils/utils.py