コード例 #1
0
class KepcoTests(object):
    """
    Tests for the KEPCO.
    """

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc("kepco", DEVICE_PREFIX)
        self.ca = ChannelAccess(default_timeout=30, device_prefix=DEVICE_PREFIX)
        self._lewis.backdoor_run_function_on_device("reset")
        self.ca.assert_that_pv_exists("VOLTAGE", timeout=30)
        reset_calibration_file(self.ca, "default_calib.dat")

    def _write_voltage(self, expected_voltage):
        self._lewis.backdoor_set_on_device("voltage", expected_voltage)
        self._ioc.set_simulated_value("SIM:VOLTAGE", expected_voltage)

    def _write_current(self, expected_current):
        self._lewis.backdoor_set_on_device("current", expected_current)
        self._ioc.set_simulated_value("SIM:CURRENT", expected_current)

    def _set_IDN(self, expected_idn_no_firmware, expected_firmware):
        self._lewis.backdoor_set_on_device("idn_no_firmware", expected_idn_no_firmware)
        self._lewis.backdoor_set_on_device("firmware", expected_firmware)
        expected_idn = "{}{}".format(expected_idn_no_firmware, str(expected_firmware))[:39]  # EPICS limited to 40 chars
        self._ioc.set_simulated_value("SIM:IDN", expected_idn)
        self._ioc.set_simulated_value("SIM:FIRMWARE", str(expected_firmware))
        # Both firmware and IDN are passive so must be updated
        self.ca.process_pv("FIRMWARE")
        self.ca.process_pv("IDN")
        return expected_idn

    def _set_output_mode(self, expected_output_mode):
        self._lewis.backdoor_set_on_device("output_mode", expected_output_mode)
        self._ioc.set_simulated_value("SIM:OUTPUTMODE", expected_output_mode)

    def _set_output_status(self, expected_output_status):
        self._lewis.backdoor_set_on_device("output_status", expected_output_status)

    def test_GIVEN_voltage_set_WHEN_read_THEN_voltage_is_as_expected(self):
        expected_voltage = 1.2
        self._write_voltage(expected_voltage)
        self.ca.assert_that_pv_is("VOLTAGE", expected_voltage)

    def test_GIVEN_current_set_WHEN_read_THEN_current_is_as_expected(self):
        expected_current = 1.5
        self._write_current(expected_current)
        self.ca.assert_that_pv_is("CURRENT", expected_current)

    def test_GIVEN_setpoint_voltage_set_WHEN_read_THEN_setpoint_voltage_is_as_expected(self):
        # Get current Voltage
        current_voltage = self.ca.get_pv_value("VOLTAGE")
        # Set new Voltage via SP
        self.ca.set_pv_value("VOLTAGE:SP", current_voltage + 5)
        # Check SP RBV matches new current
        self.ca.assert_that_pv_is("VOLTAGE:SP:RBV", current_voltage + 5)

    @parameterized.expand(parameterized_list([-5.1, 7.8]))
    def test_GIVEN_setpoint_current_set_WHEN_read_THEN_setpoint_current_is_as_expected(self, _, expected_current):
        self.ca.set_pv_value("CURRENT:SP", expected_current)
        # Check SP RBV matches new current
        self.ca.assert_that_pv_is("CURRENT:SP:RBV", expected_current)

    def test_GIVEN_output_mode_set_WHEN_read_THEN_output_mode_is_as_expected(self):
        expected_output_mode_flag = UnitFlags.CURRENT
        expected_output_mode_str = OutputMode.CURRENT
        self._set_output_mode(expected_output_mode_flag)
        # Check OUTPUT MODE matches new OUTPUT MODE
        self.ca.assert_that_pv_is("OUTPUTMODE", expected_output_mode_str)

    def test_GIVEN_output_status_set_WHEN_read_THEN_output_STATUS_is_as_expected(self):
        expected_output_status_flag = UnitFlags.ON
        expected_output_status_str = Status.ON
        self.ca.set_pv_value("OUTPUTSTATUS:SP", expected_output_status_flag)
        self.ca.assert_that_pv_is("OUTPUTSTATUS:SP:RBV", expected_output_status_str)

    @parameterized.expand(parameterized_list(IDN_LIST))
    def test_GIVEN_idn_set_WHEN_read_THEN_idn_is_as_expected(self, _, idn_no_firmware, firmware):
        expected_idn = self._set_IDN(idn_no_firmware, firmware)
        self.ca.process_pv("IDN")
        self.ca.assert_that_pv_is("IDN", expected_idn)

    @skip_if_recsim("In rec sim you can not diconnect the device")
    def test_GIVEN_diconnected_WHEN_read_THEN_alarms_on_readbacks(self):
        self._lewis.backdoor_set_on_device("connected", False)

        self.ca.assert_that_pv_alarm_is("OUTPUTMODE", self.ca.Alarms.INVALID)
        self.ca.assert_that_pv_alarm_is("CURRENT", self.ca.Alarms.INVALID)
        self.ca.assert_that_pv_alarm_is("VOLTAGE", self.ca.Alarms.INVALID)

    def _test_ramp_to_target(self, start_current, target_current, ramp_rate, step_number, wait_between_changes):
        self._write_current(start_current)
        self.ca.set_pv_value("CURRENT:SP", start_current)
        self.ca.assert_that_pv_is("CURRENT:SP:RBV", start_current)
        self.ca.set_pv_value("RAMP:RATE:SP", ramp_rate)
        self.ca.set_pv_value("RAMP:STEPS:SP", step_number)
        self.ca.set_pv_value("RAMPON:SP", "ON")
        self.ca.set_pv_value("CURRENT:SP", target_current, sleep_after_set=0.0)
        if start_current < target_current:
            self.ca.assert_that_pv_value_is_increasing("CURRENT:SP:RBV", wait=wait_between_changes)
        else:
            self.ca.assert_that_pv_value_is_decreasing("CURRENT:SP:RBV", wait=wait_between_changes)
        self.ca.assert_that_pv_is("RAMPING", "YES")
        # Device stops ramping when it gets to target
        self.ca.assert_that_pv_is("CURRENT:SP:RBV", target_current, timeout=40)
        self._write_current(target_current)
        self.ca.assert_that_pv_is("RAMPING", "NO")
        self.ca.assert_that_pv_value_is_unchanged("CURRENT:SP:RBV", wait=wait_between_changes)
        self.ca.set_pv_value("RAMPON:SP", "OFF")

    def test_GIVEN_rampon_WHEN_target_set_THEN_current_ramps_to_target(self):
        self._test_ramp_to_target(1, 2, 2, 20, 7)

    def test_GIVEN_rampon_WHEN_target_set_with_different_step_rate_THEN_current_ramps_to_target_more_finely(self):
        self._test_ramp_to_target(4, 3, 2, 60, 2)

    @parameterized.expand(parameterized_list(IDN_LIST))
    def test_GIVEN_idn_set_AND_firmware_set_THEN_firmware_pv_correct(self, _, idn_no_firmware, firmware):
        self._set_IDN(idn_no_firmware, firmware)
        self.ca.process_pv("FIRMWARE")
        self.ca.assert_that_pv_is("FIRMWARE", firmware)

    @parameterized.expand(parameterized_list([
        ("default_calib.dat", 100, 100),
        ("field_double_amps.dat", 100, 50),
    ]))
    @skip_if_recsim("Calibration lookup does not work in recsim")
    def test_GIVEN_calibration_WHEN_field_set_THEN_current_as_expected(self, _, calibration_file, field, expected_current):
        with use_calibration_file(self.ca, calibration_file, "default_calib.dat"):
            self.ca.set_pv_value("FIELD:SP", field)
            self.ca.assert_that_pv_is("FIELD:SP:RBV", field)
            self.ca.assert_that_pv_is("CURRENT:SP", expected_current)
            self.ca.assert_that_pv_is("CURRENT:SP:RBV", expected_current)

    @parameterized.expand(parameterized_list([
        ("default_calib.dat", 100, 100),
        ("field_double_amps.dat", 100, 200),
    ]))
    @skip_if_recsim("Calibration lookup does not work in recsim")
    def test_GIVEN_calibration_WHEN_current_set_THEN_field_as_expected(self, _, calibration_file, current, expected_field):
        with use_calibration_file(self.ca, calibration_file, "default_calib.dat"):
            self._write_current(current)
            self.ca.assert_that_pv_is("CURRENT", current)
            self.ca.assert_that_pv_is("FIELD", expected_field)

    @skip_if_recsim("Lewis not available in recsim")
    def test_WHEN_sending_setpoint_THEN_only_one_setpoint_sent(self):
        self._lewis.backdoor_set_and_assert_set("current_set_count", 0)
        self.ca.set_pv_value("CURRENT:SP", 100)
        self._lewis.assert_that_emulator_value_is("current_set_count", 1)

        # Wait a short time and make sure count is not being incremented again later.
        time.sleep(5)
        self._lewis.assert_that_emulator_value_is("current_set_count", 1)
