From a527eaa3de18fde7e800ae21183095195bf6259f Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Wed, 11 Feb 2026 13:58:10 +0100 Subject: [PATCH 01/23] [ADD] Enable proxy integration test and switch from pproxy to mitmproxy lib --- pyproject.toml | 2 +- test/integration/test_proxies.py | 57 ++++++++++++++++---------------- test/utils.py | 9 +++++ 3 files changed, 39 insertions(+), 29 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index f72d73813c..c724806276 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -161,7 +161,7 @@ issues = "https://github.com/Qiskit/qiskit-ibm-runtime/issues" common = [ "mypy==1.19.0", "pylint==3.0.0", - "pproxy==2.7.8", + "mitmproxy==12.2.1", "nbqa==1.5.3", "matplotlib>=2.1", "jupyter", diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index 0f0e670c41..9ebdf420d2 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -14,59 +14,60 @@ import subprocess import urllib - +import time from qiskit_ibm_runtime.proxies import ProxyConfiguration from qiskit_ibm_runtime.api.clients.runtime import RuntimeClient from ..ibm_test_case import IBMTestCase from ..decorators import IntegrationTestDependencies, integration_test_setup +from ..utils import find_free_port ADDRESS = "127.0.0.1" -PORT = 8085 +PORT = find_free_port() VALID_PROXIES = {"https": "http://{}:{}".format(ADDRESS, PORT)} INVALID_PORT_PROXIES = {"https": "http://{}:{}".format(ADDRESS, "6666")} INVALID_ADDRESS_PROXIES = {"https": "http://{}:{}".format("invalid", PORT)} - class TestProxies(IBMTestCase): """Tests for proxy capabilities.""" def setUp(self): - """Initial test setup.""" + # Pick a random free port for mitmproxy super().setUp() - # launch a mock server. - command = ["pproxy", "-v", "-l", "http://{}:{}".format(ADDRESS, PORT)] - self.proxy_process = subprocess.Popen(command, stdout=subprocess.PIPE) + # Command to start mitmproxy in non-interactive mode + self.proc = subprocess.Popen( + [ + "mitmdump", + "--listen-port", + str(PORT), + "--listen-host", + ADDRESS + ], + stdout=subprocess.PIPE, + stderr=subprocess.PIPE, + text=True, + ) + time.sleep(0.5) + def tearDown(self): - """Test cleanup.""" super().tearDown() + # Kill the proxy process + if self.proc.poll() is None: + self.proc.terminate() + try: + self.proc.wait(timeout=1) + except subprocess.TimeoutExpired: + self.proc.kill() - # terminate the mock server. - if self.proxy_process.returncode is None: - self.proxy_process.stdout.close() # close the IO buffer - self.proxy_process.terminate() # initiate process termination - - # wait for the process to terminate - self.proxy_process.wait() - - @integration_test_setup(supported_channel=["ibm_cloud"]) + @integration_test_setup(supported_channel=["ibm_cloud", "ibm_quantum_platform"]) def test_proxies_cloud_runtime_client(self, dependencies: IntegrationTestDependencies) -> None: """Should reach the proxy using RuntimeClient.""" # pylint: disable=unused-argument params = dependencies.service._client_params params.proxies = ProxyConfiguration(urls=VALID_PROXIES) + params.verify = False client = RuntimeClient(params) client.jobs_get(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) - - -def pproxy_desired_access_log_line(url): - """Return a desired pproxy log entry given a url.""" - qe_url_parts = urllib.parse.urlparse(url) - protocol_port = "443" if qe_url_parts.scheme == "https" else "80" - return "{}:{}".format(qe_url_parts.hostname, protocol_port) + self.assertTrue(True) \ No newline at end of file diff --git a/test/utils.py b/test/utils.py index b1b2341174..905c50641a 100644 --- a/test/utils.py +++ b/test/utils.py @@ -21,12 +21,14 @@ from typing import Any from datetime import datetime from ddt import data, unpack +import socket from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister, Parameter from qiskit.compiler import transpile from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit.providers.backend import Backend from qiskit.quantum_info import SparsePauliOp, Pauli + from qiskit_ibm_runtime import ( QiskitRuntimeService, Session, @@ -475,6 +477,13 @@ def _convert_paul_or_str(_obs): return out_obs +def find_free_port(): + s = socket.socket() + s.bind(("", 0)) + port = s.getsockname()[1] + s.close() + return port + class MockSession(Session): """Mock for session class""" From 7590b2e2faa0dded5be87209d870e7d03d88b033 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Wed, 11 Feb 2026 15:26:39 +0100 Subject: [PATCH 02/23] [REFACTO] IAM Service handler in CloudAccount --- qiskit_ibm_runtime/accounts/account.py | 17 +++++++++++--- test/integration/test_proxies.py | 32 +++++++++++++++++++------- 2 files changed, 38 insertions(+), 11 deletions(-) diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py index 6308630588..49ed44ad3e 100644 --- a/qiskit_ibm_runtime/accounts/account.py +++ b/qiskit_ibm_runtime/accounts/account.py @@ -271,6 +271,7 @@ def __init__( self.plans_preference = plans_preference self.tags = tags + def get_auth_handler(self) -> AuthBase: """Returns the Cloud authentication handler.""" return CloudAuth( @@ -278,8 +279,19 @@ def get_auth_handler(self) -> AuthBase: crn=self.instance, private=self.private_endpoint, proxies=self.proxies, - verify=self.verify, + verify=self.verify ) + + def get_iam_authentificator(self) -> IAMAuthenticator: + iam_url = get_iam_api_url(self.url) + proxies_kwargs = {} + if self.proxies is not None: + proxies_kwargs = self.proxies.to_request_params() + return IAMAuthenticator( + apikey=self.token, + url=iam_url, + disable_ssl_verification=not self.verify, + **proxies_kwargs) def resolve_crn(self) -> None: """Resolves the corresponding unique Cloud Resource Name (CRN) for the given non-unique service @@ -313,8 +325,7 @@ def resolve_crn(self) -> None: def list_instances(self) -> list[dict[str, Any]]: """Retrieve all crns with the IBM Cloud Global Search API.""" - iam_url = get_iam_api_url(self.url) - authenticator = IAMAuthenticator(self.token, url=iam_url) + authenticator = self.get_iam_authentificator() client = GlobalSearchV2(authenticator=authenticator) catalog = GlobalCatalogV1(authenticator=authenticator) client.set_service_url(get_global_search_api_url(self.url)) diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index 9ebdf420d2..4092929b78 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -15,16 +15,19 @@ import subprocess import urllib import time +import socket +import os from qiskit_ibm_runtime.proxies import ProxyConfiguration -from qiskit_ibm_runtime.api.clients.runtime import RuntimeClient +from qiskit_ibm_runtime import QiskitRuntimeService from ..ibm_test_case import IBMTestCase from ..decorators import IntegrationTestDependencies, integration_test_setup from ..utils import find_free_port ADDRESS = "127.0.0.1" -PORT = find_free_port() +# PORT = find_free_port() +PORT = 8089 VALID_PROXIES = {"https": "http://{}:{}".format(ADDRESS, PORT)} INVALID_PORT_PROXIES = {"https": "http://{}:{}".format(ADDRESS, "6666")} INVALID_ADDRESS_PROXIES = {"https": "http://{}:{}".format("invalid", PORT)} @@ -33,6 +36,9 @@ class TestProxies(IBMTestCase): """Tests for proxy capabilities.""" def setUp(self): + + os.environ["HTTPS_PROXY"] = VALID_PROXIES["https"] + # Pick a random free port for mitmproxy super().setUp() # Command to start mitmproxy in non-interactive mode @@ -50,6 +56,12 @@ def setUp(self): ) time.sleep(0.5) + self._original_connect = socket.socket.connect + def blocking_connect(sock, address): + if address != (ADDRESS, PORT): + raise RuntimeError(f"Blocked network access to {address}") + return self._original_connect(sock, address) + socket.socket.connect = blocking_connect def tearDown(self): super().tearDown() @@ -60,14 +72,18 @@ def tearDown(self): self.proc.wait(timeout=1) except subprocess.TimeoutExpired: self.proc.kill() + socket.socket.connect = self._original_connect - @integration_test_setup(supported_channel=["ibm_cloud", "ibm_quantum_platform"]) + @integration_test_setup(supported_channel=["ibm_cloud", "ibm_quantum_platform"], init_service=False) def test_proxies_cloud_runtime_client(self, dependencies: IntegrationTestDependencies) -> None: """Should reach the proxy using RuntimeClient.""" # pylint: disable=unused-argument - params = dependencies.service._client_params - params.proxies = ProxyConfiguration(urls=VALID_PROXIES) - params.verify = False - client = RuntimeClient(params) - client.jobs_get(limit=1) + service = QiskitRuntimeService( + instance=os.environ["QISKIT_IBM_INSTANCE"], + token=os.environ["QISKIT_IBM_TOKEN"], + channel="ibm_quantum_platform", + verify=False, + proxies={"urls": VALID_PROXIES} + ) + service.jobs(limit=1) self.assertTrue(True) \ No newline at end of file From 89155cbb30cd2580d81655f25697a891c9d8000f Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Wed, 11 Feb 2026 17:21:54 +0100 Subject: [PATCH 03/23] Refacto code and classes --- qiskit_ibm_runtime/accounts/account.py | 17 ++++++++++++----- test/integration/test_proxies.py | 25 +++++++++---------------- test/utils.py | 3 ++- 3 files changed, 23 insertions(+), 22 deletions(-) diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py index 49ed44ad3e..19eb3d6de3 100644 --- a/qiskit_ibm_runtime/accounts/account.py +++ b/qiskit_ibm_runtime/accounts/account.py @@ -271,7 +271,6 @@ def __init__( self.plans_preference = plans_preference self.tags = tags - def get_auth_handler(self) -> AuthBase: """Returns the Cloud authentication handler.""" return CloudAuth( @@ -282,16 +281,20 @@ def get_auth_handler(self) -> AuthBase: verify=self.verify ) - def get_iam_authentificator(self) -> IAMAuthenticator: - iam_url = get_iam_api_url(self.url) + def _get_proxies_kwargs(self) -> dict: proxies_kwargs = {} if self.proxies is not None: proxies_kwargs = self.proxies.to_request_params() + return proxies_kwargs + + def get_iam_authentificator(self) -> IAMAuthenticator: + iam_url = get_iam_api_url(self.url) return IAMAuthenticator( apikey=self.token, url=iam_url, disable_ssl_verification=not self.verify, - **proxies_kwargs) + **self._get_proxies_kwargs() + ) def resolve_crn(self) -> None: """Resolves the corresponding unique Cloud Resource Name (CRN) for the given non-unique service @@ -345,6 +348,8 @@ def list_instances(self) -> list[dict[str, Any]]: ], search_cursor=search_cursor, limit=100, + verify=self.verify, + **self._get_proxies_kwargs() ).get_result() except: # noqa: E722 bare-except raise InvalidAccountError( @@ -358,7 +363,9 @@ def list_instances(self) -> list[dict[str, Any]]: allocations = item.get("doc", {}).get("extensions") if allocations: catalog_result = catalog.get_catalog_entry( - id=item.get("service_plan_unique_id") + id=item.get("service_plan_unique_id"), + verify=self.verify, + **self._get_proxies_kwargs() ).get_result() plan_name = ( catalog_result.get("overview_ui", {}).get("en", {}).get("display_name", "") diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index 4092929b78..b169b450bc 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -13,12 +13,9 @@ """Tests for the proxy support.""" import subprocess -import urllib import time import socket -import os -from qiskit_ibm_runtime.proxies import ProxyConfiguration from qiskit_ibm_runtime import QiskitRuntimeService from ..ibm_test_case import IBMTestCase @@ -26,8 +23,7 @@ from ..utils import find_free_port ADDRESS = "127.0.0.1" -# PORT = find_free_port() -PORT = 8089 +PORT = find_free_port() VALID_PROXIES = {"https": "http://{}:{}".format(ADDRESS, PORT)} INVALID_PORT_PROXIES = {"https": "http://{}:{}".format(ADDRESS, "6666")} INVALID_ADDRESS_PROXIES = {"https": "http://{}:{}".format("invalid", PORT)} @@ -36,15 +32,12 @@ class TestProxies(IBMTestCase): """Tests for proxy capabilities.""" def setUp(self): - - os.environ["HTTPS_PROXY"] = VALID_PROXIES["https"] - - # Pick a random free port for mitmproxy super().setUp() # Command to start mitmproxy in non-interactive mode self.proc = subprocess.Popen( [ "mitmdump", + "--ssl-insecure", "--listen-port", str(PORT), "--listen-host", @@ -55,7 +48,7 @@ def setUp(self): text=True, ) time.sleep(0.5) - + #Block all network flow outside of the proxy self._original_connect = socket.socket.connect def blocking_connect(sock, address): if address != (ADDRESS, PORT): @@ -72,18 +65,18 @@ def tearDown(self): self.proc.wait(timeout=1) except subprocess.TimeoutExpired: self.proc.kill() + #Restore old network config socket.socket.connect = self._original_connect @integration_test_setup(supported_channel=["ibm_cloud", "ibm_quantum_platform"], init_service=False) - def test_proxies_cloud_runtime_client(self, dependencies: IntegrationTestDependencies) -> None: + def test_proxies_qiskit_runtime_service(self, dependencies: IntegrationTestDependencies) -> None: """Should reach the proxy using RuntimeClient.""" # pylint: disable=unused-argument service = QiskitRuntimeService( - instance=os.environ["QISKIT_IBM_INSTANCE"], - token=os.environ["QISKIT_IBM_TOKEN"], - channel="ibm_quantum_platform", + instance=dependencies.instance, + token=dependencies.token, + channel=dependencies.channel, verify=False, - proxies={"urls": VALID_PROXIES} + proxies={ "urls": VALID_PROXIES } ) service.jobs(limit=1) - self.assertTrue(True) \ No newline at end of file diff --git a/test/utils.py b/test/utils.py index 905c50641a..21d179e909 100644 --- a/test/utils.py +++ b/test/utils.py @@ -16,12 +16,12 @@ import logging import time import itertools +import socket import unittest from unittest import mock from typing import Any from datetime import datetime from ddt import data, unpack -import socket from qiskit.circuit import QuantumCircuit, QuantumRegister, ClassicalRegister, Parameter from qiskit.compiler import transpile @@ -478,6 +478,7 @@ def _convert_paul_or_str(_obs): return out_obs def find_free_port(): + """Return a port that is free to use on the machine""" s = socket.socket() s.bind(("", 0)) port = s.getsockname()[1] From f55944436e8041afa2b567d175c5cee83d3c882d Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Thu, 12 Feb 2026 10:04:59 +0100 Subject: [PATCH 04/23] [REFACTO] proxy call function --- qiskit_ibm_runtime/accounts/account.py | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py index 19eb3d6de3..324aa47e49 100644 --- a/qiskit_ibm_runtime/accounts/account.py +++ b/qiskit_ibm_runtime/accounts/account.py @@ -289,11 +289,12 @@ def _get_proxies_kwargs(self) -> dict: def get_iam_authentificator(self) -> IAMAuthenticator: iam_url = get_iam_api_url(self.url) + proxies_kwargs = self._get_proxies_kwargs() return IAMAuthenticator( apikey=self.token, url=iam_url, disable_ssl_verification=not self.verify, - **self._get_proxies_kwargs() + **proxies_kwargs ) def resolve_crn(self) -> None: @@ -335,6 +336,7 @@ def list_instances(self) -> list[dict[str, Any]]: catalog.set_service_url(get_global_catalog_api_url(self.url)) search_cursor = None all_crns = [] + proxies_kwargs = self._get_proxies_kwargs() while True: try: result = client.search( @@ -349,7 +351,7 @@ def list_instances(self) -> list[dict[str, Any]]: search_cursor=search_cursor, limit=100, verify=self.verify, - **self._get_proxies_kwargs() + **proxies_kwargs ).get_result() except: # noqa: E722 bare-except raise InvalidAccountError( @@ -365,7 +367,7 @@ def list_instances(self) -> list[dict[str, Any]]: catalog_result = catalog.get_catalog_entry( id=item.get("service_plan_unique_id"), verify=self.verify, - **self._get_proxies_kwargs() + **proxies_kwargs ).get_result() plan_name = ( catalog_result.get("overview_ui", {}).get("en", {}).get("display_name", "") From 546d7844e7b68a95798100376a45ca257ddda837 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Thu, 12 Feb 2026 10:35:45 +0100 Subject: [PATCH 05/23] [ADD] Release note --- release-notes/unreleased/2592.bug.rst | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 release-notes/unreleased/2592.bug.rst diff --git a/release-notes/unreleased/2592.bug.rst b/release-notes/unreleased/2592.bug.rst new file mode 100644 index 0000000000..21b7256eeb --- /dev/null +++ b/release-notes/unreleased/2592.bug.rst @@ -0,0 +1,3 @@ +The method `.accounts.CloudAccount.resolve_crn ` did not inject proxy and ssl_verification +arguments to outgoing request. Added the arguments to `GlobalSearchV2`, `GlobalCatalogV1` +and `IAMAuthenticator` services. Also debbugued integration tests for proxy usage. \ No newline at end of file From 0d03621af6d457f5773ca829a05b4ddd7b5caa4c Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Thu, 12 Feb 2026 13:17:41 +0100 Subject: [PATCH 06/23] [UPDATE] Packages for integration tests --- .github/workflows/ci.yml | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 43341047e2..774bba2947 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,7 +120,7 @@ jobs: # avoid cancellation of in-progress jobs if any matrix job fails fail-fast: false matrix: - python-version: [ '3.10' ] + python-version: [ '3.11' ] os: [ "ubuntu-latest" ] environment: ["ibm-cloud-production" ] environment: ${{ matrix.environment }} diff --git a/pyproject.toml b/pyproject.toml index 99ce833b13..6005c12712 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,7 +164,7 @@ issues = "https://github.com/Qiskit/qiskit-ibm-runtime/issues" common = [ "mypy==1.19.0", "pylint==3.0.0", - "mitmproxy==12.2.1", + "mitmproxy", "nbqa==1.5.3", "matplotlib>=2.1", "jupyter", From a99585cde175f6ad50f50ddb485f40182fef028e Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Thu, 12 Feb 2026 13:39:17 +0100 Subject: [PATCH 07/23] [REFACTO] Fix typo and linting issues --- qiskit_ibm_runtime/accounts/account.py | 15 ++++++++------- test/decorators.py | 3 ++- test/integration/test_proxies.py | 26 +++++++++++++------------- test/utils.py | 1 + 4 files changed, 24 insertions(+), 21 deletions(-) diff --git a/qiskit_ibm_runtime/accounts/account.py b/qiskit_ibm_runtime/accounts/account.py index 5437e94105..3f7cdfc441 100644 --- a/qiskit_ibm_runtime/accounts/account.py +++ b/qiskit_ibm_runtime/accounts/account.py @@ -279,24 +279,25 @@ def get_auth_handler(self) -> AuthBase: crn=self.instance, private=self.private_endpoint, proxies=self.proxies, - verify=self.verify + verify=self.verify, ) - + def _get_proxies_kwargs(self) -> dict: proxies_kwargs = {} if self.proxies is not None: proxies_kwargs = self.proxies.to_request_params() return proxies_kwargs - + def get_iam_authentificator(self) -> IAMAuthenticator: + """Return the configured IAM Authentification service""" iam_url = get_iam_api_url(self.url) proxies_kwargs = self._get_proxies_kwargs() return IAMAuthenticator( apikey=self.token, url=iam_url, disable_ssl_verification=not self.verify, - **proxies_kwargs - ) + **proxies_kwargs, + ) def resolve_crn(self) -> None: """Resolves the corresponding unique Cloud Resource Name (CRN) for the given non-unique service @@ -352,7 +353,7 @@ def list_instances(self) -> list[dict[str, Any]]: search_cursor=search_cursor, limit=100, verify=self.verify, - **proxies_kwargs + **proxies_kwargs, ).get_result() except: # noqa: E722 bare-except raise InvalidAccountError( @@ -369,7 +370,7 @@ def list_instances(self) -> list[dict[str, Any]]: catalog_result = catalog.get_catalog_entry( id=item.get("service_plan_unique_id"), verify=self.verify, - **proxies_kwargs + **proxies_kwargs, ).get_result() plan_name = ( catalog_result.get("overview_ui", {}) diff --git a/test/decorators.py b/test/decorators.py index b18053ec96..ceea015880 100644 --- a/test/decorators.py +++ b/test/decorators.py @@ -19,6 +19,7 @@ from unittest import SkipTest from qiskit_ibm_runtime import QiskitRuntimeService +from qiskit_ibm_runtime.accounts import ChannelType from .unit.mock.fake_runtime_service import FakeRuntimeService @@ -143,7 +144,7 @@ class IntegrationTestDependencies: instance: str | None qpu: str token: str - channel: str + channel: ChannelType url: str diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index b169b450bc..2f74ca10b1 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -28,6 +28,7 @@ INVALID_PORT_PROXIES = {"https": "http://{}:{}".format(ADDRESS, "6666")} INVALID_ADDRESS_PROXIES = {"https": "http://{}:{}".format("invalid", PORT)} + class TestProxies(IBMTestCase): """Tests for proxy capabilities.""" @@ -35,25 +36,20 @@ def setUp(self): super().setUp() # Command to start mitmproxy in non-interactive mode self.proc = subprocess.Popen( - [ - "mitmdump", - "--ssl-insecure", - "--listen-port", - str(PORT), - "--listen-host", - ADDRESS - ], + ["mitmdump", "--ssl-insecure", "--listen-port", str(PORT), "--listen-host", ADDRESS], stdout=subprocess.PIPE, stderr=subprocess.PIPE, text=True, ) time.sleep(0.5) - #Block all network flow outside of the proxy + # Block all network flow outside of the proxy self._original_connect = socket.socket.connect + def blocking_connect(sock, address): if address != (ADDRESS, PORT): raise RuntimeError(f"Blocked network access to {address}") return self._original_connect(sock, address) + socket.socket.connect = blocking_connect def tearDown(self): @@ -65,11 +61,15 @@ def tearDown(self): self.proc.wait(timeout=1) except subprocess.TimeoutExpired: self.proc.kill() - #Restore old network config + # Restore old network config socket.socket.connect = self._original_connect - @integration_test_setup(supported_channel=["ibm_cloud", "ibm_quantum_platform"], init_service=False) - def test_proxies_qiskit_runtime_service(self, dependencies: IntegrationTestDependencies) -> None: + @integration_test_setup( + supported_channel=["ibm_cloud", "ibm_quantum_platform"], init_service=False + ) + def test_proxies_qiskit_runtime_service( + self, dependencies: IntegrationTestDependencies + ) -> None: """Should reach the proxy using RuntimeClient.""" # pylint: disable=unused-argument service = QiskitRuntimeService( @@ -77,6 +77,6 @@ def test_proxies_qiskit_runtime_service(self, dependencies: IntegrationTestDepen token=dependencies.token, channel=dependencies.channel, verify=False, - proxies={ "urls": VALID_PROXIES } + proxies={"urls": VALID_PROXIES}, ) service.jobs(limit=1) diff --git a/test/utils.py b/test/utils.py index 21d179e909..62b3305e2d 100644 --- a/test/utils.py +++ b/test/utils.py @@ -477,6 +477,7 @@ def _convert_paul_or_str(_obs): return out_obs + def find_free_port(): """Return a port that is free to use on the machine""" s = socket.socket() From ef2bed6d2db2626ba217fa9d9727e4decd40bb73 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Thu, 12 Feb 2026 14:20:38 +0100 Subject: [PATCH 08/23] [UPGRADE] CI documentation step python version to 3.11 --- .github/workflows/ci.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 774bba2947..fb070df46b 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,10 +48,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Python 3.10 + - name: Set up Python 3.11 uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: '3.11' - name: Install dependencies run: | python -m pip install --upgrade pip From b9a91e17c744ac114aba74d1e7b791896ae6f94a Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Thu, 12 Feb 2026 14:29:56 +0100 Subject: [PATCH 09/23] [DEBBUG] Update setuptools dependency --- .github/workflows/ci.yml | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index fb070df46b..499db51706 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -48,13 +48,14 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Python 3.11 + - name: Set up Python 3.10 uses: actions/setup-python@v5 with: - python-version: '3.11' + python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip + pip install -U setuptools pip install -U tox pip install nbqa docutils sudo apt install -y graphviz pandoc From a65b89f5981c1643e6fc0fd8737a17d91280ed96 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Thu, 12 Feb 2026 14:49:59 +0100 Subject: [PATCH 10/23] =?UTF-8?q?[DEBBUG]=C2=A0Add=20proxy=20lib=20in=20an?= =?UTF-8?q?=20another=20pyproj=20group?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .github/workflows/ci.yml | 4 ++-- pyproject.toml | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 499db51706..095e8d1a09 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -55,7 +55,6 @@ jobs: - name: Install dependencies run: | python -m pip install --upgrade pip - pip install -U setuptools pip install -U tox pip install nbqa docutils sudo apt install -y graphviz pandoc @@ -145,6 +144,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -e '.[dev]' + pip install -e '.[integration_test_proxy]' - name: Run integration tests run: make integration-test tests-finished: @@ -157,4 +157,4 @@ jobs: uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - parallel-finished: true + parallel-finished: true \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 6005c12712..a2500eee2f 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -164,7 +164,6 @@ issues = "https://github.com/Qiskit/qiskit-ibm-runtime/issues" common = [ "mypy==1.19.0", "pylint==3.0.0", - "mitmproxy", "nbqa==1.5.3", "matplotlib>=2.1", "jupyter", @@ -192,6 +191,7 @@ documentation = [ visualization = ["plotly>=5.23.0"] test = ["ddt>=1.2.0,!=1.4.0,!=1.4.3"] +integration_test_proxy = ["mitmproxy"] dev = [ "qiskit-ibm-runtime[test]", From b3061bf2a296c6693472b8c4caff1c114747453a Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Thu, 12 Feb 2026 15:31:52 +0100 Subject: [PATCH 11/23] [CHANGE] Packages dependency --- .github/workflows/ci.yml | 2 +- CONTRIBUTING.md | 4 ++++ pyproject.toml | 5 +++++ 3 files changed, 10 insertions(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 095e8d1a09..83fbe71e36 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] os: [ "macos-latest", "ubuntu-latest", "windows-latest" ] exclude: - os: "macos-latest" diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1d3107bb64..3a853ec983 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,6 +37,10 @@ pip install -e ".[visualization]" ``` {.bash} pip install -e ".[dev]" ``` +- Install the integration tests dependencies +``` {.bash} +pip install -e ".[integration]" +``` ### Open an issue diff --git a/pyproject.toml b/pyproject.toml index a2500eee2f..30337a48f5 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -199,3 +199,8 @@ dev = [ "qiskit-ibm-runtime[documentation]", "qiskit-ibm-runtime[visualization]", ] + +integration = [ + "qiskit-ibm-runtime[dev]", + "qiskit-ibm-runtime[integration_test_proxy]" +] \ No newline at end of file From b9f6049f7986371a18aca52d953fde4b8bce700c Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Thu, 12 Feb 2026 16:00:23 +0100 Subject: [PATCH 12/23] [CHANGE] Integration CI step package installation --- .github/workflows/ci.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 83fbe71e36..50962592c6 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -144,7 +144,7 @@ jobs: run: | python -m pip install --upgrade pip pip install -e '.[dev]' - pip install -e '.[integration_test_proxy]' + pip install -e '.[integration]' - name: Run integration tests run: make integration-test tests-finished: From 86d34188f8bb9897601a75f584b35fc0794b35bd Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Fri, 27 Feb 2026 09:18:03 +0100 Subject: [PATCH 13/23] [REVERT] Mitmproxy proxy in IT --- .github/workflows/ci.yml | 6 +- CONTRIBUTING.md | 6 +- pyproject.toml | 20 +++---- release-notes/unreleased/2592.bug.rst | 2 +- test/integration/test_proxies.py | 80 ++++++++++++--------------- test/utils.py | 10 ---- 6 files changed, 48 insertions(+), 76 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50962592c6..b7dcbfc4a7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -120,7 +120,7 @@ jobs: # avoid cancellation of in-progress jobs if any matrix job fails fail-fast: false matrix: - python-version: [ '3.11' ] + python-version: [ '3.10' ] os: [ "ubuntu-latest" ] environment: ["ibm-cloud-production" ] environment: ${{ matrix.environment }} @@ -144,7 +144,6 @@ jobs: run: | python -m pip install --upgrade pip pip install -e '.[dev]' - pip install -e '.[integration]' - name: Run integration tests run: make integration-test tests-finished: @@ -157,4 +156,5 @@ jobs: uses: coverallsapp/github-action@v2 with: github-token: ${{ secrets.GITHUB_TOKEN }} - parallel-finished: true \ No newline at end of file + parallel-finished: true + fail-on-error: false \ No newline at end of file diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 3a853ec983..fda34d66a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -37,10 +37,6 @@ pip install -e ".[visualization]" ``` {.bash} pip install -e ".[dev]" ``` -- Install the integration tests dependencies -``` {.bash} -pip install -e ".[integration]" -``` ### Open an issue @@ -338,4 +334,4 @@ the `main` branch, such as from `stable/0.21` to `main`. [black]: https://github.com/psf/black [pylint]: https://www.pylint.org/ [ruff]: https://github.com/astral-sh/ruff -[mypy]: http://mypy-lang.org/ +[mypy]: http://mypy-lang.org/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index 30337a48f5..bcb6c7c1b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -83,12 +83,13 @@ target-version = "py310" future-annotations = true select = [ # Rules in alphabetic order - "C4", # flake8-comprehensions - "E", # pycodestyle - "F", # pyflake - "G", # flake8-logging-format - "T10", # flake8-debugger - "UP", # pyupgrade + "C4", # flake8-comprehensions + "E", # pycodestyle + "F", # pyflake + "G", # flake8-logging-format + "T10", # flake8-debugger + "UP", # pyupgrade + "RUF013", # implicit-optional ] ignore = [ "E501", # line-too-long: black handles line length @@ -164,6 +165,7 @@ issues = "https://github.com/Qiskit/qiskit-ibm-runtime/issues" common = [ "mypy==1.19.0", "pylint==3.0.0", + "pproxy==2.7.8", "nbqa==1.5.3", "matplotlib>=2.1", "jupyter", @@ -191,16 +193,10 @@ documentation = [ visualization = ["plotly>=5.23.0"] test = ["ddt>=1.2.0,!=1.4.0,!=1.4.3"] -integration_test_proxy = ["mitmproxy"] dev = [ "qiskit-ibm-runtime[test]", "qiskit-ibm-runtime[common]", "qiskit-ibm-runtime[documentation]", "qiskit-ibm-runtime[visualization]", -] - -integration = [ - "qiskit-ibm-runtime[dev]", - "qiskit-ibm-runtime[integration_test_proxy]" ] \ No newline at end of file diff --git a/release-notes/unreleased/2592.bug.rst b/release-notes/unreleased/2592.bug.rst index 21b7256eeb..47bae83253 100644 --- a/release-notes/unreleased/2592.bug.rst +++ b/release-notes/unreleased/2592.bug.rst @@ -1,3 +1,3 @@ The method `.accounts.CloudAccount.resolve_crn ` did not inject proxy and ssl_verification arguments to outgoing request. Added the arguments to `GlobalSearchV2`, `GlobalCatalogV1` -and `IAMAuthenticator` services. Also debbugued integration tests for proxy usage. \ No newline at end of file +and `IAMAuthenticator` services. \ No newline at end of file diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index 2f74ca10b1..68f796cb38 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -13,17 +13,17 @@ """Tests for the proxy support.""" import subprocess -import time -import socket +import urllib -from qiskit_ibm_runtime import QiskitRuntimeService + +from qiskit_ibm_runtime.proxies import ProxyConfiguration +from qiskit_ibm_runtime.api.clients.runtime import RuntimeClient from ..ibm_test_case import IBMTestCase from ..decorators import IntegrationTestDependencies, integration_test_setup -from ..utils import find_free_port ADDRESS = "127.0.0.1" -PORT = find_free_port() +PORT = 8085 VALID_PROXIES = {"https": "http://{}:{}".format(ADDRESS, PORT)} INVALID_PORT_PROXIES = {"https": "http://{}:{}".format(ADDRESS, "6666")} INVALID_ADDRESS_PROXIES = {"https": "http://{}:{}".format("invalid", PORT)} @@ -33,50 +33,40 @@ class TestProxies(IBMTestCase): """Tests for proxy capabilities.""" def setUp(self): + """Initial test setup.""" super().setUp() - # Command to start mitmproxy in non-interactive mode - self.proc = subprocess.Popen( - ["mitmdump", "--ssl-insecure", "--listen-port", str(PORT), "--listen-host", ADDRESS], - stdout=subprocess.PIPE, - stderr=subprocess.PIPE, - text=True, - ) - time.sleep(0.5) - # Block all network flow outside of the proxy - self._original_connect = socket.socket.connect - - def blocking_connect(sock, address): - if address != (ADDRESS, PORT): - raise RuntimeError(f"Blocked network access to {address}") - return self._original_connect(sock, address) - - socket.socket.connect = blocking_connect + # launch a mock server. + command = ["pproxy", "-v", "-l", "http://{}:{}".format(ADDRESS, PORT)] + self.proxy_process = subprocess.Popen(command, stdout=subprocess.PIPE) def tearDown(self): + """Test cleanup.""" super().tearDown() - # Kill the proxy process - if self.proc.poll() is None: - self.proc.terminate() - try: - self.proc.wait(timeout=1) - except subprocess.TimeoutExpired: - self.proc.kill() - # Restore old network config - socket.socket.connect = self._original_connect - @integration_test_setup( - supported_channel=["ibm_cloud", "ibm_quantum_platform"], init_service=False - ) - def test_proxies_qiskit_runtime_service( - self, dependencies: IntegrationTestDependencies - ) -> None: + # terminate the mock server. + if self.proxy_process.returncode is None: + self.proxy_process.stdout.close() # close the IO buffer + self.proxy_process.terminate() # initiate process termination + + # wait for the process to terminate + self.proxy_process.wait() + + @integration_test_setup(supported_channel=["ibm_cloud"]) + def test_proxies_cloud_runtime_client(self, dependencies: IntegrationTestDependencies) -> None: """Should reach the proxy using RuntimeClient.""" # pylint: disable=unused-argument - service = QiskitRuntimeService( - instance=dependencies.instance, - token=dependencies.token, - channel=dependencies.channel, - verify=False, - proxies={"urls": VALID_PROXIES}, - ) - service.jobs(limit=1) + params = dependencies.service._client_params + params.proxies = ProxyConfiguration(urls=VALID_PROXIES) + client = RuntimeClient(params) + client.jobs_get(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) + + +def pproxy_desired_access_log_line(url): + """Return a desired pproxy log entry given a url.""" + qe_url_parts = urllib.parse.urlparse(url) + protocol_port = "443" if qe_url_parts.scheme == "https" else "80" + return "{}:{}".format(qe_url_parts.hostname, protocol_port) \ No newline at end of file diff --git a/test/utils.py b/test/utils.py index 62b3305e2d..4f21276ffe 100644 --- a/test/utils.py +++ b/test/utils.py @@ -16,7 +16,6 @@ import logging import time import itertools -import socket import unittest from unittest import mock from typing import Any @@ -478,15 +477,6 @@ def _convert_paul_or_str(_obs): return out_obs -def find_free_port(): - """Return a port that is free to use on the machine""" - s = socket.socket() - s.bind(("", 0)) - port = s.getsockname()[1] - s.close() - return port - - class MockSession(Session): """Mock for session class""" From 7333b62859e1b5cfe5d607b6459a6f27c543fa31 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Fri, 27 Feb 2026 09:39:36 +0100 Subject: [PATCH 14/23] [REFACTO] Reformat/lint] --- test/integration/test_proxies.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index 68f796cb38..0f0e670c41 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -69,4 +69,4 @@ def pproxy_desired_access_log_line(url): """Return a desired pproxy log entry given a url.""" qe_url_parts = urllib.parse.urlparse(url) protocol_port = "443" if qe_url_parts.scheme == "https" else "80" - return "{}:{}".format(qe_url_parts.hostname, protocol_port) \ No newline at end of file + return "{}:{}".format(qe_url_parts.hostname, protocol_port) From ac1960e7cbadc34c439ea4e2e43fdfacb5a534c1 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo <109806147+alodolo@users.noreply.github.com> Date: Sun, 22 Mar 2026 19:49:38 +0100 Subject: [PATCH 15/23] Update release-notes/unreleased/2592.bug.rst MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Diego M. Rodríguez --- release-notes/unreleased/2592.bug.rst | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/release-notes/unreleased/2592.bug.rst b/release-notes/unreleased/2592.bug.rst index 47bae83253..daff339992 100644 --- a/release-notes/unreleased/2592.bug.rst +++ b/release-notes/unreleased/2592.bug.rst @@ -1,3 +1,4 @@ -The method `.accounts.CloudAccount.resolve_crn ` did not inject proxy and ssl_verification -arguments to outgoing request. Added the arguments to `GlobalSearchV2`, `GlobalCatalogV1` -and `IAMAuthenticator` services. \ No newline at end of file +The ``proxies`` and ``ssl_verification`` arguments for ``QiskitRuntimeService`` +are nowpropagated to the underlying HTTP requests to ```GlobalSearchV2``, +``GlobalCatalogV1`` and ``IAMAuthenticator`` services, allowing to instantiate +``QiskitRuntimeService`` correctly when using a proxy. From ef4108bc2f6261423bfaed4fbd980eded00a2158 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Sun, 22 Mar 2026 20:02:56 +0100 Subject: [PATCH 16/23] [REVERT] Revert ci.yaml, Contributing.md & pyproject.yaml commits --- .github/workflows/ci.yml | 16 ++++----- CONTRIBUTING.md | 23 +++--------- pyproject.toml | 77 ++++++---------------------------------- 3 files changed, 22 insertions(+), 94 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 041714f49d..f89498912a 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,10 +26,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Python 3.10 + - name: Set up Python 3.9 uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -48,10 +48,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Python 3.10 + - name: Set up Python 3.9 uses: actions/setup-python@v5 with: - python-version: '3.10' + python-version: 3.9 - name: Install dependencies run: | python -m pip install --upgrade pip @@ -80,11 +80,11 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] os: [ "macos-latest", "ubuntu-latest", "windows-latest" ] exclude: - os: "macos-latest" - python-version: '3.10' + python-version: 3.9 env: LOG_LEVEL: DEBUG STREAM_LOG: True @@ -110,7 +110,6 @@ jobs: flag-name: unit-tests_python${{ matrix.python-version }}-${{ matrix.os }} parallel: true file: coverage.lcov - fail-on-error: false integration-tests: if: github.event_name == 'push' && github.repository_owner == 'Qiskit' # only kick-off resource intensive integration tests if unit tests and all basic checks succeeded @@ -121,7 +120,7 @@ jobs: # avoid cancellation of in-progress jobs if any matrix job fails fail-fast: false matrix: - python-version: [ '3.10' ] + python-version: [ 3.9 ] os: [ "ubuntu-latest" ] environment: ["ibm-cloud-production" ] environment: ${{ matrix.environment }} @@ -158,4 +157,3 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true - fail-on-error: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fda34d66a5..dc7f7cfb89 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -219,7 +219,7 @@ Integration tests require an environment configuration and can be run against th Sample configuration for IBM Cloud (ibm_quantum_platform) ```bash QISKIT_IBM_TOKEN=... # IBM Cloud API key -QISKIT_IBM_URL=https://cloud.ibm.com # Cloud URL +QISKIT_IBM_URL=https://quantum.cloud.ibm.com # Cloud URL QISKIT_IBM_INSTANCE=crn:v1:bluemix:... # The CRN value of the Quantum service instance QISKIT_IBM_QPU=... # The Quantum Processing Unit to use ``` @@ -236,12 +236,9 @@ For example, in your github fork settings, add the environment you want to run t Please submit clean code and please make effort to follow existing conventions in order to keep it as readable as possible. We use: -* [black] as the tool for ensuring consistent code formatting -* [pylint] as a linter providing deeper analysis for potential style and - functionality issues, as well as good practices -* [ruff] as an additional linter, introduced recently for additional style - and functionality checks -* [mypy] as a static type checker for type hinting +* [Pylint](https://www.pylint.org) linter +* [PEP 8](https://www.python.org/dev/peps/pep-0008) style +* [mypy](http://mypy-lang.org/) type hinting To ensure your changes respect the style guidelines, you can run the following commands: @@ -252,13 +249,6 @@ make style make mypy ``` -If `make style` results in an error, you can run `make black` for automatically -update your code to conform to the style. Similarly, if `make lint` fails in the -`ruff` check, you can run `make ruff` for attempting to apply automated fixes, -if available. However, for a subset of the `ruff` errors, as well as for the -`pylint` and `mypy` errors, you will have to fix the issues manually by updating -your code. - If you edit any documentation, refer to [IBM Quantum's writing style guide](https://github.com/IBM/ibm-quantum-style-guide). You can use [Vale](https://vale.sh) to automatically check some of these rules for you. @@ -330,8 +320,3 @@ qiskit-bot should also automatically create the GitHub Release for you. Finally, you need to cherry-pick the release notes prep from `stable/*` to the `main` branch, such as from `stable/0.21` to `main`. - -[black]: https://github.com/psf/black -[pylint]: https://www.pylint.org/ -[ruff]: https://github.com/astral-sh/ruff -[mypy]: http://mypy-lang.org/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index bcb6c7c1b1..de48ad2a2c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["setuptools >= 40.6.0", "setuptools_scm[toml]~=8.0"] +requires = ["setuptools >= 40.6.0", "setuptools_scm[toml]>=6.2"] build-backend = "setuptools.build_meta" [tool.black] line-length = 100 -target-version = ['py310', 'py311', 'py312', 'py313'] +target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] [tool.towncrier] single_file = false @@ -48,61 +48,9 @@ zip-safe = false [tool.setuptools_scm] root = "." -version_file = "qiskit_ibm_runtime/VERSION.txt" +write_to = "qiskit_ibm_runtime/VERSION.txt" version_scheme = "release-branch-semver" -fallback_version = "0.45.0" - -[tool.mypy] -python_version = "3.10" -namespace_packages = true -ignore_missing_imports = true -warn_redundant_casts = true -warn_unreachable = true -strict_equality = true -disallow_untyped_calls = true -disallow_untyped_defs = true -disallow_incomplete_defs = true -strict_optional = false -show_error_codes = true -no_site_packages = true -disable_error_code = "import-untyped" - -[[tool.mypy.overrides]] -module = "test.*" -disallow_untyped_calls = false -disallow_untyped_defs = false - -[tool.coverage.run] -source = ["qiskit_ibm_runtime"] - -[tool.ruff] -line-length = 100 -target-version = "py310" - -[tool.ruff.lint] -future-annotations = true -select = [ - # Rules in alphabetic order - "C4", # flake8-comprehensions - "E", # pycodestyle - "F", # pyflake - "G", # flake8-logging-format - "T10", # flake8-debugger - "UP", # pyupgrade - "RUF013", # implicit-optional -] -ignore = [ - "E501", # line-too-long: black handles line length - "UP032", # f-string: requires manual revision of non f-string formatting -] - -[tool.ruff.lint.per-file-ignores] -"__init__.py" = [ - "E402", # module-import-not-at-top-of-file - "F401", # unused-import - "F403", # undefined-local-with-import-star -] - +fallback_version = "0.43.1" [project] name = "qiskit-ibm-runtime" @@ -122,6 +70,7 @@ classifiers=[ "Operating System :: MacOS", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", + "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -131,21 +80,18 @@ classifiers=[ keywords = ["qiskit", "sdk", "quantum", "api", "runtime", "ibm"] -requires-python=">=3.10" +requires-python=">=3.9" dependencies = [ - "requests>=2.19,!=2.32.2", + "requests>=2.19", "requests-ntlm>=1.1.0", "numpy>=1.13", "urllib3>=1.21.1", "python-dateutil>=2.8.0", "ibm-platform-services>=0.22.6", - "ibm-quantum-schemas>=0.2.20260209", "pydantic>=2.5.0", - "qiskit>=2.0.0", - "packaging", - "pybase64>=1.0", - "samplomatic>=0.13.0" + "qiskit>=1.4.1", + "packaging" ] [project.entry-points."qiskit.transpiler.translation"] @@ -163,7 +109,7 @@ issues = "https://github.com/Qiskit/qiskit-ibm-runtime/issues" [project.optional-dependencies] common = [ - "mypy==1.19.0", + "mypy==0.931", "pylint==3.0.0", "pproxy==2.7.8", "nbqa==1.5.3", @@ -174,7 +120,6 @@ common = [ "nbconvert>=5.3.1", "qiskit-aer>=0.17.0", "black~=24.1", - "ruff~=0.14.10", "coverage>=6.3", "pylatexenc", "scikit-learn", @@ -199,4 +144,4 @@ dev = [ "qiskit-ibm-runtime[common]", "qiskit-ibm-runtime[documentation]", "qiskit-ibm-runtime[visualization]", -] \ No newline at end of file +] From 7c75622a8f7b6e8a201dc67afa0fe031aba09d7f Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Sun, 22 Mar 2026 20:04:36 +0100 Subject: [PATCH 17/23] Revert "[REVERT] Revert ci.yaml, Contributing.md & pyproject.yaml commits" This reverts commit ef4108bc2f6261423bfaed4fbd980eded00a2158. --- .github/workflows/ci.yml | 16 +++++---- CONTRIBUTING.md | 23 +++++++++--- pyproject.toml | 77 ++++++++++++++++++++++++++++++++++------ 3 files changed, 94 insertions(+), 22 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f89498912a..041714f49d 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -26,10 +26,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Python 3.9 + - name: Set up Python 3.10 uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip @@ -48,10 +48,10 @@ jobs: - uses: actions/checkout@v4 with: fetch-depth: 0 - - name: Set up Python 3.9 + - name: Set up Python 3.10 uses: actions/setup-python@v5 with: - python-version: 3.9 + python-version: '3.10' - name: Install dependencies run: | python -m pip install --upgrade pip @@ -80,11 +80,11 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: [3.9, '3.10', '3.11', '3.12', '3.13'] + python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] os: [ "macos-latest", "ubuntu-latest", "windows-latest" ] exclude: - os: "macos-latest" - python-version: 3.9 + python-version: '3.10' env: LOG_LEVEL: DEBUG STREAM_LOG: True @@ -110,6 +110,7 @@ jobs: flag-name: unit-tests_python${{ matrix.python-version }}-${{ matrix.os }} parallel: true file: coverage.lcov + fail-on-error: false integration-tests: if: github.event_name == 'push' && github.repository_owner == 'Qiskit' # only kick-off resource intensive integration tests if unit tests and all basic checks succeeded @@ -120,7 +121,7 @@ jobs: # avoid cancellation of in-progress jobs if any matrix job fails fail-fast: false matrix: - python-version: [ 3.9 ] + python-version: [ '3.10' ] os: [ "ubuntu-latest" ] environment: ["ibm-cloud-production" ] environment: ${{ matrix.environment }} @@ -157,3 +158,4 @@ jobs: with: github-token: ${{ secrets.GITHUB_TOKEN }} parallel-finished: true + fail-on-error: false diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index dc7f7cfb89..fda34d66a5 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -219,7 +219,7 @@ Integration tests require an environment configuration and can be run against th Sample configuration for IBM Cloud (ibm_quantum_platform) ```bash QISKIT_IBM_TOKEN=... # IBM Cloud API key -QISKIT_IBM_URL=https://quantum.cloud.ibm.com # Cloud URL +QISKIT_IBM_URL=https://cloud.ibm.com # Cloud URL QISKIT_IBM_INSTANCE=crn:v1:bluemix:... # The CRN value of the Quantum service instance QISKIT_IBM_QPU=... # The Quantum Processing Unit to use ``` @@ -236,9 +236,12 @@ For example, in your github fork settings, add the environment you want to run t Please submit clean code and please make effort to follow existing conventions in order to keep it as readable as possible. We use: -* [Pylint](https://www.pylint.org) linter -* [PEP 8](https://www.python.org/dev/peps/pep-0008) style -* [mypy](http://mypy-lang.org/) type hinting +* [black] as the tool for ensuring consistent code formatting +* [pylint] as a linter providing deeper analysis for potential style and + functionality issues, as well as good practices +* [ruff] as an additional linter, introduced recently for additional style + and functionality checks +* [mypy] as a static type checker for type hinting To ensure your changes respect the style guidelines, you can run the following commands: @@ -249,6 +252,13 @@ make style make mypy ``` +If `make style` results in an error, you can run `make black` for automatically +update your code to conform to the style. Similarly, if `make lint` fails in the +`ruff` check, you can run `make ruff` for attempting to apply automated fixes, +if available. However, for a subset of the `ruff` errors, as well as for the +`pylint` and `mypy` errors, you will have to fix the issues manually by updating +your code. + If you edit any documentation, refer to [IBM Quantum's writing style guide](https://github.com/IBM/ibm-quantum-style-guide). You can use [Vale](https://vale.sh) to automatically check some of these rules for you. @@ -320,3 +330,8 @@ qiskit-bot should also automatically create the GitHub Release for you. Finally, you need to cherry-pick the release notes prep from `stable/*` to the `main` branch, such as from `stable/0.21` to `main`. + +[black]: https://github.com/psf/black +[pylint]: https://www.pylint.org/ +[ruff]: https://github.com/astral-sh/ruff +[mypy]: http://mypy-lang.org/ \ No newline at end of file diff --git a/pyproject.toml b/pyproject.toml index de48ad2a2c..bcb6c7c1b1 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,10 +1,10 @@ [build-system] -requires = ["setuptools >= 40.6.0", "setuptools_scm[toml]>=6.2"] +requires = ["setuptools >= 40.6.0", "setuptools_scm[toml]~=8.0"] build-backend = "setuptools.build_meta" [tool.black] line-length = 100 -target-version = ['py39', 'py310', 'py311', 'py312', 'py313'] +target-version = ['py310', 'py311', 'py312', 'py313'] [tool.towncrier] single_file = false @@ -48,9 +48,61 @@ zip-safe = false [tool.setuptools_scm] root = "." -write_to = "qiskit_ibm_runtime/VERSION.txt" +version_file = "qiskit_ibm_runtime/VERSION.txt" version_scheme = "release-branch-semver" -fallback_version = "0.43.1" +fallback_version = "0.45.0" + +[tool.mypy] +python_version = "3.10" +namespace_packages = true +ignore_missing_imports = true +warn_redundant_casts = true +warn_unreachable = true +strict_equality = true +disallow_untyped_calls = true +disallow_untyped_defs = true +disallow_incomplete_defs = true +strict_optional = false +show_error_codes = true +no_site_packages = true +disable_error_code = "import-untyped" + +[[tool.mypy.overrides]] +module = "test.*" +disallow_untyped_calls = false +disallow_untyped_defs = false + +[tool.coverage.run] +source = ["qiskit_ibm_runtime"] + +[tool.ruff] +line-length = 100 +target-version = "py310" + +[tool.ruff.lint] +future-annotations = true +select = [ + # Rules in alphabetic order + "C4", # flake8-comprehensions + "E", # pycodestyle + "F", # pyflake + "G", # flake8-logging-format + "T10", # flake8-debugger + "UP", # pyupgrade + "RUF013", # implicit-optional +] +ignore = [ + "E501", # line-too-long: black handles line length + "UP032", # f-string: requires manual revision of non f-string formatting +] + +[tool.ruff.lint.per-file-ignores] +"__init__.py" = [ + "E402", # module-import-not-at-top-of-file + "F401", # unused-import + "F403", # undefined-local-with-import-star +] + [project] name = "qiskit-ibm-runtime" @@ -70,7 +122,6 @@ classifiers=[ "Operating System :: MacOS", "Operating System :: POSIX :: Linux", "Programming Language :: Python :: 3 :: Only", - "Programming Language :: Python :: 3.9", "Programming Language :: Python :: 3.10", "Programming Language :: Python :: 3.11", "Programming Language :: Python :: 3.12", @@ -80,18 +131,21 @@ classifiers=[ keywords = ["qiskit", "sdk", "quantum", "api", "runtime", "ibm"] -requires-python=">=3.9" +requires-python=">=3.10" dependencies = [ - "requests>=2.19", + "requests>=2.19,!=2.32.2", "requests-ntlm>=1.1.0", "numpy>=1.13", "urllib3>=1.21.1", "python-dateutil>=2.8.0", "ibm-platform-services>=0.22.6", + "ibm-quantum-schemas>=0.2.20260209", "pydantic>=2.5.0", - "qiskit>=1.4.1", - "packaging" + "qiskit>=2.0.0", + "packaging", + "pybase64>=1.0", + "samplomatic>=0.13.0" ] [project.entry-points."qiskit.transpiler.translation"] @@ -109,7 +163,7 @@ issues = "https://github.com/Qiskit/qiskit-ibm-runtime/issues" [project.optional-dependencies] common = [ - "mypy==0.931", + "mypy==1.19.0", "pylint==3.0.0", "pproxy==2.7.8", "nbqa==1.5.3", @@ -120,6 +174,7 @@ common = [ "nbconvert>=5.3.1", "qiskit-aer>=0.17.0", "black~=24.1", + "ruff~=0.14.10", "coverage>=6.3", "pylatexenc", "scikit-learn", @@ -144,4 +199,4 @@ dev = [ "qiskit-ibm-runtime[common]", "qiskit-ibm-runtime[documentation]", "qiskit-ibm-runtime[visualization]", -] +] \ No newline at end of file From 332cc9502f2f02205f4a3208bd39a6cd7631cab2 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Sun, 22 Mar 2026 20:09:09 +0100 Subject: [PATCH 18/23] [REVERT] Disable python 3.14 in CI and revert typos --- CONTRIBUTING.md | 2 +- pyproject.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index fda34d66a5..1d3107bb64 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -334,4 +334,4 @@ the `main` branch, such as from `stable/0.21` to `main`. [black]: https://github.com/psf/black [pylint]: https://www.pylint.org/ [ruff]: https://github.com/astral-sh/ruff -[mypy]: http://mypy-lang.org/ \ No newline at end of file +[mypy]: http://mypy-lang.org/ diff --git a/pyproject.toml b/pyproject.toml index bcb6c7c1b1..18950dc979 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -199,4 +199,4 @@ dev = [ "qiskit-ibm-runtime[common]", "qiskit-ibm-runtime[documentation]", "qiskit-ibm-runtime[visualization]", -] \ No newline at end of file +] From 82ef0512bc943ee52dd10a3c835acb314a0c44a4 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Sun, 22 Mar 2026 20:11:27 +0100 Subject: [PATCH 19/23] [PACTH] eof in files --- .github/workflows/ci.yml | 2 +- test/utils.py | 1 - 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 041714f49d..5bea2d5aab 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -80,7 +80,7 @@ jobs: runs-on: ${{ matrix.os }} strategy: matrix: - python-version: ['3.10', '3.11', '3.12', '3.13', '3.14'] + python-version: ['3.10', '3.11', '3.12', '3.13'] os: [ "macos-latest", "ubuntu-latest", "windows-latest" ] exclude: - os: "macos-latest" diff --git a/test/utils.py b/test/utils.py index 947982b7f5..d01a006fcc 100644 --- a/test/utils.py +++ b/test/utils.py @@ -27,7 +27,6 @@ from qiskit.providers.exceptions import QiskitBackendNotFoundError from qiskit.providers.backend import Backend from qiskit.quantum_info import SparsePauliOp, Pauli - from qiskit_ibm_runtime import ( QiskitRuntimeService, Session, From 50e1ed0fb10f6000f1a7a5d61395779c421eff02 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Tue, 31 Mar 2026 00:09:05 +0200 Subject: [PATCH 20/23] [ADD] Integration tests with qiskit IBM Runtime --- test/integration/test_proxies.py | 55 +++++++++++++++++++++++++------- 1 file changed, 44 insertions(+), 11 deletions(-) diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index 0f0e670c41..9295053e01 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -14,10 +14,13 @@ import subprocess import urllib +from time import sleep +import socket -from qiskit_ibm_runtime.proxies import ProxyConfiguration -from qiskit_ibm_runtime.api.clients.runtime import RuntimeClient +from qiskit_ibm_runtime import QiskitRuntimeService +from qiskit_ibm_runtime.accounts.exceptions import InvalidAccountError + from ..ibm_test_case import IBMTestCase from ..decorators import IntegrationTestDependencies, integration_test_setup @@ -38,6 +41,17 @@ def setUp(self): # launch a mock server. command = ["pproxy", "-v", "-l", "http://{}:{}".format(ADDRESS, PORT)] self.proxy_process = subprocess.Popen(command, stdout=subprocess.PIPE) + """Time for the proxy to start""" + sleep(2) + """Block all traffic not routed to the proxy""" + self._original_connect = socket.socket.connect + + def blocking_connect(sock, address): + if address != (ADDRESS, PORT): + raise RuntimeError(f"Blocked network access to {address}") + return self._original_connect(sock, address) + + socket.socket.connect = blocking_connect def tearDown(self): """Test cleanup.""" @@ -50,20 +64,39 @@ def tearDown(self): # wait for the process to terminate self.proxy_process.wait() + socket.socket.connect = self._original_connect - @integration_test_setup(supported_channel=["ibm_cloud"]) - def test_proxies_cloud_runtime_client(self, dependencies: IntegrationTestDependencies) -> None: - """Should reach the proxy using RuntimeClient.""" - # pylint: disable=unused-argument - params = dependencies.service._client_params - params.proxies = ProxyConfiguration(urls=VALID_PROXIES) - client = RuntimeClient(params) - client.jobs_get(limit=1) - api_line = pproxy_desired_access_log_line(params.url) + @integration_test_setup(supported_channel=["ibm_quantum_platform"], init_service=False) + def test_proxies_qiskit_runtime_service( + self, dependencies: IntegrationTestDependencies + ) -> None: + """Should reach the proxy using QiskitRuntimeService.""" + # Allow some time for `pproxy` to receive requests. + service = QiskitRuntimeService( + instance=dependencies.instance, + token=dependencies.token, + channel=dependencies.channel, + verify=False, + proxies={"urls": VALID_PROXIES}, + ) + service.jobs(limit=1) + + api_line = pproxy_desired_access_log_line(dependencies.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) + @integration_test_setup(supported_channel=["ibm_quantum_platform"], init_service=False) + def test_no_proxy_raises_exception(self, dependencies: IntegrationTestDependencies) -> None: + """Should raise an exception when no proxy is specified.""" + with self.assertRaises(InvalidAccountError): + service = QiskitRuntimeService( + instance=dependencies.instance, + token=dependencies.token, + channel=dependencies.channel, + ) + service.jobs(limit=1) + def pproxy_desired_access_log_line(url): """Return a desired pproxy log entry given a url.""" From 2efc440f32c2bf0318e108757940b9fa0974da55 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Tue, 31 Mar 2026 09:38:01 +0200 Subject: [PATCH 21/23] [PATCH] Fix typos --- test/integration/test_proxies.py | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index 9295053e01..012bd6ca52 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -41,9 +41,9 @@ def setUp(self): # launch a mock server. command = ["pproxy", "-v", "-l", "http://{}:{}".format(ADDRESS, PORT)] self.proxy_process = subprocess.Popen(command, stdout=subprocess.PIPE) - """Time for the proxy to start""" + #Time for the proxy to start sleep(2) - """Block all traffic not routed to the proxy""" + #Block all traffic not routed to the proxy self._original_connect = socket.socket.connect def blocking_connect(sock, address): @@ -71,7 +71,6 @@ def test_proxies_qiskit_runtime_service( self, dependencies: IntegrationTestDependencies ) -> None: """Should reach the proxy using QiskitRuntimeService.""" - # Allow some time for `pproxy` to receive requests. service = QiskitRuntimeService( instance=dependencies.instance, token=dependencies.token, From 2af9c137598865d3fda383c40e56c69ccc389f59 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Tue, 31 Mar 2026 09:41:43 +0200 Subject: [PATCH 22/23] spaces --- test/integration/test_proxies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index 012bd6ca52..560550b2a4 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -41,9 +41,9 @@ def setUp(self): # launch a mock server. command = ["pproxy", "-v", "-l", "http://{}:{}".format(ADDRESS, PORT)] self.proxy_process = subprocess.Popen(command, stdout=subprocess.PIPE) - #Time for the proxy to start + # Time for the proxy to start sleep(2) - #Block all traffic not routed to the proxy + # Block all traffic not routed to the proxy self._original_connect = socket.socket.connect def blocking_connect(sock, address): From e8ab981ede43c4e42408f96caf7739edc2c10a11 Mon Sep 17 00:00:00 2001 From: Alexander Lodolo Date: Tue, 31 Mar 2026 13:04:09 +0200 Subject: [PATCH 23/23] [Add] RuntimeClient test --- pyproject.toml | 2 +- test/integration/test_proxies.py | 22 +++++++++++++++++++++- 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 647e9e1df1..99ca415c48 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -140,7 +140,7 @@ dependencies = [ "numpy>=1.26", "urllib3>=2.4.0", "python-dateutil>=2.9.0", - "ibm-platform-services>=0.55.3", + "ibm-platform-services>=0.61.1", "ibm-quantum-schemas>=0.5.20260320", "pydantic>=2.7.0", "qiskit>=2.2.0", diff --git a/test/integration/test_proxies.py b/test/integration/test_proxies.py index 560550b2a4..8c3535279c 100644 --- a/test/integration/test_proxies.py +++ b/test/integration/test_proxies.py @@ -19,8 +19,10 @@ from qiskit_ibm_runtime import QiskitRuntimeService +from qiskit_ibm_runtime.proxies import ProxyConfiguration from qiskit_ibm_runtime.accounts.exceptions import InvalidAccountError - +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 ..decorators import IntegrationTestDependencies, integration_test_setup @@ -66,6 +68,24 @@ def tearDown(self): self.proxy_process.wait() socket.socket.connect = self._original_connect + @integration_test_setup(supported_channel=["ibm_quantum_platform"], init_service=False) + def test_proxies_cloud_runtime_client(self, dependencies: IntegrationTestDependencies) -> None: + """Should reach the proxy using RuntimeClient.""" + params = ClientParameters( + instance=dependencies.instance, + token=dependencies.token, + channel=dependencies.channel, + verify=False, + proxies=ProxyConfiguration(urls=VALID_PROXIES), + url=dependencies.url, + ) + client = RuntimeClient(params) + client.jobs_get(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) + @integration_test_setup(supported_channel=["ibm_quantum_platform"], init_service=False) def test_proxies_qiskit_runtime_service( self, dependencies: IntegrationTestDependencies