From 3c6d297331e07094207d10b378c29f8e0d3d75e0 Mon Sep 17 00:00:00 2001 From: JavierSab <66302375+JavierSab@users.noreply.github.com> Date: Tue, 22 Oct 2024 16:48:52 +0200 Subject: [PATCH] =?UTF-8?q?feat(api):=20added=20annealing=20schedule=20exe?= =?UTF-8?q?cuting=20capability=20to=20qiboconne=E2=80=A6=20(#166)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat(api): added annealing schedule executing capability to qiboconnection * feat(calibrations): added endpoints for getting and saving calibrations * docs(changelog): added changes to changelog * tests(job): recognized anneal programs * feat(public): add tests to calibration new models * feat(calibrations): added basic tests * feat(public): removed unused import htat local pylint did not catch * feat(version): created prerelease so that it can be qtested * test(api): added tests for calibration * test(JobResults): added tests for annealing results and updated the other types from the deprecated old format * feat(JobResults): adjusted documentation about the tests. REmoved unused imports@ * tests(style): pylint ignore too many lines * tests(calibrations): uploaded calibrations --- changelog-dev.md | 2 + dev-requirements.txt | 1 + src/qiboconnection/__init__.py | 2 +- src/qiboconnection/api.py | 223 +++++++++++++- src/qiboconnection/api_utils.py | 2 +- src/qiboconnection/models/__init__.py | 1 + src/qiboconnection/models/calibration.py | 74 +++++ src/qiboconnection/models/job.py | 19 +- src/qiboconnection/models/job_result.py | 5 +- src/qiboconnection/typings/enums/job_type.py | 1 + .../typings/requests/__init__.py | 1 + .../typings/requests/calibration_request.py | 28 ++ .../typings/responses/__init__.py | 1 + .../typings/responses/calibration_response.py | 35 +++ tests/unit/conftest.py | 14 +- tests/unit/data/__init__.py | 1 + tests/unit/data/testing_data.py | 25 ++ tests/unit/data/web_responses/calibrations.py | 47 +++ .../unit/data/web_responses/web_responses.py | 2 + tests/unit/test_api.py | 283 +++++++++++++++++- tests/unit/test_calibration.py | 64 ++++ tests/unit/test_calibration_typings.py | 38 +++ tests/unit/test_job.py | 71 ++++- tests/unit/test_job_result.py | 31 +- tests/unit/test_util.py | 11 +- 25 files changed, 952 insertions(+), 30 deletions(-) create mode 100644 src/qiboconnection/models/calibration.py create mode 100644 src/qiboconnection/typings/requests/calibration_request.py create mode 100644 src/qiboconnection/typings/responses/calibration_response.py create mode 100644 tests/unit/data/web_responses/calibrations.py create mode 100644 tests/unit/test_calibration.py create mode 100644 tests/unit/test_calibration_typings.py diff --git a/changelog-dev.md b/changelog-dev.md index 677e612b..f2762e1c 100644 --- a/changelog-dev.md +++ b/changelog-dev.md @@ -4,6 +4,8 @@ This document contains the changes of the current release. ### New features since last release +- [#166](https://github.com/qilimanjaro-tech/qiboconnection/pull/166) + ### Improvements ### Breaking changes diff --git a/dev-requirements.txt b/dev-requirements.txt index 7c30ef19..81c6d086 100644 --- a/dev-requirements.txt +++ b/dev-requirements.txt @@ -14,3 +14,4 @@ bandit==1.7.4 responses types-requests==2.28.11.8 responses==0.23.3 +python-dotenv==1.0.1 diff --git a/src/qiboconnection/__init__.py b/src/qiboconnection/__init__.py index c0989b37..20f0d239 100644 --- a/src/qiboconnection/__init__.py +++ b/src/qiboconnection/__init__.py @@ -23,7 +23,7 @@ """ -__version__ = "0.22.1" +__version__ = "0.23.0-alpha.0" from .about import about diff --git a/src/qiboconnection/api.py b/src/qiboconnection/api.py index 9dbd7e46..e124f728 100644 --- a/src/qiboconnection/api.py +++ b/src/qiboconnection/api.py @@ -38,12 +38,12 @@ from qiboconnection.connection import Connection from qiboconnection.constants import API_CONSTANTS, REST, REST_ERROR from qiboconnection.errors import ConnectionException, RemoteExecutionException -from qiboconnection.models import Job, JobListing, Runcard +from qiboconnection.models import Calibration, Job, JobListing, Runcard from qiboconnection.models.devices import Device, Devices, create_device from qiboconnection.typings.connection import ConnectionConfiguration from qiboconnection.typings.enums import JobStatus from qiboconnection.typings.job_data import JobData -from qiboconnection.typings.responses import JobListingItemResponse, RuncardResponse +from qiboconnection.typings.responses import CalibrationResponse, JobListingItemResponse, RuncardResponse from qiboconnection.typings.responses.job_response import JobResponse from qiboconnection.typings.vqa import VQA from qiboconnection.util import unzip @@ -67,6 +67,7 @@ class API(ABC): _CIRCUITS_CALL_PATH = "/circuits" _DEVICES_CALL_PATH = "/devices" _RUNCARDS_CALL_PATH = "/runcards" + _CALIBRATIONS_CALL_PATH = "/calibrations" _PING_CALL_PATH = "/status" @typechecked @@ -80,6 +81,7 @@ def __init__( self._jobs_listing: JobListing | None = None self._selected_devices: List[Device] | None = None self._runcard: Runcard | None = None + self._calibration: Calibration | None = None @classmethod def login(cls, username: str, api_key: str): @@ -124,6 +126,15 @@ def last_runcard(self) -> Runcard | None: """ return self._runcard + @property + def last_calibration(self) -> Calibration | None: + """Returns the last calibration uploaded in the current session, in case there has been one. + + Returns: + Calibration | None: last uploaded calibration + """ + return self._calibration + @property def user_id(self) -> int: """Exposes the id of the authenticated user @@ -291,6 +302,7 @@ def execute( # pylint: disable=too-many-locals, disable=too-many-branches self, circuit: Circuit | List[Circuit] | None = None, qprogram: str | None = None, + anneal_program_args: dict | None = None, vqa: VQA | None = None, nshots: int = 10, device_ids: List[int] | None = None, @@ -303,7 +315,8 @@ def execute( # pylint: disable=too-many-locals, disable=too-many-branches Args: circuit (Circuit or List[Circuit]): a Qibo circuit to execute - qprogram (str): a QProgram description, result of Qililab's QProgram.to_dict() function. + qprogram (str): a QProgram description, result of Qililab utils `serialize(qprogram)` function. + anneal_program_args (dict): an annealing implementation. It is supposed to contain a dict with everything needed for a platform. vqa (dict): a Variational Quantum Algorithm, result of applications-sdk' VQA.to_dict() method. nshots (int): number of times the execution is to be done. device_ids (List[int]): list of devices where the execution should be performed. If set, any device set @@ -314,8 +327,8 @@ def execute( # pylint: disable=too-many-locals, disable=too-many-branches List[int]: list of job ids Raises: - ValueError: Both circuit and experiment were provided, but execute() only takes at most of them. - ValueError: Neither of experiment or circuit were provided, but execute() only takes at least one of them. + ValueError: VQA, circuit, qprogram and anneal_program_args were provided, but execute() only takes one of them. + ValueError: Neither of circuit, vqa, qprogram or anneal_program_args were provided. """ # Ensure provided selected_devices are valid. If not provided, use the ones selected by API.select_device_id. @@ -356,6 +369,7 @@ def execute( # pylint: disable=too-many-locals, disable=too-many-branches Job( circuit=circuit, qprogram=qprogram, + anneal_program_args=anneal_program_args, vqa=vqa, nshots=nshots, name=name, @@ -767,6 +781,205 @@ def delete_runcard(self, runcard_id: int) -> None: raise RemoteExecutionException(message="Runcard could not be removed.", status_code=status_code) logger.info("Runcard %i deleted successfully with message: %s", runcard_id, response) + # CALIBRATIONS + + def _create_calibration_response(self, calibration: Calibration): + """Make the calibration create request and parse the response""" + response, status_code = self._connection.send_post_auth_remote_api_call( + path=self._CALIBRATIONS_CALL_PATH, + data=asdict(calibration.calibration_request()), + ) + if status_code not in [200, 201]: + raise RemoteExecutionException(message="Calibration could not be saved.", status_code=status_code) + logger.debug("Experiment saved successfully.") + return CalibrationResponse.from_kwargs(**response) + + @typechecked + def save_calibration( + self, + name: str, + description: str, + calibration_serialized: str, + device_id: int, + user_id: int, + qililab_version: str, + ): + """Save a calibration into the database af our servers, for it to be easily recovered when needed. + + .. warning:: + + This method is only available for Qilimanjaro members. + + Args: + name: Name the experiment is going to be saved with. + description: Short descriptive text to more easily identify this specific experiment instance. + calibration_dict: Serialized calibration (using its `.to_dict()` method) + device_id: Id of the device the experiment was executed in + user_id: Id of the user that is executing the experiment + qililab_version: version of qililab the experiment was executed with + + Returns: + new saved calibration + + """ + + calibration = Calibration( + id=None, + name=name, + description=description, + calibration=calibration_serialized, + device_id=device_id, + user_id=user_id, + qililab_version=qililab_version, + ) + + calibration_response = self._create_calibration_response(calibration=calibration) + created_calibration = Calibration.from_response(response=calibration_response) + + self._calibration = created_calibration + return created_calibration.id # type: ignore[attr-defined] + + @typechecked + def _get_calibration_response(self, calibration_id: int): + """Gets complete information of a specific calibration + + Raises: + RemoteExecutionException: Calibration could not be retrieved + + Returns: + CalibrationResponse: response with the info of the requested calibration""" + response, status_code = self._connection.send_get_auth_remote_api_call( + path=f"{self._CALIBRATIONS_CALL_PATH}/{calibration_id}" + ) + if status_code != 200: + raise RemoteExecutionException(message="Calibration could not be retrieved.", status_code=status_code) + return CalibrationResponse.from_kwargs(**response) + + @typechecked + def _get_calibration_by_name_response(self, calibration_name: str): + """Gets complete information of a specific calibration by name + + Raises: + RemoteExecutionException: Calibration could not be retrieved + + Returns: + CalibrationResponse: response with the info of the requested calibration""" + response, status_code = self._connection.send_get_auth_remote_api_call( + path=f"{self._CALIBRATIONS_CALL_PATH}/by_keys", params={"name": calibration_name} + ) + + if status_code != 200: + raise RemoteExecutionException(message="Calibration could not be retrieved.", status_code=status_code) + + return CalibrationResponse.from_kwargs(**response) + + @typechecked + def get_calibration(self, calibration_id: int | None = None, calibration_name: str | None = None) -> Calibration: + """Get full information of a specific calibration + + .. warning:: + + This method is only available for Qilimanjaro members. + + Args: + calibration_id(int, optional): id of the calibration to retrieve. Incompatible with providing a name. + calibration_name(str, optional): name of the calibration to retrieve. Incompatible with providing an id. + + Raises: + RemoteExecutionException: Calibration could not be retrieved + + Returns: + Calibration: serialized calibration dictionary + """ + if calibration_id is not None and calibration_name is not None: + raise ValueError("Both of of id and name cannot be simultaneously provided") + if calibration_id is not None: + return Calibration.from_response(self._get_calibration_response(calibration_id=calibration_id)) + if calibration_name is not None: + return Calibration.from_response(self._get_calibration_by_name_response(calibration_name=calibration_name)) + raise ValueError("At least one of id and name must be provided") + + def _get_list_calibration_response( + self, + ) -> List[CalibrationResponse]: + """Performs the actual calibration listing request + Returns + List[CalibrationResponse]: list of objects encoding the expected response structure""" + responses, status_codes = unzip( + self._connection.send_get_auth_remote_api_call_all_pages(path=self._CALIBRATIONS_CALL_PATH) + ) + for status_code in status_codes: + if status_code != 200: + raise RemoteExecutionException(message="Calibrations could not be listed.", status_code=status_code) + + items = [item for response in responses for item in response[REST.ITEMS]] + return [CalibrationResponse.from_kwargs(**item) for item in items] + + @typechecked + def list_calibrations(self) -> List[Calibration]: + """List all calibrations + + Raises: + RemoteExecutionException: Devices could not be retrieved + + Returns: + Calibrations: All Calibrations + """ + calibrations_response = self._get_list_calibration_response() + return [Calibration.from_response(response=response) for response in calibrations_response] + + @typechecked + def update_calibration(self, calibration: Calibration) -> Calibration: + """Update the info of a calibration in the database + + .. warning:: + + This method is only available for Qilimanjaro members. + + Raises: + RemoteExecutionException: Calibration could not be retrieved + + Returns: + Calibration: serialized calibration dictionary + """ + if calibration.id is None: # type: ignore[attr-defined] + raise ValueError("Calibration id must be defined for updating its info in the database.") + + calibration_response = self._update_calibration_response(calibration=calibration) + updated_calibration = Calibration.from_response(response=calibration_response) + + self._calibration = updated_calibration + return updated_calibration + + def _update_calibration_response(self, calibration: Calibration): + """Make the calibration update request and parse the response""" + response, status_code = self._connection.send_put_auth_remote_api_call( + path=f"{self._CALIBRATIONS_CALL_PATH}/{calibration.id}", # type: ignore[attr-defined] + data=asdict(calibration.calibration_request()), + ) + if status_code not in [200, 201]: + raise RemoteExecutionException(message="Calibration could not be saved.", status_code=status_code) + logger.debug("Calibration updated successfully.") + return CalibrationResponse.from_kwargs(**response) + + @typechecked + def delete_calibration(self, calibration_id: int) -> None: + """Deletes a job from the database. + + .. warning:: + + This method is only available for Qilimanjaro members. + + Raises: + RemoteExecutionException: Devices could not be retrieved + """ + response, status_code = self._connection.send_delete_auth_remote_api_call( + path=f"{self._CALIBRATIONS_CALL_PATH}/{calibration_id}" + ) + if status_code != 204: + raise RemoteExecutionException(message="Calibration could not be removed.", status_code=status_code) + logger.info("Calibration %i deleted successfully with message: %s", calibration_id, response) + @typechecked def delete_job(self, job_id: int) -> None: """Deletes a job from the database. diff --git a/src/qiboconnection/api_utils.py b/src/qiboconnection/api_utils.py index f402bd8a..b451255d 100644 --- a/src/qiboconnection/api_utils.py +++ b/src/qiboconnection/api_utils.py @@ -83,7 +83,7 @@ def deserialize_job_description(raw_description: str, job_type: str) -> dict: } if job_type == JobType.VQA: return {**description_dict, "vqa_dict": decompressed_data} - if job_type in [JobType.QPROGRAM, JobType.OTHER]: + if job_type in [JobType.QPROGRAM, JobType.ANNEALING_PROGRAM, JobType.OTHER]: return {**description_dict, "data": decompressed_data} return {**description_dict, "data": compressed_data} diff --git a/src/qiboconnection/models/__init__.py b/src/qiboconnection/models/__init__.py index 9f433c45..5110da0e 100644 --- a/src/qiboconnection/models/__init__.py +++ b/src/qiboconnection/models/__init__.py @@ -14,6 +14,7 @@ """ Models: things the api acts with / upon. """ +from .calibration import Calibration from .job import Job from .job_listing import JobListing from .job_listing_item import JobListingItem diff --git a/src/qiboconnection/models/calibration.py b/src/qiboconnection/models/calibration.py new file mode 100644 index 00000000..a4f24d6c --- /dev/null +++ b/src/qiboconnection/models/calibration.py @@ -0,0 +1,74 @@ +# Copyright 2023 Qilimanjaro Quantum Tech +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Calibration class""" + +from dataclasses import field + +from qiboconnection.typings.requests import CalibrationRequest +from qiboconnection.typings.responses import CalibrationResponse +from qiboconnection.util import base64_decode, base64url_encode + + +# pylint: disable=too-many-instance-attributes +# pylint: disable=no-member +class Calibration: + """Calibration representation""" + + name: str + description: str + calibration: str = "" + id: int | None = field(default=None) + + def __init__(self, **kwargs): + for k, v in kwargs.items(): + setattr(self, k, v) + + @property + def _encoded_calibration(self): + """return base64-encoded stringified jsonified experiment""" + return base64url_encode(self.calibration) if self.calibration is not None else None + + @classmethod + def from_response(cls, response: CalibrationResponse): + """Calibration constructor that takes in an instance from a CalibrationResponse""" + return cls( + id=response.calibration_id, + created_at=response.created_at, + updated_at=response.updated_at, + name=response.name, + description=response.description, + user_id=response.user_id, + device_id=response.device_id, + calibration=base64_decode(response.calibration), + qililab_version=response.qililab_version, + ) + + def calibration_request(self): + """Created a Request instance""" + return CalibrationRequest( + name=self.name, + user_id=self.user_id, + device_id=self.device_id, + description=self.description, + calibration=self._encoded_calibration, + qililab_version=self.qililab_version, + ) + + def __repr__(self): + # Use dataclass-like formatting, excluding attributes starting with an underscore + + return ( + f"Calibration(name={self.name},id={self.id},description={self.description},calibration={self.calibration})" + ) diff --git a/src/qiboconnection/models/job.py b/src/qiboconnection/models/job.py index 29a1d65c..3c750807 100644 --- a/src/qiboconnection/models/job.py +++ b/src/qiboconnection/models/job.py @@ -42,6 +42,7 @@ class Job(ABC): # pylint: disable=too-many-instance-attributes program: ProgramDefinition | None = field(default=None) circuit: list[Circuit] | None = None qprogram: str | None = None + anneal_program_args: dict | None = None vqa: VQA | None = None nshots: int = 10 job_status: JobStatus = JobStatus.NOT_SENT @@ -51,12 +52,14 @@ class Job(ABC): # pylint: disable=too-many-instance-attributes id: int = 0 # pylint: disable=invalid-name def __post_init__(self): - n = len([arg for arg in [self.qprogram, self.circuit, self.vqa] if arg is not None]) + n = len([arg for arg in [self.qprogram, self.circuit, self.anneal_program_args, self.vqa] if arg is not None]) match n: case n if n > 1: - raise ValueError("VQA, circuit and qprogram were provided, but execute() only takes one of them.") + raise ValueError( + "VQA, circuit, qprogram and anneal_program_args were provided, but execute() only takes one of them." + ) case 0: - raise ValueError("Neither of circuit, vqa or qprogram were provided.") + raise ValueError("Neither of circuit, vqa, qprogram or anneal_program_args were provided.") @property def user_id(self) -> int | None: @@ -127,11 +130,13 @@ def job_id(self, job_id: int) -> None: @property def job_type(self): """Get the type of the job, checking whether the user has defined circuit or experiment.""" - if self.circuit is not None and self.qprogram is None and self.vqa is None: + if self.circuit is not None and self.qprogram is None and self.anneal_program_args is None and self.vqa is None: return JobType.CIRCUIT - if self.qprogram is not None and self.circuit is None and self.vqa is None: + if self.qprogram is not None and self.circuit is None and self.anneal_program_args is None and self.vqa is None: return JobType.QPROGRAM - if self.vqa is not None and self.circuit is None and self.qprogram is None: + if self.anneal_program_args is not None and self.circuit is None and self.qprogram is None and self.vqa is None: + return JobType.ANNEALING_PROGRAM + if self.vqa is not None and self.circuit is None and self.qprogram is None and self.anneal_program_args is None: return JobType.VQA raise ValueError("Could not determine JobType") @@ -140,6 +145,8 @@ def _get_job_description(self) -> str: if self.qprogram is not None: return json.dumps(compress_any(self.qprogram)) + if self.anneal_program_args is not None: + return json.dumps(compress_any(self.anneal_program_args)) if self.vqa is not None: vqa_as_dict = asdict(self.vqa) vqa_as_dict.pop("vqa_dict") diff --git a/src/qiboconnection/models/job_result.py b/src/qiboconnection/models/job_result.py index 27f56513..3832f0e2 100644 --- a/src/qiboconnection/models/job_result.py +++ b/src/qiboconnection/models/job_result.py @@ -56,10 +56,7 @@ def __post_init__(self) -> None: if self.job_type == JobType.CIRCUIT: self.data = decode_results_from_circuit(self.http_response) return - if self.job_type == JobType.QPROGRAM: - self.data = decode_results_from_qprogram(self.http_response) - return - if self.job_type == JobType.VQA: + if self.job_type in {JobType.QPROGRAM, JobType.ANNEALING_PROGRAM, JobType.VQA}: self.data = decode_results_from_qprogram(self.http_response) return diff --git a/src/qiboconnection/typings/enums/job_type.py b/src/qiboconnection/typings/enums/job_type.py index 7b1ee1c6..68b4eafc 100644 --- a/src/qiboconnection/typings/enums/job_type.py +++ b/src/qiboconnection/typings/enums/job_type.py @@ -28,5 +28,6 @@ class JobType(StrEnum): CIRCUIT = "circuit" QPROGRAM = "qprogram" + ANNEALING_PROGRAM = "annealing_program" VQA = "vqa" OTHER = "other" diff --git a/src/qiboconnection/typings/requests/__init__.py b/src/qiboconnection/typings/requests/__init__.py index 7e41b971..b25961e7 100644 --- a/src/qiboconnection/typings/requests/__init__.py +++ b/src/qiboconnection/typings/requests/__init__.py @@ -14,5 +14,6 @@ """ Requests typings module""" from .assertion_payload import AssertionPayload +from .calibration_request import CalibrationRequest from .job_request import JobRequest from .runcard_request import RuncardRequest diff --git a/src/qiboconnection/typings/requests/calibration_request.py b/src/qiboconnection/typings/requests/calibration_request.py new file mode 100644 index 00000000..7d561ba8 --- /dev/null +++ b/src/qiboconnection/typings/requests/calibration_request.py @@ -0,0 +1,28 @@ +# Copyright 2023 Qilimanjaro Quantum Tech +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Runcard typing classes """ +from dataclasses import dataclass + + +@dataclass +class CalibrationRequest: + """Base structure for a SavedExperiment body of any rest interaction""" + + name: str + calibration: str + device_id: int + user_id: int + description: str + qililab_version: str diff --git a/src/qiboconnection/typings/responses/__init__.py b/src/qiboconnection/typings/responses/__init__.py index 8e0b5e91..b68612b0 100644 --- a/src/qiboconnection/typings/responses/__init__.py +++ b/src/qiboconnection/typings/responses/__init__.py @@ -14,5 +14,6 @@ """ ""Responses typings module """ from .access_token_response import AccessTokenResponse +from .calibration_response import CalibrationResponse from .job_listing_item_response import JobListingItemResponse from .runcard_response import RuncardResponse diff --git a/src/qiboconnection/typings/responses/calibration_response.py b/src/qiboconnection/typings/responses/calibration_response.py new file mode 100644 index 00000000..9324ad82 --- /dev/null +++ b/src/qiboconnection/typings/responses/calibration_response.py @@ -0,0 +1,35 @@ +# Copyright 2023 Qilimanjaro Quantum Tech +# +# Licensed under the Apache License, Version 2.0 (the "License"); +# you may not use this file except in compliance with the License. +# You may obtain a copy of the License at +# +# http://www.apache.org/licenses/LICENSE-2.0 +# +# Unless required by applicable law or agreed to in writing, software +# distributed under the License is distributed on an "AS IS" BASIS, +# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +# See the License for the specific language governing permissions and +# limitations under the License. + +""" Calibration typing classes """ +from dataclasses import dataclass +from datetime import datetime + +from qiboconnection.util import from_kwargs + +from ..requests import CalibrationRequest + + +@dataclass +class CalibrationResponse(CalibrationRequest): + """Class for accommodating Calibrations.""" + + calibration_id: int + created_at: datetime + updated_at: datetime + + @classmethod + def from_kwargs(cls, **kwargs): + "Returns an instance of CalibrationResponse including non-typed attributes" + return from_kwargs(cls, **kwargs) diff --git a/tests/unit/conftest.py b/tests/unit/conftest.py index 15ba4a27..66a91d07 100644 --- a/tests/unit/conftest.py +++ b/tests/unit/conftest.py @@ -201,7 +201,19 @@ def compressed_qibo_circuit(): @pytest.fixture(name="compressed_qililab_qprogram") def compressed_qililab_qprogram(): - """qiililab experiment base64 encoding""" + """qililab experiment base64 encoding""" + return json.dumps( + { + "data": "H4sIABxaDWYC/82QsQrCMBRFf0XebEFtS4ujuLjp4CQSXtJXCaat9iVCkf67SVGw4uDokCX33ENu7mC7C8FyArtt25xarGA6AbS21dJZYp/codCM0pBAZxvuauUvSzRMnhSyKboBenlWplHnLxLhnC5G5H6/WQfwGUCWqcVM5WUUJzFFiUwxQlKLKE1lksU55fOkhN4XyFBFteWRzWi2MA4Px77vh0c6pjHN9AUO6A1bHcb+Ih/UYa5gi370e6Ogq6PPyv/+UjgPKagRmgwCAAA=", + "encoding": "utf-8", + "compression": "gzip", + } + ) + + +@pytest.fixture(name="compressed_qililab_annealing_program") +def compressed_qililab_annealing_program(): + """qililab annealing base64 encoding""" return json.dumps( { "data": "H4sIABxaDWYC/82QsQrCMBRFf0XebEFtS4ujuLjp4CQSXtJXCaat9iVCkf67SVGw4uDokCX33ENu7mC7C8FyArtt25xarGA6AbS21dJZYp/codCM0pBAZxvuauUvSzRMnhSyKboBenlWplHnLxLhnC5G5H6/WQfwGUCWqcVM5WUUJzFFiUwxQlKLKE1lksU55fOkhN4XyFBFteWRzWi2MA4Px77vh0c6pjHN9AUO6A1bHcb+Ih/UYa5gi370e6Ogq6PPyv/+UjgPKagRmgwCAAA=", diff --git a/tests/unit/data/__init__.py b/tests/unit/data/__init__.py index 1ec3cefd..c1a24932 100644 --- a/tests/unit/data/__init__.py +++ b/tests/unit/data/__init__.py @@ -1,6 +1,7 @@ """Data module""" from .testing_data import ( + calibration_serialized, device_inputs, experiment_dict, offline_device_inputs, diff --git a/tests/unit/data/testing_data.py b/tests/unit/data/testing_data.py index 258a278c..b88a618d 100644 --- a/tests/unit/data/testing_data.py +++ b/tests/unit/data/testing_data.py @@ -353,6 +353,31 @@ }, } +calibration_serialized = """ +!Calibration +crosstalk_matrix: !CrosstalkMatrix + matrix: + flux_0: {flux_0: 1.47046905, flux_1: 0.12276261} + flux_1: {flux_0: -0.55322207, flux_1: 1.58247856} +waveforms: + drive_q0_bus: + Xpi: !IQPair + I: &id001 !Gaussian {amplitude: 1.0, duration: 40, num_sigmas: 4.5} + Q: !DragCorrection + drag_coefficient: -2.5 + waveform: *id001 + readout_bus: + Measure: !IQPair + I: !Square {amplitude: 1.0, duration: 200} + Q: !Square {amplitude: 0.0, duration: 200} +weights: + readout_bus: + optimal_weights: !IQPair + I: !Square {amplitude: 1.0, duration: 200} + Q: !Square {amplitude: 1.0, duration: 200} +""" + + experiment_dict = { "platform": { "settings": { diff --git a/tests/unit/data/web_responses/calibrations.py b/tests/unit/data/web_responses/calibrations.py new file mode 100644 index 00000000..3af20eb8 --- /dev/null +++ b/tests/unit/data/web_responses/calibrations.py @@ -0,0 +1,47 @@ +""" Calibrations Web Responses """ + +calibration_base_response = { + "calibration_id": 1, + "name": "MyDemoCalibration", + "user_id": 1, + "device_id": 1, + "description": "A test calibration", + "calibration": "IUNhbGlicmF0aW9uCmNyb3NzdGFsa19tYXRyaXg6ICFDcm9zc3RhbGtNYXRyaXgKICBtYXRyaXg6CiAgICBmbHV4XzA6IHtmbHV4XzA6IDEuNDcwNDY5MDUsIGZsdXhfMTogMC4xMjI3NjI2MX0KICAgIGZsdXhfMToge2ZsdXhfMDogLTAuNTUzMjIyMDcsIGZsdXhfMTogMS41ODI0Nzg1Nn0Kd2F2ZWZvcm1zOgogIGRyaXZlX3EwX2J1czoKICAgIFhwaTogIUlRUGFpcgogICAgICBJOiAmaWQwMDEgIUdhdXNzaWFuIHthbXBsaXR1ZGU6IDEuMCwgZHVyYXRpb246IDQwLCBudW1fc2lnbWFzOiA0LjV9CiAgICAgIFE6ICFEcmFnQ29ycmVjdGlvbgogICAgICAgIGRyYWdfY29lZmZpY2llbnQ6IC0yLjUKICAgICAgICB3YXZlZm9ybTogKmlkMDAxCiAgcmVhZG91dF9idXM6CiAgICBNZWFzdXJlOiAhSVFQYWlyCiAgICAgIEk6ICFTcXVhcmUge2FtcGxpdHVkZTogMS4wLCBkdXJhdGlvbjogMjAwfQogICAgICBROiAhU3F1YXJlIHthbXBsaXR1ZGU6IDAuMCwgZHVyYXRpb246IDIwMH0Kd2VpZ2h0czoKICByZWFkb3V0X2J1czoKICAgIG9wdGltYWxfd2VpZ2h0czogIUlRUGFpcgogICAgICBJOiAhU3F1YXJlIHthbXBsaXR1ZGU6IDEuMCwgZHVyYXRpb246IDIwMH0KICAgICAgUTogIVNxdWFyZSB7YW1wbGl0dWRlOiAxLjAsIGR1cmF0aW9uOiAyMDB9Cg==", + "created_at": "Fri, 16 Dec 2022 18:40:24 GMT", + "updated_at": "Fri, 16 Dec 2022 18:40:24 GMT", + "qililab_version": "0.0.0", + "new_field": "hello", +} + + +class Calibrations: + """Calibrations Web Responses""" + + ise_response: tuple[dict, int] = ({}, 500) + create_response: tuple[dict, int] = (calibration_base_response, 201) + retrieve_response: tuple[dict, int] = (calibration_base_response, 200) + retrieve_many_response: tuple[tuple[dict, int]] = ( + ( + { + "items": [calibration_base_response], + "total": 2, + "per_page": 5, + "self": "https://qilimanjarodev.ddns.net:8080/api/v1/calibrations?page=1&per_page=5", + "links": { + "first": "https://qilimanjarodev.ddns.net:8080/api/v1/calibrations?page=1&per_page=5", + "prev": "https://qilimanjarodev.ddns.net:8080/api/v1/calibrations?page=None&per_page=5", + "next": "https://qilimanjarodev.ddns.net:8080/api/v1/calibrations?page=None&per_page=5", + "last": "https://qilimanjarodev.ddns.net:8080/api/v1/calibrations?page=1&per_page=5", + }, + }, + 200, + ), + ) + ise_many_response: tuple[tuple[dict, int]] = ( + ( + {}, + 500, + ), + ) + update_response: tuple[dict, int] = retrieve_response + delete_response: tuple[str, int] = ("Calibration deleted", 204) diff --git a/tests/unit/data/web_responses/web_responses.py b/tests/unit/data/web_responses/web_responses.py index 7027dc2f..3a64d2e6 100644 --- a/tests/unit/data/web_responses/web_responses.py +++ b/tests/unit/data/web_responses/web_responses.py @@ -1,6 +1,7 @@ """ Web Responses class""" from .auth import Auth +from .calibrations import Calibrations from .devices import Devices from .job import JobResponse from .ping import Ping @@ -15,6 +16,7 @@ class WebResponses: ping = Ping() devices = Devices() runcards = Runcards() + calibrations = Calibrations() users = Users() auth = Auth() raw = ResponsesRaw() diff --git a/tests/unit/test_api.py b/tests/unit/test_api.py index 02bf1e34..1c9f01e0 100644 --- a/tests/unit/test_api.py +++ b/tests/unit/test_api.py @@ -1,4 +1,6 @@ """API testing""" +# pylint: disable=too-many-lines + import base64 import gzip import json @@ -14,6 +16,7 @@ from qiboconnection.api import API from qiboconnection.connection import ConnectionConfiguration from qiboconnection.errors import ConnectionException, RemoteExecutionException +from qiboconnection.models.calibration import Calibration from qiboconnection.models.devices.devices import Devices from qiboconnection.models.devices.util import create_device from qiboconnection.models.job_listing import JobListing @@ -23,7 +26,7 @@ from qiboconnection.typings.vqa import VQA from qiboconnection.util import compress_any -from .data import runcard_dict, web_responses +from .data import calibration_serialized, runcard_dict, web_responses from .data.web_responses.job import JobResponse @@ -80,6 +83,28 @@ def test_last_runcard(mocked_web_call: MagicMock, mocked_api: API): assert isinstance(mocked_api.last_runcard, Runcard) +@patch("qiboconnection.connection.Connection.send_post_auth_remote_api_call", autospec=True) +def test_last_calibration(mocked_web_call: MagicMock, mocked_api: API): + """Test last_runcard property""" + mocked_web_call.return_value = web_responses.calibrations.create_response + + assert mocked_api.last_calibration is None + name = "MyDemoCalibration" + description = "A test calibration" + device_id = 1 + user_id = 1 + qililab_version = "0.0.0" + _ = mocked_api.save_calibration( + name=name, + description=description, + calibration_serialized=calibration_serialized, + device_id=device_id, + user_id=user_id, + qililab_version=qililab_version, + ) + assert isinstance(mocked_api.last_calibration, Calibration) + + def test_user_id(mocked_api: API): """Test last_runcard property""" assert mocked_api.user_id == 666 @@ -295,6 +320,15 @@ def test_get_runcard_by_name(mocked_web_call: MagicMock, mocked_api: API): assert isinstance(runcard, Runcard) +@patch("qiboconnection.connection.Connection.send_get_auth_remote_api_call", autospec=True) +def test_get_runcard_by_name_ise(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.get_runcard() method using runcard name""" + mocked_web_call.return_value = web_responses.runcards.ise_response + + with pytest.raises(RemoteExecutionException, match="Runcard could not be retrieved."): + _ = mocked_api.get_runcard(runcard_name="DEMO_RUNCARD") + + @patch("qiboconnection.connection.Connection.send_get_auth_remote_api_call", autospec=True) def test_get_runcard_with_redundant_info(mocked_web_call: MagicMock, mocked_api: API): """Tests API.get_runcard() method fails when providing name and id at the same time""" @@ -489,6 +523,253 @@ def test_get_results_exception(mocked_api_call: MagicMock, mocked_api: API): mocked_api_call.assert_called_with(self=mocked_api._connection, path=f"{mocked_api._JOBS_CALL_PATH}/{0}") +@patch("qiboconnection.connection.Connection.send_post_auth_remote_api_call", autospec=True) +def test_save_calibration(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.save_calibration() method""" + mocked_web_call.return_value = web_responses.calibrations.create_response + + name = "MyDemoCalibration" + description = "A test calibration" + device_id = 1 + user_id = 1 + qililab_version = "0.0.0" + + calibration_id = mocked_api.save_calibration( + name=name, + description=description, + calibration_serialized=calibration_serialized, + device_id=device_id, + user_id=user_id, + qililab_version=qililab_version, + ) + + mocked_web_call.assert_called() + assert calibration_id == web_responses.calibrations.create_response[0]["calibration_id"] + + +@patch("qiboconnection.connection.Connection.send_post_auth_remote_api_call", autospec=True) +def test_save_calibration_ise(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.save_calibration() method""" + mocked_web_call.return_value = web_responses.calibrations.ise_response + + name = "MyDemoCalibration" + description = "A test calibration" + device_id = 1 + user_id = 1 + qililab_version = "0.0.0" + + with pytest.raises(RemoteExecutionException): + _ = mocked_api.save_calibration( + name=name, + description=description, + calibration_serialized=calibration_serialized, + device_id=device_id, + user_id=user_id, + qililab_version=qililab_version, + ) + + mocked_web_call.assert_called() + + +@patch("qiboconnection.connection.Connection.send_get_auth_remote_api_call", autospec=True) +def test_get_calibration(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.get_calibration() method using calibration id""" + mocked_web_call.return_value = web_responses.calibrations.retrieve_response + + calibration = mocked_api.get_calibration(calibration_id=1) + + mocked_web_call.assert_called_with(self=mocked_api._connection, path=f"{mocked_api._CALIBRATIONS_CALL_PATH}/1") + assert isinstance(calibration, Calibration) + + +@patch("qiboconnection.connection.Connection.send_get_auth_remote_api_call", autospec=True) +def test_get_calibration_by_name(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.get_calibration() method using calibration name""" + mocked_web_call.return_value = web_responses.calibrations.retrieve_response + + calibration = mocked_api.get_calibration(calibration_name="DEMO_CALIBRATION") + + mocked_web_call.assert_called_with( + self=mocked_api._connection, + path=f"{mocked_api._CALIBRATIONS_CALL_PATH}/by_keys", + params={"name": "DEMO_CALIBRATION"}, + ) + assert isinstance(calibration, Calibration) + + +@patch("qiboconnection.connection.Connection.send_get_auth_remote_api_call", autospec=True) +def test_get_calibration_by_name_ise(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.get_calibration() method using calibration name""" + mocked_web_call.return_value = web_responses.calibrations.ise_response + + with pytest.raises(RemoteExecutionException, match="Calibration could not be retrieved."): + _ = mocked_api.get_calibration(calibration_name="DEMO_CALIBRATION") + + +@patch("qiboconnection.connection.Connection.send_get_auth_remote_api_call", autospec=True) +def test_get_calibration_with_redundant_info(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.get_calibration() method fails when providing name and id at the same time""" + mocked_web_call.return_value = web_responses.calibrations.retrieve_response + + with pytest.raises(ValueError): + _ = mocked_api.get_calibration(calibration_id=1, calibration_name="TEST_CALIBRATION") + + mocked_web_call.assert_not_called() + + +@patch("qiboconnection.connection.Connection.send_get_auth_remote_api_call", autospec=True) +def test_get_calibration_with_insufficient_info(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.get_calibration() method fails if not name nor id are provided""" + mocked_web_call.return_value = web_responses.calibrations.retrieve_response + + with pytest.raises(ValueError): + _ = mocked_api.get_calibration() + + mocked_web_call.assert_not_called() + + +@patch("qiboconnection.connection.Connection.send_get_auth_remote_api_call", autospec=True) +def test_get_calibration_ise(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.get_calibration() method when server response raises an error""" + mocked_web_call.return_value = web_responses.calibrations.ise_response + + with pytest.raises(RemoteExecutionException): + _ = mocked_api.get_calibration(calibration_id=1) + + mocked_web_call.assert_called_with(self=mocked_api._connection, path=f"{mocked_api._CALIBRATIONS_CALL_PATH}/1") + + +@patch("qiboconnection.connection.Connection.send_get_auth_remote_api_call_all_pages", autospec=True) +def test_list_calibrations(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.list_calibration() method""" + mocked_web_call.return_value = web_responses.calibrations.retrieve_many_response + + calibrations = mocked_api.list_calibrations() + + mocked_web_call.assert_called() + assert isinstance(calibrations[0], Calibration) + + +@patch("qiboconnection.connection.Connection.send_get_auth_remote_api_call_all_pages", autospec=True) +def test_list_calibrations_ise(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.list_calibration() method""" + mocked_web_call.return_value = web_responses.calibrations.ise_many_response + + with pytest.raises(RemoteExecutionException): + _ = mocked_api.list_calibrations() + + mocked_web_call.assert_called() + + +@patch("qiboconnection.connection.Connection.send_put_auth_remote_api_call", autospec=True) +def test_update_calibration(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.update_calibration() method""" + mocked_web_call.return_value = web_responses.calibrations.update_response + + calibration_id = 1 + name = "MyDemoCalibration" + description = "A test calibration" + device_id = 1 + user_id = 1 + qililab_version = "0.0.0" + modified_calibration = Calibration( + id=calibration_id, + name=name, + description=description, + calibration_serialized=calibration_serialized, + device_id=device_id, + user_id=user_id, + qililab_version=qililab_version, + ) + + updated_calibration = mocked_api.update_calibration(calibration=modified_calibration) + + mocked_web_call.assert_called_with( + self=mocked_api._connection, + path=f"{mocked_api._CALIBRATIONS_CALL_PATH}/1", + data=asdict(modified_calibration.calibration_request()), + ) + assert isinstance(updated_calibration, Calibration) + + +@patch("qiboconnection.connection.Connection.send_put_auth_remote_api_call", autospec=True) +def test_update_calibration_with_no_id(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.update_calibration() method""" + mocked_web_call.return_value = web_responses.calibrations.update_response + + name = "MyDemoCalibration" + description = "A test calibration" + device_id = 1 + user_id = 1 + qililab_version = "0.0.0" + modified_calibration = Calibration( + id=None, + name=name, + description=description, + calibration_serialized=calibration_serialized, + device_id=device_id, + user_id=user_id, + qililab_version=qililab_version, + ) + + with pytest.raises(ValueError): + _ = mocked_api.update_calibration(calibration=modified_calibration) + + mocked_web_call.assert_not_called() + + +@patch("qiboconnection.connection.Connection.send_put_auth_remote_api_call", autospec=True) +def test_update_calibration_ise(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.update_calibration() method""" + mocked_web_call.return_value = web_responses.calibrations.ise_response + + calibration_id = 1 + name = "MyDemoCalibration" + description = "A test calibration" + device_id = 1 + user_id = 1 + qililab_version = "0.0.0" + modified_calibration = Calibration( + id=calibration_id, + name=name, + description=description, + calibration_serialized=calibration_serialized, + device_id=device_id, + user_id=user_id, + qililab_version=qililab_version, + ) + + with pytest.raises(RemoteExecutionException): + _ = mocked_api.update_calibration(calibration=modified_calibration) + + mocked_web_call.assert_called_with( + self=mocked_api._connection, + path=f"{mocked_api._CALIBRATIONS_CALL_PATH}/1", + data=asdict(modified_calibration.calibration_request()), + ) + + +@patch("qiboconnection.connection.Connection.send_delete_auth_remote_api_call", autospec=True) +def test_delete_calibration(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.delete_calibration() method""" + mocked_web_call.return_value = web_responses.calibrations.delete_response + + mocked_api.delete_calibration(calibration_id=1) + + mocked_web_call.assert_called_with(self=mocked_api._connection, path=f"{mocked_api._CALIBRATIONS_CALL_PATH}/1") + + +@patch("qiboconnection.connection.Connection.send_delete_auth_remote_api_call", autospec=True) +def test_delete_calibration_ise(mocked_web_call: MagicMock, mocked_api: API): + """Tests API.delete_calibration() method""" + mocked_web_call.return_value = web_responses.calibrations.ise_response + + with pytest.raises(RemoteExecutionException): + mocked_api.delete_calibration(calibration_id=1) + + mocked_web_call.assert_called_with(self=mocked_api._connection, path=f"{mocked_api._CALIBRATIONS_CALL_PATH}/1") + + @patch("qiboconnection.connection.Connection.send_get_auth_remote_api_call_all_pages", autospec=True) def test_no_devices_selected_exception(mocked_api_call: MagicMock, mocked_api: API): """Tests API.execute() method with no devices selected""" diff --git a/tests/unit/test_calibration.py b/tests/unit/test_calibration.py new file mode 100644 index 00000000..ef331143 --- /dev/null +++ b/tests/unit/test_calibration.py @@ -0,0 +1,64 @@ +""" Test methods for Calibrations classes """ + +from datetime import datetime, timezone + +import yaml + +from qiboconnection.models.calibration import Calibration +from qiboconnection.typings.responses.calibration_response import CalibrationRequest, CalibrationResponse + +# pylint: disable=no-member + + +def test_calibration_creation(): + """Tests Calibration Creation""" + + calibration = Calibration( + name="calibration", + description="description", + user_id=0, + device_id=0, + calibration={"a": 0}, + qililab_version="0.0.0", + ) + + assert isinstance(calibration, Calibration) + + +def test_calibration_creation_from_response(): + """Tests Calibration Creation""" + + calibration_response = CalibrationResponse( + name="calibration", + description="description", + user_id=0, + device_id=0, + calibration="eyJhIjogMH0=", + qililab_version="0.0.0", + calibration_id=0, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + + calibration = Calibration.from_response(response=calibration_response) + + assert isinstance(calibration, Calibration) + assert yaml.safe_load(calibration.calibration) == {"a": 0}, "Decoded calibration does not coincide with expected" + + +def test_calibration_request(): + """Tests Calibration request builder method""" + + calibration = Calibration( + name="calibration", + description="description", + user_id=0, + device_id=0, + calibration={"a": 0}, + qililab_version="0.0.0", + ) + + calibration_request = calibration.calibration_request() + + assert isinstance(calibration_request, CalibrationRequest) + assert calibration_request.calibration == "eyJhIjogMH0=" diff --git a/tests/unit/test_calibration_typings.py b/tests/unit/test_calibration_typings.py new file mode 100644 index 00000000..7e59e910 --- /dev/null +++ b/tests/unit/test_calibration_typings.py @@ -0,0 +1,38 @@ +""" Test methods for Calibration typing classes""" + +from datetime import datetime, timezone + +from qiboconnection.typings.responses.calibration_response import CalibrationRequest, CalibrationResponse + + +def test_calibration_request_creation(): + """Tests CalibrationRequest creation""" + + calibration_request = CalibrationRequest( + name="calibration", + description="description", + user_id=0, + device_id=0, + calibration="eyJhIjogMH0=", + qililab_version="0.0.0", + ) + + assert isinstance(calibration_request, CalibrationRequest) + + +def test_calibration_response_creation(): + """Tests CalibrationRequest creation""" + + calibration_response = CalibrationResponse( + name="calibration", + description="description", + user_id=0, + device_id=0, + calibration="eyJhIjogMH0=", + qililab_version="0.0.0", + calibration_id=0, + created_at=datetime.now(timezone.utc), + updated_at=datetime.now(timezone.utc), + ) + + assert isinstance(calibration_response, CalibrationResponse) diff --git a/tests/unit/test_job.py b/tests/unit/test_job.py index 68365753..c26e7653 100644 --- a/tests/unit/test_job.py +++ b/tests/unit/test_job.py @@ -160,7 +160,19 @@ def test_job_creation_qprogram(user: User, simulator_device: Device): assert job.job_type == JobType.QPROGRAM -def test_job_creation_raises_value_error_when_circuit_qprogram_or_vqa_are_defined_simultaneously( +def test_job_creation_annealing(user: User, simulator_device: Device): + """test job creation using an qprogram instead of a circuit + + Args: + user (User): User + simulator_device (SimulatorDevice): SimulatorDevice + """ + + job = Job(anneal_program_args={}, user=user, device=cast(Device, simulator_device)) + assert job.job_type == JobType.ANNEALING_PROGRAM + + +def test_job_creation_raises_value_error_when_circuit_qprogram_anneal_or_vqa_are_defined_simultaneously( circuits: list[Circuit], user: User, simulator_device: Device ): """test job creation only allows defining one of the following: qprogram, circuit or vqa. @@ -173,10 +185,13 @@ def test_job_creation_raises_value_error_when_circuit_qprogram_or_vqa_are_define with pytest.raises(ValueError) as e_info: _ = Job(qprogram={}, circuit=circuits, user=user, device=cast(Device, simulator_device)) - assert e_info.value.args[0] == "VQA, circuit and qprogram were provided, but execute() only takes one of them." + assert ( + e_info.value.args[0] + == "VQA, circuit, qprogram and anneal_program_args were provided, but execute() only takes one of them." + ) -def test_job_creation_qprogram_raises_value_error_when_neither_of_circuit_qprogram_or_vqa_are_defined( +def test_job_creation_qprogram_raises_value_error_when_neither_of_circuit_qprogram_anneal_or_vqa_are_defined( user: User, simulator_device: Device ): """test job creation using neither qprogram nor circuit or vqa @@ -187,7 +202,7 @@ def test_job_creation_qprogram_raises_value_error_when_neither_of_circuit_qprogr """ with pytest.raises(ValueError) as e_info: _ = Job(qprogram=None, circuit=None, user=user, device=cast(Device, simulator_device)) - assert e_info.value.args[0] == "Neither of circuit, vqa or qprogram were provided." + assert e_info.value.args[0] == "Neither of circuit, vqa, qprogram or anneal_program_args were provided." def test_job_request_with_circuit(circuits: list[Circuit], user: User, simulator_device: Device): @@ -283,7 +298,51 @@ def test_job_request_with_qprogram(user: User, simulator_device: Device): assert reconstructed_qprogram == {} -def test_job_request_raises_value_error_if_not_circuit_or_qprogram( +def test_job_request_with_annealing_program(user: User, simulator_device: Device): + """test job request + + Args: + user (User): User + simulator_device (SimulatorDevice): SimulatorDevice + """ + job_status = JobStatus.COMPLETED + user_id = user.user_id + job = Job( + anneal_program_args={}, + user=user, + device=cast(Device, simulator_device), + job_status=job_status, + id=23, + nshots=10, + name="test", + summary="test", + ) + expected_job_request = JobRequest( + user_id=user_id, + device_id=simulator_device.id, + description=job.job_request.description, + number_shots=10, + job_type=JobType.ANNEALING_PROGRAM, + name="test", + summary="test", + ) + + assert isinstance(job, Job) + assert job.job_request == expected_job_request + assert all( + [ + "data" in job.job_request.description, + "encoding" in job.job_request.description, + "compression" in job.job_request.description, + ] + ) + reconstructed_annealing_program = deserialize_job_description( + raw_description=job.job_request.description, job_type=JobType.ANNEALING_PROGRAM + )["data"] + assert reconstructed_annealing_program == {} + + +def test_job_request_raises_value_error_if_no_input_description_for_any_type( circuits: list[Circuit], user: User, simulator_device: Device ): """test job raises proper exceptions when trying to build request with none of circuit, qprogram @@ -307,7 +366,7 @@ def test_job_request_raises_value_error_if_not_circuit_or_qprogram( assert e_info.value.args[0] == "Could not determine JobType" -def test_job_request_raises_value_error_if_several_of_circuit_and_qprogram( +def test_job_request_raises_value_error_if_several_of_description_input_types( circuits: list[Circuit], user: User, simulator_device: Device ): """test job raises proper exceptions when trying to build request with more than one of circuit, qprogram diff --git a/tests/unit/test_job_result.py b/tests/unit/test_job_result.py index 93b3cab2..b6b3fac4 100644 --- a/tests/unit/test_job_result.py +++ b/tests/unit/test_job_result.py @@ -1,5 +1,4 @@ """ Tests methods for job result """ -import numpy as np from qiboconnection.models.job_result import JobResult from qiboconnection.typings.enums import JobType @@ -9,7 +8,7 @@ def test_job_result_creation(): """Test job result creation""" job_result = JobResult( job_id=1, - http_response="gASVsAAAAAAAAACMFW51bXB5LmNvcmUubXVsdGlhcnJheZSMDF9yZWNvbnN0cnVjdJSTlIwFbnVtcHmUjAduZGFycmF5lJOUSwCFlEMBYpSHlFKUKEsBSwWFlGgDjAVkdHlwZZSTlIwCZjiUiYiHlFKUKEsDjAE8lE5OTkr_____Sv____9LAHSUYolDKAAAAAAAAPA_AAAAAAAA8D8AAAAAAADwPwAAAAAAAPA_AAAAAAAA8D-UdJRiLg==", + http_response='{"data": "H4sIABrWEWcC/4uuViooyk9KTMrMySzJTC1WslKoVjIAkgZ6JjoKSoZgllktkJmcX5pXgpCHyYLlihNzC3LAmqOjDWJ1FKIN4QQaF03CIBZIKqVl5iXmxBeXJJakws1X0jDQMzcwNzQwM7cwNLQwMzUxN9U2yNJUgtqLU74W1cB4nN6zRAVw36JL1NbGAgB4XNujJgEAAA==", "encoding": "utf-8", "compression": "gzip"}', job_type=JobType.CIRCUIT, ) @@ -17,14 +16,27 @@ def test_job_result_creation(): assert job_result.job_id == 1 assert ( job_result.http_response - == "gASVsAAAAAAAAACMFW51bXB5LmNvcmUubXVsdGlhcnJheZSMDF9yZWNvbnN0cnVjdJSTlIwFbnVtcHmUjAduZGFycmF5lJOUSwCFlEMBYpSHlFKUKEsBSwWFlGgDjAVkdHlwZZSTlIwCZjiUiYiHlFKUKEsDjAE8lE5OTkr_____Sv____9LAHSUYolDKAAAAAAAAPA_AAAAAAAA8D8AAAAAAADwPwAAAAAAAPA_AAAAAAAA8D-UdJRiLg==" + == '{"data": "H4sIABrWEWcC/4uuViooyk9KTMrMySzJTC1WslKoVjIAkgZ6JjoKSoZgllktkJmcX5pXgpCHyYLlihNzC3LAmqOjDWJ1FKIN4QQaF03CIBZIKqVl5iXmxBeXJJakws1X0jDQMzcwNzQwM7cwNLQwMzUxN9U2yNJUgtqLU74W1cB4nN6zRAVw36JL1NbGAgB4XNujJgEAAA==", "encoding": "utf-8", "compression": "gzip"}' ) - assert (job_result.data == np.array([1.0, 1.0, 1.0, 1.0, 1.0])).all() + assert isinstance(job_result.data, list) + for item in job_result.data: + assert isinstance(item, dict) def test_job_result_qprogram_works(): """Test QProgrom results are returned as dicts""" + job_result = JobResult( + job_id=0, + http_response='{"data": "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", "encoding": "utf-8", "compression": "gzip"}', + job_type="qprogram", + ) + assert isinstance(job_result.data, str) + + +def test_job_result_qprogram_works_legacy(): + """Test QProgrom results are returned as dicts""" + job_result = JobResult( job_id=0, http_response="eyJ0eXBlIjogIlFQcm9ncmFtIiwgImF0dHJpYnV0ZXMiOiB7Il9ib2R5IjogeyJ0eXBlIjogIkJsb2NrIiwgImF0dHJpYnV0ZXMiOiB7Il91dWlkIjogeyJ0eXBlIjogIlVVSUQiLCAidXVpZCI6ICI2YmNlYjQ1MS0wZWUxLTQ1N2MtYTE1Ny1iMmYyZDUzOGZmYjUifSwgImVsZW1lbnRzIjogeyJ0eXBlIjogImxpc3QiLCAiZWxlbWVudHMiOiBbeyJ0eXBlIjogIkF2ZXJhZ2UiLCAiYXR0cmlidXRlcyI6IHsiX3V1aWQiOiB7InR5cGUiOiAiVVVJRCIsICJ1dWlkIjogIjIwM2MzY2U2LTVjNzktNGQwZS1iMzYzLTgyZDQ4NDgwYWI5OCJ9LCAiZWxlbWVudHMiOiB7InR5cGUiOiAibGlzdCIsICJlbGVtZW50cyI6IFt7InR5cGUiOiAiRm9yTG9vcCIsICJhdHRyaWJ1dGVzIjogeyJfdXVpZCI6IHsidHlwZSI6ICJVVUlEIiwgInV1aWQiOiAiYzhkNjYzMzgtYzU0OS00MjcwLWI0NjAtNjA4NmE0OTE0ODMzIn0sICJlbGVtZW50cyI6IHsidHlwZSI6ICJsaXN0IiwgImVsZW1lbnRzIjogW3sidHlwZSI6ICJTZXRHYWluIiwgImF0dHJpYnV0ZXMiOiB7Il91dWlkIjogeyJ0eXBlIjogIlVVSUQiLCAidXVpZCI6ICJmZGFkMjAwOC1iODViLTRiYjYtYjIwYi1jZjQ2NmQxZGFlNTgifSwgImJ1cyI6ICJkcml2ZV9xMF9idXMiLCAiZ2FpbiI6IHsidHlwZSI6ICJGbG9hdFZhcmlhYmxlIiwgImF0dHJpYnV0ZXMiOiB7Il91dWlkIjogeyJ0eXBlIjogIlVVSUQiLCAidXVpZCI6ICI2MzVkZjdjYS1jZTM4LTRkNjItOWZlZC03ZjFjYmFmNzFlNzkifSwgIl9zb3VyY2UiOiB7InR5cGUiOiAiVmFsdWVTb3VyY2UiLCAiYXR0cmlidXRlcyI6IHsidmFsdWUiOiAiRnJlZSJ9fSwgIl92YWx1ZSI6IG51bGwsICJfZG9tYWluIjogeyJ0eXBlIjogIkRvbWFpbiIsICJhdHRyaWJ1dGVzIjogeyJ2YWx1ZSI6ICJWb2x0YWdlIn19fX19fSwgeyJ0eXBlIjogIlBsYXkiLCAiYXR0cmlidXRlcyI6IHsiX3V1aWQiOiB7InR5cGUiOiAiVVVJRCIsICJ1dWlkIjogImFiY2FjZDExLWU0YWEtNDI5Yi1iNjNjLTNhMjUwOWJjYTE3ZiJ9LCAiYnVzIjogImRyaXZlX3EwX2J1cyIsICJ3YXZlZm9ybSI6IHsidHlwZSI6ICJJUVBhaXIiLCAiYXR0cmlidXRlcyI6IHsiSSI6IHsidHlwZSI6ICJHYXVzc2lhbiIsICJhdHRyaWJ1dGVzIjogeyJhbXBsaXR1ZGUiOiAxLjAsICJkdXJhdGlvbiI6IDQwLCAibnVtX3NpZ21hcyI6IDQuNX19LCAiUSI6IHsidHlwZSI6ICJEcmFnQ29ycmVjdGlvbiIsICJhdHRyaWJ1dGVzIjogeyJkcmFnX2NvZWZmaWNpZW50IjogLTIuMCwgIndhdmVmb3JtIjogeyJ0eXBlIjogIkdhdXNzaWFuIiwgImF0dHJpYnV0ZXMiOiB7ImFtcGxpdHVkZSI6IDEuMCwgImR1cmF0aW9uIjogNDAsICJudW1fc2lnbWFzIjogNC41fX19fX19LCAid2FpdF90aW1lIjogbnVsbH19LCB7InR5cGUiOiAiU3luYyIsICJhdHRyaWJ1dGVzIjogeyJfdXVpZCI6IHsidHlwZSI6ICJVVUlEIiwgInV1aWQiOiAiOWRhZDJhYzYtYzk1OC00Y2Q5LTg1MzItMmMyMzJhNTJkMzg3In0sICJidXNlcyI6IG51bGx9fSwgeyJ0eXBlIjogIlBsYXkiLCAiYXR0cmlidXRlcyI6IHsiX3V1aWQiOiB7InR5cGUiOiAiVVVJRCIsICJ1dWlkIjogIjRlMmEzYTc1LWJkYzEtNDEzZi04ZDg1LTY3MDM5NThkMzg3NSJ9LCAiYnVzIjogInJlYWRvdXRfcTBfYnVzIiwgIndhdmVmb3JtIjogeyJ0eXBlIjogIklRUGFpciIsICJhdHRyaWJ1dGVzIjogeyJJIjogeyJ0eXBlIjogIlNxdWFyZSIsICJhdHRyaWJ1dGVzIjogeyJhbXBsaXR1ZGUiOiAxLjAsICJkdXJhdGlvbiI6IDIwMDB9fSwgIlEiOiB7InR5cGUiOiAiU3F1YXJlIiwgImF0dHJpYnV0ZXMiOiB7ImFtcGxpdHVkZSI6IDAuMCwgImR1cmF0aW9uIjogMjAwMH19fX0sICJ3YWl0X3RpbWUiOiA0MH19LCB7InR5cGUiOiAiQWNxdWlyZSIsICJhdHRyaWJ1dGVzIjogeyJfdXVpZCI6IHsidHlwZSI6ICJVVUlEIiwgInV1aWQiOiAiM2Y2YmNhYjctOTJlOS00M2I4LTg5YjAtOTNiNDcxMDNjNDlhIn0sICJidXMiOiAicmVhZG91dF9xMF9idXMiLCAid2VpZ2h0cyI6IHsidHlwZSI6ICJJUVBhaXIiLCAiYXR0cmlidXRlcyI6IHsiSSI6IHsidHlwZSI6ICJTcXVhcmUiLCAiYXR0cmlidXRlcyI6IHsiYW1wbGl0dWRlIjogMS4wLCAiZHVyYXRpb24iOiAyMDAwfX0sICJRIjogeyJ0eXBlIjogIlNxdWFyZSIsICJhdHRyaWJ1dGVzIjogeyJhbXBsaXR1ZGUiOiAxLjAsICJkdXJhdGlvbiI6IDIwMDB9fX19LCAibmFtZSI6IG51bGx9fSwgeyJ0eXBlIjogIldhaXQiLCAiYXR0cmlidXRlcyI6IHsiX3V1aWQiOiB7InR5cGUiOiAiVVVJRCIsICJ1dWlkIjogIjMzNTQzMDdlLWU0OTUtNDNhMy1hODI1LTE5ZGEzZTcxMTA0ZSJ9LCAiYnVzIjogInJlYWRvdXRfcTBfYnVzIiwgImR1cmF0aW9uIjogMTAwMDB9fV19LCAidmFyaWFibGUiOiB7InR5cGUiOiAiRmxvYXRWYXJpYWJsZSIsICJhdHRyaWJ1dGVzIjogeyJfdXVpZCI6IHsidHlwZSI6ICJVVUlEIiwgInV1aWQiOiAiNjM1ZGY3Y2EtY2UzOC00ZDYyLTlmZWQtN2YxY2JhZjcxZTc5In0sICJfc291cmNlIjogeyJ0eXBlIjogIlZhbHVlU291cmNlIiwgImF0dHJpYnV0ZXMiOiB7InZhbHVlIjogIkZyZWUifX0sICJfdmFsdWUiOiBudWxsLCAiX2RvbWFpbiI6IHsidHlwZSI6ICJEb21haW4iLCAiYXR0cmlidXRlcyI6IHsidmFsdWUiOiAiVm9sdGFnZSJ9fX19LCAic3RhcnQiOiAwLjAsICJzdG9wIjogMS4wLCAic3RlcCI6IDAuMX19XX0sICJzaG90cyI6IDEwMDB9fV19fX0sICJfYnVzZXMiOiB7InR5cGUiOiAic2V0IiwgImVsZW1lbnRzIjogWyJyZWFkb3V0X3EwX2J1cyIsICJkcml2ZV9xMF9idXMiXX0sICJfdmFyaWFibGVzIjogeyJ0eXBlIjogImxpc3QiLCAiZWxlbWVudHMiOiBbeyJ0eXBlIjogIkZsb2F0VmFyaWFibGUiLCAiYXR0cmlidXRlcyI6IHsiX3V1aWQiOiB7InR5cGUiOiAiVVVJRCIsICJ1dWlkIjogIjYzNWRmN2NhLWNlMzgtNGQ2Mi05ZmVkLTdmMWNiYWY3MWU3OSJ9LCAiX3NvdXJjZSI6IHsidHlwZSI6ICJWYWx1ZVNvdXJjZSIsICJhdHRyaWJ1dGVzIjogeyJ2YWx1ZSI6ICJGcmVlIn19LCAiX3ZhbHVlIjogbnVsbCwgIl9kb21haW4iOiB7InR5cGUiOiAiRG9tYWluIiwgImF0dHJpYnV0ZXMiOiB7InZhbHVlIjogIlZvbHRhZ2UifX19fV19LCAiX2Jsb2NrX3N0YWNrIjogeyJ0eXBlIjogImRlcXVlIiwgImVsZW1lbnRzIjogW3sidHlwZSI6ICJCbG9jayIsICJhdHRyaWJ1dGVzIjogeyJfdXVpZCI6IHsidHlwZSI6ICJVVUlEIiwgInV1aWQiOiAiNmJjZWI0NTEtMGVlMS00NTdjLWExNTctYjJmMmQ1MzhmZmI1In0sICJlbGVtZW50cyI6IHsidHlwZSI6ICJsaXN0IiwgImVsZW1lbnRzIjogW3sidHlwZSI6ICJBdmVyYWdlIiwgImF0dHJpYnV0ZXMiOiB7Il91dWlkIjogeyJ0eXBlIjogIlVVSUQiLCAidXVpZCI6ICIyMDNjM2NlNi01Yzc5LTRkMGUtYjM2My04MmQ0ODQ4MGFiOTgifSwgImVsZW1lbnRzIjogeyJ0eXBlIjogImxpc3QiLCAiZWxlbWVudHMiOiBbeyJ0eXBlIjogIkZvckxvb3AiLCAiYXR0cmlidXRlcyI6IHsiX3V1aWQiOiB7InR5cGUiOiAiVVVJRCIsICJ1dWlkIjogImM4ZDY2MzM4LWM1NDktNDI3MC1iNDYwLTYwODZhNDkxNDgzMyJ9LCAiZWxlbWVudHMiOiB7InR5cGUiOiAibGlzdCIsICJlbGVtZW50cyI6IFt7InR5cGUiOiAiU2V0R2FpbiIsICJhdHRyaWJ1dGVzIjogeyJfdXVpZCI6IHsidHlwZSI6ICJVVUlEIiwgInV1aWQiOiAiZmRhZDIwMDgtYjg1Yi00YmI2LWIyMGItY2Y0NjZkMWRhZTU4In0sICJidXMiOiAiZHJpdmVfcTBfYnVzIiwgImdhaW4iOiB7InR5cGUiOiAiRmxvYXRWYXJpYWJsZSIsICJhdHRyaWJ1dGVzIjogeyJfdXVpZCI6IHsidHlwZSI6ICJVVUlEIiwgInV1aWQiOiAiNjM1ZGY3Y2EtY2UzOC00ZDYyLTlmZWQtN2YxY2JhZjcxZTc5In0sICJfc291cmNlIjogeyJ0eXBlIjogIlZhbHVlU291cmNlIiwgImF0dHJpYnV0ZXMiOiB7InZhbHVlIjogIkZyZWUifX0sICJfdmFsdWUiOiBudWxsLCAiX2RvbWFpbiI6IHsidHlwZSI6ICJEb21haW4iLCAiYXR0cmlidXRlcyI6IHsidmFsdWUiOiAiVm9sdGFnZSJ9fX19fX0sIHsidHlwZSI6ICJQbGF5IiwgImF0dHJpYnV0ZXMiOiB7Il91dWlkIjogeyJ0eXBlIjogIlVVSUQiLCAidXVpZCI6ICJhYmNhY2QxMS1lNGFhLTQyOWItYjYzYy0zYTI1MDliY2ExN2YifSwgImJ1cyI6ICJkcml2ZV9xMF9idXMiLCAid2F2ZWZvcm0iOiB7InR5cGUiOiAiSVFQYWlyIiwgImF0dHJpYnV0ZXMiOiB7IkkiOiB7InR5cGUiOiAiR2F1c3NpYW4iLCAiYXR0cmlidXRlcyI6IHsiYW1wbGl0dWRlIjogMS4wLCAiZHVyYXRpb24iOiA0MCwgIm51bV9zaWdtYXMiOiA0LjV9fSwgIlEiOiB7InR5cGUiOiAiRHJhZ0NvcnJlY3Rpb24iLCAiYXR0cmlidXRlcyI6IHsiZHJhZ19jb2VmZmljaWVudCI6IC0yLjAsICJ3YXZlZm9ybSI6IHsidHlwZSI6ICJHYXVzc2lhbiIsICJhdHRyaWJ1dGVzIjogeyJhbXBsaXR1ZGUiOiAxLjAsICJkdXJhdGlvbiI6IDQwLCAibnVtX3NpZ21hcyI6IDQuNX19fX19fSwgIndhaXRfdGltZSI6IG51bGx9fSwgeyJ0eXBlIjogIlN5bmMiLCAiYXR0cmlidXRlcyI6IHsiX3V1aWQiOiB7InR5cGUiOiAiVVVJRCIsICJ1dWlkIjogIjlkYWQyYWM2LWM5NTgtNGNkOS04NTMyLTJjMjMyYTUyZDM4NyJ9LCAiYnVzZXMiOiBudWxsfX0sIHsidHlwZSI6ICJQbGF5IiwgImF0dHJpYnV0ZXMiOiB7Il91dWlkIjogeyJ0eXBlIjogIlVVSUQiLCAidXVpZCI6ICI0ZTJhM2E3NS1iZGMxLTQxM2YtOGQ4NS02NzAzOTU4ZDM4NzUifSwgImJ1cyI6ICJyZWFkb3V0X3EwX2J1cyIsICJ3YXZlZm9ybSI6IHsidHlwZSI6ICJJUVBhaXIiLCAiYXR0cmlidXRlcyI6IHsiSSI6IHsidHlwZSI6ICJTcXVhcmUiLCAiYXR0cmlidXRlcyI6IHsiYW1wbGl0dWRlIjogMS4wLCAiZHVyYXRpb24iOiAyMDAwfX0sICJRIjogeyJ0eXBlIjogIlNxdWFyZSIsICJhdHRyaWJ1dGVzIjogeyJhbXBsaXR1ZGUiOiAwLjAsICJkdXJhdGlvbiI6IDIwMDB9fX19LCAid2FpdF90aW1lIjogNDB9fSwgeyJ0eXBlIjogIkFjcXVpcmUiLCAiYXR0cmlidXRlcyI6IHsiX3V1aWQiOiB7InR5cGUiOiAiVVVJRCIsICJ1dWlkIjogIjNmNmJjYWI3LTkyZTktNDNiOC04OWIwLTkzYjQ3MTAzYzQ5YSJ9LCAiYnVzIjogInJlYWRvdXRfcTBfYnVzIiwgIndlaWdodHMiOiB7InR5cGUiOiAiSVFQYWlyIiwgImF0dHJpYnV0ZXMiOiB7IkkiOiB7InR5cGUiOiAiU3F1YXJlIiwgImF0dHJpYnV0ZXMiOiB7ImFtcGxpdHVkZSI6IDEuMCwgImR1cmF0aW9uIjogMjAwMH19LCAiUSI6IHsidHlwZSI6ICJTcXVhcmUiLCAiYXR0cmlidXRlcyI6IHsiYW1wbGl0dWRlIjogMS4wLCAiZHVyYXRpb24iOiAyMDAwfX19fSwgIm5hbWUiOiBudWxsfX0sIHsidHlwZSI6ICJXYWl0IiwgImF0dHJpYnV0ZXMiOiB7Il91dWlkIjogeyJ0eXBlIjogIlVVSUQiLCAidXVpZCI6ICIzMzU0MzA3ZS1lNDk1LTQzYTMtYTgyNS0xOWRhM2U3MTEwNGUifSwgImJ1cyI6ICJyZWFkb3V0X3EwX2J1cyIsICJkdXJhdGlvbiI6IDEwMDAwfX1dfSwgInZhcmlhYmxlIjogeyJ0eXBlIjogIkZsb2F0VmFyaWFibGUiLCAiYXR0cmlidXRlcyI6IHsiX3V1aWQiOiB7InR5cGUiOiAiVVVJRCIsICJ1dWlkIjogIjYzNWRmN2NhLWNlMzgtNGQ2Mi05ZmVkLTdmMWNiYWY3MWU3OSJ9LCAiX3NvdXJjZSI6IHsidHlwZSI6ICJWYWx1ZVNvdXJjZSIsICJhdHRyaWJ1dGVzIjogeyJ2YWx1ZSI6ICJGcmVlIn19LCAiX3ZhbHVlIjogbnVsbCwgIl9kb21haW4iOiB7InR5cGUiOiAiRG9tYWluIiwgImF0dHJpYnV0ZXMiOiB7InZhbHVlIjogIlZvbHRhZ2UifX19fSwgInN0YXJ0IjogMC4wLCAic3RvcCI6IDEuMCwgInN0ZXAiOiAwLjF9fV19LCAic2hvdHMiOiAxMDAwfX1dfX19XX19fQ==", @@ -44,6 +56,17 @@ def test_job_result_vqa_works(): assert isinstance(job_result.data, dict) +def test_job_result_annealing_program_works(): + """Test vqa results are returned as dicts""" + + job_result = JobResult( + job_id=0, + http_response='{"data": "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", "encoding": "utf-8", "compression": "gzip"}', + job_type="annealing_program", + ) + assert isinstance(job_result.data, str) + + def test_job_result_program_raises_error(): """Test we are rising exceptions to inform correctly that PROGRAMS are not currently supported.""" diff --git a/tests/unit/test_util.py b/tests/unit/test_util.py index aa3683fd..4b5ed78a 100644 --- a/tests/unit/test_util.py +++ b/tests/unit/test_util.py @@ -74,7 +74,10 @@ def test_process_response_non_json(response_plain_text: Response): def test_deserialize_job_description( - compressed_qibo_circuit: str, compressed_qibo_circuits: str, compressed_qililab_qprogram: str + compressed_qibo_circuit: str, + compressed_qibo_circuits: str, + compressed_qililab_qprogram: str, + compressed_qililab_annealing_program: str, ): """Unit test of deserialize_job_description()""" assert isinstance( @@ -84,6 +87,12 @@ def test_deserialize_job_description( deserialize_job_description(raw_description=compressed_qililab_qprogram, job_type=JobType.QPROGRAM)["data"], dict, ) + assert isinstance( + deserialize_job_description( + raw_description=compressed_qililab_annealing_program, job_type=JobType.ANNEALING_PROGRAM + )["data"], + dict, + ) assert isinstance( deserialize_job_description(raw_description=compressed_qibo_circuit, job_type=JobType.OTHER)["data"], list )