Commit 1d79031b authored by Victor Negîrneac's avatar Victor Negîrneac
Browse files

Merge branch 'no_gc_iccs' into 'develop'

Avoid garbage collection for instrument coordinator components

Closes #151

See merge request !162
parents 762b280d 318ce96f
Loading
Loading
Loading
Loading
Loading
+22 −1
Original line number Diff line number Diff line
@@ -4,7 +4,7 @@
from __future__ import annotations

from abc import abstractmethod
from typing import Any
from typing import Any, Dict
from qcodes.instrument import parameter
from qcodes.instrument import base
from qcodes.utils import validators
@@ -13,6 +13,27 @@ from qcodes.utils import validators
class InstrumentCoordinatorComponentBase(base.Instrument):
    """The InstrumentCoordinator component abstract interface."""

    # NB `_instances` also used by `Instrument` class
    _no_gc_intances: Dict[str, InstrumentCoordinatorComponentBase] = dict()

    def __new__(
        cls, instrument: base.InstrumentBase, *args, **kwargs
    ) -> InstrumentCoordinatorComponentBase:
        """Keeps track of the intances of this class.

        NB This is done intentionally to prevent the intances from being garbage
        collected.
        """
        instance = super().__new__(cls, *args, **kwargs)
        cls._no_gc_intances[instrument.name] = instance
        return instance

    def close(self):
        """Makes sure the intance reference is realeased so that garbage collector can
        claim the object"""
        _ = self._no_gc_intances.pop(self.instrument_ref())
        super().close()

    def __init__(
        self,
        instrument: base.InstrumentBase,
+135 −105
Original line number Diff line number Diff line
@@ -3,46 +3,94 @@
# pylint: disable=missing-module-docstring
# pylint: disable=missing-class-docstring
# pylint: disable=missing-function-docstring
# pylint: disable=unused-argument
# pylint: disable=too-many-arguments
from __future__ import annotations
from typing import List
import gc

from dataclasses import dataclass
from unittest.mock import call

import pytest
from qcodes import Instrument
from quantify_scheduler.instrument_coordinator import InstrumentCoordinator
from quantify_scheduler.instrument_coordinator.components import base as base_component


def make_component(
    mocker, name: str
class MyICC(base_component.InstrumentCoordinatorComponentBase):
    @property
    def is_running(self):
        pass

    def start(self):
        pass

    def stop(self):
        pass

    def prepare(self, options):
        pass

    def retrieve_acquisition(self):
        pass

    def wait_done(self, timeout_sec: int = 10):
        pass


@pytest.fixture(autouse=True, name="close_all_instruments")
def fixture_close_all_instruments():
    """Makes sure that after startup and teardown all instruments are closed"""
    Instrument.close_all()
    yield
    Instrument.close_all()


# cretes a few dummy compoents avialable to be used in each test
@pytest.fixture(scope="function", name="dummy_components")
def fixture_dummy_components(
    mocker, request
) -> base_component.InstrumentCoordinatorComponentBase:
    mocker.patch("qcodes.instrument.Instrument.record_instance")
    component = mocker.create_autospec(
        base_component.InstrumentCoordinatorComponentBase, instance=True

    # Crete a QCoDeS intrument for realistic emulation
    instruments = [Instrument(f"dev{i}") for i in range(3)]
    components = []

    for instrument in instruments:
        comp = MyICC(instrument)
        for func in ("prepare", "start", "stop", "wait_done", "retrieve_acquisition"):
            mocker.patch.object(
                comp,
                func,
                wraps=getattr(comp, func),
            )
    component.name = name
    return component
        components.append(comp)

    def cleanup_tmp():
        # This should prevent the garbage collector from colleting the qcodes instrument
        for instrument in instruments:
            instrument.close()

def make_instrument_coordinator(mocker, name: str) -> InstrumentCoordinator:
    mocker.patch("qcodes.instrument.Instrument.record_instance")
    instrument_coordinator = InstrumentCoordinator(name)
    request.addfinalizer(cleanup_tmp)

    instrument_coordinator._mock_instr_dict = {}
    return components

    # add a mock find instrument
    def mock_find_instrument(instr_name: str):
        return instrument_coordinator._mock_instr_dict[instr_name]

    instrument_coordinator.find_instrument = mock_find_instrument
@pytest.fixture(scope="function", name="instrument_coordinator")
def fixture_instrument_coordinator(request) -> InstrumentCoordinator:
    instrument_coordinator = InstrumentCoordinator("ic_0000")

    def cleanup_tmp():
        # This should prevent the garbage collector from colleting the qcodes instrument
        instrument_coordinator.close()

    request.addfinalizer(cleanup_tmp)

    return instrument_coordinator


def test_constructor(mocker):
    # Act
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")
def test_constructor(close_all_instruments, instrument_coordinator):

    # Assert
    assert len(instrument_coordinator.components()) == 0
@@ -56,16 +104,25 @@ def test_constructor(mocker):
        ([False, False], False),
    ],
)
def test_is_running(mocker, states: List[bool], expected: bool):
def test_is_running(
    close_all_instruments,
    instrument_coordinator,
    dummy_components,
    states: List[bool],
    expected: bool,
    mocker,
):
    # Arrange
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")
    mocker.patch.object(MyICC, "is_running")  # necessary for overriding `is_running`

    for i, state in enumerate(states):
        component = make_component(mocker, f"dev{i}")
        component.is_running = state
    for state in states:
        # popping ensures the reference to the object is released after this for loop
        component = dummy_components.pop(0)
        instrument_coordinator.add_component(component)
        component.is_running = state

        instrument_coordinator._mock_instr_dict[component.name] = component
    # force garbage collection to emulate qcodes correcly
    gc.collect()

    # Act
    is_running = instrument_coordinator.is_running
@@ -74,52 +131,47 @@ def test_is_running(mocker, states: List[bool], expected: bool):
    assert is_running == expected


def test_get_component(mocker):
def test_get_component(close_all_instruments, instrument_coordinator, dummy_components):
    for i in range(len(dummy_components)):
        # Arrange
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")
    component1 = make_component(mocker, "dev1234")
    instrument_coordinator.add_component(component1)

    # required for the mock find_instrument to work
    instrument_coordinator._mock_instr_dict[component1.name] = component1
        component_ = dummy_components.pop(0)
        instrument_coordinator.add_component(component_)

        # Act
    component = instrument_coordinator.get_component("dev1234")
        component = instrument_coordinator.get_component(f"ic_dev{i}")

        # Assert
    assert component1 == component
        assert component_ == component


def test_get_component_failed(mocker):
    # Arrange
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")
def test_get_component_failed(close_all_instruments, instrument_coordinator):

    # Act
    with pytest.raises(KeyError) as execinfo:
        instrument_coordinator.get_component("dev1234")
        instrument_coordinator.get_component("ic_dev1234")

    # Assert
    assert execinfo.value.args[0] == "'dev1234' is not a component of ic0!"
    assert execinfo.value.args[0] == "'ic_dev1234' is not a component of ic_0000!"


def test_add_component_failed_duplicate(mocker):
def test_add_component_failed_duplicate(
    close_all_instruments, instrument_coordinator, dummy_components
):
    # Arrange
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")
    component1 = make_component(mocker, "dev1234")
    component1 = dummy_components.pop(0)
    instrument_coordinator.add_component(component1)

    # Act
    with pytest.raises(ValueError) as execinfo:
        instrument_coordinator.add_component(component1)
        instrument_coordinator.add_component(component1)

    # Assert
    assert execinfo.value.args[0] == "'dev1234' has already been added!"

    assert execinfo.value.args[0] == "'ic_dev0' has already been added!"

def test_add_component_failed_type_validation(mocker):
    # Arrange
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")

def test_add_component_failed_type_validation(
    close_all_instruments, instrument_coordinator
):
    @dataclass
    class DummyComponent:
        name: str
@@ -140,34 +192,28 @@ def test_add_component_failed_type_validation(mocker):
    )