コード例 #2
0
class ZeroFieldTests(unittest.TestCase):
    """
    Tests for the muon zero field controller IOC.
    """
    def _set_simulated_measured_fields(self,
                                       fields,
                                       overload=False,
                                       wait_for_update=True):
        """
        Args:
            fields (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to the
              required fields
            overload (bool): whether to simulate the magnetometer being overloaded
            wait_for_update (bool): whether to wait for the statemachine to pick up the new readings
        """
        for axis in FIELD_AXES:
            self.magnetometer_ca.set_pv_value("SIM:DAQ:{}".format(axis),
                                              fields[axis],
                                              sleep_after_set=0)

        # Just overwrite the calculation to return a constant as we are not interested in testing the
        # overload logic in the magnetometer in these tests (that logic is tested separately).
        self.magnetometer_ca.set_pv_value("OVERLOAD:_CALC.CALC",
                                          "1" if overload else "0",
                                          sleep_after_set=0)

        if wait_for_update:
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_is("FIELD:{}".format(axis),
                                                  fields[axis])
                self.zfcntrl_ca.assert_that_pv_is("FIELD:{}:MEAS".format(axis),
                                                  fields[axis])

    def _set_user_setpoints(self, fields):
        """
        Args:
            fields (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to the
              required fields 
        """
        for axis in FIELD_AXES:
            self.zfcntrl_ca.set_pv_value("FIELD:{}:SP".format(axis),
                                         fields[axis],
                                         sleep_after_set=0)

    def _set_simulated_power_supply_currents(self,
                                             currents,
                                             wait_for_update=True):
        """
        Args:
            currents (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to
              the required currents
            wait_for_update (bool): whether to wait for the readback and setpoint readbacks to update
        """
        for axis in FIELD_AXES:
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP".format(axis),
                                         currents[axis],
                                         sleep_after_set=0)

        if wait_for_update:
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_is(
                    "OUTPUT:{}:CURR".format(axis), currents[axis])
                self.zfcntrl_ca.assert_that_pv_is(
                    "OUTPUT:{}:CURR:SP:RBV".format(axis), currents[axis])

    def _set_simulated_power_supply_voltages(self,
                                             voltages,
                                             wait_for_update=True):
        """
        Args:
            voltages (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to
              the required voltages
            wait_for_update (bool): whether to wait for the readback and setpoint readbacks to update
        """
        for axis in FIELD_AXES:
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:VOLT:SP".format(axis),
                                         voltages[axis],
                                         sleep_after_set=0)

        if wait_for_update:
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_is(
                    "OUTPUT:{}:VOLT".format(axis), voltages[axis])
                self.zfcntrl_ca.assert_that_pv_is(
                    "OUTPUT:{}:VOLT:SP:RBV".format(axis), voltages[axis])

    def _assert_at_setpoint(self, status):
        """
        Args:
            status (string): value of AT_SETPOINT PV (either Yes, No or N/A)
        """
        self.zfcntrl_ca.assert_that_pv_is("AT_SETPOINT", status)

    def _assert_status(self, status):
        """
        Args:
            status (Tuple[str, str]): the controller status and error to assert.
        """
        name, expected_alarm = status

        # Special case - this alarm should be suppressed in manual mode. This is because, in manual mode, the
        # scientists will intentionally apply large fields (which overload the magnetometer), but they do not want
        # alarms for this case as it is a "normal" mode of operation.
        if name == Statuses.MAGNETOMETER_OVERLOAD[
                0] and self.zfcntrl_ca.get_pv_value(
                    "AUTOFEEDBACK") == "Manual":
            expected_alarm = self.zfcntrl_ca.Alarms.NONE

        self.zfcntrl_ca.assert_that_pv_is("STATUS", name)
        self.zfcntrl_ca.assert_that_pv_alarm_is("STATUS", expected_alarm)

    def _set_autofeedback(self, autofeedback):
        self.zfcntrl_ca.set_pv_value(
            "AUTOFEEDBACK", "Auto-feedback" if autofeedback else "Manual")

    def _set_scaling_factors(self, px, py, pz, fiddle):
        """
        Args:
            px (float): Amps per mG for the X axis.
            py (float): Amps per mG for the Y axis.
            pz (float): Amps per mG for the Z axis.
            fiddle (float): The feedback (sometimes called "fiddle") factor.
        """
        self.zfcntrl_ca.set_pv_value("P:X", px, sleep_after_set=0)
        self.zfcntrl_ca.set_pv_value("P:Y", py, sleep_after_set=0)
        self.zfcntrl_ca.set_pv_value("P:Z", pz, sleep_after_set=0)
        self.zfcntrl_ca.set_pv_value("P:FEEDBACK", fiddle, sleep_after_set=0)

    def _set_output_limits(self, lower_limits, upper_limits):
        """
        Args:
            lower_limits (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to
              the required output lower limits
            upper_limits (dict[AnyStr, float]): A dictionary with the same keys as FIELD_AXES and values corresponding to
              the required output upper limits
        """
        for axis in FIELD_AXES:
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.DRVL".format(axis),
                                         lower_limits[axis],
                                         sleep_after_set=0)
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.LOLO".format(axis),
                                         lower_limits[axis],
                                         sleep_after_set=0)
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.DRVH".format(axis),
                                         upper_limits[axis],
                                         sleep_after_set=0)
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR:SP.HIHI".format(axis),
                                         upper_limits[axis],
                                         sleep_after_set=0)

            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR.LOLO".format(axis),
                                         lower_limits[axis],
                                         sleep_after_set=0)
            self.zfcntrl_ca.set_pv_value("OUTPUT:{}:CURR.HIHI".format(axis),
                                         upper_limits[axis],
                                         sleep_after_set=0)

            self.zfcntrl_ca.set_pv_value(
                "OUTPUT:{}:CURR:SP:RBV.LOLO".format(axis),
                lower_limits[axis],
                sleep_after_set=0)
            self.zfcntrl_ca.set_pv_value(
                "OUTPUT:{}:CURR:SP:RBV.HIHI".format(axis),
                upper_limits[axis],
                sleep_after_set=0)

    @contextlib.contextmanager
    def _simulate_disconnected_magnetometer(self):
        """
        While this context manager is active, the magnetometer IOC will fail to take any new readings or process any PVs
        """
        self.magnetometer_ca.set_pv_value("DISABLE", 1, sleep_after_set=0)
        try:
            yield
        finally:
            self.magnetometer_ca.set_pv_value("DISABLE", 0, sleep_after_set=0)

    @contextlib.contextmanager
    def _simulate_invalid_magnetometer_readings(self):
        """
        While this context manager is active, any new readings from the magnetometer will be marked as INVALID
        """
        for axis in FIELD_AXES:
            self.magnetometer_ca.set_pv_value(
                "DAQ:{}:_RAW.SIMS".format(axis),
                self.magnetometer_ca.Alarms.INVALID,
                sleep_after_set=0)

        # Wait for RAW PVs to process
        for axis in FIELD_AXES:
            self.magnetometer_ca.assert_that_pv_alarm_is(
                "DAQ:{}:_RAW.SEVR".format(axis),
                self.magnetometer_ca.Alarms.INVALID)
        try:
            yield
        finally:
            for axis in FIELD_AXES:
                self.magnetometer_ca.set_pv_value(
                    "DAQ:{}:_RAW.SIMS".format(axis),
                    self.magnetometer_ca.Alarms.NONE,
                    sleep_after_set=0)
            # Wait for RAW PVs to process
            for axis in FIELD_AXES:
                self.magnetometer_ca.assert_that_pv_alarm_is(
                    "DAQ:{}:_RAW.SEVR".format(axis),
                    self.magnetometer_ca.Alarms.NONE)

    @contextlib.contextmanager
    def _simulate_invalid_power_supply(self):
        """
        While this context manager is active, the readback values from all power supplies will be marked as INVALID
        (this simulates the device not being plugged in, for example)
        """
        pvs_to_make_invalid = ("CURRENT", "_CURRENT:SP:RBV", "OUTPUTMODE",
                               "OUTPUTSTATUS", "VOLTAGE", "VOLTAGE:SP:RBV")

        for ca, pv in itertools.product(
            (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca),
                pvs_to_make_invalid):
            # 3 is the Enum value for an invalid alarm
            ca.set_pv_value("{}.SIMS".format(pv), 3, sleep_after_set=0)

        # Use a separate loop to avoid needing to wait for a 1-second scan 6 times.
        for ca, pv in itertools.product(
            (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca),
                pvs_to_make_invalid):
            ca.assert_that_pv_alarm_is(pv, ca.Alarms.INVALID)

        try:
            yield
        finally:
            for ca, pv in itertools.product(
                (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca),
                    pvs_to_make_invalid):
                ca.set_pv_value("{}.SIMS".format(pv), 0, sleep_after_set=0)

            # Use a separate loop to avoid needing to wait for a 1-second scan 6 times.
            for ca, pv in itertools.product(
                (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca),
                    pvs_to_make_invalid):
                ca.assert_that_pv_alarm_is(pv, ca.Alarms.NONE)

    @contextlib.contextmanager
    def _simulate_failing_power_supply_writes(self):
        """
        While this context manager is active, any writes to the power supply PVs will be ignored. This simulates the
        device being in local mode, for example. Note that this does not mark readbacks as invalid (for that, use
        _simulate_invalid_power_supply instead).
        """
        pvs = [
            "CURRENT:SP.DISP", "VOLTAGE:SP.DISP", "OUTPUTMODE:SP.DISP",
            "OUTPUTSTATUS:SP.DISP"
        ]

        for ca, pv in itertools.product(
            (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca), pvs):
            ca.set_pv_value(pv, 1, sleep_after_set=0)
        try:
            yield
        finally:
            for ca, pv in itertools.product(
                (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca), pvs):
                ca.set_pv_value(pv, 0, sleep_after_set=0)

    @contextlib.contextmanager
    def _simulate_measured_fields_changing_with_outputs(
            self, psu_amps_at_measured_zero):
        """
        Calculates and sets somewhat realistic simulated measured fields based on the current values of power supplies.

        Args:
            psu_amps_at_measured_zero: Dictionary containing the Amps of the power supplies when the measured field
              corresponds to zero. i.e. if the system is told to go to zero field, these are the power supply readings
              it will require to get there.
        """
        # Always start at zero current
        self._set_simulated_power_supply_currents({"X": 0, "Y": 0, "Z": 0})

        thread = multiprocessing.Process(target=_update_fields_continuously,
                                         args=(psu_amps_at_measured_zero, ))
        thread.start()
        try:
            yield
        finally:
            thread.terminate()

    def _wait_for_all_iocs_up(self):
        """
        Waits for the "primary" pv(s) from each ioc to be available
        """
        for ca in (self.x_psu_ca, self.y_psu_ca, self.z_psu_ca):
            ca.assert_that_pv_exists("CURRENT")
            ca.assert_that_pv_exists("CURRENT:SP")
            ca.assert_that_pv_exists("CURRENT:SP:RBV")

        for axis in FIELD_AXES:
            self.zfcntrl_ca.assert_that_pv_exists("FIELD:{}".format(axis))
            self.magnetometer_ca.assert_that_pv_exists(
                "CORRECTEDFIELD:{}".format(axis))

    def setUp(self):
        _, self._ioc = get_running_lewis_and_ioc(None, ZF_DEVICE_PREFIX)

        timeout = 20
        self.zfcntrl_ca = ChannelAccess(device_prefix=ZF_DEVICE_PREFIX,
                                        default_timeout=timeout)
        self.magnetometer_ca = ChannelAccess(
            device_prefix=MAGNETOMETER_DEVICE_PREFIX, default_timeout=timeout)
        self.x_psu_ca = ChannelAccess(default_timeout=timeout,
                                      device_prefix=X_KEPCO_DEVICE_PREFIX)
        self.y_psu_ca = ChannelAccess(default_timeout=timeout,
                                      device_prefix=Y_KEPCO_DEVICE_PREFIX)
        self.z_psu_ca = ChannelAccess(default_timeout=timeout,
                                      device_prefix=Z_KEPCO_DEVICE_PREFIX)

        self._wait_for_all_iocs_up()

        self.zfcntrl_ca.set_pv_value("TOLERANCE",
                                     STABILITY_TOLERANCE,
                                     sleep_after_set=0)
        self.zfcntrl_ca.set_pv_value("STATEMACHINE:LOOP_DELAY",
                                     LOOP_DELAY_MS,
                                     sleep_after_set=0)
        self._set_autofeedback(False)

        # Set the magnetometer calibration to the 3x3 identity matrix
        for x, y in itertools.product(range(1, 3 + 1), range(1, 3 + 1)):
            self.magnetometer_ca.set_pv_value("SENSORMATRIX:{}{}".format(x, y),
                                              1 if x == y else 0,
                                              sleep_after_set=0)

        self._set_simulated_measured_fields(ZERO_FIELD, overload=False)
        self._set_user_setpoints(ZERO_FIELD)
        self._set_simulated_power_supply_currents(ZERO_FIELD,
                                                  wait_for_update=True)
        self._set_scaling_factors(1, 1, 1, 1)
        self._set_output_limits(
            lower_limits={
                "X": DEFAULT_LOW_OUTPUT_LIMIT,
                "Y": DEFAULT_LOW_OUTPUT_LIMIT,
                "Z": DEFAULT_LOW_OUTPUT_LIMIT
            },
            upper_limits={
                "X": DEFAULT_HIGH_OUTPUT_LIMIT,
                "Y": DEFAULT_HIGH_OUTPUT_LIMIT,
                "Z": DEFAULT_HIGH_OUTPUT_LIMIT
            },
        )

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.NO_ERROR)

    def test_WHEN_ioc_is_started_THEN_it_is_not_disabled(self):
        self.zfcntrl_ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    @parameterized.expand(parameterized_list(FIELD_AXES))
    def test_WHEN_manual_mode_and_any_readback_value_is_not_equal_to_setpoint_THEN_at_setpoint_field_is_na(
            self, _, axis_to_vary):
        fields = {"X": 10, "Y": 20, "Z": 30}
        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)

        # Set one of the parameters to a completely different value
        self.zfcntrl_ca.set_pv_value("FIELD:{}:SP".format(axis_to_vary),
                                     100,
                                     sleep_after_set=0)

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.NO_ERROR)

    def test_GIVEN_manual_mode_and_magnetometer_not_overloaded_WHEN_readback_values_are_equal_to_setpoints_THEN_at_setpoint_field_is_na(
            self):
        fields = {"X": 55, "Y": 66, "Z": 77}
        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.NO_ERROR)

    def test_GIVEN_manual_mode_and_within_tolerance_WHEN_magnetometer_is_overloaded_THEN_status_overloaded_and_setpoint_field_is_na(
            self):
        fields = {"X": 55, "Y": 66, "Z": 77}
        self._set_simulated_measured_fields(fields, overload=True)
        self._set_user_setpoints(fields)

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.MAGNETOMETER_OVERLOAD)

    def test_GIVEN_manual_mode_and_just_outside_tolerance_WHEN_magnetometer_is_overloaded_THEN_status_overloaded_and_setpoint_field_is_na(
            self):
        fields = {"X": 55, "Y": 66, "Z": 77}
        self._set_simulated_measured_fields(fields, overload=True)
        self._set_user_setpoints({
            k: v + 1.01 * STABILITY_TOLERANCE
            for k, v in six.iteritems(fields)
        })

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.MAGNETOMETER_OVERLOAD)

    def test_GIVEN_manual_mode_and_just_within_tolerance_WHEN_magnetometer_is_overloaded_THEN_status_overloaded_and_setpoint_field_is_na(
            self):
        fields = {"X": 55, "Y": 66, "Z": 77}
        self._set_simulated_measured_fields(fields, overload=True)
        self._set_user_setpoints({
            k: v + 0.99 * STABILITY_TOLERANCE
            for k, v in six.iteritems(fields)
        })

        self._assert_at_setpoint(AtSetpointStatuses.NA)
        self._assert_status(Statuses.MAGNETOMETER_OVERLOAD)

    def test_WHEN_magnetometer_ioc_does_not_respond_THEN_status_is_magnetometer_read_error(
            self):
        fields = {"X": 1, "Y": 2, "Z": 3}
        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)

        with self._simulate_disconnected_magnetometer():
            self._assert_status(Statuses.MAGNETOMETER_READ_ERROR)
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_alarm_is(
                    "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.INVALID)
                self.zfcntrl_ca.assert_that_pv_alarm_is(
                    "FIELD:{}:MEAS".format(axis),
                    self.zfcntrl_ca.Alarms.INVALID)

        # Now simulate recovery and assert error gets cleared correctly
        self._assert_status(Statuses.NO_ERROR)
        for axis in FIELD_AXES:
            self.zfcntrl_ca.assert_that_pv_alarm_is(
                "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.NONE)
            self.zfcntrl_ca.assert_that_pv_alarm_is(
                "FIELD:{}:MEAS".format(axis), self.zfcntrl_ca.Alarms.NONE)

    def test_WHEN_magnetometer_ioc_readings_are_invalid_THEN_status_is_magnetometer_invalid(
            self):
        fields = {"X": 1, "Y": 2, "Z": 3}
        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)

        with self._simulate_invalid_magnetometer_readings():
            self._assert_status(Statuses.MAGNETOMETER_DATA_INVALID)
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_alarm_is(
                    "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.INVALID)
                self.zfcntrl_ca.assert_that_pv_alarm_is(
                    "FIELD:{}:MEAS".format(axis),
                    self.zfcntrl_ca.Alarms.INVALID)

        # Now simulate recovery and assert error gets cleared correctly
        self._assert_status(Statuses.NO_ERROR)
        for axis in FIELD_AXES:
            self.zfcntrl_ca.assert_that_pv_alarm_is(
                "FIELD:{}".format(axis), self.zfcntrl_ca.Alarms.NONE)
            self.zfcntrl_ca.assert_that_pv_alarm_is(
                "FIELD:{}:MEAS".format(axis), self.zfcntrl_ca.Alarms.NONE)

    def test_WHEN_power_supplies_are_invalid_THEN_status_is_power_supplies_invalid(
            self):
        fields = {"X": 1, "Y": 2, "Z": 3}
        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)
        self._set_autofeedback(True)

        with self._simulate_invalid_power_supply():
            self._assert_at_setpoint(
                AtSetpointStatuses.TRUE
            )  # Invalid power supplies do not mark the field as "not at setpoint"
            self._assert_status(Statuses.PSU_INVALID)

        # Now simulate recovery and assert error gets cleared correctly
        self._assert_at_setpoint(AtSetpointStatuses.TRUE)
        self._assert_status(Statuses.NO_ERROR)

    def test_WHEN_power_supplies_writes_fail_THEN_status_is_power_supply_writes_failed(
            self):
        fields = {"X": 1, "Y": 2, "Z": 3}
        self._set_simulated_measured_fields(fields, overload=False)

        # For this test we need changing fields so that we can detect that the writes failed
        self._set_user_setpoints({
            k: v + 10 * STABILITY_TOLERANCE
            for k, v in six.iteritems(fields)
        })
        # ... and we also need large limits so that we see that the writes failed as opposed to a limits error
        self._set_output_limits(lower_limits={k: -999999
                                              for k in FIELD_AXES},
                                upper_limits={k: 999999
                                              for k in FIELD_AXES})
        self._set_autofeedback(True)

        with self._simulate_failing_power_supply_writes():
            self._assert_status(Statuses.PSU_WRITE_FAILED)

        # Now simulate recovery and assert error gets cleared correctly
        self._assert_status(Statuses.NO_ERROR)

    def test_GIVEN_measured_field_and_setpoints_are_identical_THEN_setpoints_remain_unchanged(
            self):
        fields = {"X": 5, "Y": 10, "Z": -5}
        outputs = {"X": -1, "Y": -2, "Z": -3}

        self._set_simulated_measured_fields(fields, overload=False)
        self._set_user_setpoints(fields)
        self._set_simulated_power_supply_currents(outputs,
                                                  wait_for_update=True)

        self._set_autofeedback(True)

        for axis in FIELD_AXES:
            self.zfcntrl_ca.assert_that_pv_is_number(
                "OUTPUT:{}:CURR".format(axis), outputs[axis], tolerance=0.0001)
            self.zfcntrl_ca.assert_that_pv_value_is_unchanged(
                "OUTPUT:{}:CURR".format(axis), wait=5)

    @parameterized.expand(
        parameterized_list([
            # If measured field is smaller than the setpoint, we want to adjust the output upwards to compensate
            (operator.sub, operator.gt, 1),
            # If measured field is larger than the setpoint, we want to adjust the output downwards to compensate
            (operator.add, operator.lt, 1),
            # If measured field is smaller than the setpoint, and A/mg is negative, we want to adjust the output downwards
            # to compensate
            (operator.sub, operator.lt, -1),
            # If measured field is larger than the setpoint, and A/mg is negative, we want to adjust the output upwards
            # to compensate
            (operator.add, operator.gt, -1),
            # If measured field is smaller than the setpoint, and A/mg is zero, then power supply output should remain
            # unchanged
            (operator.sub, operator.eq, 0),
            # If measured field is larger than the setpoint, and A/mg is zero, then power supply output should remain
            # unchanged
            (operator.add, operator.eq, 0),
        ]))
    def test_GIVEN_autofeedback_WHEN_measured_field_different_from_setpoints_THEN_power_supply_outputs_move_in_correct_direction(
            self, _, measured_field_modifier, output_comparator,
            scaling_factor):

        fields = {"X": 5, "Y": 0, "Z": -5}

        adjustment_amount = 10 * STABILITY_TOLERANCE  # To ensure that it is not considered stable to start with
        measured_fields = {
            k: measured_field_modifier(v, adjustment_amount)
            for k, v in six.iteritems(fields)
        }

        self._set_scaling_factors(scaling_factor,
                                  scaling_factor,
                                  scaling_factor,
                                  fiddle=1)
        self._set_simulated_measured_fields(measured_fields, overload=False)
        self._set_user_setpoints(fields)
        self._set_simulated_power_supply_currents({
            "X": 0,
            "Y": 0,
            "Z": 0
        },
                                                  wait_for_update=True)
        self._set_output_limits(lower_limits={k: -999999
                                              for k in FIELD_AXES},
                                upper_limits={k: 999999
                                              for k in FIELD_AXES})

        self._assert_status(Statuses.NO_ERROR)

        self._set_autofeedback(True)
        self._assert_at_setpoint(AtSetpointStatuses.FALSE)

        for axis in FIELD_AXES:
            self.zfcntrl_ca.assert_that_pv_value_over_time_satisfies_comparator(
                "OUTPUT:{}:CURR".format(axis),
                wait=5,
                comparator=output_comparator)

        # In this happy-path case, we shouldn't be hitting any long timeouts, so loop times should remain fairly quick
        self.zfcntrl_ca.assert_that_pv_is_within_range(
            "STATEMACHINE:LOOP_TIME", min_value=0, max_value=2 * LOOP_DELAY_MS)

    def test_GIVEN_output_limits_too_small_for_required_field_THEN_status_error_and_alarm(
            self):
        self._set_output_limits(
            lower_limits={
                "X": -0.1,
                "Y": -0.1,
                "Z": -0.1
            },
            upper_limits={
                "X": 0.1,
                "Y": 0.1,
                "Z": 0.1
            },
        )

        # The measured field is smaller than the setpoint, i.e. the output needs to go up to the limits
        self._set_simulated_measured_fields({"X": -1, "Y": -1, "Z": -1})
        self._set_user_setpoints(ZERO_FIELD)
        self._set_simulated_power_supply_currents(ZERO_FIELD)

        self._set_autofeedback(True)

        self._assert_status(Statuses.PSU_ON_LIMITS)
        for axis in FIELD_AXES:
            # Value should be on one of the limits
            self.zfcntrl_ca.assert_that_pv_is_one_of(
                "OUTPUT:{}:CURR:SP".format(axis), [-0.1, 0.1])
            # ...and in alarm
            self.zfcntrl_ca.assert_that_pv_alarm_is(
                "OUTPUT:{}:CURR:SP".format(axis), self.zfcntrl_ca.Alarms.MAJOR)

    def test_GIVEN_limits_wrong_way_around_THEN_appropriate_error_raised(self):
        # Set upper limits < lower limits
        self._set_output_limits(
            lower_limits={
                "X": 0.1,
                "Y": 0.1,
                "Z": 0.1
            },
            upper_limits={
                "X": -0.1,
                "Y": -0.1,
                "Z": -0.1
            },
        )
        self._set_autofeedback(True)
        self._assert_status(Statuses.INVALID_PSU_LIMITS)

    @parameterized.expand(
        parameterized_list([
            {
                "X": 45.678,
                "Y": 0.123,
                "Z": 12.345
            },
            {
                "X": 0,
                "Y": 0,
                "Z": 0
            },
            {
                "X": -45.678,
                "Y": -0.123,
                "Z": -12.345
            },
        ]))
    def test_GIVEN_measured_values_updating_realistically_WHEN_in_auto_mode_THEN_converges_to_correct_answer(
            self, _, psu_amps_at_zero_field):
        self._set_output_limits(lower_limits={k: -100
                                              for k in FIELD_AXES},
                                upper_limits={k: 100
                                              for k in FIELD_AXES})
        self._set_user_setpoints({"X": 0, "Y": 0, "Z": 0})
        self._set_simulated_power_supply_currents({"X": 0, "Y": 0, "Z": 0})

        # Set fiddle small to get a relatively slow response, which should theoretically be stable
        self._set_scaling_factors(0.001, 0.001, 0.001, fiddle=0.05)

        with self._simulate_measured_fields_changing_with_outputs(
                psu_amps_at_measured_zero=psu_amps_at_zero_field):
            self._set_autofeedback(True)
            for axis in FIELD_AXES:
                self.zfcntrl_ca.assert_that_pv_is_number(
                    "OUTPUT:{}:CURR:SP:RBV".format(axis),
                    psu_amps_at_zero_field[axis],
                    tolerance=STABILITY_TOLERANCE * 0.001,
                    timeout=60)
                self.zfcntrl_ca.assert_that_pv_is_number(
                    "FIELD:{}".format(axis),
                    0.0,
                    tolerance=STABILITY_TOLERANCE)

            self._assert_at_setpoint(AtSetpointStatuses.TRUE)
            self.zfcntrl_ca.assert_that_pv_value_is_unchanged("AT_SETPOINT",
                                                              wait=20)
            self._assert_status(Statuses.NO_ERROR)

    @parameterized.expand(parameterized_list(FIELD_AXES))
    def test_GIVEN_output_is_off_WHEN_autofeedback_switched_on_THEN_psu_is_switched_back_on(
            self, _, axis):
        self.zfcntrl_ca.assert_setting_setpoint_sets_readback(
            "Off", "OUTPUT:{}:STATUS".format(axis))
        self._set_autofeedback(True)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:STATUS".format(axis),
                                          "On")

    @parameterized.expand(parameterized_list(FIELD_AXES))
    def test_GIVEN_output_mode_is_voltage_WHEN_autofeedback_switched_on_THEN_psu_is_switched_to_current_mode(
            self, _, axis):
        self.zfcntrl_ca.assert_setting_setpoint_sets_readback(
            "Voltage",
            "OUTPUT:{}:MODE".format(axis),
            expected_alarm=self.zfcntrl_ca.Alarms.MAJOR)
        self._set_autofeedback(True)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:MODE".format(axis),
                                          "Current")

    @parameterized.expand(parameterized_list(FIELD_AXES))
    def test_GIVEN_output_is_off_and_cannot_write_to_psu_WHEN_autofeedback_switched_on_THEN_get_psu_write_error(
            self, _, axis):
        self.zfcntrl_ca.assert_setting_setpoint_sets_readback(
            "Off", "OUTPUT:{}:STATUS".format(axis))
        with self._simulate_failing_power_supply_writes():
            self._set_autofeedback(True)
            self._assert_status(Statuses.PSU_WRITE_FAILED)

        # Check it can recover when writes work again
        self._assert_status(Statuses.NO_ERROR)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:STATUS".format(axis),
                                          "On")

    @parameterized.expand(parameterized_list(FIELD_AXES))
    def test_GIVEN_output_mode_is_voltage_and_cannot_write_to_psu_WHEN_autofeedback_switched_on_THEN_get_psu_write_error(
            self, _, axis):
        self.zfcntrl_ca.assert_setting_setpoint_sets_readback(
            "Voltage",
            "OUTPUT:{}:MODE".format(axis),
            expected_alarm=self.zfcntrl_ca.Alarms.MAJOR)

        with self._simulate_failing_power_supply_writes():
            self._set_autofeedback(True)
            self._assert_status(Statuses.PSU_WRITE_FAILED)

        # Check it can recover when writes work again
        self._assert_status(Statuses.NO_ERROR)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:{}:MODE".format(axis),
                                          "Current")

    @parameterized.expand(
        parameterized_list([
            (True, True),
            (False, True),
            (True, False),
            (False, False),
        ]))
    def test_GIVEN_magnetometer_overloaded_THEN_error_suppressed_if_in_manual_mode(
            self, _, autofeedback, overloaded):
        self._set_autofeedback(autofeedback)
        self._set_simulated_measured_fields(ZERO_FIELD,
                                            overload=overloaded,
                                            wait_for_update=True)
        self._assert_status(Statuses.MAGNETOMETER_OVERLOAD
                            if overloaded else Statuses.NO_ERROR)
        self.zfcntrl_ca.assert_that_pv_alarm_is(
            "STATUS", self.zfcntrl_ca.Alarms.MAJOR
            if overloaded and autofeedback else self.zfcntrl_ca.Alarms.NONE)

    def test_GIVEN_power_supply_voltage_limit_is_set_incorrectly_WHEN_going_into_auto_mode_THEN_correct_limits_applied(
            self):
        self._set_simulated_power_supply_voltages({"X": 0, "Y": 0, "Z": 0})

        self._set_autofeedback(True)

        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:X:VOLT:SP:RBV",
                                          X_KEPCO_VOLTAGE_LIMIT)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:Y:VOLT:SP:RBV",
                                          Y_KEPCO_VOLTAGE_LIMIT)
        self.zfcntrl_ca.assert_that_pv_is("OUTPUT:Z:VOLT:SP:RBV",
                                          Z_KEPCO_VOLTAGE_LIMIT)
