class MercuryTests(unittest.TestCase):
    """
    Tests for the Mercury IOC.
    """
    def setUp(self):
        self._lewis, self._ioc = get_running_lewis_and_ioc(
            "mercuryitc", DEVICE_PREFIX)
        self._lewis.backdoor_set_on_device("connected", True)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX,
                                default_timeout=20)
        card_pv_prefix = get_card_pv_prefix(TEMP_CARDS[0])
        self.ca.assert_setting_setpoint_sets_readback(
            "OFF",
            readback_pv="{}:SPC".format(card_pv_prefix),
            expected_alarm=self.ca.Alarms.MAJOR)

    @parameterized.expand(
        parameterized_list(
            itertools.product(PID_PARAMS, PID_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_pid_params_set_via_backdoor_THEN_readback_updates(
            self, _, param, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [card, param.lower(), test_value])
        self.ca.assert_that_pv_is("{}:{}".format(card_pv_prefix, param),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(PID_PARAMS, PID_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_pid_params_set_THEN_readback_updates(self, _, param,
                                                       test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            test_value,
            readback_pv="{}:{}".format(card_pv_prefix, param),
            set_point_pv="{}:{}:SP".format(card_pv_prefix, param))

    @parameterized.expand(
        parameterized_list(
            itertools.product(AUTOPID_MODES, TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_autopid_set_THEN_readback_updates(self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            test_value,
            readback_pv="{}:PID:AUTO".format(card_pv_prefix),
            set_point_pv="{}:PID:AUTO:SP".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMPERATURE_TEST_VALUES, TEMP_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_actual_temp_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property", [card, "temperature", test_value])
        self.ca.assert_that_pv_is("{}:TEMP".format(card_pv_prefix), test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMPERATURE_TEST_VALUES, PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_actual_pressure_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property", [card, "pressure", test_value])
        self.ca.assert_that_pv_is("{}:PRESSURE".format(card_pv_prefix),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(RESISTANCE_TEST_VALUES, TEMP_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_resistance_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property", [card, "resistance", test_value])
        self.ca.assert_that_pv_is("{}:RESISTANCE".format(card_pv_prefix),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(RESISTANCE_TEST_VALUES, PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_voltage_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property", [card, "voltage", test_value])
        self.ca.assert_that_pv_is("{}:VOLT".format(card_pv_prefix), test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMPERATURE_TEST_VALUES, TEMP_CARDS)))
    def test_WHEN_sp_temp_is_set_THEN_pv_updates(self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            test_value,
            set_point_pv="{}:TEMP:SP".format(card_pv_prefix),
            readback_pv="{}:TEMP:SP:RBV".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMPERATURE_TEST_VALUES, PRESSURE_CARDS)))
    def test_WHEN_sp_pressure_is_set_THEN_pv_updates(self, _, test_value,
                                                     card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            test_value,
            set_point_pv="{}:PRESSURE:SP".format(card_pv_prefix),
            readback_pv="{}:PRESSURE:SP:RBV".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_MODES, TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_heater_mode_is_set_THEN_pv_updates(self, _, mode, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            mode,
            set_point_pv="{}:HEATER:MODE:SP".format(card_pv_prefix),
            readback_pv="{}:HEATER:MODE".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(GAS_FLOW_MODES, TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_gas_flow_mode_is_set_THEN_pv_updates(self, _, mode, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            mode,
            set_point_pv="{}:FLOW:STAT:SP".format(card_pv_prefix),
            readback_pv="{}:FLOW:STAT".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(GAS_FLOW_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_gas_flow_is_set_THEN_pv_updates(self, _, mode, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            mode,
            set_point_pv="{}:FLOW:SP".format(card_pv_prefix),
            readback_pv="{}:FLOW".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_PERCENT_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_heater_percent_is_set_THEN_pv_updates(self, _, mode, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            mode,
            set_point_pv="{}:HEATER:SP".format(card_pv_prefix),
            readback_pv="{}:HEATER".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_PERCENT_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_heater_voltage_limit_is_set_THEN_pv_updates(
            self, _, mode, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            mode,
            set_point_pv="{}:HEATER:VOLT_LIMIT:SP".format(card_pv_prefix),
            readback_pv="{}:HEATER:VOLT_LIMIT".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_PERCENT_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_power_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        heater_chan_name = self.ca.get_pv_value(
            "{}:HTRCHAN".format(card_pv_prefix))

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [heater_chan_name, "power", test_value])
        self.ca.assert_that_pv_is("{}:HEATER:POWER".format(card_pv_prefix),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_PERCENT_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_curr_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        heater_chan_name = self.ca.get_pv_value(
            "{}:HTRCHAN".format(card_pv_prefix))

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [heater_chan_name, "current", test_value])
        self.ca.assert_that_pv_is("{}:HEATER:CURR".format(card_pv_prefix),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(HEATER_PERCENT_TEST_VALUES,
                              TEMP_CARDS + PRESSURE_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_heater_voltage_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        heater_chan_name = self.ca.get_pv_value(
            "{}:HTRCHAN".format(card_pv_prefix))

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [heater_chan_name, "voltage", test_value])
        self.ca.assert_that_pv_is("{}:HEATER:VOLT".format(card_pv_prefix),
                                  test_value)

    @parameterized.expand(
        parameterized_list(
            itertools.product(MOCK_NICKNAMES,
                              TEMP_CARDS + PRESSURE_CARDS + LEVEL_CARDS)))
    def test_WHEN_name_is_set_THEN_pv_updates(self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self.ca.assert_setting_setpoint_sets_readback(
            test_value,
            readback_pv="{}:NAME".format(card_pv_prefix),
            set_point_pv="{}:NAME:SP".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(GAS_LEVEL_TEST_VALUES, LEVEL_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_helium_level_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [card, "helium_level", test_value])
        self.ca.assert_that_pv_is_number("{}:HELIUM".format(card_pv_prefix),
                                         test_value,
                                         tolerance=0.01)

    @parameterized.expand(
        parameterized_list(
            itertools.product(GAS_LEVEL_TEST_VALUES, LEVEL_CARDS)))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_nitrogen_level_is_set_via_backdoor_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)

        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [card, "nitrogen_level", test_value])
        self.ca.assert_that_pv_is_number("{}:NITROGEN".format(card_pv_prefix),
                                         test_value,
                                         tolerance=0.01)

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMP_CARDS + PRESSURE_CARDS, HEATER_CARDS)))
    def test_WHEN_heater_association_is_set_THEN_pv_updates(
            self, _, parent_card, associated_card):
        card_pv_prefix = get_card_pv_prefix(parent_card)

        with ManagerMode(ChannelAccess()):
            self.ca.assert_setting_setpoint_sets_readback(
                associated_card, "{}:HTRCHAN".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(
            itertools.product(TEMP_CARDS + PRESSURE_CARDS, AUX_CARDS)))
    def test_WHEN_aux_association_is_set_THEN_pv_updates(
            self, _, parent_card, associated_card):
        card_pv_prefix = get_card_pv_prefix(parent_card)
        with ManagerMode(ChannelAccess()):
            self.ca.assert_setting_setpoint_sets_readback(
                associated_card, "{}:AUXCHAN".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list(itertools.product(HELIUM_READ_RATES, LEVEL_CARDS)))
    def test_WHEN_he_read_rate_is_set_THEN_pv_updates(self, _, test_value,
                                                      card):
        card_pv_prefix = get_card_pv_prefix(card)
        self.ca.assert_setting_setpoint_sets_readback(
            test_value, "{}:HELIUM:READ_RATE".format(card_pv_prefix))

    @parameterized.expand(
        parameterized_list([
            ("CATALOG:PARSE.VALA", TEMP_CARDS),
            ("CATALOG:PARSE.VALB", PRESSURE_CARDS),
            ("CATALOG:PARSE.VALC", LEVEL_CARDS),
            ("CATALOG:PARSE.VALD", HEATER_CARDS),
            ("CATALOG:PARSE.VALE", AUX_CARDS),
        ]))
    @skip_if_recsim("Complex logic not tested in recsim")
    def test_WHEN_getting_catalog_it_contains_all_cards(self, _, pv, cards):
        for card in cards:
            self.ca.assert_that_pv_value_causes_func_to_return_true(
                pv, lambda val: card in val)

    @parameterized.expand(
        parameterized_list(
            itertools.product(MOCK_CALIB_FILES, TEMP_CARDS + PRESSURE_CARDS)))
    def test_WHEN_setting_calibration_file_THEN_pv_updates(
            self, _, test_value, card):
        card_pv_prefix = get_card_pv_prefix(card)
        with ManagerMode(ChannelAccess()):
            self.ca.assert_setting_setpoint_sets_readback(
                test_value, "{}:CALFILE".format(card_pv_prefix))

    @parameterized.expand(parameterized_list(["O", "R"]))
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_resistance_suffix_is_changed_THEN_resistance_reads_correctly(
            self, _, resistance_suffix):
        self._lewis.backdoor_set_on_device("resistance_suffix",
                                           resistance_suffix)
        resistance_value = 3
        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [PRIMARY_TEMPERATURE_CHANNEL, "resistance", resistance_value])
        self.ca.assert_that_pv_is(
            "{}:RESISTANCE".format(
                get_card_pv_prefix(PRIMARY_TEMPERATURE_CHANNEL)),
            resistance_value)
        self.ca.assert_that_pv_alarm_is(
            "{}:RESISTANCE".format(
                get_card_pv_prefix(PRIMARY_TEMPERATURE_CHANNEL)),
            self.ca.Alarms.NONE)

    def test_WHEN_auto_flow_set_THEN_pv_updates_and_states_are_set(self):
        card_pv_prefix = get_card_pv_prefix(TEMP_CARDS[0])
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])

        self.ca.set_pv_value("{}:PID:AUTO:SP".format(card_pv_prefix), "OFF")
        self.ca.set_pv_value("{}:FLOW:STAT:SP".format(card_pv_prefix), "Auto")
        self.ca.set_pv_value("{}:HEATER:MODE:SP".format(card_pv_prefix),
                             "Manual")
        self.ca.set_pv_value("{}:FLOW:STAT:SP".format(pressure_card_pv_prefix),
                             "Manual")

        self.ca.assert_setting_setpoint_sets_readback(
            "ON",
            set_point_pv="{}:SPC:SP".format(card_pv_prefix),
            readback_pv="{}:SPC".format(card_pv_prefix))
        self.ca.assert_that_pv_is("{}:PID:AUTO".format(card_pv_prefix), "ON")
        self.ca.assert_that_pv_is("{}:FLOW:STAT".format(card_pv_prefix),
                                  "Manual")
        self.ca.assert_that_pv_is("{}:HEATER:MODE".format(card_pv_prefix),
                                  "Auto")
        self.ca.assert_that_pv_is(
            "{}:FLOW:STAT".format(pressure_card_pv_prefix), "Auto")
        self.ca.assert_that_pv_is("{}:SPC:SP".format(pressure_card_pv_prefix),
                                  "ON")

    def test_WHEN_auto_flow_set_off_THEN_pv_updates_and_states_are_not_set(
            self):
        card_pv_prefix = get_card_pv_prefix(TEMP_CARDS[0])
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])

        self.ca.set_pv_value("{}:PID:AUTO:SP".format(card_pv_prefix), "OFF")
        self.ca.set_pv_value("{}:FLOW:STAT:SP".format(card_pv_prefix), "Auto")
        self.ca.set_pv_value("{}:HEATER:MODE:SP".format(card_pv_prefix),
                             "Manual")
        self.ca.set_pv_value("{}:FLOW:STAT:SP".format(pressure_card_pv_prefix),
                             "Manual")

        self.ca.assert_setting_setpoint_sets_readback(
            "OFF",
            set_point_pv="{}:SPC:SP".format(card_pv_prefix),
            readback_pv="{}:SPC".format(card_pv_prefix),
            expected_alarm=self.ca.Alarms.MAJOR)
        self.ca.assert_that_pv_is("{}:PID:AUTO".format(card_pv_prefix), "OFF")
        self.ca.assert_that_pv_is("{}:FLOW:STAT".format(card_pv_prefix),
                                  "Auto")
        self.ca.assert_that_pv_is("{}:HEATER:MODE".format(card_pv_prefix),
                                  "Manual")
        self.ca.assert_that_pv_is(
            "{}:FLOW:STAT".format(pressure_card_pv_prefix), "Manual")
        self.ca.assert_that_pv_is("{}:SPC:SP".format(pressure_card_pv_prefix),
                                  "OFF")

    def set_temp_reading_and_sp(self, reading, set_point, spc_state="On"):
        """
        Set the temperature in lewis and the set point on the first card
        :param reading: The reading lewis will return
        :param set_point: The set point to set on the device
        :param spc_state: State to set SPC to (defaults to On)
        """
        card_pv_prefix = get_card_pv_prefix(TEMP_CARDS[0])
        self.ca.set_pv_value("{}:SPC:SP".format(card_pv_prefix), spc_state)
        self.ca.set_pv_value("{}:TEMP:SP".format(card_pv_prefix), set_point)
        self._lewis.backdoor_run_function_on_device(
            "backdoor_set_channel_property",
            [TEMP_CARDS[0], "temperature", reading])

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_set_on_and_temp_lower_than_2_deadbands_THEN_pressure_set_to_minimum_pressure(
            self):
        set_point = 10
        reading = 10 - SPC_TEMP_DEADBAND * 2.1
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        self.set_temp_reading_and_sp(reading, set_point)

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP:RBV".format(pressure_card_pv_prefix),
            SPC_MIN_PRESSURE)

    @parameterized.expand([(10, ), (1, ), (300, ), (12, ), (20, )])
    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_set_on_and_temp_low_but_within_1_to_2_deadbands_THEN_pressure_set_to_pressure_for_setpoint_temp_and_does_not_ramp(
            self, set_point):
        reading = set_point - SPC_TEMP_DEADBAND * 1.5
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        self.set_temp_reading_and_sp(reading, set_point)

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP:RBV".format(pressure_card_pv_prefix),
            pressure_for(set_point))
        sleep(1.5)
        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP:RBV".format(pressure_card_pv_prefix),
            pressure_for(set_point))

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_set_on_and_temp_at_setpoint_THEN_pressure_set_to_pressure_for_setpoint_temp_and_does_ramp_down(
            self):
        set_point = 10
        reading = set_point
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        self.set_temp_reading_and_sp(reading, set_point)

        self.ca.assert_that_pv_is_number(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix),
            pressure_for(set_point) + SPC_OFFSET,
            tolerance=SPC_OFFSET / 4)  # should see number in ramp
        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix),
            pressure_for(set_point))  # final value

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_set_on_and_temp_above_setpoint_by_more_than_deadband_THEN_pressure_set_to_pressure_for_setpoint_temp_plus_gain_and_does_ramp(
            self):
        diff = SPC_TEMP_DEADBAND * 1.1
        set_point = 10
        reading = set_point + diff
        expected_pressure = pressure_for(set_point) + SPC_OFFSET + (
            abs(reading - set_point - SPC_TEMP_DEADBAND) * SPC_GAIN)**2
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        self.set_temp_reading_and_sp(reading, set_point)

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix),
            expected_pressure)  # final value
        sleep(1.5)  # wait for possible ramp
        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix),
            expected_pressure)  # final value

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_set_on_and_pressure_would_be_high_THEN_pressure_set_to_maximum_pressure(
            self):
        diff = 1000
        set_point = 10
        reading = set_point + diff
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])

        self.set_temp_reading_and_sp(reading, set_point)

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix),
            SPC_MAX_PRESSURE)  # final value

    def test_WHEN_auto_flow_set_off_THEN_pressure_is_not_updated(self):
        diff = 1000
        set_point = 10
        reading = set_point + diff

        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        expected_value = -10
        self.ca.set_pv_value("{}:PRESSURE:SP".format(pressure_card_pv_prefix),
                             expected_value)

        self.set_temp_reading_and_sp(reading, set_point, "OFF")

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix), expected_value)

    @skip_if_recsim("Lewis backdoor not available in recsim")
    def test_WHEN_auto_flow_on_but_error_in_temp_readback_THEN_pressure_is_not_updated(
            self):
        set_point = 10

        card_pv_prefix = get_card_pv_prefix(TEMP_CARDS[0])
        pressure_card_pv_prefix = get_card_pv_prefix(PRESSURE_CARDS[0])
        expected_value = -10
        self.ca.set_pv_value("{}:PRESSURE:SP".format(pressure_card_pv_prefix),
                             expected_value)
        self._lewis.backdoor_set_on_device("connected", False)
        self.ca.assert_that_pv_alarm_is(
            "{}:TEMP:SP:RBV".format(card_pv_prefix), self.ca.Alarms.INVALID)

        self.ca.set_pv_value("{}:SPC:SP".format(card_pv_prefix), "ON")
        self.ca.set_pv_value("{}:TEMP:SP".format(card_pv_prefix), set_point)

        self.ca.assert_that_pv_is(
            "{}:PRESSURE:SP".format(pressure_card_pv_prefix), expected_value)