def test_remove_component(mocker):
def test_remove_component(
    close_all_instruments, instrument_coordinator, dummy_components
):
    # Arrange
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")
    component1 = make_component(mocker, "dev1234")
    component2 = make_component(mocker, "dev5678")
    component1, component2 = dummy_components.pop(0), dummy_components.pop(0)
    instrument_coordinator.add_component(component1)
    instrument_coordinator.add_component(component2)
    # required for the mock find_instrument to work
    instrument_coordinator._mock_instr_dict[component1.name] = component1
    instrument_coordinator._mock_instr_dict[component2.name] = component2

    # Act
    assert instrument_coordinator.components() == ["dev1234", "dev5678"]
    instrument_coordinator.remove_component("dev1234")
    assert instrument_coordinator.components() == ["dev5678"]
    assert instrument_coordinator.components() == ["ic_dev0", "ic_dev1"]
    instrument_coordinator.remove_component("ic_dev0")
    assert instrument_coordinator.components() == ["ic_dev1"]


def test_prepare(mocker):
def test_prepare(
    close_all_instruments, instrument_coordinator, dummy_components, mocker
):  # NB order of fixtures matters for teardown, keep mocker as last!
    # Arrange
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")
    component1 = make_component(mocker, "dev1234")
    component2 = make_component(mocker, "dev5678")

    component1 = dummy_components.pop(0)
    component2 = dummy_components.pop(0)
    instrument_coordinator.add_component(component1)
    instrument_coordinator.add_component(component2)
    # required for the mock find_instrument to work
    instrument_coordinator._mock_instr_dict[component1.name] = component1
    instrument_coordinator._mock_instr_dict[component2.name] = component2

    get_component_spy = mocker.patch.object(
        instrument_coordinator,
@@ -176,28 +222,22 @@ def test_prepare(mocker):
    )

    # Act
    args = {"dev1234": {"foo": 1}, "dev5678": {"foo": 2}}
    args = {"ic_dev0": {"foo": 0}, "ic_dev1": {"foo": 1}}
    instrument_coordinator.prepare(args)

    # Assert
    assert get_component_spy.call_args_list == [
        call("dev1234"),
        call("dev5678"),
    ]
    component1.prepare.assert_called_with(args["dev1234"])
    component2.prepare.assert_called_with(args["dev5678"])
    assert get_component_spy.call_args_list == [call("ic_dev0"), call("ic_dev1")]

    component1.prepare.assert_called_with(args["ic_dev0"])
    component2.prepare.assert_called_with(args["ic_dev1"])