コード例 #3
0
class Jsco4180Tests(unittest.TestCase):
    """
    Tests for the Jsco4180 IOC.
    """

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(DEVICE_NAME, DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=30)
        for pv in required_pvs:
            self.ca.assert_that_pv_exists(pv, timeout=30)
        self._lewis.backdoor_run_function_on_device("reset")

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_wrong_component_on_device_WHEN_running_THEN_retry_run_and_updates_component(self):
        expected_value_A = 30
        expected_value_B = 15
        expected_value_C = 55

        self.ca.set_pv_value("COMP:A:SP", expected_value_A)
        self.ca.set_pv_value("COMP:B:SP", expected_value_B)
        self.ca.set_pv_value("COMP:C:SP", expected_value_C)

        self.ca.set_pv_value("START:SP", 1)

        sleep(10)
        # Setting an incorrect component on the device will result in the state machine attempting
        # to rerun the pump and reset components.
        self._lewis.backdoor_set_on_device("component_A", 25)
        self._lewis.backdoor_set_on_device("component_B", 10)
        self._lewis.backdoor_set_on_device("component_C", 14)

        self.ca.assert_that_pv_is("COMP:A", expected_value_A, timeout=30)
        self.ca.assert_that_pv_is("COMP:B", expected_value_B, timeout=30)
        self.ca.assert_that_pv_is("COMP:C", expected_value_C, timeout=30)

    # there was a previous problem where if setpoint and readback differed a sleep and resend was started,
    # but the old state machine did not look to see if a new sp was issued while it was asleep and so then
    # resent the old out of date SP
    @unstable_test(max_retries=2, wait_between_runs=60)
    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_wrong_component_on_device_WHEN_send_new_sp_THEN_state_machine_aborts_resend(self):
        value = 50
        self.ca.set_pv_value("COMP:A:SP", value)
        self.ca.set_pv_value("COMP:B:SP", value)
        self.ca.set_pv_value("START:SP", 1)
        self.ca.assert_that_pv_is("STATUS", "Pumping", timeout=5)
        self.ca.assert_that_pv_is("COMP:A", value, timeout=30)
        self.ca.assert_that_pv_is("COMP:B", value, timeout=30)

        # Setting an incorrect component on the device will result in the state machine attempting
        # to rerun the pump and reset components after a delay
        initial_delay = self.ca.get_pv_value("ERROR:DELAY")  # delay before state machine reset
        delay = 30  # Increase delay to avoid race conditions
        self.ca.set_pv_value("ERROR:DELAY", delay)
        try:
            with self.ca.assert_pv_not_processed("RESET:SP"):
                self._lewis.backdoor_set_on_device("component_A", value - 5)
                self.ca.assert_that_pv_is("COMP:A", value - 5, timeout=5)
                sleep(delay / 2.0)

                # however if we change setpoint, the loop should start again
                self._lewis.backdoor_set_on_device("component_A", value - 5)
                self.ca.set_pv_value("COMP:A:SP", value - 10)
                self.ca.set_pv_value("COMP:B:SP", value + 10)
                # reset should not have happened yet
                self.ca.assert_that_pv_is("COMP:A", value - 5, timeout=delay / 2.0)
                self.ca.assert_that_pv_value_is_unchanged("COMP:A", wait=delay / 2.0)

            # Reset should now happen within a further timeout/2 seconds (but give it longer to avoid races)
            with self.ca.assert_pv_processed("RESET:SP"):
                self.ca.assert_that_pv_is("COMP:A", value - 10, timeout=delay * 2)
        finally:
            # Put error delay back to it's initial value
            self.ca.set_pv_value("ERROR:DELAY", initial_delay)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_wrong_component_on_device_WHEN_running_continuous_THEN_retry_run_and_updates_component_in_correct_mode(
            self):
        value = 50
        expected_value = "Pumping"
        self.ca.set_pv_value("COMP:A:SP", value)
        self.ca.set_pv_value("COMP:B:SP", value)

        self.ca.set_pv_value("START:SP", 1)

        # Give the device some time running in a good state
        sleep(10)
        # Sabotage! - Setting an incorrect component on the device will result in the state machine attempting
        # to rerun the pump and reset components.
        self._lewis.backdoor_set_on_device("component_A", 33)

        self.ca.assert_that_pv_is("STATUS", expected_value, timeout=30)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_wrong_component_on_device_WHEN_running_timed_THEN_retry_run_and_updates_component_in_correct_mode(
            self):
        value = 50
        expected_value = "Pumping"
        self.ca.set_pv_value("COMP:A:SP", value)
        self.ca.set_pv_value("COMP:B:SP", value)
        self.ca.set_pv_value("TIME:RUN:SP", 100)
        self.ca.set_pv_value("PUMP_FOR_TIME:SP", 1)

        # Give the device some time running in a good state
        sleep(10)
        # Sabotage! - Setting an incorrect component on the device will result in the state machine attempting
        # to rerun the pump and reset components.
        self._lewis.backdoor_set_on_device("component_A", 33)

        self.ca.assert_that_pv_is("STATUS", expected_value, timeout=30)

    @skip_if_recsim("Flowrate device logic not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_set_flowrate_THEN_flowrate_setpoint_is_correct(self):

        error_delay = float(self.ca.get_pv_value("ERROR:DELAY"))
        sleep(2 * error_delay)  # To make sure we're not in the middle of the error-checking state machine

        expected_value = 1.000
        self.ca.set_pv_value("FLOWRATE:SP", expected_value)

        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_value)

        self.ca.set_pv_value("TIME:RUN:SP", 100)
        self.ca.set_pv_value("START:SP", "Start")

        self.ca.assert_that_pv_is("FLOWRATE", expected_value)

    @skip_if_recsim("LeWIS backdoor not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_set_flowrate_and_pump_volume_THEN_ioc_uses_rbv_for_calculation_of_remaining_time(self):
        expected_sp_value = 1.000
        expected_rbv_value = 2.000
        pump_for_volume = 2
        expected_time_value = (pump_for_volume / expected_rbv_value) * 60

        error_delay = float(self.ca.get_pv_value("ERROR:DELAY"))
        sleep(2 * error_delay)  # To make sure we're not in the middle of the error-checking state machine

        # 1. set invalid flowrate setpoint (FLOWRATE:SP)
        self.ca.set_pv_value("FLOWRATE:SP", expected_sp_value)
        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_sp_value)

        # 2. set valid hardware flowrate (FLOWRATE:SP:RBV) via backdoor command
        self._lewis.backdoor_set_on_device("flowrate_rbv", expected_rbv_value)
        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_rbv_value)

        # 3. set volume setpoint and start pump
        self.ca.set_pv_value("TIME:VOL:SP", pump_for_volume)
        self.ca.set_pv_value("START:SP", "Start")

        # 4. check calculated time is based on flowrate setpoint readback (:SP:RBV rather than :SP)
        self.ca.assert_that_pv_is("TIME:VOL:CALCRUN", expected_time_value)

    @skip_if_recsim("LeWIS backdoor not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_set_flowrate_and_pump_time_THEN_ioc_uses_rbv_for_calculation_of_remaining_volume(self):
        expected_sp_value = 1.000
        expected_rbv_value = 2.000
        pump_for_time = 120
        expected_volume_value = (pump_for_time * expected_rbv_value) / 60

        error_delay = float(self.ca.get_pv_value("ERROR:DELAY"))
        sleep(2 * error_delay)  # To make sure we're not in the middle of the error-checking state machine

        # 1. set invalid flowrate setpoint (FLOWRATE:SP)
        self.ca.set_pv_value("FLOWRATE:SP", expected_sp_value)
        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_sp_value)

        # 2. set valid hardware flowrate (FLOWRATE:SP:RBV) via backdoor command
        self._lewis.backdoor_set_on_device("flowrate_rbv", expected_rbv_value)
        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_rbv_value)

        # 3. set time setpoint and start pump
        self.ca.set_pv_value("TIME:RUN:SP", pump_for_time)
        self.ca.set_pv_value("START:SP", "Start")

        # 4. check calculated volume is based on flowrate setpoint readback (:SP:RBV rather than :SP)
        self.ca.assert_that_pv_is("TIME:RUN:CALCVOL", expected_volume_value)

    # test to check that the IOC updates the flowrate RBV quickly enough
    # for the remaining volume calculation to be valid.  simulates operation of a script.
    def test_GIVEN_an_ioc_WHEN_set_flowrate_and_immediately_set_pump_to_start_THEN_ioc_updates_rbv_for_calculation_of_remaining_volume(
            self):
        expected_sp_value = 2.000
        script_sp_value = 3.000
        pump_for_time = 120

        # 1. initialize flowrate
        self.ca.set_pv_value("FLOWRATE:SP", expected_sp_value)
        self.ca.assert_that_pv_is("FLOWRATE:SP:RBV", expected_sp_value, timeout=5)

        # 2. set new flowrate and immediately set pump to run, to simulate script
        self.ca.set_pv_value("FLOWRATE:SP", script_sp_value)
        self.ca.set_pv_value("TIME:RUN:SP", pump_for_time)
        self.ca.set_pv_value("START:SP", "Start")

        # 3. calculate remaining volume
        expected_volume_value = (pump_for_time * self.ca.get_pv_value("FLOWRATE:SP:RBV")) / 60

        # 4. check ioc calculation is as expected
        self.ca.assert_that_pv_is("TIME:RUN:CALCVOL", expected_volume_value)

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_set_maximum_pressure_limit_THEN_maximum_pressure_limit_is_correct(self):
        expected_value = 200
        self.ca.assert_setting_setpoint_sets_readback(expected_value, "PRESSURE:MAX")

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_set_minimum_pressure_limit_THEN_minimum_pressure_limit_is_correct(self):
        expected_value = 100
        self.ca.set_pv_value("PRESSURE:MIN:SP", expected_value)
        self.ca.assert_setting_setpoint_sets_readback(expected_value, "PRESSURE:MIN")

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_continuous_pump_set_THEN_pump_on(self):
        self.ca.set_pv_value("START:SP", 1)

        self.ca.assert_that_pv_is("STATUS", "Pumping")

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_an_ioc_WHEN_timed_pump_set_THEN_timed_pump_on(self):
        # Set a run time for a timed run
        self.ca.set_pv_value("TIME:RUN:SP", 10000)
        self.ca.set_pv_value("PUMP_FOR_TIME:SP", 1)

        self.ca.assert_that_pv_is("STATUS", "Pumping")

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_an_ioc_WHEN_get_current_pressure_THEN_current_pressure_returned(self):
        expected_value = 300
        self._lewis.backdoor_set_on_device("pressure", expected_value)

        self.ca.assert_that_pv_is("PRESSURE", expected_value)

    @parameterized.expand([
        ("component_{}".format(suffix), suffix) for suffix in ["A", "B", "C", "D"]
    ])
    @skip_if_recsim("Reliant on setUP lewis backdoor call")
    def test_GIVEN_an_ioc_WHEN_get_component_THEN_correct_component_returned(self, component, suffix):
        expected_value = 10.0
        self._lewis.backdoor_set_on_device(component, expected_value)

        self.ca.assert_that_pv_is("COMP:{}".format(suffix), expected_value)

    @parameterized.expand([
        ("COMP:{}".format(suffix), suffix) for suffix in ["A", "B", "C"]
    ])
    @skip_if_recsim("Reliant on setUP lewis backdoor call")
    def test_GIVEN_an_ioc_WHEN_set_component_THEN_correct_component_set(self, component, suffix):
        expected_value = 100.0
        self.ca.set_pv_value("COMP:{}:SP".format(suffix), expected_value)
        if component == "COMP:A":
            self.ca.set_pv_value("COMP:B:SP", 0)
            self.ca.set_pv_value("COMP:C:SP", 0)
        elif component == "COMP:B":
            self.ca.set_pv_value("COMP:A:SP", 0)
            self.ca.set_pv_value("COMP:C:SP", 0)
        elif component == "COMP:C":
            self.ca.set_pv_value("COMP:A:SP", 0)
            self.ca.set_pv_value("COMP:B:SP", 0)
        self.ca.set_pv_value("PUMP_FOR_TIME:SP", "Start")

        self.ca.assert_that_pv_is(component, expected_value)

    def test_GIVEN_ioc_initial_state_WHEN_get_error_THEN_error_returned(self):
        expected_value = "No error"

        self.ca.assert_that_pv_is("ERROR", expected_value)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_ioc_in_hardware_error_state_WHEN_get_error_THEN_hardware_error_returned(self):
        expected_value = "Hardware error"
        self._lewis.backdoor_set_on_device("error", ERROR_STATE_HARDWARE_FAULT)

        self.ca.assert_that_pv_is("ERROR", expected_value)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_ioc_in_error_state_WHEN_reset_error_THEN_error_reset(self):
        expected_value = "No error"
        self._lewis.backdoor_set_on_device("error", ERROR_STATE_NO_ERROR)
        self.ca.set_pv_value("ERROR:SP", "Reset")

        self.ca.assert_that_pv_is("ERROR", expected_value)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_ioc_in_error_state_WHEN_reset_error_THEN_error_reset(self):
        expected_value = "No error"
        self._lewis.backdoor_set_on_device("error", ERROR_STATE_HARDWARE_FAULT)

        self.ca.assert_that_pv_is("ERROR", expected_value)

    @skip_if_recsim("Unable to use lewis backdoor in RECSIM")
    def test_GIVEN_device_not_connected_WHEN_get_error_THEN_alarm(self):
        self._lewis.backdoor_set_on_device('connected', False)

        self.ca.assert_that_pv_alarm_is('ERROR:SP', ChannelAccess.Alarms.INVALID)

    @skip_if_recsim("Reliant on setUP lewis backdoor call")
    def test_GIVEN_timed_pump_WHEN_get_program_runtime_THEN_program_runtime_increments(self):
        self.ca.set_pv_value("TIME:RUN:SP", 10000)
        self.ca.set_pv_value("PUMP_FOR_TIME:SP", 1)

        self.ca.assert_that_pv_value_is_increasing("TIME", wait=2)

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_timed_pump_WHEN_set_constant_pump_THEN_state_updated_to_constant_pump(self):
        # Set a run time for a timed run
        self.ca.set_pv_value("TIME:RUN:SP", 10000)
        self.ca.process_pv("PUMP_FOR_TIME:SP")
        expected_value = "Pumping"
        self.ca.assert_that_pv_is("STATUS", expected_value)

        self.ca.process_pv("START:SP")
        expected_value = "Pumping"
        self.ca.assert_that_pv_is("STATUS", expected_value)

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_constant_pump_WHEN_set_timed_pump_THEN_state_updated_to_timed_pump(self):
        expected_value = "Pumping"

        self.ca.process_pv("START:SP")
        self.ca.assert_that_pv_is("STATUS", expected_value)

        # Set a run time for a timed run
        self.ca.set_pv_value("TIME:RUN:SP", 10000)
        self.ca.process_pv("PUMP_FOR_TIME:SP")
        self.ca.assert_that_pv_is("STATUS", expected_value)

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_input_incorrect_WHEN_set_flowrate_THEN_trouble_message_returned(self):
        self._lewis.backdoor_set_on_device("input_correct", False)
        self.ca.set_pv_value("FLOWRATE:SP", 0.010)

        self.ca.assert_that_pv_is("ERROR:STR", "[Error:stack underflow]")

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_command_seq_that_would_crash_pump_WHEN_command_seq_called_THEN_pump_crashes(self):
        self.ca.set_pv_value("_TEST_CRASH.PROC", 1)

        self.ca.assert_that_pv_alarm_is("COMP:A", ChannelAccess.Alarms.INVALID, timeout=30)

    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_pump_running_WHEN_set_file_number_command_called_THEN_program_is_busy_error(self):
        expected_value = "[Program is Busy]"
        self.ca.set_pv_value("START:SP", 1)
        self.ca.set_pv_value("FILE:SP", 0)

        self.ca.assert_that_pv_is("ERROR:STR", expected_value)

    @parameterized.expand([("low_set_time", 100, 1, 1),
                           ("high_set_time", 1000, 10, 1),
                           ("non_standard_set_time", 456, 5, 1)])
    @unstable_test(max_retries=5)
    @skip_if_recsim("Lewis device logic not supported in RECSIM")
    def test_GIVEN_pump_for_volume_WHEN_pumping_THEN_device_is_pumping_set_volume(self, _, time, volume, flowrate):
        # Set a target pump time a target pump volume. When we start a pump set volume run, then the remaining
        # time should be related to the target volume, and not the target time (that would be used for a pump for time).
        set_time = time
        set_volume = volume
        set_flowrate = flowrate
        expected_time = set_volume * set_flowrate * 60  # flow rate units = mL/min, so convert to seconds

        self.ca.set_pv_value("TIME:RUN:SP", set_time)
        self.ca.set_pv_value("TIME:VOL:SP", set_volume)
        self.ca.set_pv_value("FLOWRATE:SP", set_flowrate)

        self.ca.process_pv("PUMP_SET_VOLUME:SP")

        self.ca.assert_that_pv_is_within_range("TIME:REMAINING", min_value=expected_time - 20,
                                               max_value=expected_time + 20)
コード例 #4
0
class CybamanTests(unittest.TestCase):
    """
    Tests for the cybaman IOC.
    """

    AXES = ["A", "B", "C"]
    test_positions = [-200, -1.23, 0, 180.0]

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            EMULATOR_DEVICE, DEVICE_PREFIX)
        self.ca = ChannelAccess(default_timeout=20,
                                device_prefix=DEVICE_PREFIX)
        self.ca.assert_that_pv_exists("INITIALIZE", timeout=30)

        self._lewis.backdoor_set_on_device('connected', True)

        # Check that all the relevant PVs are up.
        for axis in self.AXES:
            self.ca.assert_that_pv_exists(axis)
            self.ca.assert_that_pv_exists("{}:SP".format(axis))

        # Initialize the device, do this in setup to avoid doing it in every test
        self.ca.set_pv_value("INITIALIZE", 1)
        self.ca.assert_that_pv_is("INITIALIZED", "TRUE")

    def test_WHEN_ioc_is_started_THEN_ioc_is_not_disabled(self):
        self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    @skip_if_recsim("Uses lewis backdoor command")
    def test_WHEN_position_setpoints_are_set_via_backdoor_THEN_positions_move_towards_setpoints(
            self):
        for axis in self.AXES:
            for pos in self.test_positions:
                self._lewis.backdoor_set_on_device(
                    "{}_setpoint".format(axis.lower()), pos)
                self.ca.assert_that_pv_is_number("{}".format(axis),
                                                 pos,
                                                 tolerance=0.01)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_GIVEN_home_position_is_set_WHEN_home_pv_is_set_THEN_position_moves_towards_home(
            self):
        for axis in self.AXES:
            for pos in self.test_positions:
                self._lewis.backdoor_set_on_device(
                    "home_position_axis_{}".format(axis.lower()), pos)
                self.ca.set_pv_value("{}:HOME".format(axis), 1)
                self.ca.assert_that_pv_is_number("{}".format(axis),
                                                 pos,
                                                 tolerance=0.01)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_GIVEN_a_device_in_some_other_state_WHEN_reset_command_is_sent_THEN_device_is_reset_to_original_state(
            self):

        modifier = 12.34

        # Reset cybaman
        self.ca.set_pv_value("RESET", 1)
        self.ca.assert_that_pv_is("INITIALIZED", "FALSE")
        self.ca.set_pv_value("INITIALIZE", 1)
        self.ca.assert_that_pv_is("INITIALIZED", "TRUE")
        self.ca.assert_that_pv_value_is_unchanged("INITIALIZED", 10)

        original = {}
        for axis in self.AXES:
            original[axis] = float(
                self.ca.get_pv_value("{}".format(axis.upper())))

            # Set both value and setpoint to avoid the device moving back towards the setpoint
            self._lewis.backdoor_set_on_device(
                "{}_setpoint".format(axis.lower()), original[axis] + modifier)
            self._lewis.backdoor_set_on_device("{}".format(axis.lower()),
                                               original[axis] + modifier)

            self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                             original[axis] + modifier,
                                             tolerance=0.001)

        # Reset cybaman
        self.ca.set_pv_value("RESET", 1)

        # Check that a, b and c values are now at original values
        for axis in self.AXES:
            self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                             original[axis],
                                             tolerance=0.001)

    def test_GIVEN_a_device_in_initialized_state_WHEN_setpoints_are_sent_THEN_device_goes_to_setpoint(
            self):
        for axis in self.AXES:
            for pos in self.test_positions:
                self.ca.set_pv_value("{}:SP".format(axis.upper()), pos)
                self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                                 pos)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_GIVEN_a_device_with_a_setpoint_less_than_minus_150_WHEN_homed_THEN_setpoint_is_set_to_minus_150_before_home(
            self):
        for axis in self.AXES:
            # Ensure home position is known
            self._lewis.backdoor_set_on_device(
                "home_position_axis_{}".format(axis.lower()), 100)

            # Ensure setpoint and readback are less than -150
            self.ca.set_pv_value("{}:SP".format(axis.upper()), -155)
            self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                             -155,
                                             tolerance=0.01)

            # Tell axis to home
            self.ca.set_pv_value("{}:HOME".format(axis.upper()), 1)

            # Ensure that setpoint is updated to -150 before home
            self.ca.assert_that_pv_is_number("{}:SP".format(axis.upper()),
                                             -150,
                                             tolerance=0.01)

            # Let device actually reach home position
            self.ca.assert_that_pv_is_number("{}".format(axis.upper()), 100)

    @skip_if_recsim("Uses lewis backdoor command")
    def test_GIVEN_a_device_with_a_setpoint_more_than_minus_150_WHEN_homed_THEN_setpoint_is_not_set_before_home(
            self):
        for axis in self.AXES:
            # Ensure home position is known
            self._lewis.backdoor_set_on_device(
                "home_position_axis_{}".format(axis.lower()), 100)

            # Ensure setpoint and readback are more than -150
            self.ca.set_pv_value("{}:SP".format(axis.upper()), -145)
            self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                             -145,
                                             tolerance=0.01)

            # Tell axis to home
            self.ca.set_pv_value("{}:HOME".format(axis.upper()), 1)

            # Ensure that setpoint has not been updated
            self.ca.assert_that_pv_is_number("{}:SP".format(axis.upper()),
                                             -145,
                                             tolerance=0.01)

            # Let device actually reach home position
            self.ca.assert_that_pv_is_number("{}".format(axis.upper()), 100)

    def test_GIVEN_a_device_at_a_specific_position_WHEN_setpoint_is_updated_THEN_tm_val_is_calculated_correctly(
            self):

        test_cases = (
            # No change in setpoint, TM val should be 4000
            {
                "old_pos": (-1, -2, -3),
                "axis_to_change": "A",
                "new_setpoint": -1,
                "expected_tm_val": 4000
            },
            # Test case provided from flowchart specification
            {
                "old_pos": (0, 0, 0),
                "axis_to_change": "A",
                "new_setpoint": 30,
                "expected_tm_val": 6000
            },
            # Test case provided from flowchart specification
            {
                "old_pos": (11, -5, 102),
                "axis_to_change": "C",
                "new_setpoint": 50,
                "expected_tm_val": 10000
            },
            # Very small change, TM val should be 4000
            {
                "old_pos": (10, 20, 30),
                "axis_to_change": "B",
                "new_setpoint": 21,
                "expected_tm_val": 4000
            },
        )

        for case in test_cases:
            # Ensure original position is what it's meant to be
            for axis, setpoint in zip(self.AXES, case["old_pos"]):
                self.ca.set_pv_value("{}:SP".format(axis.upper()), setpoint)
                self.ca.assert_that_pv_is_number("{}".format(axis.upper()),
                                                 setpoint,
                                                 tolerance=0.01)

            # Change the relevant axis to a new setpoint
            self.ca.set_pv_value(
                "{}:SP".format(case["axis_to_change"].upper()),
                case["new_setpoint"])

            # Assert that the TM val calculation record contains the correct value
            # Tolerance is 1001 because rounding errors would get multiplied by 1000
            self.ca.assert_that_pv_is_number("{}:_CALC_TM_AND_SET".format(
                case["axis_to_change"].upper()),
                                             case["expected_tm_val"],
                                             tolerance=1001)