Exemplo n.º 2
0
class LSITests(unittest.TestCase):
    """
    Tests for LSi Correlator
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("LSI")
        self.ca = ChannelAccess(default_timeout=30,
                                device_prefix=DEVICE_PREFIX)

    def test_GIVEN_setting_pv_WHEN_pv_written_to_THEN_new_value_read_back(
            self):
        pv_name = "MEASUREMENTDURATION"
        pv_value = 1000

        self.ca.set_pv_value(pv_name, pv_value)
        self.ca.assert_that_pv_is_number(pv_name, pv_value)

    def test_GIVEN_setting_pv_WHEN_pv_written_to_with_invalid_value_THEN_value_not_updated(
            self):
        pv_name = "MEASUREMENTDURATION"
        original_value = self.ca.get_pv_value(pv_name)

        self.ca.set_pv_value(pv_name, -1)
        self.ca.assert_that_pv_is_number(pv_name, original_value)

    def test_GIVEN_integer_device_setting_WHEN_pv_written_to_with_a_float_THEN_value_is_rounded_before_setting(
            self):
        pv_name = "MEASUREMENTDURATION"
        new_value = 12.3

        self.ca.set_pv_value(pv_name, new_value)
        self.ca.assert_that_pv_is_number(pv_name, 12)

    def test_GIVEN_monitor_on_setting_pv_WHEN_pv_changed_THEN_monitor_gets_updated(
            self):
        pv_name = "MEASUREMENTDURATION"
        self.ca.set_pv_value(pv_name, 10.0, wait=True)
        new_value = 12.3
        expected_value = 12.0

        with self.ca.assert_that_pv_monitor_is(pv_name, expected_value):
            self.ca.set_pv_value(pv_name + ":SP", new_value)

    def test_GIVEN_invalid_value_for_setting_WHEN_setting_pv_written_THEN_status_pv_updates_with_error(
            self):
        setting_pv = "MEASUREMENTDURATION"
        self.ca.set_pv_value(setting_pv, -1)
        error_message = "LSI --- wrong value assigned to MeasurementDuration"

        self.ca.assert_that_pv_is("ERRORMSG", error_message)

    @parameterized.expand([
        ("NORMALIZATION", ("SYMMETRIC", "COMPENSATED")),
        ("SWAPCHANNELS", ("ChA_ChB", "ChB_ChA")),
        ("CORRELATIONTYPE", ("AUTO", "CROSS")),
        ("TRANSFERRATE", ("ms100", "ms150", "ms200", "ms250", "ms300", "ms400",
                          "ms500", "ms600", "ms700")),
        ("SAMPLINGTIMEMULTIT", ("ns12_5", "ns200", "ns400", "ns800", "ns1600",
                                "ns3200"))
    ])
    def test_GIVEN_enum_setting_WHEN_setting_pv_written_to_THEN_new_value_read_back(
            self, pv, values):
        for value in values:
            self.ca.set_pv_value(pv, value, sleep_after_set=0.0)
            self.ca.assert_that_pv_is(pv, value)

    @parameterized.expand([("OVERLOADLIMIT", "Mcps"),
                           ("SCATTERING_ANGLE", "degree"),
                           ("SAMPLE_TEMP", "K"), ("SOLVENT_VISCOSITY", "mPas"),
                           ("SOLVENT_REFRACTIVE_INDEX", ""),
                           ("LASER_WAVELENGTH", "nm")])
    def test_GIVEN_pv_with_unit_WHEN_EGU_field_read_from_THEN_unit_returned(
            self, pv, expected_unit):
        self.ca.assert_that_pv_is("{pv}.EGU".format(pv=pv), expected_unit)

    @parameterized.expand([
        ("CORRELATION_FUNCTION", 400),
        ("LAGS", 400),
    ])
    def test_GIVEN_array_pv_WHEN_NELM_field_read_THEN_length_of_array_returned(
            self, pv, expected_length):
        self.ca.assert_that_pv_is_number("{pv}.NELM".format(pv=pv),
                                         expected_length)

    @parameterized.expand(parameterized_list(SETTING_PVS))
    def test_GIVEN_pv_name_THEN_setpoint_exists_for_that_pv(
            self, _, pv, value):
        self.ca.assert_setting_setpoint_sets_readback(value, pv)

    @parameterized.expand(parameterized_list(PV_NAMES))
    def test_GIVEN_pv_name_THEN_val_field_exists_for_that_pv(self, _, pv):
        self.ca.assert_that_pv_is("{pv}.VAL".format(pv=pv),
                                  self.ca.get_pv_value(pv))

    @parameterized.expand(parameterized_list(PV_NAMES))
    def test_GIVEN_pv_WHEN_pv_read_THEN_pv_has_no_alarms(self, _, pv):
        self.ca.assert_that_pv_alarm_is(pv, self.ca.Alarms.NONE)

    @parameterized.expand(parameterized_list(["CORRELATION_FUNCTION", "LAGS"]))
    def test_GIVEN_start_pressed_WHEN_measurement_is_possible_THEN_correlation_and_lags_populated(
            self, _, pv):
        self.ca.assert_that_pv_is("RUNNING", "NO", timeout=10)

        self.ca.set_pv_value("START", 1, sleep_after_set=0.0)

        array_size = self.ca.get_pv_value("{pv}.NELM".format(pv=pv))

        test_data = np.linspace(0, array_size, array_size)

        self.ca.assert_that_pv_value_causes_func_to_return_true(
            pv, lambda pv_value: np.allclose(pv_value, test_data))

    def test_GIVEN_start_pressed_WHEN_measurement_already_on_THEN_error_raised(
            self):
        self.ca.set_pv_value("START", 1, sleep_after_set=0.0)
        self.ca.set_pv_value("START", 1, sleep_after_set=0.0)

        error_message = "LSI --- Cannot configure: Measurement active"

        self.ca.assert_that_pv_is("ERRORMSG", error_message)
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)
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)
Exemplo n.º 5
0
class MezfliprTests(unittest.TestCase):

    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=30)

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

    @parameterized.expand(parameterized_list(["On", "Off"]))
    def test_GIVEN_power_is_set_THEN_can_be_read_back(self, _, state):
        self.ca.assert_setting_setpoint_sets_readback(state, readback_pv="{}:POWER".format(flipper))

    @parameterized.expand(parameterized_list([0., 0.12, 5000.5]))
    def test_GIVEN_compensation_is_set_THEN_compensation_can_be_read_back(self, _, compensation):
        self.ca.assert_setting_setpoint_sets_readback(compensation, readback_pv="{}:COMPENSATION".format(flipper))

    def _assert_mode(self, mode):
        self.ca.assert_that_pv_is("{}:MODE".format(flipper), mode)
        self.ca.assert_that_pv_alarm_is("{}:MODE".format(flipper), self.ca.Alarms.NONE)

    def _assert_params(self, param):
        self.ca.assert_that_pv_value_causes_func_to_return_true("{}:PARAMS".format(flipper),
                                                                lambda val: val is not None and val.rstrip() == param)
        self.ca.assert_that_pv_alarm_is("{}:PARAMS".format(flipper), self.ca.Alarms.NONE)

    @skip_if_recsim("State of device not simulated in recsim")
    def test_WHEN_constant_current_mode_set_THEN_parameters_reflected_and_mode_is_constant_current(self):
        param = 25
        self.ca.set_pv_value("{}:CURRENT:SP".format(flipper), param)

        self._assert_params("{:.1f}".format(param))
        self._assert_mode("static")

    @skip_if_recsim("State of device not simulated in recsim")
    def test_WHEN_steps_mode_set_THEN_parameters_reflected_and_mode_is_steps(self):
        param = "[some, random, list, of, data]"
        self.ca.set_pv_value("{}:CURRENT_STEPS:SP".format(flipper), param)

        self._assert_params(param)
        self._assert_mode("steps")

    @skip_if_recsim("State of device not simulated in recsim")
    def test_WHEN_analytical_mode_set_THEN_parameters_reflected_and_mode_is_analytical(self):
        param = "a long string of parameters which is longer than 40 characters"
        self.ca.set_pv_value("{}:CURRENT_ANALYTICAL:SP".format(flipper), param)

        self._assert_params(param)
        self._assert_mode("analytical")

    @skip_if_recsim("State of device not simulated in recsim")
    def test_WHEN_file_mode_set_THEN_parameters_reflected_and_mode_is_file(self):
        param = r"C:\some\file\path\to\a\file\in\a\really\deep\directory\structure\with\path\longer\than\40\characters"
        self.ca.set_pv_value("{}:FILENAME:SP".format(flipper), param)

        self._assert_params(param)
        self._assert_mode("file")

    @parameterized.expand(parameterized_list(["MODE", "COMPENSATION", "PARAMS"]))
    @skip_if_recsim("Recsim cannot test disconnected device")
    def test_WHEN_device_is_disconnected_THEN_pvs_are_in_invalid_alarm(self, _, pv):
        try:
            self._lewis.backdoor_set_on_device("connected", False)
            self.ca.assert_that_pv_alarm_is("{}:{}".format(flipper, pv), self.ca.Alarms.INVALID)
        finally:
            self._lewis.backdoor_set_on_device("connected", True)
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))