def test_start(mocker):
def test_start(close_all_instruments, instrument_coordinator, dummy_components):
    # Arrange
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")
    component1 = make_component(mocker, "dev1234")
    component2 = make_component(mocker, "dev5678")
    component1 = dummy_components.pop(0)
    component2 = dummy_components.pop(0)
    instrument_coordinator.add_component(component1)
    instrument_coordinator.add_component(component2)
    # required for the mock find_instrument to work
    instrument_coordinator._mock_instr_dict[component1.name] = component1
    instrument_coordinator._mock_instr_dict[component2.name] = component2

    # Act
    instrument_coordinator.start()
@@ -207,16 +247,12 @@ def test_start(mocker):
    component2.start.assert_called()


def test_stop(mocker):
def test_stop(close_all_instruments, instrument_coordinator, dummy_components):
    # Arrange
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")
    component1 = make_component(mocker, "dev1234")
    component2 = make_component(mocker, "dev5678")
    component1 = dummy_components.pop(0)
    component2 = dummy_components.pop(0)
    instrument_coordinator.add_component(component1)
    instrument_coordinator.add_component(component2)
    # required for the mock find_instrument to work
    instrument_coordinator._mock_instr_dict[component1.name] = component1
    instrument_coordinator._mock_instr_dict[component2.name] = component2

    # Act
    instrument_coordinator.stop()
@@ -226,16 +262,14 @@ def test_stop(mocker):
    component2.stop.assert_called()


def test_retrieve_acquisition(mocker):
def test_retrieve_acquisition(
    close_all_instruments, instrument_coordinator, dummy_components
):
    # Arrange
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")
    component1 = make_component(mocker, "dev1234")
    component2 = make_component(mocker, "dev5678")
    component1 = dummy_components.pop(0)
    component2 = dummy_components.pop(0)
    instrument_coordinator.add_component(component1)
    instrument_coordinator.add_component(component2)
    # required for the mock find_instrument to work
    instrument_coordinator._mock_instr_dict[component1.name] = component1
    instrument_coordinator._mock_instr_dict[component2.name] = component2

    component1.retrieve_acquisition.return_value = {0: [1, 2, 3, 4]}
    component2.retrieve_acquisition.return_value = None
@@ -246,19 +280,15 @@ def test_retrieve_acquisition(mocker):
    # Assert
    component1.retrieve_acquisition.assert_called()
    component2.retrieve_acquisition.assert_called()
    assert {"dev1234": {0: [1, 2, 3, 4]}} == data
    assert {"ic_dev0": {0: [1, 2, 3, 4]}} == data


def test_wait_done(mocker):
def test_wait_done(close_all_instruments, instrument_coordinator, dummy_components):
    # Arrange
    instrument_coordinator = make_instrument_coordinator(mocker, "ic0")
    component1 = make_component(mocker, "dev1234")
    component2 = make_component(mocker, "dev5678")
    component1 = dummy_components.pop(0)
    component2 = dummy_components.pop(0)
    instrument_coordinator.add_component(component1)
    instrument_coordinator.add_component(component2)
    # required for the mock find_instrument to work
    instrument_coordinator._mock_instr_dict[component1.name] = component1
    instrument_coordinator._mock_instr_dict[component2.name] = component2

    timeout: int = 1