コード例 #5
0
class GemorcTests(unittest.TestCase):
    """
    Tests for the Gemorc IOC.
    """
    def reset_emulator(self):
        self._lewis.backdoor_set_on_device("reset", True)
        sleep(
            1
        )  # Wait for reset to finish so we don't jump the gun. No external indicator from emulator

    def reset_ioc(self):
        self.ca.set_pv_value("RESET", 1)
        # INIT:ONCE is a property held exclusively in the IOC
        calc_pv = "INIT:ONCE:CALC.CALC"
        original_calc = self.ca.get_pv_value(calc_pv)
        self.ca.set_pv_value(calc_pv, "0")
        self.ca.assert_that_pv_is("INIT:ONCE", "No")
        self.ca.set_pv_value(calc_pv, original_calc)

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "gemorc", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=DEFAULT_TIMEOUT)
        self.ca.assert_that_pv_exists("ID", timeout=30)
        self.reset_ioc()
        if not IOCRegister.uses_rec_sim:
            self.reset_emulator()
            self.check_init_state(False, False, True, False)
            self.ca.assert_that_pv_is_number("CYCLES", 0)

    def check_init_state(self, initialising, initialised,
                         initialisation_required, oscillating):
        def bi_to_bool(val):
            return val == "Yes"

        # Do all states at once.
        match = False
        total_time = 0.0
        max_wait = DEFAULT_TIMEOUT
        interval = 1.0

        actual_initialising = None
        actual_initialised = None
        actual_initialisation_required = None
        actual_oscillating = None

        while not match and total_time < max_wait:
            actual_initialising = bi_to_bool(
                self.ca.get_pv_value("INIT:PROGRESS"))
            actual_initialised = bi_to_bool(self.ca.get_pv_value("INIT:DONE"))
            actual_initialisation_required = bi_to_bool(
                self.ca.get_pv_value("INIT:REQUIRED"))
            actual_oscillating = bi_to_bool(self.ca.get_pv_value("STAT:OSC"))

            match = all([
                initialising == actual_initialising,
                initialised == actual_initialised,
                initialisation_required == actual_initialisation_required,
                oscillating == actual_oscillating
            ])

            total_time += interval
            sleep(interval)

        try:
            self.assertTrue(match)
        except AssertionError:
            message_format = "State did not match the required state (initialising, initialised, initialisation " \
                             "required, oscillating)\nExpected: ({}, {}, {}, {})\nActual: ({}, {}, {}, {})"
            self.fail(
                message_format.format(initialising, initialised,
                                      initialisation_required, oscillating,
                                      actual_initialising, actual_initialised,
                                      actual_initialisation_required,
                                      actual_oscillating))

    def initialise(self):
        self.ca.set_pv_value("INIT", 1)
        self.ca.assert_that_pv_is("INIT:DONE", "Yes", timeout=10)

    def start_oscillating(self):
        self.initialise()
        self.ca.set_pv_value("START", 1)

    def wait_for_re_initialisation_required(self, interval=10):
        self.ca.set_pv_value("INIT:OPT", interval)
        self.start_oscillating()
        while self.ca.get_pv_value("CYCLES") < interval:
            sleep(1)

    @staticmethod
    def backlash(speed, acceleration):
        return int(0.5 * speed**2 / float(acceleration))

    @staticmethod
    def utility(width, backlash):
        return width / float(width + backlash) * 100.0

    @staticmethod
    def period(width, backlash, speed):
        return 2.0 * (width + backlash) / float(speed)

    @staticmethod
    def frequency(width, backlash, speed):
        return 1.0 / GemorcTests.period(width, backlash, speed)

    def set_and_confirm_state(self,
                              width=None,
                              speed=None,
                              acceleration=None,
                              offset=None):
        pv_value_pairs = [("WIDTH", width), ("SPEED", speed),
                          ("ACC", acceleration), ("OFFSET", offset)]
        filtered_pv_values = [(pv, value) for pv, value in pv_value_pairs
                              if value is not None]
        for pv, value in filtered_pv_values:
            self.ca.set_pv_value("{}:SP".format(pv), value)
        # Do all sets then all confirms to reduce wait time
        for pv, value in filtered_pv_values:
            self.ca.assert_that_pv_is_number(pv, value)

    def test_WHEN_width_setpoint_set_THEN_local_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_WIDTH + 1,
                                                      "WIDTH:SP:RBV",
                                                      "WIDTH:SP")

    def test_WHEN_width_setpoint_set_THEN_remote_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_WIDTH + 1,
                                                      "WIDTH")

    def test_WHEN_speed_setpoint_set_THEN_local_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_SPEED + 1,
                                                      "SPEED:SP:RBV",
                                                      "SPEED:SP")

    def test_WHEN_speed_setpoint_set_THEN_remote_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_SPEED + 1,
                                                      "SPEED")

    def test_WHEN_acceleration_setpoint_set_THEN_local_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_ACCELERATION + 1,
                                                      "ACC:SP:RBV", "ACC:SP")

    def test_WHEN_acceleration_setpoint_set_THEN_remote_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_ACCELERATION + 1,
                                                      "ACC")

    def test_WHEN_offset_setpoint_set_THEN_local_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_OFFSET + 1,
                                                      "OFFSET:SP:RBV",
                                                      "OFFSET:SP")

    def test_WHEN_offset_setpoint_set_THEN_remote_readback_matches(self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_OFFSET + 1,
                                                      "OFFSET")

    def test_WHEN_offset_setpoint_set_to_negative_value_THEN_remote_readback_matches(
            self):
        self.ca.assert_setting_setpoint_sets_readback(-DEFAULT_OFFSET,
                                                      "OFFSET")

    def test_WHEN_device_first_started_THEN_initialisation_required(self):
        self.check_init_state(initialising=False,
                              initialised=False,
                              initialisation_required=True,
                              oscillating=False)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_starting_state_WHEN_initialisation_requested_THEN_initialising_becomes_true(
            self):
        self.ca.set_pv_value("INIT", 1)
        self.check_init_state(initialising=True,
                              initialised=False,
                              initialisation_required=False,
                              oscillating=False)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_starting_state_WHEN_initialisation_requested_THEN_becomes_initialised_when_no_longer_in_progress(
            self):
        self.ca.set_pv_value("INIT", 1)

        total_wait = 0
        max_wait = DEFAULT_TIMEOUT
        interval = 1
        initialisation_complete = self.ca.get_pv_value("INIT:DONE")
        while self.ca.get_pv_value(
                "INIT:PROGRESS") == "Yes" and total_wait < max_wait:
            # Always check value from before we confirmed initialisation was in progress to avoid race conditions
            self.assertNotEqual(initialisation_complete, 1)
            sleep(interval)
            total_wait += interval
            initialisation_complete = self.ca.get_pv_value("INIT:DONE")
        self.check_init_state(initialising=False,
                              initialised=True,
                              initialisation_required=False,
                              oscillating=False)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_initialised_WHEN_oscillation_requested_THEN_reports_oscillating(
            self):
        self.start_oscillating()
        self.ca.assert_that_pv_is("STAT:OSC", "Yes")

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_initialised_WHEN_oscillation_requested_THEN_complete_cycles_increases(
            self):
        self.start_oscillating()
        self.ca.assert_that_pv_value_is_increasing("CYCLES", DEFAULT_TIMEOUT)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_oscillating_WHEN_oscillation_stopped_THEN_reports_not_oscillating(
            self):
        self.start_oscillating()
        self.ca.set_pv_value("STOP", 1)
        self.ca.assert_that_pv_is("STAT:OSC", "No")

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_initialised_WHEN_oscillation_requested_THEN_complete_cycles_does_not_change(
            self):
        self.start_oscillating()
        self.ca.set_pv_value("STOP", 1)
        self.ca.assert_that_pv_value_is_unchanged("CYCLES", DEFAULT_TIMEOUT)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_oscillating_WHEN_initialisation_requested_THEN_initialises(
            self):
        self.start_oscillating()
        self.ca.set_pv_value("INIT", 1)
        self.check_init_state(initialising=True,
                              initialised=False,
                              initialisation_required=False,
                              oscillating=False)

    @skip_if_recsim("Device reset requires Lewis backdoor")
    def test_GIVEN_oscillating_and_initialisation_requested_WHEN_initialisation_complete_THEN_resumes_oscillation(
            self):
        self.start_oscillating()
        self.initialise()
        self.check_init_state(initialising=False,
                              initialised=True,
                              initialisation_required=False,
                              oscillating=True)

    def test_WHEN_settings_reset_requested_THEN_settings_return_to_default_values(
            self):
        settings = (
            ("WIDTH", DEFAULT_WIDTH),
            ("ACC", DEFAULT_ACCELERATION),
            ("SPEED", DEFAULT_SPEED),
            ("OFFSET", DEFAULT_OFFSET),
            ("INIT:AUTO", DEFAULT_AUTO_INITIALISE),
            ("INIT:OPT", DEFAULT_OPT_INITIALISE),
        )
        for pv, default in settings:
            self.ca.set_pv_value("{}:SP".format(pv),
                                 default + 1)  # I prefer the two lines here
            self.ca.assert_that_pv_is_not_number(pv, default)

        self.ca.set_pv_value("RESET", 1)

        for pv, default in settings:
            self.ca.assert_that_pv_is_number(pv, default)

    @skip_if_recsim("ID is emulator specific")
    def test_WHEN_device_is_running_THEN_it_gets_PnP_identity_from_emulator(
            self):
        self.ca.assert_that_pv_is(
            "ID",
            "0002 0001 ISIS Gem Oscillating Rotary Collimator (IBEX EMULATOR)",
            timeout=20)  # On a very slow scan

    def test_GIVEN_standard_test_cases_WHEN_backlash_calculated_locally_THEN_result_is_in_range_supported_by_device(
            self):
        for _, speed, acceleration in SETTINGS_TEST_CASES:
            self.assertTrue(0 <= self.backlash(speed, acceleration) <= 999)

    @skip_if_recsim("Depends on emulator value")
    def test_WHEN_emulator_running_THEN_backlash_has_value_derived_from_speed_and_acceleration(
            self):
        for width, speed, acceleration in SETTINGS_TEST_CASES:
            self.set_and_confirm_state(speed=speed, acceleration=acceleration)
            self.ca.assert_that_pv_is_number(
                "BACKLASH", self.backlash(speed, acceleration))

    def test_GIVEN_non_zero_speed_WHEN_width_and_speed_set_THEN_utility_time_corresponds_to_formula_in_test(
            self):
        for width, speed, acceleration in SETTINGS_TEST_CASES:
            self.set_and_confirm_state(width, speed, acceleration)
            backlash = self.ca.get_pv_value("BACKLASH")
            self.ca.assert_that_pv_is_number("UTILITY",
                                             self.utility(width, backlash),
                                             tolerance=DEFAULT_TOLERANCE)

    def test_WHEN_emulator_running_THEN_period_has_value_as_derived_from_speed_width_and_backlash(
            self):
        for width, speed, acceleration in SETTINGS_TEST_CASES:
            self.set_and_confirm_state(width, speed, acceleration)
            backlash = self.ca.get_pv_value("BACKLASH")
            self.ca.assert_that_pv_is_number("PERIOD",
                                             self.period(
                                                 width, backlash, speed),
                                             tolerance=DEFAULT_TOLERANCE)

    def test_WHEN_emulator_running_THEN_frequency_has_value_as_derived_from_speed_width_and_backlash(
            self):
        for width, speed, acceleration in SETTINGS_TEST_CASES:
            self.set_and_confirm_state(width, speed, acceleration)
            backlash = self.ca.get_pv_value("BACKLASH")
            self.ca.assert_that_pv_is_number("FREQ",
                                             self.frequency(
                                                 width, backlash, speed),
                                             tolerance=DEFAULT_TOLERANCE)

    @skip_if_recsim("This behaviour not implemented in recsim")
    def test_GIVEN_non_zero_offset_WHEN_re_zeroed_to_datum_THEN_offset_is_zero(
            self):
        self.ca.assert_setting_setpoint_sets_readback(DEFAULT_OFFSET + 1,
                                                      "OFFSET", "OFFSET:SP")
        self.ca.assert_that_pv_is_not_number("OFFSET", 0)
        self.ca.set_pv_value("ZERO", 1)
        self.ca.assert_that_pv_is_number("OFFSET", 0)

    def test_WHEN_auto_initialisation_interval_set_THEN_readback_matches_set_value(
            self):
        self.ca.assert_setting_setpoint_sets_readback(
            DEFAULT_AUTO_INITIALISE + 1, "INIT:AUTO")

    def test_WHEN_opt_initialisation_interval_set_THEN_readback_matches_set_value(
            self):
        self.ca.assert_setting_setpoint_sets_readback(
            DEFAULT_OPT_INITIALISE + 1, "INIT:OPT")

    @skip_if_recsim("Cycle counting not performed in Recsim")
    def test_GIVEN_oscillating_WHEN_number_of_cycles_exceeds_optional_init_interval_THEN_initialisation_required(
            self):
        self.wait_for_re_initialisation_required()
        self.check_init_state(False, True, True, True)

    @skip_if_recsim("Cycle counting not performed in Recsim")
    def test_GIVEN_initialisation_required_after_oscillating_WHEN_reinitialised_THEN_re_initialisation_not_required(
            self):
        self.wait_for_re_initialisation_required()
        self.ca.set_pv_value("INIT:OPT", DEFAULT_OPT_INITIALISE)
        self.initialise()
        self.check_init_state(False, True, False, True)

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_device_initialised_THEN_initialised_once(self):
        self.initialise()
        self.ca.assert_that_pv_is("INIT:ONCE", "Yes")

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_oscillating_THEN_initialised_once(self):
        self.start_oscillating()
        self.ca.assert_that_pv_is("INIT:ONCE", "Yes")

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_oscillating_and_initialisation_required_THEN_initialised_once(
            self):
        self.wait_for_re_initialisation_required()
        self.ca.assert_that_pv_is("INIT:ONCE", "Yes")

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_reinitialising_THEN_initialised_once(self):
        self.wait_for_re_initialisation_required()
        self.ca.set_pv_value("INIT", 1)
        self.ca.assert_that_pv_is("INIT:ONCE", "Yes")

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_reinitialised_THEN_initialised_once(self):
        self.wait_for_re_initialisation_required()
        self.initialise()
        self.ca.assert_that_pv_is("INIT:ONCE", "Yes")

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_GIVEN_oscillating_WHEN_stopped_and_immediately_initialised_THEN_number_of_cycles_goes_to_zero(
            self):
        self.start_oscillating()
        self.ca.set_pv_value("STOP", 1)
        self.ca.set_pv_value("INIT", 1)
        self.ca.assert_that_pv_is_number("CYCLES", 0)

    @skip_if_recsim("Initialisation logic not performed in Recsim")
    def test_WHEN_oscillating_THEN_auto_reinitialisation_triggers_after_counter_reaches_auto_trigger_value(
            self):
        initialisation_interval = 100
        initial_status_string = "Sequence not run since IOC startup"
        self.ca.set_pv_value("INIT:AUTO", initialisation_interval)
        self.start_oscillating()
        while self.ca.get_pv_value("CYCLES") < initialisation_interval:
            self.ca.assert_that_pv_is("INIT:PROGRESS", "No")
            self.ca.assert_that_pv_is("INIT:STAT", initial_status_string)
            sleep(1)
        self.ca.assert_that_pv_is_not("INIT:STAT", initial_status_string)
        self.ca.assert_that_pv_is(
            "STAT:OSC", "No",
            timeout=10)  # Initialisation seq has a 5s wait at the start
コード例 #6
0
class Keithley2400Tests(unittest.TestCase):
    """
    Tests for the keithley 2400.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "keithley_2400", DEVICE_PREFIX)

        self._lewis.backdoor_set_on_device("random_output", False)

        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)

    def calculate_resistance_range(self, value):
        """
        The resistance ranges of the device are 2.1*10^x, where x is between 0 and 8
        """

        for r in [2.1 * 10**i for i in range(1, 8)]:
            if value < r:
                return r / 10

    @skip_if_recsim("Recsim does not work with lewis backdoor tests")
    def test_WHEN_current_value_is_set_THEN_readback_returns_value_just_set(
            self):
        self.ca.set_pv_value("OUTPUT:MODE:SP", "On")
        for test_val in TEST_OUTPUTS:
            self._lewis.backdoor_set_on_device("current", test_val)
            self.ca.assert_that_pv_is_number("CURR",
                                             test_val,
                                             tolerance=0.05 * abs(test_val))

    @skip_if_recsim("Recsim does not work with lewis backdoor tests")
    def test_WHEN_voltage_value_is_set_THEN_readback_returns_value_just_set(
            self):
        self.ca.set_pv_value("OUTPUT:MODE:SP", "On")
        for test_val in TEST_OUTPUTS:
            self._lewis.backdoor_set_on_device("voltage", test_val)
            self.ca.assert_that_pv_is_number("VOLT",
                                             test_val,
                                             tolerance=0.05 * abs(test_val))

    @skip_if_recsim("Recsim does not work with lewis backdoor tests")
    def test_WHEN_voltage_and_current_are_set_THEN_readback_returns_valid_resistance(
            self):
        self.ca.set_pv_value("OUTPUT:MODE:SP", "On")
        for volts, amps in itertools.product([4.5, 6.7], [6.7, 4.5]):
            self._lewis.backdoor_set_on_device("current", amps)
            self._lewis.backdoor_set_on_device("voltage", volts)

            resistance = volts / amps

            self.ca.assert_that_pv_is_number("RES",
                                             resistance,
                                             tolerance=0.05 * resistance)

    def test_WHEN_ioc_is_started_THEN_ioc_is_not_disabled(self):
        self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    def test_WHEN_output_mode_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for mode in ["On", "Off"]:
            self.ca.assert_setting_setpoint_sets_readback(mode, "OUTPUT:MODE")

    @skip_if_recsim("Recsim does not work with lewis backdoor tests")
    def test_WHEN_output_mode_is_unset_THEN_current_readback_does_not_update(
            self):
        # Write to the RAW pv, test that the CURR pv does not update with the new value when output is off
        self.ca.set_pv_value("CURR:RAW", 1.0)
        self.ca.set_pv_value("OUTPUT:MODE:SP", "Off")

        self._lewis.backdoor_set_on_device("current", 5.0)

        self.ca.assert_that_pv_value_is_unchanged("CURR", 1.0)

    @skip_if_recsim("Recsim does not work with lewis backdoor tests")
    def test_WHEN_output_mode_is_unset_THEN_voltage_readback_does_not_update(
            self):
        # Write to the RAW value, test that the VOLT pv does not update with the new value when output is off
        self.ca.set_pv_value("VOLT:RAW", 1.0)
        self.ca.set_pv_value("OUTPUT:MODE:SP", "Off")

        self._lewis.backdoor_set_on_device("voltage", 5.0)

        self.ca.assert_that_pv_value_is_unchanged("VOLT", 1.0)

    def test_WHEN_resistance_mode_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for mode in ["Manual", "Auto"]:
            self.ca.assert_setting_setpoint_sets_readback(mode, "RES:MODE")

    def test_WHEN_remote_sensing_mode_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for mode in ["On", "Off"]:
            self.ca.assert_setting_setpoint_sets_readback(mode, "SENS:MODE")

    def test_WHEN_automatic_range_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["Manual", "Auto"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "RES:RANGE:AUTO")

    @skip_if_recsim("Banded record behaviour too complex for recsim")
    def test_WHEN_range_is_set_THEN_readback_updates_with_appropriate_range_for_value_just_set(
            self):
        for test_val in TEST_OUTPUTS:
            ideal_range = self.calculate_resistance_range(test_val)
            self.ca.set_pv_value("RES:RANGE:SP", test_val)
            self.ca.assert_that_pv_is_number("RES:RANGE",
                                             ideal_range,
                                             tolerance=0.05 * ideal_range)

    def test_WHEN_source_mode_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["Current", "Voltage"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "SOURCE:MODE")

    def test_WHEN_current_compliance_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in TEST_OUTPUTS:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "CURR:COMPLIANCE")

    def test_WHEN_voltage_compliance_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in TEST_OUTPUTS:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "VOLT:COMPLIANCE")

    def test_WHEN_source_voltage_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in TEST_OUTPUTS:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "VOLT:SOURCE")

    def test_WHEN_source_current_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in TEST_OUTPUTS:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "CURR:SOURCE")

    def test_WHEN_source_current_autoranging_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["Off", "On"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "CURR:SOURCE:AUTORANGE")

    def test_WHEN_source_voltage_autoranging_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["On", "Off"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "VOLT:SOURCE:AUTORANGE")

    def test_WHEN_measurement_current_autoranging_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["On", "Off"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "CURR:MEAS:AUTORANGE")

    def test_WHEN_measurement_voltage_autoranging_is_set_THEN_readback_updates_with_the_value_just_set(
            self):
        for test_val in ["On", "Off"]:
            self.ca.assert_setting_setpoint_sets_readback(
                test_val, "VOLT:MEAS:AUTORANGE")

    @skip_if_recsim("Banded record behaviour too complex for recsim")
    def test_WHEN_source_current_range_is_set_THEN_readback_updates_with_the_appropriate_range_for_value_just_set(
            self):
        """
        Current ranges on the KHLY2400 are simply powers of ten, the whole range of which is covered here
        """
        for test_val in RANGE_MAGNITUDES:
            self.ca.set_pv_value("CURR:SOURCE:RANGE:SP", test_val)
            self.ca.assert_that_pv_is_number("CURR:SOURCE:RANGE",
                                             test_val,
                                             tolerance=0.05 * test_val)

    @skip_if_recsim("Banded record behaviour too complex for recsim")
    def test_WHEN_source_voltage_range_is_set_THEN_readback_updates_with_the_appropriate_range_for_value_just_set(
            self):
        """
        Voltage ranges on the KHLY2400 are 2*multiples of ten, the whole range is covered here.

        """
        for magnitude in RANGE_MAGNITUDES:
            test_val = 2. * magnitude
            self.ca.set_pv_value("VOLT:SOURCE:RANGE:SP", test_val)
            self.ca.assert_that_pv_is_number("VOLT:SOURCE:RANGE",
                                             test_val,
                                             tolerance=0.05 * test_val)

    @skip_if_recsim("Banded record behaviour too complex for recsim")
    def test_WHEN_volts_measurement_range_is_set_THEN_readback_updates_with_appropriate_range_for_value_just_set(
            self):
        """
        Voltage ranges on the KHLY2400 are 2*multiples of ten, the whole range is covered here.

        """
        for magnitude in RANGE_MAGNITUDES:
            test_val = 2. * magnitude
            self.ca.set_pv_value("VOLT:MEAS:RANGE:SP", test_val)
            self.ca.assert_that_pv_is_number("VOLT:MEAS:RANGE",
                                             test_val,
                                             tolerance=0.05 * test_val)

    @skip_if_recsim("Banded record behaviour too complex for recsim")
    def test_WHEN_current_measurement_range_is_set_THEN_readback_updates_with_appropriate_range_for_value_just_set(
            self):
        """
        Current ranges on the KHLY2400 are simply powers of ten, the whole range of which is covered here
        """
        for test_val in RANGE_MAGNITUDES:
            self.ca.set_pv_value("CURR:MEAS:RANGE:SP", test_val)
            self.ca.assert_that_pv_is_number("CURR:MEAS:RANGE",
                                             test_val,
                                             tolerance=0.05 * test_val)
コード例 #7
0
class TritonTests(unittest.TestCase):
    """
    Tests for the Triton IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "triton", DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)

    def test_WHEN_device_is_started_THEN_it_is_not_disabled(self):
        self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    def test_WHEN_P_setpoint_is_set_THEN_readback_updates(self):
        for value in PID_TEST_VALUES:
            self.ca.assert_setting_setpoint_sets_readback(value, "P")

    def test_WHEN_I_setpoint_is_set_THEN_readback_updates(self):
        for value in PID_TEST_VALUES:
            self.ca.assert_setting_setpoint_sets_readback(value, "I")

    def test_WHEN_D_setpoint_is_set_THEN_readback_updates(self):
        for value in PID_TEST_VALUES:
            self.ca.assert_setting_setpoint_sets_readback(value, "D")

    def test_WHEN_temperature_setpoint_is_set_THEN_readback_updates(self):
        for value in TEMPERATURE_TEST_VALUES:
            self.ca.assert_setting_setpoint_sets_readback(
                value, set_point_pv="TEMP:SP", readback_pv="TEMP:SP:RBV")

    @skip_if_recsim(
        "This is implemented at the protocol level, so does not work in recsim"
    )
    def test_WHEN_temperature_setpoint_is_set_THEN_closed_loop_turned_on_automatically(
            self):
        for value in TEMPERATURE_TEST_VALUES:
            self.ca.set_pv_value("CLOSEDLOOP:SP", "Off")
            self.ca.assert_that_pv_is("CLOSEDLOOP", "Off")
            self.ca.assert_setting_setpoint_sets_readback(
                value, set_point_pv="TEMP:SP", readback_pv="TEMP:SP:RBV")
            self.ca.assert_that_pv_is("CLOSEDLOOP", "On")

    def test_GIVEN_closed_loop_already_on_WHEN_temperature_setpoint_is_set_THEN_closed_loop_setpoint_not_reprocessed(
            self):
        for value in TEMPERATURE_TEST_VALUES:
            self.ca.set_pv_value("CLOSEDLOOP:SP", "On")
            self.ca.assert_that_pv_is("CLOSEDLOOP", "On")
            timestamp_before = self.ca.get_pv_value("CLOSEDLOOP:SP.TSEL")
            self.ca.assert_setting_setpoint_sets_readback(
                value, set_point_pv="TEMP:SP", readback_pv="TEMP:SP:RBV")
            self.ca.assert_that_pv_is("CLOSEDLOOP", "On")
            self.ca.assert_that_pv_is("CLOSEDLOOP:SP.TSEL", timestamp_before)
            self.ca.assert_that_pv_value_is_unchanged("CLOSEDLOOP:SP.TSEL",
                                                      wait=5)

    def test_WHEN_heater_range_is_set_THEN_readback_updates(self):
        for value in HEATER_RANGE_TEST_VALUES:
            self.ca.assert_setting_setpoint_sets_readback(
                value, "HEATER:RANGE")

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_power_is_set_via_backdoor_THEN_pv_has_the_value_just_set(
            self):
        for value in HEATER_RANGE_TEST_VALUES:
            self._lewis.backdoor_set_on_device("heater_power", value)
            self.ca.assert_that_pv_is("HEATER:POWER", value)
            self.ca.assert_that_pv_alarm_is("HEATER:POWER",
                                            self.ca.Alarms.NONE)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_closed_loop_mode_is_set_via_backdoor_THEN_the_closed_loop_pv_updates_with_value_just_set(
            self):
        for value in [False, True,
                      False]:  # Need to check both transitions work properly
            self._lewis.backdoor_set_on_device("closed_loop", value)
            self.ca.assert_that_pv_is("CLOSEDLOOP", "On" if value else "Off")

    @skip_if_recsim("Behaviour too complex for recsim")
    def test_WHEN_channels_are_enabled_and_disabled_via_pv_THEN_the_readback_pv_updates_with_value_just_set(
            self):
        for chan in VALID_TEMPERATURE_SENSORS:
            for enabled in [False, True, False
                            ]:  # Need to check both transitions work properly
                self.ca.assert_setting_setpoint_sets_readback(
                    "ON" if enabled else "OFF",
                    "CHANNELS:T{}:STATE".format(chan))

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_a_short_status_is_set_on_device_THEN_displayed_status_is_identical(
            self):
        # Status message that could be contained in an EPICS string type
        short_status = "Device status"
        assert 0 < len(short_status) < 40

        # Status message that device is likely to return - longer than EPICS string type but reasonable for a protocol
        medium_status = "This is a device status that contains a bit more information"
        assert 40 < len(medium_status) < 256

        # Short and medium statuses should be displayed in full.
        for status in [short_status, medium_status]:
            self._lewis.backdoor_set_on_device("status", status)
            self.ca.assert_that_pv_is("STATUS", status)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_long_status_is_set_on_device_THEN_displayed_status_truncated_but_displays_at_least_500_chars(
            self):

        # Somewhat arbitrary, but decide on a minimum number of characters that should be displayed in a
        # status message to the user if the status message is very long. This seems to be a reasonable
        # number given the messages expected, but the manual does not provide an exhaustive list.
        minimum_characters_in_pv = 500

        # Very long status message, used to check that very long messages can be handled gracefully
        long_status = "This device status is quite long:" + " (here is a load of information)" * 50

        assert minimum_characters_in_pv < len(long_status)

        # Allow truncation for long status, but it should still display as many characters as possible
        self._lewis.backdoor_set_on_device("status", long_status)
        self.ca.assert_that_pv_value_causes_func_to_return_true(
            "STATUS", lambda val: long_status.startswith(val) and len(val) >=
            minimum_characters_in_pv)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_automation_is_set_on_device_THEN_displayed_automation_is_identical(
            self):
        automations = [
            "Warming up to 200K",
            "Cooling down to 1K",
        ]

        for automation in automations:
            self._lewis.backdoor_set_on_device("automation", automation)
            self.ca.assert_that_pv_is("AUTOMATION", automation)

    def _set_temp_via_backdoor(self, channel, temp):
        self._lewis.backdoor_command([
            "device", "set_temperature_backdoor", "'{}'".format(channel),
            "{}".format(temp)
        ])

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_stil_temp_is_set_via_backdoor_THEN_pv_updates(self):
        for temp in TEMPERATURE_TEST_VALUES:
            self._set_temp_via_backdoor("STIL", temp)
            self.ca.assert_that_pv_is("STIL:TEMP", temp)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_mc_temp_is_set_via_backdoor_THEN_pv_updates(self):
        for temp in TEMPERATURE_TEST_VALUES:
            self._set_temp_via_backdoor("MC", temp)
            self.ca.assert_that_pv_is("MC:TEMP", temp)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_sorb_temp_is_set_via_backdoor_THEN_pv_updates(self):
        for temp in TEMPERATURE_TEST_VALUES:
            self._set_temp_via_backdoor("SORB", temp)
            self.ca.assert_that_pv_is("SORB:TEMP", temp)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_4KHX_temp_is_set_via_backdoor_THEN_pv_updates(self):
        for temp in TEMPERATURE_TEST_VALUES:
            self._set_temp_via_backdoor("PT2", temp)
            self.ca.assert_that_pv_is("4KHX:TEMP", temp)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_jthx_temp_is_set_via_backdoor_THEN_pv_updates(self):
        for temp in TEMPERATURE_TEST_VALUES:
            self._set_temp_via_backdoor("PT1", temp)
            self.ca.assert_that_pv_is("JTHX:TEMP", temp)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_pressure_is_set_via_backdoor_THEN_pressure_pv_updates(self):
        for sensor, pressure in itertools.product(VALID_PRESSURE_SENSORS,
                                                  PRESSURE_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_pressure_backdoor",
                str(sensor),
                str(pressure)
            ])
            self.ca.assert_that_pv_is("PRESSURE:P{}".format(sensor), pressure)

    def test_WHEN_closed_loop_is_set_via_pv_THEN_readback_updates(self):
        for state in [False, True, False]:
            self.ca.assert_setting_setpoint_sets_readback(
                "On" if state else "Off", "CLOSEDLOOP")

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_read_mc_id_is_issued_via_arbitrary_command_THEN_response_is_in_format_device_uses(
            self):
        self.ca.set_pv_value("ARBITRARY:SP", "READ:SYS:DR:CHAN:MC")
        self.ca.assert_that_pv_value_causes_func_to_return_true(
            "ARBITRARY", lambda val: val.startswith("STAT:SYS:DR:CHAN:MC:"))

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_temperature_is_set_via_backdoor_THEN_the_pvs_update_with_values_just_written(
            self):
        for chan, value in itertools.product(VALID_TEMPERATURE_SENSORS,
                                             TEMPERATURE_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_sensor_property_backdoor",
                str(chan), "temperature",
                str(value)
            ])
            self.ca.assert_that_pv_is("CHANNELS:T{}:TEMP".format(chan), value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_resistance_is_set_via_backdoor_THEN_the_pvs_update_with_values_just_written(
            self):
        for chan, value in itertools.product(VALID_TEMPERATURE_SENSORS,
                                             RESISTANCE_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_sensor_property_backdoor",
                str(chan), "resistance",
                str(value)
            ])
            self.ca.assert_that_pv_is("CHANNELS:T{}:RES".format(chan), value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_excitation_is_set_via_backdoor_THEN_the_pvs_update_with_values_just_written(
            self):
        for chan, value in itertools.product(VALID_TEMPERATURE_SENSORS,
                                             EXCITATION_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_sensor_property_backdoor",
                str(chan), "excitation",
                str(value)
            ])
            self.ca.assert_that_pv_is("CHANNELS:T{}:EXCITATION".format(chan),
                                      value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_pause_is_set_via_backdoor_THEN_the_pvs_update_with_values_just_written(
            self):
        for chan, value in itertools.product(VALID_TEMPERATURE_SENSORS,
                                             TIME_DELAY_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_sensor_property_backdoor",
                str(chan), "pause",
                str(value)
            ])
            self.ca.assert_that_pv_is("CHANNELS:T{}:PAUSE".format(chan), value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_dwell_is_set_via_backdoor_THEN_the_pvs_update_with_values_just_written(
            self):
        for chan, value in itertools.product(VALID_TEMPERATURE_SENSORS,
                                             TIME_DELAY_TEST_VALUES):
            self._lewis.backdoor_command([
                "device", "set_sensor_property_backdoor",
                str(chan), "dwell",
                str(value)
            ])
            self.ca.assert_that_pv_is("CHANNELS:T{}:DWELL".format(chan), value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_resistance_is_changed_THEN_heater_heater_resistance_pv_updates(
            self):
        for heater_resistance in RESISTANCE_TEST_VALUES:
            self._lewis.backdoor_set_on_device("heater_resistance",
                                               heater_resistance)
            self.ca.assert_that_pv_is_number("HEATER:RES", heater_resistance)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_resistance_and_power_are_changed_THEN_heater_current_is_calculated_correctly(
            self):
        for res, power in itertools.product(RESISTANCE_TEST_VALUES,
                                            POWER_TEST_VALUES):
            self._lewis.backdoor_set_on_device("heater_resistance", res)
            self._lewis.backdoor_set_on_device("heater_power", power)

            self.ca.assert_that_pv_is_number(
                "HEATER:CURR", (power / res)**0.5,
                tolerance=0.01)  # Ohm's law P = RI^2

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_current_and_range_are_changed_THEN_heater_percent_power_is_calculated_correctly(
            self):

        for rang, res, power in itertools.product(HEATER_RANGE_TEST_VALUES,
                                                  RESISTANCE_TEST_VALUES,
                                                  POWER_TEST_VALUES):
            self._lewis.backdoor_set_on_device("heater_resistance", res)
            self._lewis.backdoor_set_on_device("heater_power", power)
            self._lewis.backdoor_set_on_device("heater_range", rang)

            assert rang != 0, "Heater range of zero will cause a zero division error!"

            self.ca.assert_that_pv_is_number("HEATER:PERCENT",
                                             100 * ((power / res)**0.5) / rang,
                                             tolerance=0.05)
コード例 #8
0
class AstriumTests(unittest.TestCase):
    """
    Tests for the Astrium Chopper.
    """
    def setUp(self):
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=30)

    @parameterized.expand(parameterized_list(VALID_FREQUENCIES))
    def test_that_WHEN_setting_the_frequency_setpoint_THEN_it_is_set(
            self, _, value):
        self.ca.set_pv_value("CH1:FREQ:SP", value)
        self.ca.assert_that_pv_is("CH1:FREQ", value)

    @parameterized.expand(parameterized_list(VALID_PHASE_DELAYS))
    def test_that_WHEN_setting_the_phase_setpoint_THEN_it_is_set(
            self, _, value):
        self.ca.set_pv_value("CH1:PHASE:SP", value)
        self.ca.assert_that_pv_is("CH1:PHASE", value)

    @parameterized.expand(parameterized_list(VALID_PHASE_DELAYS))
    @skip_if_recsim("Behaviour of phase readback not implemented in recsim")
    def test_that_WHEN_setting_the_phase_setpoint_and_then_speed_THEN_phases_to_the_correct_place(
            self, _, value):
        """
        This test simulates the bug in https://github.com/ISISComputingGroup/IBEX/issues/4123
        """

        # Arrange - set initial speed and phase
        old_speed = 10
        self.ca.set_pv_value("CH1:FREQ:SP", old_speed)
        self.ca.assert_that_pv_is_number("CH1:FREQ",
                                         old_speed)  # Wait for it to get there
        self.ca.set_pv_value("CH1:PHASE:SP", value)
        self.ca.assert_that_pv_is_number("CH1:PHASE", value)
        self.ca.assert_that_pv_is_number("CH1:PHASE:SP:RBV", value)

        # Act - set frequency
        new_speed = 20
        self.ca.set_pv_value("CH1:FREQ:SP", new_speed)
        self.ca.assert_that_pv_is_number("CH1:FREQ",
                                         new_speed)  # Wait for it to get there

        # Assert - both the actual phase and the setpoint readback should be correct after setting speed.
        self.ca.assert_that_pv_is_number("CH1:PHASE", value)
        self.ca.assert_that_pv_value_is_unchanged("CH1:PHASE", wait=10)
        self.ca.assert_that_pv_is_number("CH1:PHASE:SP:RBV", value)
        self.ca.assert_that_pv_value_is_unchanged("CH1:PHASE:SP:RBV", wait=10)

    def test_WHEN_frequency_set_to_180_THEN_actual_setpoint_not_updated(self):
        sent_frequency = 180
        self.ca.set_pv_value("CH1:FREQ:SP", sent_frequency)
        self.ca.assert_that_pv_is_not("CH1:FREQ:SP_ACTUAL", sent_frequency)
        self.ca.assert_that_pv_is_not("CH1:FREQ", sent_frequency)

    @skip_if_recsim("No state changes in recsim")
    def test_WHEN_brake_called_THEN_state_is_BRAKE(self):
        self.ca.set_pv_value("CH1:BRAKE", 1)
        self.ca.assert_that_pv_is("CH1:STATE", "BRAKE")

    @skip_if_recsim("No state changes in recsim")
    def test_WHEN_speed_set_THEN_state_is_POSITION(self):
        self.ca.set_pv_value("CH1:FREQ:SP", 10)
        self.ca.assert_that_pv_is("CH1:STATE", "POSITION")

    @skip_if_devsim("No backdoor to state in devsim")
    def test_WHEN_one_channel_state_not_ESTOP_THEN_calibration_disabled(self):
        self.ca.set_pv_value("CH1:SIM:STATE", "NOT_ESTOP")
        self.ca.set_pv_value("CH2:SIM:STATE", "E_STOP")
        # Need to process to update disabled status.
        # We don't want the db to process when disabled changes as this will write to the device.
        self.ca.process_pv("CALIB")
        self.ca.assert_that_pv_is("CALIB.STAT",
                                  self.ca.Alarms.DISABLE,
                                  timeout=1)

    @skip_if_devsim("No backdoor to state in devsim")
    def test_WHEN_both_channels_state_ESTOP_THEN_calibration_enabled(self):
        self.ca.set_pv_value("CH1:SIM:STATE", "E_STOP")
        self.ca.set_pv_value("CH2:SIM:STATE", "E_STOP")
        # Need to process to update disabled status.
        # We don't want the db to process when disabled changes as this will write to the device.
        self.ca.process_pv("CALIB")
        self.ca.assert_that_pv_is_not("CALIB.STAT",
                                      self.ca.Alarms.DISABLE,
                                      timeout=1)
コード例 #9
0
class FermichopperBase(object):
    """
    Tests for the Fermi Chopper IOC.
    """

    valid_commands = ["0001", "0002", "0003", "0004", "0005"]

    # Values that will be tested in the parametrized tests.
    test_chopper_speeds = [100, 350, 600]
    test_delay_durations = [0, 2.5, 18]
    test_gatewidth_values = [0, 0.5, 5]
    test_temperature_values = [20.0, 25.0, 37.5, 47.5]
    test_current_values = [0, 1.37, 2.22]
    test_voltage_values = [0, 282.9, 333.3]
    test_autozero_values = [-5.0, -2.22, 0, 1.23, 5]

    @abstractmethod
    def _get_device_prefix(self):
        pass

    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "fermichopper", self._get_device_prefix())

        self.ca = ChannelAccess(device_prefix=self._get_device_prefix(),
                                default_timeout=30)
        self.ca.assert_that_pv_exists("SPEED")

        self.ca.set_pv_value("DELAY:SP", 0)
        self.ca.set_pv_value("GATEWIDTH:SP", 0)
        self.ca.assert_that_pv_is_number("DELAY:SP:RBV", 0)
        self.ca.assert_that_pv_is_number("GATEWIDTH", 0)

        if not IOCRegister.uses_rec_sim:
            self._lewis.backdoor_run_function_on_device("reset")

    def is_device_broken(self):
        if IOCRegister.uses_rec_sim:
            return False  # In recsim, assume device is always ok
        else:
            return self._lewis.backdoor_get_from_device("is_broken")

    def tearDown(self):
        self.assertFalse(self.is_device_broken(), "Device was broken.")

    def _turn_on_bearings_and_run(self):
        self.ca.set_pv_value("COMMAND:SP", 4)  # Switch magnetic bearings on
        self.ca.assert_that_pv_is("STATUS.B3", "1")
        self.ca.set_pv_value("COMMAND:SP", 3)  # Switch drive on and run
        self.ca.assert_that_pv_is("STATUS.B5", "1")

    def test_WHEN_ioc_is_started_THEN_ioc_is_not_disabled(self):
        self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_last_command_is_set_via_backdoor_THEN_pv_updates(self):
        for value in self.valid_commands:
            self._lewis.backdoor_set_on_device("last_command", value)
            self.ca.assert_that_pv_is("LASTCOMMAND", value)

    def test_WHEN_speed_setpoint_is_set_THEN_readback_updates(self):
        for speed in self.test_chopper_speeds:
            self.ca.set_pv_value("SPEED:SP", speed)
            self.ca.assert_that_pv_is("SPEED:SP", speed)
            self.ca.assert_that_pv_alarm_is("SPEED:SP", self.ca.Alarms.NONE)
            self.ca.assert_that_pv_is("SPEED:SP:RBV", speed)
            self.ca.assert_that_pv_alarm_is("SPEED:SP:RBV",
                                            self.ca.Alarms.NONE)

    @skip_if_recsim("Recsim does not handle this")
    def test_WHEN_speed_setpoint_is_set_via_gui_pv_THEN_readback_updates(self):
        for speed in self.test_chopper_speeds:
            self.ca.set_pv_value("SPEED:SP:GUI", "{} Hz".format(speed))
            self.ca.assert_that_pv_is("SPEED:SP", speed)
            self.ca.assert_that_pv_alarm_is("SPEED:SP", self.ca.Alarms.NONE)
            self.ca.assert_that_pv_is("SPEED:SP:RBV", speed)
            self.ca.assert_that_pv_alarm_is("SPEED:SP:RBV",
                                            self.ca.Alarms.NONE)

    def test_WHEN_delay_setpoint_is_set_THEN_readback_updates(self):
        for value in self.test_delay_durations:
            self.ca.set_pv_value("DELAY:SP", value)
            self.ca.assert_that_pv_is("DELAY:SP", value)
            self.ca.assert_that_pv_alarm_is("DELAY:SP", self.ca.Alarms.NONE)
            self.ca.assert_that_pv_is_number("DELAY:SP:RBV",
                                             value,
                                             tolerance=0.05)
            self.ca.assert_that_pv_alarm_is("DELAY:SP:RBV",
                                            self.ca.Alarms.NONE)

    def test_WHEN_gatewidth_is_set_THEN_readback_updates(self):
        for value in self.test_gatewidth_values:
            self.ca.set_pv_value("GATEWIDTH:SP", value)
            self.ca.assert_that_pv_is("GATEWIDTH:SP", value)
            self.ca.assert_that_pv_alarm_is("GATEWIDTH:SP",
                                            self.ca.Alarms.NONE)
            self.ca.assert_that_pv_is_number("GATEWIDTH",
                                             value,
                                             tolerance=0.05)
            self.ca.assert_that_pv_alarm_is("GATEWIDTH", self.ca.Alarms.NONE)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_autozero_voltages_are_set_via_backdoor_THEN_pvs_update(self):
        for number, boundary, value in itertools.product(
            [1, 2], ["upper", "lower"], self.test_autozero_values):
            self._lewis.backdoor_set_on_device(
                "autozero_{n}_{b}".format(n=number, b=boundary), value)
            self.ca.assert_that_pv_is_number("AUTOZERO:{n}:{b}".format(
                n=number, b=boundary.upper()),
                                             value,
                                             tolerance=0.05)
            self.ca.assert_that_pv_alarm_is(
                "AUTOZERO:{n}:{b}".format(n=number, b=boundary.upper()),
                self.ca.Alarms.NONE)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_drive_current_is_set_via_backdoor_THEN_pv_updates(self):
        for current in self.test_current_values:
            self._lewis.backdoor_set_on_device("current", current)
            self.ca.assert_that_pv_is_number("CURRENT", current, tolerance=0.1)
            self.ca.assert_that_pv_alarm_is("CURRENT", self.ca.Alarms.NONE)

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_a_stopped_chopper_WHEN_start_command_is_sent_THEN_chopper_goes_to_setpoint(
            self):

        for speed in self.test_chopper_speeds:
            # Setup setpoint speed
            self.ca.set_pv_value("SPEED:SP", speed)
            self.ca.assert_that_pv_is_number("SPEED:SP:RBV", speed)

            self._turn_on_bearings_and_run()

            self.ca.assert_that_pv_is_number("SPEED", speed, tolerance=0.1)

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_a_stopped_chopper_WHEN_start_command_is_sent_without_magnetic_bearings_on_THEN_chopper_does_not_go_to_setpoint(
            self):

        self.ca.assert_that_pv_is_number("SPEED", 0)

        # Switch OFF magnetic bearings
        self.ca.set_pv_value("COMMAND:SP", 5)
        self.ca.assert_that_pv_is("LASTCOMMAND", "0005")
        self.ca.assert_that_pv_is("STATUS.B3", "0")

        for speed in self.test_chopper_speeds:
            # Setup setpoint speed
            self.ca.set_pv_value("SPEED:SP", speed)
            self.ca.assert_that_pv_is_number("SPEED:SP:RBV", speed)

            with assert_log_messages(self._ioc,
                                     in_time=2,
                                     must_contain=ErrorStrings.
                                     ATTEMPT_TO_TURN_ON_WITHOUT_BEARINGS):
                # Run mode ON
                self.ca.set_pv_value("COMMAND:SP", 3)
                # Ensure the ON command has been ignored and last command is still "switch off bearings"
                self.ca.assert_that_pv_is("LASTCOMMAND", "0005")

            self.ca.assert_that_pv_is_number("SPEED", 0, tolerance=0.1)

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_a_chopper_at_speed_WHEN_switch_off_magnetic_bearings_command_is_sent_THEN_magnetic_bearings_do_not_switch_off(
            self):

        speed = 150

        # Setup setpoint speed
        self.ca.set_pv_value("SPEED:SP", speed)
        self.ca.assert_that_pv_is_number("SPEED:SP:RBV", speed)

        # Run mode ON
        self._turn_on_bearings_and_run()

        # Wait for chopper to get up to speed
        self.ca.assert_that_pv_is_number("SPEED", speed, tolerance=0.1)

        with assert_log_messages(self._ioc,
                                 in_time=2,
                                 must_contain=ErrorStrings.
                                 ATTEMPT_TO_TURN_OFF_BEARINGS_AT_SPEED):
            # Attempt to switch OFF magnetic bearings
            self.ca.set_pv_value("COMMAND:SP", 5)

        # Assert that bearings did not switch off
        sleep(5)
        self.ca.assert_that_pv_is("LASTCOMMAND", "0003")
        self.ca.assert_that_pv_is("STATUS.B3", "1")
        self.ca.assert_that_pv_is("SPEED", speed)

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_a_stopped_chopper_WHEN_switch_on_and_off_magnetic_bearings_commands_are_sent_THEN_magnetic_bearings_switch_on_and_off(
            self):

        # Switch ON magnetic bearings
        self.ca.set_pv_value("COMMAND:SP", 4)
        self.ca.assert_that_pv_is("LASTCOMMAND", "0004")
        self.ca.assert_that_pv_is("STATUS.B3", "1")

        # Switch OFF magnetic bearings
        self.ca.set_pv_value("COMMAND:SP", 5)
        self.ca.assert_that_pv_is("LASTCOMMAND", "0005")
        self.ca.assert_that_pv_is("STATUS.B3", "0")

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_chopper_speed_is_too_high_THEN_status_updates(self):

        too_fast = 700

        # Turn on magnetic bearings otherwise device will report it is broken
        self._lewis.backdoor_set_on_device("magneticbearing", True)
        self.ca.assert_that_pv_is("STATUS.B3", "1")

        with assert_log_messages(
                self._ioc,
                in_time=2,
                must_contain=ErrorStrings.CONTROLLER_OVERSPEED):
            self._lewis.backdoor_set_on_device("speed", too_fast)
            self.ca.assert_that_pv_is("STATUS.BA", "1")

        self._lewis.backdoor_set_on_device("speed", 0)
        self.ca.assert_that_pv_is("STATUS.BA", "0")

    @skip_if_recsim("Uses lewis backdoor")
    def test_GIVEN_autozero_voltages_are_out_of_range_WHEN_chopper_is_moving_THEN_range_check_fails(
            self):
        for number, position in itertools.product([1, 2], ["upper", "lower"]):
            self.ca.assert_that_pv_is("AUTOZERO:RANGECHECK", 0)

            with assert_log_messages(self._ioc,
                                     in_time=2,
                                     must_contain=ErrorStrings.
                                     CONTROLLER_AUTOZERO_OUT_OF_RANGE):
                # Set autozero voltage too high
                self._lewis.backdoor_set_on_device(
                    "autozero_{n}_{p}".format(n=number, p=position), 3.2)
                # Assert
                self.ca.assert_that_pv_is("AUTOZERO:RANGECHECK", 1)

            # Reset relevant autozero voltage back to zero
            self._lewis.backdoor_set_on_device(
                "autozero_{n}_{p}".format(n=number, p=position), 0)
            self.ca.assert_that_pv_is_number("AUTOZERO:{n}:{p}".format(
                n=number, p=position.upper()),
                                             0,
                                             tolerance=0.1)

    @contextmanager
    def _lie_about(self, lie):
        if IOCRegister.uses_rec_sim:
            raise IOError("Can't use lewis backdoor in recsim!")

        self._lewis.backdoor_set_on_device("is_lying_about_{}".format(lie),
                                           True)
        try:
            yield
        finally:
            self._lewis.backdoor_set_on_device("is_lying_about_{}".format(lie),
                                               False)

    def _lie_about_delay_setpoint_readback(self):
        return self._lie_about("delay_sp_rbv")

    def _lie_about_gatewidth(self):
        return self._lie_about("gatewidth")

    @skip_if_recsim("Lying about setpoint readback not possible in recsim")
    def test_GIVEN_device_lies_about_delay_setpoint_WHEN_setting_a_delay_THEN_keeps_trying_until_device_does_not_lie(
            self):

        test_value = 567.8
        tolerance = 0.05

        self.ca.set_pv_value("DELAY:SP", test_value)
        self.ca.assert_that_pv_is_number("DELAY:SP:RBV",
                                         test_value,
                                         tolerance=tolerance)

        with self._lie_about_delay_setpoint_readback():
            self.ca.assert_that_pv_value_causes_func_to_return_true(
                "DELAY:SP:RBV", lambda v: abs(v - test_value) > tolerance)

            # Some time later the driver should resend the setpoint which causes the device to behave properly again:
            self.ca.assert_that_pv_is_number("DELAY:SP:RBV",
                                             test_value,
                                             tolerance=tolerance)

    @skip_if_recsim("Lying about gate width not possible in recsim")
    def test_GIVEN_device_lies_about_gatewidth_WHEN_setting_a_gatewidth_THEN_keeps_trying_until_device_does_not_lie(
            self):

        test_value = 567.8
        tolerance = 0.05

        self.ca.set_pv_value("GATEWIDTH:SP", test_value)
        self.ca.assert_that_pv_is_number("GATEWIDTH",
                                         test_value,
                                         tolerance=tolerance)

        with self._lie_about_gatewidth():
            self.ca.assert_that_pv_value_causes_func_to_return_true(
                "GATEWIDTH", lambda v: abs(v - test_value) > tolerance)

            # Some time later the driver should resend the setpoint which causes the device to behave properly again:
            self.ca.assert_that_pv_is_number("GATEWIDTH",
                                             test_value,
                                             tolerance=tolerance)

    @unstable_test()
    @skip_if_recsim("Device breakage not simulated in RECSIM")
    def test_GIVEN_setpoint_is_already_at_600Hz_WHEN_setting_setpoint_to_600Hz_THEN_device_does_not_break(
            self):

        self._turn_on_bearings_and_run()

        self.ca.set_pv_value("SPEED:SP", 600)
        self.ca.assert_that_pv_is_number("SPEED", 600, tolerance=0.1)

        self.ca.set_pv_value("SPEED:SP", 600)
        self.ca.assert_that_pv_is_number("SPEED", 600, tolerance=0.1)

        # Assertion that device is not broken occurs in tearDown()

    @skip_if_recsim("Device breakage not simulated in RECSIM")
    def test_GIVEN_setpoint_is_at_600Hz_and_device_already_running_WHEN_send_run_command_THEN_device_does_not_break(
            self):

        self._turn_on_bearings_and_run()

        self.ca.set_pv_value("SPEED:SP", 600)
        self.ca.assert_that_pv_is_number("SPEED", 600, tolerance=0.1)

        with assert_log_messages(self._ioc,
                                 in_time=2,
                                 must_contain=ErrorStrings.TURN_ON_AT_600HZ):
            self.ca.set_pv_value("COMMAND:SP", 3)  # Switch drive on and run

        # Assertion that device is not broken occurs in tearDown()

    @skip_if_recsim("Cannot use backdoor in recsim")
    def test_GIVEN_speed_setpoint_has_been_sent_to_device_WHEN_the_device_decides_to_go_to_another_setpoint_THEN_the_original_setpoint_is_resent(
            self):
        """
        This test simulates some behaviour seen on MERLIN where the device just decided to reset it's speed setpoint
        without being provoked.
        """
        self._turn_on_bearings_and_run()

        for speed in self.test_chopper_speeds:
            self.ca.assert_setting_setpoint_sets_readback(
                speed, set_point_pv="SPEED:SP", readback_pv="SPEED:SP:RBV")

            # At some point the device starts to do it's own thing...
            self._lewis.backdoor_set_on_device("speed_setpoint", speed - 50)
            self.ca.assert_that_pv_is("SPEED:SP:RBV", speed - 50)

            # Within a minute, the IOC should notice and resend the setpoint to what it should really be
            self.ca.assert_that_pv_is("SPEED:SP:RBV", speed, timeout=60)

    #
    #   Mandatory safety tests
    #
    #   The following behaviours MUST be implemented by the chopper according to the manual
    #

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_chopper_speed_is_too_high_THEN_switch_drive_off_is_sent(
            self):
        self._lewis.backdoor_set_on_device("magneticbearing", True)
        self.ca.assert_that_pv_is("STATUS.B3", "1")

        # Reset last command so that we can tell that it's changed later on
        self._lewis.backdoor_set_on_device("last_command", "0000")
        self.ca.assert_that_pv_is("LASTCOMMAND", "0000")

        with assert_log_messages(self._ioc,
                                 in_time=2,
                                 must_contain=ErrorStrings.SOFTWARE_OVERSPEED):
            # Speed = 610, this is higher than the maximum allowed speed (606)
            self._lewis.backdoor_set_on_device("speed", 610)

            # Assert that "switch drive off" was sent
            self.ca.assert_that_pv_is("LASTCOMMAND", "0002")

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_magnetic_bearing_is_off_WHEN_chopper_speed_is_moving_THEN_switch_drive_on_and_stop_is_sent(
            self):

        # Magnetic bearings should have been turned off in setUp
        self.ca.assert_that_pv_is("STATUS.B3", "0")

        # Reset last command so that we can tell that it's changed later on
        self._lewis.backdoor_set_on_device("last_command", "0000")
        self.ca.assert_that_pv_is("LASTCOMMAND", "0000")

        with assert_log_messages(
                self._ioc,
                in_time=2,
                must_contain=ErrorStrings.CHOPPER_AT_SPEED_WITH_BEARINGS_OFF):
            # Speed = 7 because that's higher than the threshold in the IOC (5)
            # but lower than the threshold in the emulator (10)
            self._lewis.backdoor_set_on_device("speed", 7)

            # Assert that "switch drive on and stop" was sent
            self.ca.assert_that_pv_is("LASTCOMMAND", "0001")

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_autozero_voltages_are_out_of_range_WHEN_chopper_is_moving_THEN_switch_drive_on_and_stop_is_sent(
            self):
        for number, position in itertools.product([1, 2], ["upper", "lower"]):

            err = None
            for i in range(10):  # Try up to 10 times.
                try:
                    self._lewis.backdoor_run_function_on_device("reset")
                    # Assert that the last command is zero as expected
                    self.ca.assert_that_pv_is("LASTCOMMAND", "0000")
                    # Check that the last command is not being set to something else by the IOC
                    self.ca.assert_that_pv_value_is_unchanged("LASTCOMMAND",
                                                              wait=10)
                    break
                except AssertionError as e:
                    err = e
            else:  # no-break
                raise err

            with assert_log_messages(
                    self._ioc,
                    in_time=2,
                    must_contain=ErrorStrings.SOFTWARE_AUTOZERO_OUT_OF_RANGE):
                # Set autozero voltage too high and set device moving
                self._lewis.backdoor_set_on_device(
                    "autozero_{n}_{p}".format(n=number, p=position), 3.2)
                self._lewis.backdoor_set_on_device("speed", 7)

                # Assert that "switch drive on and stop" was sent
                self.ca.assert_that_pv_is("LASTCOMMAND", "0001")

            # Reset relevant autozero voltage back to zero
            self._lewis.backdoor_set_on_device(
                "autozero_{n}_{p}".format(n=number, p=position), 0)
            self.ca.assert_that_pv_is_number("AUTOZERO:{n}:{p}".format(
                n=number, p=position.upper()),
                                             0,
                                             tolerance=0.1)
コード例 #10
0
class IpsTests(unittest.TestCase):
    """
    Tests for the Ips IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(EMULATOR_NAME, DEVICE_PREFIX)
        # Some changes happen on the order of HEATER_WAIT_TIME seconds. Use a significantly longer timeout
        # to capture a few heater wait times plus some time for PVs to update.
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX, default_timeout=HEATER_WAIT_TIME*10)

        # Wait for some critical pvs to be connected.
        for pv in ["MAGNET:FIELD:PERSISTENT", "FIELD", "FIELD:SP:RBV", "HEATER:STATUS"]:
            self.ca.assert_that_pv_exists(pv)

        # Ensure in the correct mode
        self.ca.set_pv_value("CONTROL:SP", "Remote & Unlocked")
        self.ca.set_pv_value("ACTIVITY:SP", "To Setpoint")

        # Don't run reset as the sudden change of state confuses the IOC's state machine. No matter what the initial
        # state of the device the SNL should be able to deal with it.
        # self._lewis.backdoor_run_function_on_device("reset")

        self.ca.set_pv_value("HEATER:WAITTIME", HEATER_WAIT_TIME)

        self.ca.set_pv_value("FIELD:RATE:SP", 10)
        # self.ca.assert_that_pv_is_number("FIELD:RATE:SP", 10)

        self.ca.process_pv("FIELD:SP")

        # Wait for statemachine to reach "at field" state before every test.
        self.ca.assert_that_pv_is("STATEMACHINE", "At field")

    def tearDown(self):
        # Wait for statemachine to reach "at field" state after every test.
        self.ca.assert_that_pv_is("STATEMACHINE", "At field")

        self.assertEqual(self._lewis.backdoor_get_from_device("quenched"), False)

    def test_WHEN_ioc_is_started_THEN_ioc_is_not_disabled(self):
        self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    def _assert_field_is(self, field, check_stable=False):
        self.ca.assert_that_pv_is_number("FIELD", field, tolerance=TOLERANCE)
        self.ca.assert_that_pv_is_number("FIELD:USER", field, tolerance=TOLERANCE)
        if check_stable:
            self.ca.assert_that_pv_value_is_unchanged("FIELD", wait=30)
            self.ca.assert_that_pv_is_number("FIELD", field, tolerance=TOLERANCE, timeout=10)
            self.ca.assert_that_pv_is_number("FIELD:USER", field, tolerance=TOLERANCE, timeout=10)

    def _assert_heater_is(self, heater_state):
        self.ca.assert_that_pv_is("HEATER:STATUS:SP", "On" if heater_state else "Off")
        if heater_state:
            self.ca.assert_that_pv_is("HEATER:STATUS", "On",)
        else:
            self.ca.assert_that_pv_is_one_of("HEATER:STATUS", HEATER_OFF_STATES)

    def _set_and_check_persistent_mode(self, mode):
        self.ca.assert_setting_setpoint_sets_readback("YES" if mode else "NO", "PERSISTENT")

    @parameterized.expand(val for val in parameterized_list(TEST_VALUES))
    def test_GIVEN_persistent_mode_enabled_WHEN_magnet_told_to_go_to_field_setpoint_THEN_goes_to_that_setpoint_and_psu_ramps_to_zero(self, _, val):

        self._set_and_check_persistent_mode(True)

        # Field in the magnet already from persistent mode.
        persistent_field = float(self.ca.get_pv_value("MAGNET:FIELD:PERSISTENT"))

        # Set the new field. This will cause all of the following events based on the state machine.
        self.ca.set_pv_value("FIELD:SP", val)

        # PSU should be ramped to match the persistent field inside the magnet
        self._assert_field_is(persistent_field)
        self.ca.assert_that_pv_is("ACTIVITY", "To Setpoint")

        # Then it is safe to turn on the heater
        self._assert_heater_is(True)

        # Assert that value gets passed to device by SNL. SNL waits 30s for the heater to cool down/warm up
        # after being set.
        self._assert_field_is(val)

        # Now that the correct current is in the magnet, the SNL should turn the heater off
        self._assert_heater_is(False)

        # Now that the heater is off, can ramp down the PSU to zero (SNL waits some time for heater to be off before
        # ramping PSU to zero)
        self.ca.assert_that_pv_is_number("FIELD", 0, tolerance=TOLERANCE)  # PSU field
        self.ca.assert_that_pv_is_number("MAGNET:FIELD:PERSISTENT", val, tolerance=TOLERANCE)  # Persistent field
        self.ca.assert_that_pv_is_number("FIELD:USER", val, tolerance=TOLERANCE)  # User field should be tracking persistent field here
        self.ca.assert_that_pv_is("ACTIVITY", "To Zero")

        # ...And the magnet should now be in the right state!
        self.ca.assert_that_pv_is("STATEMACHINE", "At field")
        self.ca.assert_that_pv_is_number("MAGNET:FIELD:PERSISTENT", val, tolerance=TOLERANCE)

        # "User" field should take the value put in the setpoint, even when the actual field provided by the supply
        # drops to zero
        self.ca.assert_that_pv_is_number("FIELD", 0, tolerance=TOLERANCE)  # PSU field
        self.ca.assert_that_pv_is_number("MAGNET:FIELD:PERSISTENT", val, tolerance=TOLERANCE)  # Persistent field
        self.ca.assert_that_pv_is_number("FIELD:USER", val, tolerance=TOLERANCE)  # User field should be tracking persistent field here

    @parameterized.expand(val for val in parameterized_list(TEST_VALUES))
    def test_GIVEN_non_persistent_mode_WHEN_magnet_told_to_go_to_field_setpoint_THEN_goes_to_that_setpoint_and_psu_does_not_ramp_to_zero(self, _, val):

        self._set_and_check_persistent_mode(False)

        # Field in the magnet already from persistent mode.
        persistent_field = float(self.ca.get_pv_value("MAGNET:FIELD:PERSISTENT"))

        # Set the new field. This will cause all of the following events based on the state machine.
        self.ca.set_pv_value("FIELD:SP", val)

        # PSU should be ramped to match the persistent field inside the magnet (if there was one)
        self._assert_field_is(persistent_field)

        # Then it is safe to turn on the heater (the heater is explicitly switched on and we wait for it even if it
        # was already on out of an abundance of caution).
        self._assert_heater_is(True)

        # Assert that value gets passed to device by SNL. SNL waits 30s for the heater to cool down/warm up
        # after being set.
        self._assert_field_is(val)

        # ...And the magnet should now be in the right state!
        self.ca.assert_that_pv_is_number("MAGNET:FIELD:PERSISTENT", val, tolerance=TOLERANCE)

        # And the PSU should remain stable providing the required current/field
        self.ca.assert_that_pv_is("STATEMACHINE", "At field")
        self._assert_field_is(val, check_stable=True)

    @contextmanager
    def _backdoor_magnet_quench(self, reason="Test framework quench"):
        self._lewis.backdoor_run_function_on_device("quench", [reason])
        try:
            yield
        finally:
            # Get back out of the quenched state. This is because the tearDown method checks that magnet has not
            # quenched.
            self._lewis.backdoor_run_function_on_device("unquench")
            # Wait for IOC to notice quench state has gone away
            self.ca.assert_that_pv_alarm_is("STS:SYSTEM:FAULT", self.ca.Alarms.NONE)

    @parameterized.expand(field for field in parameterized_list(TEST_VALUES))
    def test_GIVEN_magnet_quenches_while_at_field_THEN_ioc_displays_this_quench_in_statuses(self, _, field):

        self._set_and_check_persistent_mode(False)
        self.ca.set_pv_value("FIELD:SP", field)
        self._assert_field_is(field)
        self.ca.assert_that_pv_is("STATEMACHINE", "At field")

        with self._backdoor_magnet_quench():
            self.ca.assert_that_pv_is("STS:SYSTEM:FAULT", "Quenched")
            self.ca.assert_that_pv_alarm_is("STS:SYSTEM:FAULT", self.ca.Alarms.MAJOR)
            self.ca.assert_that_pv_is("CONTROL", "Auto-Run-Down")
            self.ca.assert_that_pv_alarm_is("CONTROL", self.ca.Alarms.MAJOR)

            # The trip field should be the field at the point when the magnet quenched.
            self.ca.assert_that_pv_is_number("FIELD:TRIP", field, tolerance=TOLERANCE)

            # Field should be set to zero by emulator (mirroring what the field ought to do in the real device)
            self.ca.assert_that_pv_is_number("FIELD", 0, tolerance=TOLERANCE)
            self.ca.assert_that_pv_is_number("FIELD:USER", 0, tolerance=TOLERANCE)
            self.ca.assert_that_pv_is_number("MAGNET:FIELD:PERSISTENT", 0, tolerance=TOLERANCE)

    @parameterized.expand(val for val in parameterized_list(TEST_VALUES))
    def test_WHEN_inductance_set_via_backdoor_THEN_value_in_ioc_updates(self, _, val):
        self._lewis.backdoor_set_on_device("inductance", val)
        self.ca.assert_that_pv_is_number("MAGNET:INDUCTANCE", val, tolerance=TOLERANCE)

    @parameterized.expand(val for val in parameterized_list(TEST_VALUES))
    def test_WHEN_measured_current_set_via_backdoor_THEN_value_in_ioc_updates(self, _, val):
        self._lewis.backdoor_set_on_device("measured_current", val)
        self.ca.assert_that_pv_is_number("MAGNET:CURR:MEAS", val, tolerance=TOLERANCE)

    @parameterized.expand(val for val in parameterized_list(TEST_SWEEP_RATES))
    def test_WHEN_sweep_rate_set_THEN_sweep_rate_on_ioc_updates(self, _, val):
        self.ca.set_pv_value("FIELD:RATE:SP", val)
        self.ca.assert_that_pv_is_number("FIELD:RATE:SP", val, tolerance=TOLERANCE)
        self.ca.assert_that_pv_is_number("FIELD:RATE", val, tolerance=TOLERANCE)
        self.ca.assert_that_pv_alarm_is("FIELD:RATE", self.ca.Alarms.NONE)

    @parameterized.expand(activity_state for activity_state in parameterized_list(ACTIVITY_STATES))
    @unstable_test()
    def test_WHEN_activity_set_via_backdoor_to_clamped_THEN_alarm_major_ELSE_no_alarm(self, _, activity_state):
        self.ca.set_pv_value("ACTIVITY", activity_state)
        if activity_state == "Clamped":
            self.ca.assert_that_pv_alarm_is("ACTIVITY", "MAJOR")
        else:
            self.ca.assert_that_pv_alarm_is("ACTIVITY", "NO_ALARM")

    @parameterized.expand(control_command for control_command in parameterized_list(CONTROL_COMMANDS_WITH_VALUES))
    def test_WHEN_control_command_value_set_THEN_remote_unlocked_set(self, _, control_pv, set_value):
        self.ca.set_pv_value("CONTROL", "Local & Locked")
        self.ca.set_pv_value(control_pv, set_value)
        self.ca.assert_that_pv_is("CONTROL", "Remote & Unlocked")

    @parameterized.expand(control_pv for control_pv in parameterized_list(CONTROL_COMMANDS_WITHOUT_VALUES))
    def test_WHEN_control_command_processed_THEN_remote_unlocked_set(self, _, control_pv):
        self.ca.set_pv_value("CONTROL", "Local & Locked")
        self.ca.process_pv(control_pv)
        self.ca.assert_that_pv_is("CONTROL", "Remote & Unlocked")
コード例 #11
0
class EurothermTests(unittest.TestCase):
    """
    Tests for the Eurotherm temperature controller.
    """
    def setUp(self):
        self._setup_lewis_and_channel_access()
        self._reset_device_state()

    def _setup_lewis_and_channel_access(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            EMULATOR_DEVICE, DEVICE)
        self.ca = ChannelAccess(device_prefix=PREFIX)
        self.ca.assert_that_pv_exists(RBV_PV, timeout=30)
        self.ca.assert_that_pv_exists("CAL:SEL", timeout=10)
        self._lewis.backdoor_set_on_device("address", ADDRESS)

    def _reset_device_state(self):
        self._lewis.backdoor_set_on_device('connected', True)
        reset_calibration_file(self.ca)

        intial_temp = 0.0

        self._set_setpoint_and_current_temperature(intial_temp)

        self._lewis.backdoor_set_on_device("ramping_on", False)
        self._lewis.backdoor_set_on_device("ramp_rate", 1.0)
        self.ca.set_pv_value("RAMPON:SP", 0)

        self._set_setpoint_and_current_temperature(intial_temp)
        self.ca.assert_that_pv_is("TEMP", intial_temp)
        # Ensure the temperature isn't being changed by a ramp any more
        self.ca.assert_that_pv_value_is_unchanged("TEMP", 5)

    def _set_setpoint_and_current_temperature(self, temperature):
        if IOCRegister.uses_rec_sim:
            self.ca.set_pv_value("SIM:TEMP:SP", temperature)
            self.ca.assert_that_pv_is("SIM:TEMP", temperature)
            self.ca.assert_that_pv_is("SIM:TEMP:SP", temperature)
            self.ca.assert_that_pv_is("SIM:TEMP:SP:RBV", temperature)
        else:
            self._lewis.backdoor_set_on_device("current_temperature",
                                               temperature)
            self.ca.assert_that_pv_is_number("TEMP", temperature, 0.1)
            self._lewis.backdoor_set_on_device("ramp_setpoint_temperature",
                                               temperature)
            self.ca.assert_that_pv_is_number("TEMP:SP:RBV", temperature, 0.1)

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_read_rbv_temperature_THEN_rbv_value_is_same_as_backdoor(
            self):
        expected_temperature = 10.0
        self._set_setpoint_and_current_temperature(expected_temperature)
        self.ca.assert_that_pv_is(RBV_PV, expected_temperature)

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_a_sp_WHEN_sp_read_rbv_temperature_THEN_rbv_value_is_same_as_sp(
            self):
        expected_temperature = 10.0
        self.ca.assert_setting_setpoint_sets_readback(expected_temperature,
                                                      "SP:RBV", "SP")

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_set_ramp_rate_in_K_per_min_THEN_current_temperature_reaches_set_point_in_expected_time(
            self):
        start_temperature = 5.0
        ramp_on = 1
        ramp_rate = 60.0
        setpoint_temperature = 25.0

        self._set_setpoint_and_current_temperature(start_temperature)

        self.ca.set_pv_value("RATE:SP", ramp_rate)
        self.ca.assert_that_pv_is_number("RATE", ramp_rate, 0.1)
        self.ca.set_pv_value("RAMPON:SP", ramp_on)
        self.ca.set_pv_value("TEMP:SP", setpoint_temperature)

        start = time.time()
        self.ca.assert_that_pv_is_number("TEMP:SP:RBV",
                                         setpoint_temperature,
                                         tolerance=0.1,
                                         timeout=60)
        end = time.time()
        self.assertAlmostEquals(
            end - start,
            60. * (setpoint_temperature - start_temperature) / ramp_rate,
            delta=0.1 * (end - start)
        )  # Tolerance of 10%. Tolerance of 1s is too tight given scan rate

    @skip_if_recsim("In rec sim this test fails")
    def test_WHEN_sensor_disconnected_THEN_ramp_setting_is_disabled(self):
        self._lewis.backdoor_set_on_device("current_temperature",
                                           SENSOR_DISCONNECTED_VALUE)

        self.ca.assert_that_pv_is_number("RAMPON:SP.DISP", 1)

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_sensor_disconnected_WHEN_sensor_reconnected_THEN_ramp_setting_is_enabled(
            self):
        self._lewis.backdoor_set_on_device("current_temperature",
                                           SENSOR_DISCONNECTED_VALUE)

        self._lewis.backdoor_set_on_device("current_temperature", 0)

        self.ca.assert_that_pv_is_number("RAMPON:SP.DISP", 0)

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_ramp_was_off_WHEN_sensor_disconnected_THEN_ramp_is_off_and_cached_ramp_value_is_off(
            self):
        self.ca.set_pv_value("RAMPON:SP", 0)

        self._lewis.backdoor_set_on_device("current_temperature",
                                           SENSOR_DISCONNECTED_VALUE)

        self.ca.assert_that_pv_is("RAMPON", "OFF")
        self.ca.assert_that_pv_is("RAMPON:CACHE", "OFF")

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_ramp_was_on_WHEN_sensor_disconnected_THEN_ramp_is_off_and_cached_ramp_value_is_on(
            self):
        self.ca.set_pv_value("RAMPON:SP", 1)

        self._lewis.backdoor_set_on_device("current_temperature",
                                           SENSOR_DISCONNECTED_VALUE)

        self.ca.assert_that_pv_is("RAMPON", "OFF")
        self.ca.assert_that_pv_is("RAMPON:CACHE", "ON")

    @skip_if_recsim("In rec sim this test fails")
    def test_GIVEN_ramp_was_on_WHEN_sensor_disconnected_and_reconnected_THEN_ramp_is_on(
            self):
        self.ca.set_pv_value("RAMPON:SP", 1)

        self._lewis.backdoor_set_on_device("current_temperature",
                                           SENSOR_DISCONNECTED_VALUE)
        self.ca.assert_that_pv_is("RAMPON", "OFF")
        self._lewis.backdoor_set_on_device("current_temperature", 0)

        self.ca.assert_that_pv_is("RAMPON", "ON")

    def test_GIVEN_temperature_setpoint_followed_by_calibration_change_WHEN_same_setpoint_set_again_THEN_setpoint_readback_updates_to_set_value(
            self):

        # Arrange
        temperature = 50.0
        rbv_change_timeout = 10
        tolerance = 0.01
        self.ca.set_pv_value("RAMPON:SP", 0)
        reset_calibration_file(self.ca)
        self.ca.set_pv_value("TEMP:SP", temperature)
        self.ca.assert_that_pv_is_number("TEMP:SP:RBV",
                                         temperature,
                                         tolerance=tolerance,
                                         timeout=rbv_change_timeout)
        with use_calibration_file(self.ca, "C006.txt"):
            self.ca.assert_that_pv_is_not_number("TEMP:SP:RBV",
                                                 temperature,
                                                 tolerance=tolerance,
                                                 timeout=rbv_change_timeout)

            # Act
            self.ca.set_pv_value("TEMP:SP", temperature)

            # Assert
            self.ca.assert_that_pv_is_number("TEMP:SP:RBV",
                                             temperature,
                                             tolerance=tolerance,
                                             timeout=rbv_change_timeout)

    def _assert_units(self, units):
        # High timeouts because setting units does not cause processing - wait for normal scan loop to come around.
        self.ca.assert_that_pv_is("TEMP.EGU", units, timeout=30)
        self.ca.assert_that_pv_is("TEMP:SP.EGU", units, timeout=30)
        self.ca.assert_that_pv_is("TEMP:SP:RBV.EGU", units, timeout=30)

    def _assert_using_mock_table_location(self):
        for pv in ["TEMP", "TEMP:SP:CONV", "TEMP:SP:RBV:CONV"]:
            self.ca.assert_that_pv_is(
                "{}.TDIR".format(pv),
                r"eurotherm2k/master/example_temp_sensor")
            self.ca.assert_that_pv_is("{}.BDIR".format(pv),
                                      EPICS_TOP.replace("\\", "/") + "support")

    @skip_if_recsim("Recsim does not use mocked set of tables")
    def test_WHEN_calibration_file_is_in_units_of_K_THEN_egu_of_temperature_pvs_is_K(
            self):
        self._assert_using_mock_table_location()
        with use_calibration_file(self.ca, "K.txt"):
            self._assert_units("K")

    @skip_if_recsim("Recsim does not use mocked set of tables")
    def test_WHEN_calibration_file_is_in_units_of_C_THEN_egu_of_temperature_pvs_is_C(
            self):
        self._assert_using_mock_table_location()
        with use_calibration_file(self.ca, "C.txt"):
            self._assert_units("C")

    @skip_if_recsim("Recsim does not use mocked set of tables")
    def test_WHEN_calibration_file_has_no_units_THEN_egu_of_temperature_pvs_is_K(
            self):
        self._assert_using_mock_table_location()
        with use_calibration_file(self.ca, "None.txt"):
            self._assert_units("K")

    @skip_if_recsim("Recsim does not use mocked set of tables")
    def test_WHEN_config_file_and_temperature_unit_changed_THEN_then_ramp_rate_unit_changes(
            self):
        self._assert_using_mock_table_location()
        with use_calibration_file(self.ca, "None.txt"):
            self._assert_units("K")
            self.ca.assert_that_pv_is("RATE.EGU", "K/min")

        with use_calibration_file(self.ca, "C.txt"):
            self._assert_units("C")
            self.ca.assert_that_pv_is("RATE.EGU", "C/min")

    @parameterized.expand([("under_range_calc_pv_is_under_range",
                            NONE_TXT_CALIBRATION_MIN_TEMPERATURE - 5.0, 1.0),
                           ("under_range_calc_pv_is_within_range",
                            NONE_TXT_CALIBRATION_MIN_TEMPERATURE + 200, 0.0),
                           ("under_range_calc_pv_is_within_range",
                            NONE_TXT_CALIBRATION_MIN_TEMPERATURE, 0.0)])
    @skip_if_recsim("Recsim does not use mocked set of tables")
    def test_GIVEN_None_txt_calibration_file_WHEN_temperature_is_set_THEN(
            self, _, temperature, expected_value_of_under_range_calc_pv):
        # Arrange

        self._assert_using_mock_table_location()
        with use_calibration_file(self.ca, "None.txt"):
            self.ca.assert_that_pv_exists("CAL:RANGE")
            self.ca.assert_that_pv_is("TEMP:RANGE:UNDER.B",
                                      NONE_TXT_CALIBRATION_MIN_TEMPERATURE)

            # Act:
            self._set_setpoint_and_current_temperature(temperature)

            # Assert

            self.ca.assert_that_pv_is("TEMP:RANGE:UNDER.A", temperature)
            self.ca.assert_that_pv_is("TEMP:RANGE:UNDER",
                                      expected_value_of_under_range_calc_pv)

    @parameterized.expand([("over_range_calc_pv_is_over_range",
                            NONE_TXT_CALIBRATION_MAX_TEMPERATURE + 5.0, 1.0),
                           ("over_range_calc_pv_is_within_range",
                            NONE_TXT_CALIBRATION_MAX_TEMPERATURE - 200, 0.0),
                           ("over_range_calc_pv_is_within_range",
                            NONE_TXT_CALIBRATION_MAX_TEMPERATURE, 0.0)])
    @skip_if_recsim("Recsim does not use mocked set of tables")
    def test_GIVEN_None_txt_calibration_file_WHEN_temperature_is_set_THEN(
            self, _, temperature, expected_value_of_over_range_calc_pv):
        # Arrange

        self._assert_using_mock_table_location()
        with use_calibration_file(self.ca, "None.txt"):
            self.ca.assert_that_pv_exists("CAL:RANGE")
            self.ca.assert_that_pv_is("TEMP:RANGE:OVER.B",
                                      NONE_TXT_CALIBRATION_MAX_TEMPERATURE)

            # Act:
            self._set_setpoint_and_current_temperature(temperature)

            # Assert
            self.ca.assert_that_pv_is("TEMP:RANGE:OVER.A", temperature)
            self.ca.assert_that_pv_is("TEMP:RANGE:OVER",
                                      expected_value_of_over_range_calc_pv)

    @skip_if_recsim("Recsim does not use mocked set of tables")
    def test_GIVEN_None_txt_calibration_file_WHEN_changed_to_C006_txt_calibration_file_THEN_the_calibration_limits_change(
            self):
        C006_CALIBRATION_FILE_MAX = 330.26135292267900000000
        C006_CALIBRATION_FILE_MIN = 1.20927230303971000000

        # Arrange
        self._assert_using_mock_table_location()
        with use_calibration_file(self.ca, "None.txt"):
            self.ca.assert_that_pv_exists("CAL:RANGE")
            self.ca.assert_that_pv_is("TEMP:RANGE:OVER.B",
                                      NONE_TXT_CALIBRATION_MAX_TEMPERATURE)
            self.ca.assert_that_pv_is("TEMP:RANGE:UNDER.B",
                                      NONE_TXT_CALIBRATION_MIN_TEMPERATURE)

        # Act:
        with use_calibration_file(self.ca, "C006.txt"):

            # Assert
            self.ca.assert_that_pv_is("TEMP:RANGE:OVER.B",
                                      C006_CALIBRATION_FILE_MAX)
            self.ca.assert_that_pv_is("TEMP:RANGE:UNDER.B",
                                      C006_CALIBRATION_FILE_MIN)

    @parameterized.expand([
        "TEMP", "TEMP:SP:RBV", "P", "I", "D", "AUTOTUNE", "MAX_OUTPUT",
        "LOWLIM"
    ])
    @skip_if_recsim("Can not test disconnection in rec sim")
    def test_WHEN_disconnected_THEN_in_alarm(self, record):
        self._lewis.backdoor_set_on_device('connected', False)
        self.ca.assert_that_pv_alarm_is(record, ChannelAccess.Alarms.INVALID)
コード例 #12
0
class HelioxTests(unittest.TestCase):
    """
    Tests for the heliox IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            EMULATOR_NAME, DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=10)
        self._lewis.backdoor_run_function_on_device("reset")

    def test_WHEN_ioc_is_started_THEN_it_is_not_disabled(self):
        self.ca.assert_that_pv_is("DISABLE", "COMMS ENABLED")

    @parameterized.expand(parameterized_list(TEST_TEMPERATURES))
    def test_WHEN_temperature_setpoint_is_set_THEN_setpoint_readback_updates(
            self, _, temp):
        self.ca.assert_setting_setpoint_sets_readback(
            temp, set_point_pv="TEMP:SP", readback_pv="TEMP:SP:RBV")

    @parameterized.expand(parameterized_list(TEST_TEMPERATURES))
    def test_WHEN_temperature_setpoint_is_set_THEN_actual_temperature_updates(
            self, _, temp):
        self.ca.assert_setting_setpoint_sets_readback(temp,
                                                      set_point_pv="TEMP:SP",
                                                      readback_pv="TEMP")

    @skip_if_recsim("Lewis backdoor is not available in recsim")
    def test_WHEN_temperature_fluctuates_between_stable_and_unstable_THEN_readback_updates(
            self):
        for stable in [True, False, True]:  # Check both transitions
            self._lewis.backdoor_set_on_device("temperature_stable", stable)
            self.ca.assert_that_pv_is("STABILITY",
                                      "Stable" if stable else "Unstable")

    @parameterized.expand(
        parameterized_list(itertools.product(CHANNELS, TEST_TEMPERATURES)))
    @skip_if_recsim("Lewis Backdoor not available in recsim")
    def test_WHEN_individual_channel_temperature_is_set_THEN_readback_updates(
            self, _, chan, temperature):
        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_temperature", [chan, temperature])
        self.ca.assert_that_pv_is_number("{}:TEMP".format(chan),
                                         temperature,
                                         tolerance=0.005)

    @parameterized.expand(
        parameterized_list(itertools.product(CHANNELS, TEST_TEMPERATURES)))
    @skip_if_recsim("Lewis Backdoor not available in recsim")
    def test_WHEN_individual_channel_temperature_setpoint_is_set_THEN_readback_updates(
            self, _, chan, temperature):
        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_temperature_sp", [chan, temperature])
        self.ca.assert_that_pv_is_number("{}:TEMP:SP:RBV".format(chan),
                                         temperature,
                                         tolerance=0.005)

    @parameterized.expand(parameterized_list(CHANNELS_WITH_STABILITY))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_statbility_is_set_via_backdoor_THEN_readback_updates(
            self, _, chan):
        for stability in [True, False, True]:  # Check both transitions
            self._lewis.backdoor_run_function_on_device(
                "backdoor_set_channel_stability", [chan, stability])
            self.ca.assert_that_pv_is("{}:STABILITY".format(chan),
                                      "Stable" if stability else "Unstable")

    @parameterized.expand(parameterized_list(CHANNELS_WITH_HEATER_AUTO))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_channel_heater_auto_is_set_via_backdoor_THEN_readback_updates(
            self, _, chan):
        for heater_auto in [True, False, True]:  # Check both transitions
            self._lewis.backdoor_run_function_on_device(
                "backdoor_set_channel_heater_auto", [chan, heater_auto])
            self.ca.assert_that_pv_is("{}:HEATER:AUTO".format(chan),
                                      "On" if heater_auto else "Off")

    @parameterized.expand(
        parameterized_list(itertools.product(CHANNELS,
                                             TEST_HEATER_PERCENTAGES)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_individual_channel_heater_percentage_is_set_THEN_readback_updates(
            self, _, chan, percent):
        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_heater_percent", [chan, percent])
        self.ca.assert_that_pv_is_number("{}:HEATER:PERCENT".format(chan),
                                         percent,
                                         tolerance=0.005)

    @skip_if_recsim("Cannot properly simulate disconnected device in recsim")
    def test_WHEN_device_disconnected_THEN_temperature_goes_into_alarm(self):
        self.ca.assert_that_pv_alarm_is("TEMP", self.ca.Alarms.NONE)
        self._lewis.backdoor_set_on_device("connected", False)
        self.ca.assert_that_pv_alarm_is("TEMP", self.ca.Alarms.INVALID)

    @skip_if_recsim("Cannot properly simulate disconnected device in recsim")
    @slow_test
    def test_WHEN_device_disconnected_THEN_temperature_comms_error_stays_on_for_at_least_60s_afterwards(
            self):
        """
        Test is slow because the logic under test is checking whether any comms errors have occured in last 120 sec.
        """
        self.ca.assert_that_pv_alarm_is("TEMP", self.ca.Alarms.NONE)
        self.ca.assert_that_pv_is("REGEN:NO_RECENT_COMMS_ERROR",
                                  1,
                                  timeout=150)
        self._lewis.backdoor_set_on_device("connected", False)
        self.ca.assert_that_pv_alarm_is("TEMP", self.ca.Alarms.INVALID)
        # Should immediately indicate that there was an error
        self.ca.assert_that_pv_is("REGEN:NO_RECENT_COMMS_ERROR", 0)
        self._lewis.backdoor_set_on_device("connected", True)
        self.ca.assert_that_pv_alarm_is("TEMP", self.ca.Alarms.NONE)

        # Should stay unchanged for 120s but only assert that it doesn't change for 60 secs.
        self.ca.assert_that_pv_is("REGEN:NO_RECENT_COMMS_ERROR", 0)
        self.ca.assert_that_pv_value_is_unchanged(
            "REGEN:NO_RECENT_COMMS_ERROR", wait=60)

        # Make sure it does eventually clear (within a further 150s)
        self.ca.assert_that_pv_is("REGEN:NO_RECENT_COMMS_ERROR",
                                  1,
                                  timeout=150)

    @contextmanager
    def _simulate_helium_3_pot_empty(self):
        """
        Simulates the helium 3 pot being empty. In this state, the he3 pot temperature will drift towards 1.5K
        regardless of the current temperature setpoint.
        """
        self._lewis.backdoor_set_on_device("helium_3_pot_empty", True)
        try:
            yield
        finally:
            self._lewis.backdoor_set_on_device("helium_3_pot_empty", False)

    @skip_if_recsim(
        "Complex device behaviour (drifting) is not captured in recsim.")
    def test_GIVEN_helium_3_pot_is_empty_WHEN_temperature_stays_above_setpoint_for_coarse_time_THEN_regeneration_logic_detects_this(
            self):
        self.ca.assert_setting_setpoint_sets_readback(
            0.01, readback_pv="TEMP:SP:RBV", set_point_pv="TEMP:SP")
        self.ca.assert_that_pv_is("REGEN:TEMP_COARSE_CHECK",
                                  0,
                                  timeout=(HE3POT_COARSE_TIME + 10))

        with self._simulate_helium_3_pot_empty(
        ):  # Will cause temperature to drift to 1.5K
            self.ca.assert_that_pv_is("REGEN:TEMP_COARSE_CHECK",
                                      1,
                                      timeout=(HE3POT_COARSE_TIME + 10))

        self.ca.assert_that_pv_is("REGEN:TEMP_COARSE_CHECK", 0)

    @parameterized.expand(parameterized_list([0.3, 1.0, 1.234, 5.67, 12.34]))
    @skip_if_recsim(
        "Complex device behaviour (drifting) is not captured in recsim.")
    @slow_test
    def test_GIVEN_helium_3_pot_is_empty_WHEN_drifting_THEN_drift_rate_correct(
            self, _, value):
        self.ca.assert_setting_setpoint_sets_readback(
            0.01, readback_pv="TEMP:SP:RBV", set_point_pv="TEMP:SP")

        self._lewis.backdoor_set_on_device("drift_towards", 9999999999)
        self._lewis.backdoor_set_on_device(
            "drift_rate",
            value / 100)  # Emulator runs at 100x speed in framework

        with self._simulate_helium_3_pot_empty(
        ):  # Will cause temperature to drift upwards continuously
            self.ca.assert_that_pv_is_number(
                "REGEN:_CALCULATE_TEMP_DRIFT.VALB",
                value,
                timeout=(DRIFT_BUFFER_SIZE + 10),
                tolerance=0.05)
            self.ca.assert_that_pv_value_over_time_satisfies_comparator(
                "REGEN:_CALCULATE_TEMP_DRIFT.VALB",
                wait=DRIFT_BUFFER_SIZE,
                comparator=lambda initial, final: abs(
                    initial - final) < 0.05 and abs(value - final) < 0.05)
            self.ca.assert_that_pv_is("REGEN:TEMP_DRIFT_RATE", 1)

        # Assert that if the temperature stops drifting the check goes false (after potentially some delay)
        self.ca.assert_that_pv_is("REGEN:TEMP_DRIFT_RATE",
                                  0,
                                  timeout=(DRIFT_BUFFER_SIZE + 10))

    @parameterized.expand(parameterized_list(HELIOX_STATUSES))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_GIVEN_heliox_status_set_via_backdoor_THEN_status_record_updates(
            self, _, status):
        self._lewis.backdoor_set_on_device("status", status)
        self.ca.assert_that_pv_is("STATUS", status)

    @parameterized.expand(parameterized_list(HELIOX_STATUSES))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_GIVEN_heliox_status_set_via_backdoor_THEN_regeneration_low_temp_status_record_updates(
            self, _, status):
        self._lewis.backdoor_set_on_device("status", status)
        self.ca.assert_that_pv_is("REGEN:LOW_TEMP_MODE",
                                  1 if status == "Low Temp" else 0)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    @slow_test
    def test_WHEN_all_regeneration_conditions_are_met_THEN_regeneration_required_pv_is_true(
            self):
        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_heater_auto", ["HE3SORB", True])
        self.ca.assert_that_pv_is("HE3SORB:HEATER:AUTO", "On")

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_heater_percent", ["HE3SORB", 0.0])
        self.ca.assert_that_pv_is("HE3SORB:HEATER:PERCENT", 0.0)

        self._lewis.backdoor_set_on_device("status", "Low Temp")
        self.ca.assert_that_pv_is("STATUS", "Low Temp")

        self.ca.assert_that_pv_is("REGEN:NO_RECENT_COMMS_ERROR",
                                  1,
                                  timeout=150)

        self._lewis.backdoor_set_on_device("drift_towards", 9999999999)
        self._lewis.backdoor_set_on_device(
            "drift_rate",
            1.0 / 100)  # Emulator runs at 100x speed in framework

        with self._simulate_helium_3_pot_empty():
            self.ca.assert_that_pv_is("REGEN:REQUIRED",
                                      "Yes",
                                      timeout=(DRIFT_BUFFER_SIZE + 10))
コード例 #13
0
class SampleChangerTests(unittest.TestCase):
    """
    Tests for the sample changer.
    """
    def setUp(self):
        self.ca = ChannelAccess(default_timeout=5)

        self.ca.assert_that_pv_exists("SAMPCHNG:SLOT")
        for axis in AXES:
            self.ca.assert_that_pv_exists("MOT:{}".format(axis))

        # Select one of the standard slots.
        self.ca.assert_setting_setpoint_sets_readback(readback_pv="SAMPCHNG:SLOT", value=SLOTS[0])

    @parameterized.expand(parameterized_list([
        {
            "slot_name": "_ALL",
            "positions_exist": ["{}CB".format(n) for n in range(1, 14+1)] + ["{}CT".format(n) for n in range(1, 14+1)],
            "positions_not_exist": [],
        },
        {
            "slot_name": "CT",
            "positions_exist": ["{}CT".format(n) for n in range(1, 14+1)],
            "positions_not_exist": ["{}CB".format(n) for n in range(1, 14+1)],
        },
        {
            "slot_name": "CB",
            "positions_exist": ["{}CB".format(n) for n in range(1, 14+1)],
            "positions_not_exist": ["{}CT".format(n) for n in range(1, 14+1)],
        },
    ]))
    def test_WHEN_slot_set_to_empty_string_THEN_all_positions_listed(self, _, settings):

        self.ca.assert_setting_setpoint_sets_readback(readback_pv="SAMPCHNG:SLOT", value=settings["slot_name"])

        for pos in settings["positions_exist"]:
            self.ca.assert_that_pv_value_causes_func_to_return_true("LKUP:SAMPLE:POSITIONS",
                                                                    func=lambda val: pos in val)

        for pos in settings["positions_not_exist"]:
            self.ca.assert_that_pv_value_causes_func_to_return_true("LKUP:SAMPLE:POSITIONS",
                                                                    func=lambda val: pos not in val)

    def test_WHEN_invalid_slot_is_entered_THEN_old_slot_kept(self):
        # First set a valid slot
        self.ca.assert_setting_setpoint_sets_readback(readback_pv="SAMPCHNG:SLOT", value="CT")

        self.ca.set_pv_value("SAMPCHNG:SLOT:SP", "does_not_exist", sleep_after_set=0)
        self.ca.assert_that_pv_alarm_is("SAMPCHNG:SLOT:SP", self.ca.Alarms.INVALID)
        self.ca.assert_that_pv_is("SAMPCHNG:SLOT", "CT")
        self.ca.assert_that_pv_value_is_unchanged("SAMPCHNG:SLOT", wait=3)

        for pos in ["{}CT".format(n) for n in range(1, 14+1)]:
            self.ca.assert_that_pv_value_causes_func_to_return_true("LKUP:SAMPLE:POSITIONS",
                                                                    func=lambda val: pos in val)

    def test_available_slots_can_be_loaded(self):
        self.ca.assert_that_pv_value_causes_func_to_return_true("SAMPCHNG:AVAILABLE_SLOTS",
                                                                func=lambda val: all(s in val for s in SLOTS))

    @contextlib.contextmanager
    def _temporarily_add_slot(self, new_slot_name):

        file_paths = [os.path.join(test_path, "samplechanger.xml"), os.path.join(test_path, "rack_definitions.xml")]
        xml_trees = {}

        for file_path in file_paths:
            xml_trees[file_path] = etree.parse(file_path)
            shutil.copy2(file_path, file_path + ".backup")

        try:
            for path, tree in xml_trees.items():
                slot = tree.find("//slot")

                # Overwrite an existing slot rather than duplicating, otherwise we end up with duplicate positions
                # and the file fails to write correctly.
                slot.set("name", new_slot_name)

                tree.write(path)

            yield

        finally:
            for file_path in file_paths:
                os.remove(file_path)
                shutil.move(file_path + ".backup", file_path)

    def new_slot_positions_exist(self, new_slot_name):
        def _wrapper(val):
            return any(prefix + new_slot_name in val for prefix in ["1", "A"])
        return _wrapper

    def new_slot_positions_do_not_exist(self, new_slot_name):
        def _wrapper(val):
            # Check none of first 5 positions exist
            return all(str(prefix) + new_slot_name not in val for prefix in ["1", "A"])
        return _wrapper

    def test_GIVEN_sample_changer_file_modified_THEN_new_changer_available(self):
        new_slot_name = "NEWSLOT"
        with self._temporarily_add_slot(new_slot_name):
            # assert new slot has been picked up
            self.ca.assert_that_pv_value_causes_func_to_return_true("SAMPCHNG:AVAILABLE_SLOTS",
                                                                    func=lambda val: new_slot_name in val)

        self.ca.assert_that_pv_value_causes_func_to_return_true("SAMPCHNG:AVAILABLE_SLOTS",
                                                                func=lambda val: new_slot_name not in val)

    def test_GIVEN_sample_changer_file_modified_and_selected_THEN_new_positions_available_without_needing_recalc(self):
        new_slot_name = "NEWSLOT"
        with self._temporarily_add_slot(new_slot_name):
            # assert new slot has been picked up
            self.ca.assert_that_pv_value_causes_func_to_return_true("SAMPCHNG:AVAILABLE_SLOTS",
                                                                    func=lambda val: new_slot_name in val)

            # Select newly added sample changer
            self.ca.set_pv_value("SAMPCHNG:SLOT:SP", new_slot_name)

            # Assert positions from newly added sample changer exist
            # Position names are slot_name + either 1 or A depending on naming convention of slot
            self.ca.assert_that_pv_value_causes_func_to_return_true("LKUP:SAMPLE:POSITIONS",
                                                                    func=self.new_slot_positions_exist(new_slot_name))

        # Now out of context manager, NEWSLOT no longer exists
        self.ca.assert_that_pv_value_causes_func_to_return_true("SAMPCHNG:AVAILABLE_SLOTS",
                                                                func=lambda val: new_slot_name not in val)

        # Use all positions from all available changers
        self.ca.set_pv_value("SAMPCHNG:SLOT:SP", "_ALL")

        # Positions from the now-deleted changer shouldn't exist
        self.ca.assert_that_pv_value_causes_func_to_return_true("LKUP:SAMPLE:POSITIONS",
                                                                func=self.new_slot_positions_do_not_exist(new_slot_name))

    def test_GIVEN_sample_changer_file_modified_THEN_new_positions_available(self):
        # Select all positions from all changers
        self.ca.set_pv_value("SAMPCHNG:SLOT:SP", "_ALL")

        new_slot_name = "NEWSLOT"
        with self._temporarily_add_slot(new_slot_name):
            # assert new slot has been picked up
            self.ca.assert_that_pv_value_causes_func_to_return_true("SAMPCHNG:AVAILABLE_SLOTS",
                                                                    func=lambda val: new_slot_name in val)

            self.ca.set_pv_value("SAMPCHNG:RECALC.PROC", 1)

            # Assert positions from newly added sample changer exist
            self.ca.assert_that_pv_value_causes_func_to_return_true("LKUP:SAMPLE:POSITIONS",
                                                                    func=self.new_slot_positions_exist(new_slot_name))

        # Now out of context manager, NEWSLOT no longer exists
        self.ca.assert_that_pv_value_causes_func_to_return_true("SAMPCHNG:AVAILABLE_SLOTS",
                                                                func=lambda val: new_slot_name not in val)

        # Explicit recalc as we are still looking at all positions so haven't changed sample changer.
        self.ca.set_pv_value("SAMPCHNG:RECALC.PROC", 1)

        # Positions from the now-deleted changer shouldn't exist
        self.ca.assert_that_pv_value_causes_func_to_return_true("LKUP:SAMPLE:POSITIONS",
                                                                func=self.new_slot_positions_do_not_exist(new_slot_name))