class ReflTests(unittest.TestCase):
    """
    Tests for reflectometry server
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running("refl")
        self.ca = ChannelAccess(default_timeout=30,
                                device_prefix=DEVICE_PREFIX)
        self.ca_galil = ChannelAccess(default_timeout=30, device_prefix="MOT")
        self.ca_cs = ChannelAccess(default_timeout=30, device_prefix="CS")
        self.ca_no_prefix = ChannelAccess()
        self.ca_cs.set_pv_value("MOT:STOP:ALL", 1)
        self.ca_cs.assert_that_pv_is("MOT:MOVING", 0, timeout=60)
        self.ca.set_pv_value("BL:MODE:SP", "NR")
        self.ca.set_pv_value("PARAM:S1:SP", 0)
        self.ca.set_pv_value("PARAM:S3:SP", 0)
        self.ca.set_pv_value("PARAM:SMANGLE:SP", 0)
        self.ca.set_pv_value("PARAM:SMOFFSET:SP", 0)
        self.ca.set_pv_value("PARAM:SMINBEAM:SP", "OUT")
        self.ca.set_pv_value("PARAM:THETA:SP", 0)
        self.ca.set_pv_value("PARAM:DET_POS:SP", 0)
        self.ca.set_pv_value("PARAM:DET_ANG:SP", 0)
        self.ca.set_pv_value("PARAM:DET_LONG:SP", 0)
        self.ca.set_pv_value("PARAM:S3INBEAM:SP", "IN")
        self.ca.set_pv_value("PARAM:CHOICE:SP", "MTR0205")
        self.ca_galil.set_pv_value("MTR0207", 0)
        self.ca.set_pv_value("PARAM:NOTINMODE:SP", 0)
        self.ca.set_pv_value("BL:MODE:SP", "NR")
        self.ca.set_pv_value("BL:MOVE", 1)
        self.ca_galil.assert_that_pv_is("MTR0105", 0.0)
        self.ca_cs.assert_that_pv_is("MOT:MOVING", 0, timeout=60)

    def set_up_velocity_tests(self, velocity):
        self.ca_galil.set_pv_value("MTR0102.VELO", velocity)
        self.ca_galil.set_pv_value("MTR0104.VELO", velocity)
        self.ca_galil.set_pv_value(
            "MTR0105.VELO",
            FAST_VELOCITY)  # Remove angle as a speed limiting factor

    def _check_param_pvs(self, param_name, expected_value):
        self.ca.assert_that_pv_is_number("PARAM:%s" % param_name,
                                         expected_value, 0.01)
        self.ca.assert_that_pv_is_number("PARAM:%s:SP" % param_name,
                                         expected_value, 0.01)
        self.ca.assert_that_pv_is_number("PARAM:%s:SP:RBV" % param_name,
                                         expected_value, 0.01)

    @contextmanager
    def _assert_pv_monitors(self, param_name, expected_value):
        with self.ca.assert_that_pv_monitor_is_number("PARAM:%s" % param_name, expected_value, 0.01), \
             self.ca.assert_that_pv_monitor_is_number("PARAM:%s:SP" % param_name, expected_value, 0.01), \
             self.ca.assert_that_pv_monitor_is_number("PARAM:%s:SP:RBV" % param_name, expected_value, 0.01):
            yield

    def test_GIVEN_loaded_WHEN_read_status_THEN_status_ok(self):
        self.ca.assert_that_pv_is("STAT", "OKAY")

    def test_GIVEN_slit_with_beam_along_z_axis_WHEN_set_value_THEN_read_back_MTR_and_setpoints_moves_to_given_value(
            self):
        expected_value = 3.0

        self.ca.set_pv_value("PARAM:S1:SP_NO_ACTION", expected_value)
        self.ca.assert_that_pv_is("PARAM:S1:SP_NO_ACTION", expected_value)
        self.ca.set_pv_value("BL:MOVE", 1)

        self.ca.assert_that_pv_is("PARAM:S1:SP:RBV", expected_value)
        self.ca_galil.assert_that_pv_is("MTR0101", expected_value)
        self.ca_galil.assert_that_pv_is("MTR0101.RBV", expected_value)
        self.ca.assert_that_pv_is("PARAM:S1", expected_value)

    def test_GIVEN_slit_with_beam_along_z_axis_WHEN_set_value_THEN_monitors_updated(
            self):
        expected_value = 3.0

        self.ca.set_pv_value("PARAM:S1:SP_NO_ACTION", expected_value)
        self.ca.set_pv_value("BL:MOVE", 1)
        self.ca.assert_that_pv_monitor_is("PARAM:S1", expected_value)

    def test_GIVEN_theta_with_detector_and_slits3_WHEN_set_theta_THEN_values_are_all_correct_rbvs_updated_via_monitors_and_are_available_via_gets(
            self):
        theta_angle = 2
        self.ca.set_pv_value("PARAM:THETA:SP", theta_angle)

        expected_s3_value = SPACING * tan(radians(theta_angle * 2.0))

        with self._assert_pv_monitors("S1", 0.0), \
             self._assert_pv_monitors("S3", 0.0), \
             self._assert_pv_monitors("THETA", theta_angle), \
             self._assert_pv_monitors("DET_POS", 0.0), \
             self._assert_pv_monitors("DET_ANG", 0.0):

            self.ca.set_pv_value("BL:MOVE", 1)

        # s1 not moved
        self._check_param_pvs("S1", 0.0)
        self.ca_galil.assert_that_pv_is_number("MTR0101", 0.0, 0.01)

        # s3 moved in line
        self._check_param_pvs("S3", 0.0)
        self.ca_galil.assert_that_pv_is_number("MTR0102", expected_s3_value,
                                               0.01)

        # theta set
        self._check_param_pvs("THETA", theta_angle)

        # detector moved in line
        self._check_param_pvs("DET_POS", 0.0)
        expected_det_value = 2 * SPACING * tan(radians(theta_angle * 2.0))
        self.ca_galil.assert_that_pv_is_number("MTR0104", expected_det_value,
                                               0.01)

        # detector angle faces beam
        self._check_param_pvs("DET_ANG", 0.0)
        expected_det_angle = 2.0 * theta_angle
        self.ca_galil.assert_that_pv_is_number("MTR0105", expected_det_angle,
                                               0.01)

    def test_GIVEN_s3_in_beam_WHEN_disable_THEN_monitor_updates_and_motor_moves_to_out_of_beam_position(
            self):
        expected_value = "OUT"

        with self.ca.assert_that_pv_monitor_is("PARAM:S3INBEAM",
                                               expected_value):
            self.ca.set_pv_value("PARAM:S3INBEAM:SP_NO_ACTION", expected_value)
            self.ca.set_pv_value("BL:MOVE", 1)

        self.ca_galil.assert_that_pv_is("MTR0102", OUT_POSITION_HIGH)

    def test_GIVEN_mode_is_NR_WHEN_change_mode_THEN_monitor_updates_to_new_mode(
            self):
        expected_value = "POLARISED"

        with self.ca.assert_that_pv_monitor_is("BL:MODE", expected_value), \
             self.ca.assert_that_pv_monitor_is("BL:MODE.VAL", expected_value):
            self.ca.set_pv_value("BL:MODE:SP", expected_value)

    @unstable_test()
    def test_GIVEN_new_parameter_setpoint_WHEN_triggering_move_THEN_SP_is_only_set_on_motor_when_difference_above_motor_resolution(
            self):
        target_mres = 0.001
        pos_above_res = 0.01
        pos_below_res = pos_above_res + 0.0001
        self.ca_galil.set_pv_value("MTR0101.MRES", target_mres)

        with self.ca_galil.assert_that_pv_monitor_is_number("MTR0101.VAL", pos_above_res), \
             self.ca_galil.assert_that_pv_monitor_is_number("MTR0101.RBV", pos_above_res):

            self.ca.set_pv_value("PARAM:S1:SP", pos_above_res)

        with self.ca_galil.assert_that_pv_monitor_is_number("MTR0101.VAL", pos_above_res), \
             self.ca_galil.assert_that_pv_monitor_is_number("MTR0101.RBV", pos_above_res):

            self.ca.set_pv_value("PARAM:S1:SP", pos_below_res)

    def test_GIVEN_motor_velocity_altered_by_move_WHEN_move_completed_THEN_velocity_reverted_to_original_value(
            self):
        expected = INITIAL_VELOCITY
        self.set_up_velocity_tests(expected)

        self.ca.set_pv_value("PARAM:THETA:SP", 5)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 1, timeout=10)
        self.ca_galil.assert_that_pv_is("MTR0102.VELO", expected)
        self.ca_galil.assert_that_pv_is("MTR0104.DMOV", 1, timeout=10)
        self.ca_galil.assert_that_pv_is("MTR0104.VELO", expected)

    def test_GIVEN_motor_velocity_altered_by_move_WHEN_moving_THEN_velocity_altered(
            self):
        # Given a known initial velocity, confirm that on a move the velocity has changed for the axes
        self.set_up_velocity_tests(INITIAL_VELOCITY)

        self.ca.set_pv_value("PARAM:THETA:SP", 15)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 0, timeout=10)
        self.ca_galil.assert_that_pv_is_not("MTR0102.VELO", INITIAL_VELOCITY)
        self.ca_galil.assert_that_pv_is("MTR0104.DMOV", 0, timeout=10)
        self.ca_galil.assert_that_pv_is_not("MTR0104.VELO", INITIAL_VELOCITY)

    def test_GIVEN_motor_velocity_altered_by_move_WHEN_move_interrupted_THEN_velocity_reverted_to_original_value(
            self):
        expected = INITIAL_VELOCITY
        final_position = SPACING
        self.set_up_velocity_tests(expected)

        # move and wait for completion
        self.ca.set_pv_value("PARAM:THETA:SP", 22.5)
        self.ca_galil.set_pv_value("MTR0102.STOP", 1)
        self.ca_galil.set_pv_value("MTR0104.STOP", 1)
        self.ca_galil.set_pv_value("MTR0105.STOP", 1)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 1, timeout=2)
        self.ca_galil.assert_that_pv_is_not_number("MTR0102.RBV",
                                                   final_position,
                                                   tolerance=0.1)
        self.ca_galil.assert_that_pv_is("MTR0102.VELO", expected)
        self.ca_galil.assert_that_pv_is("MTR0104.DMOV", 1, timeout=2)
        self.ca_galil.assert_that_pv_is_not_number("MTR0104.RBV",
                                                   2 * final_position,
                                                   tolerance=0.1)
        self.ca_galil.assert_that_pv_is("MTR0104.VELO", expected)

    def test_GIVEN_move_was_issued_while_different_move_already_in_progress_WHEN_move_completed_THEN_velocity_reverted_to_value_before_first_move(
            self):
        expected = INITIAL_VELOCITY
        self.set_up_velocity_tests(expected)
        self.ca_galil.set_pv_value("MTR0102", -4)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 0, timeout=1)
        self.ca.set_pv_value("PARAM:THETA:SP", 22.5)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 1, timeout=30)
        self.ca_galil.assert_that_pv_is("MTR0102.VELO", expected)

    def test_GIVEN_move_in_progress_WHEN_modifying_motor_velocity_THEN_velocity_reverted_to_value_before_modified_velocity(
            self):
        # The by-design behaviour (but maybe not expected by the user) is that if a velocity is sent during a move
        # then we ignore this and restore the cached value we had for the currently issued move.
        initial = INITIAL_VELOCITY
        altered = INITIAL_VELOCITY + 5
        expected = INITIAL_VELOCITY
        self.set_up_velocity_tests(initial)

        self.ca.set_pv_value("PARAM:THETA:SP", 22.5)
        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 0, timeout=1)

        self.ca_galil.set_pv_value("MTR0102.VELO", altered)

        self.ca_galil.assert_that_pv_is("MTR0102.DMOV", 1, timeout=30)
        self.ca_galil.assert_that_pv_is("MTR0102.VELO", expected)

    def test_GIVEN_mode_is_NR_WHEN_change_mode_THEN_monitor_updates_to_new_mode_and_PVs_inmode_are_labeled_as_such(
            self):

        expected_mode_value = "TESTING"
        PARAM_PREFIX = "PARAM:"
        IN_MODE_SUFFIX = ":IN_MODE"
        expected_in_mode_value = "YES"
        expected_out_of_mode_value = "NO"

        with self.ca.assert_that_pv_monitor_is("BL:MODE", expected_mode_value), \
             self.ca.assert_that_pv_monitor_is("BL:MODE.VAL", expected_mode_value):
            self.ca.set_pv_value("BL:MODE:SP", expected_mode_value)

        test_in_mode_param_names = ["S1", "S3", "THETA", "DET_POS", "S3INBEAM"]
        test_out_of_mode_params = ["DET_ANG", "THETA_AUTO"]

        for param in test_in_mode_param_names:
            self.ca.assert_that_pv_monitor_is(
                "{}{}{}".format(PARAM_PREFIX, param, IN_MODE_SUFFIX),
                expected_in_mode_value)

        for param in test_out_of_mode_params:
            self.ca.assert_that_pv_monitor_is(
                "{}{}{}".format(PARAM_PREFIX, param, IN_MODE_SUFFIX),
                expected_out_of_mode_value)

    def test_GIVEN_jaws_set_to_value_WHEN_change_sp_at_low_level_THEN_jaws_sp_rbv_does_not_change(
            self):

        expected_gap_in_refl = 0.2
        expected_change_to_gap = 1.0

        self.ca.set_pv_value("PARAM:S1HG:SP", expected_gap_in_refl)
        self.ca.assert_that_pv_is_number("PARAM:S1HG",
                                         expected_gap_in_refl,
                                         timeout=15,
                                         tolerance=MOTOR_TOLERANCE)

        self.ca_galil.set_pv_value("JAWS1:HGAP:SP", expected_change_to_gap)
        self.ca_galil.assert_that_pv_is_number("JAWS1:HGAP",
                                               expected_change_to_gap,
                                               timeout=15,
                                               tolerance=MOTOR_TOLERANCE)

        self.ca.assert_that_pv_is("PARAM:S1HG", expected_change_to_gap)
        self.ca.assert_that_pv_is("PARAM:S1HG:SP:RBV", expected_gap_in_refl)

    @parameterized.expand([("slits", "S1", 30.00),
                           ("multi_component", "THETA", 20.00),
                           ("angle", "DET_ANG", -80.0),
                           ("displacement", "DET_POS", 20.0),
                           ("binary", "S3INBEAM", 0)])
    def test_GIVEN_new_parameter_sp_WHEN_parameter_rbv_changing_THEN_parameter_changing_pv_correct(
            self, _, param, value):
        expected_value = "YES"
        value = value

        self.ca.set_pv_value("PARAM:{}:SP".format(param), value)
        self.ca.assert_that_pv_is("PARAM:{}:CHANGING".format(param),
                                  expected_value)

    @parameterized.expand([("slits", "S1", 500.00),
                           ("multi_component", "THETA", -500.00),
                           ("angle", "DET_ANG", -800.0),
                           ("displacement", "DET_POS", 500.0),
                           ("binary", "S3INBEAM", 0)])
    def test_GIVEN_new_parameter_sp_WHEN_parameter_rbv_not_changing_THEN_parameter_changing_pv_correct(
            self, _, param, value):
        expected_value = "NO"
        value = value

        self.ca.set_pv_value("PARAM:{}:SP".format(param), value)
        self.ca_cs.set_pv_value("MOT:STOP:ALL", 1)

        self.ca.assert_that_pv_is("PARAM:{}:CHANGING".format(param),
                                  expected_value)

    @parameterized.expand([("slits", "S1", 500.00),
                           ("multi_component", "THETA", 500.00),
                           ("angle", "DET_ANG", -800.0),
                           ("displacement", "DET_POS", 500.0),
                           ("binary", "S3INBEAM", "OUT")])
    def test_GIVEN_new_parameter_sp_WHEN_parameter_rbv_outside_of_sp_target_tolerance_THEN_parameter_at_rbv_pv_correct(
            self, _, param, value):
        expected_value = "NO"
        value = value

        self.ca.set_pv_value("PARAM:{}:SP".format(param), value)
        self.ca_cs.set_pv_value("MOT:STOP:ALL", 1)

        self.ca.assert_that_pv_is("PARAM:{}:RBV:AT_SP".format(param),
                                  expected_value)

    @parameterized.expand([("slits", "S1", 0.00),
                           ("multi_component", "THETA", 0.00),
                           ("angle", "DET_ANG", 0.0),
                           ("displacement", "DET_POS", 0.0),
                           ("binary", "S3INBEAM", 1)])
    def test_GIVEN_new_parameter_sp_WHEN_parameter_rbv_within_sp_target_tolerance_THEN_parameter_at_rbv_pv_correct(
            self, _, param, value):
        expected_value = "YES"
        value = value

        self.ca.set_pv_value("PARAM:{}:SP".format(param), value)
        self.ca_cs.set_pv_value("MOT:STOP:ALL", 1)

        self.ca.assert_that_pv_is("PARAM:{}:RBV:AT_SP".format(param),
                                  expected_value)

    def test_GIVEN_a_low_level_beamline_change_WHEN_values_changed_THEN_high_level_parameters_updated(
            self):
        self.ca_galil.set_pv_value("MTR0102", -400)

        self.ca.assert_that_pv_value_is_changing("PARAM:S3", wait=2)
        self.ca.assert_that_pv_is("PARAM:S3:RBV:AT_SP", "NO")

    def test_GIVEN_engineering_correction_WHEN_move_THEN_move_includes_engineering_correction(
            self):
        theta = 2
        self.ca.set_pv_value("PARAM:THETA:SP", theta)
        self.ca.set_pv_value("PARAM:S5:SP", 0)

        self.ca.assert_that_pv_is(
            "COR:MOT:MTR0206:DESC",
            "Interpolated from file s4_correction.dat on MOT:MTR0206 for s5")
        self.ca.assert_that_pv_is("COR:MOT:MTR0206", theta /
                                  10.0)  # s4 correction is a 1/10 of theta

        # soon after movement starts and before movement stops the velocity should be the same
        distance_from_sample_to_s4 = (3.5 - 2.0) * 2
        expected_position = distance_from_sample_to_s4 * tan(radians(
            theta * 2)) + theta / 10.0
        self.ca_galil.assert_that_pv_is_number("MTR0206.RBV",
                                               expected_position,
                                               tolerance=MOTOR_TOLERANCE,
                                               timeout=30)
        self.ca.assert_that_pv_is_number("PARAM:S5",
                                         0,
                                         tolerance=MOTOR_TOLERANCE,
                                         timeout=10)

    def test_GIVEN_param_not_in_mode_and_sp_changed_WHEN_performing_beamline_move_THEN_sp_is_applied(
            self):
        expected = 1.0
        self.ca.set_pv_value("PARAM:NOTINMODE:SP_NO_ACTION", expected)

        self.ca.set_pv_value("BL:MOVE", 1, wait=True)

        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", expected)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", expected)

    def test_GIVEN_param_not_in_mode_and_sp_changed_WHEN_performing_individual_move_THEN_sp_is_applied(
            self):
        expected = 1.0
        self.ca.set_pv_value("PARAM:NOTINMODE:SP_NO_ACTION", expected)

        self.ca.set_pv_value("PARAM:NOTINMODE:ACTION", 1, wait=True)

        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", expected)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", expected)

    def test_GIVEN_param_not_in_mode_and_sp_changed_WHEN_performing_individual_move_on_other_param_THEN_no_value_applied(
            self):
        param_sp = 0.0
        motor_pos = 1.0
        self.ca.set_pv_value("PARAM:NOTINMODE:SP", param_sp)
        self.ca_galil.set_pv_value("MTR0205", motor_pos, wait=True)
        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", motor_pos)

        self.ca.set_pv_value("PARAM:THETA:SP", 0.2, wait=True)
        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP", param_sp)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", param_sp)
        self.ca_galil.assert_that_pv_is_number("MTR0205", motor_pos)

    def test_GIVEN_param_not_in_mode_and_sp_unchanged_WHEN_performing_beamline_move_THEN_no_value_applied(
            self):
        param_sp = 0.0
        motor_pos = 1.0
        self.ca_galil.set_pv_value("MTR0205", motor_pos, wait=True)
        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", motor_pos)

        self.ca.set_pv_value("BL:MOVE", 1, wait=True)

        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP", param_sp)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", param_sp)
        self.ca_galil.assert_that_pv_is_number("MTR0205", motor_pos)

    def test_GIVEN_param_not_in_mode_and_sp_unchanged_WHEN_performing_individual_move_THEN_sp_is_applied(
            self):
        param_sp = 0.0
        motor_pos = 1.0
        self.ca_galil.set_pv_value("MTR0205", motor_pos, wait=True)
        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", motor_pos)

        self.ca.set_pv_value("PARAM:NOTINMODE:ACTION", 1, wait=True)

        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP", param_sp)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", param_sp)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", param_sp)

    def test_GIVEN_param_not_in_mode_and_sp_unchanged_WHEN_performing_individual_move_on_other_param_THEN_no_value_applied(
            self):
        param_sp = 0.0
        motor_pos = 1.0
        self.ca_galil.set_pv_value("MTR0205", motor_pos, wait=True)
        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE", motor_pos)

        self.ca.set_pv_value("PARAM:THETA:SP", 0.2, wait=True)

        self.ca_galil.assert_that_pv_is("MTR0205.DMOV", 1, timeout=10)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP", param_sp)
        self.ca.assert_that_pv_is_number("PARAM:NOTINMODE:SP:RBV", param_sp)
        self.ca_galil.assert_that_pv_is_number("MTR0205", motor_pos)

    def test_GIVEN_non_synchronised_axis_WHEN_move_which_should_change_velocity_THEN_velocity_not_changed(
            self):
        self.ca_galil.set_pv_value("MTR0103.VELO", MEDIUM_VELOCITY)

        self.ca.set_pv_value("PARAM:THETA:SP", 22.5)

        # soon after movement starts and before movement stops the velocity should be the same
        self.ca_galil.assert_that_pv_is("MTR0103.DMOV", 0, timeout=10)
        self.ca_galil.assert_that_pv_is("MTR0103.VELO",
                                        MEDIUM_VELOCITY,
                                        timeout=0.5)
        self.ca_galil.assert_that_pv_is("MTR0103.DMOV", 0, timeout=10)

        # when the movement finishes it should still be the same
        self.ca_galil.assert_that_pv_is("MTR0103.DMOV", 1, timeout=10)
        self.ca_galil.assert_that_pv_is("MTR0103.VELO", MEDIUM_VELOCITY)

    def test_GIVEN_motor_axis_is_angle_WHEN_motor_alarm_status_is_updated_THEN_alarms_propagate_to_correct_parameters_on_component(
            self):
        expected_severity_code = "MINOR"
        expected_status_code = "HIGH"
        no_alarm_code = "NO_ALARM"

        # Setting High Limit = Low limit produces alarm on 0105 (detector angle)
        self.ca_galil.set_pv_value("MTR0105.HLM", SOFT_LIMIT_LO)

        # detector angle should be in alarm
        self.ca.assert_that_pv_is("PARAM:DET_ANG.STAT", expected_status_code)
        self.ca.assert_that_pv_is("PARAM:DET_ANG.SEVR", expected_severity_code)
        # detector offset is independent and should not be in alarm
        self.ca.assert_that_pv_is("PARAM:DET_POS.STAT", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:DET_POS.SEVR", no_alarm_code)

        # Setting High Limit back clears alarm
        self.ca_galil.set_pv_value("MTR0105.HLM", SOFT_LIMIT_HI)

        self.ca.assert_that_pv_is("PARAM:DET_ANG.STAT", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:DET_ANG.SEVR", no_alarm_code)

    def test_GIVEN_motor_axis_is_displacement_WHEN_motor_alarm_status_is_updated_THEN_alarms_propagate_to_correct_parameters_on_component(
            self):
        expected_severity_code = "MINOR"
        expected_status_code = "HIGH"
        no_alarm_code = "NO_ALARM"

        # Setting High Limit = Low limit produces alarm on 0104 (detector height)
        self.ca_galil.set_pv_value("MTR0104.HLM", SOFT_LIMIT_LO)

        # detector offset should be in alarm
        self.ca.assert_that_pv_is("PARAM:DET_POS.STAT", expected_status_code)
        self.ca.assert_that_pv_is("PARAM:DET_POS.SEVR", expected_severity_code)
        # theta is derived from detector offset and should be in alarm
        self.ca.assert_that_pv_is("PARAM:THETA.STAT", expected_status_code)
        self.ca.assert_that_pv_is("PARAM:THETA.SEVR", expected_severity_code)
        # detector angle is independent and should not be in alarm
        self.ca.assert_that_pv_is("PARAM:DET_ANG.STAT", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:DET_ANG.SEVR", no_alarm_code)

        # Setting High Limit back clears alarm
        self.ca_galil.set_pv_value("MTR0104.HLM", SOFT_LIMIT_HI)

        self.ca.assert_that_pv_is("PARAM:DET_POS.STAT", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:DET_POS.SEVR", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:THETA.STAT", no_alarm_code)
        self.ca.assert_that_pv_is("PARAM:THETA.SEVR", no_alarm_code)

    @parameterized.expand([("Variable", "DET_POS", "MTR0104"),
                           ("Frozen", "DET_POS", "MTR0104"),
                           ("Frozen", "DET_ANG", "MTR0105")])
    def test_GIVEN_motors_not_at_zero_WHEN_define_motor_position_to_THEN_motor_position_is_changed_without_move(
            self, initial_foff, param_name, motor_name):
        offset = 10
        new_position = 2
        self.ca.set_pv_value("PARAM:{}:SP".format(param_name), offset)
        self.ca_galil.set_pv_value("MTR0104.FOFF", initial_foff)
        self.ca_galil.set_pv_value("MTR0104.OFF", 0)
        self.ca.assert_that_pv_is_number("PARAM:{}".format(param_name),
                                         offset,
                                         tolerance=MOTOR_TOLERANCE,
                                         timeout=30)
        self.ca_galil.assert_that_pv_is("MTR0104.DMOV", 1, timeout=30)

        with ManagerMode(self.ca_no_prefix):
            self.ca.set_pv_value(
                "PARAM:{}:DEFINE_POSITION_AS".format(param_name), new_position)

        # soon after change there should be no movement, ie a move is triggered but the motor itself does not move so it
        # is very quick
        self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                        1,
                                        timeout=1)
        self.ca_galil.assert_that_pv_is("{}.RBV".format(motor_name),
                                        new_position)
        self.ca_galil.assert_that_pv_is("{}.VAL".format(motor_name),
                                        new_position)
        self.ca_galil.assert_that_pv_is("{}.SET".format(motor_name), "Use")
        self.ca_galil.assert_that_pv_is("{}.FOFF".format(motor_name),
                                        initial_foff)
        self.ca_galil.assert_that_pv_is_number("{}.OFF".format(motor_name),
                                               0.0,
                                               tolerance=MOTOR_TOLERANCE)

        self.ca.assert_that_pv_is("PARAM:{}".format(param_name), new_position)
        self.ca.assert_that_pv_is("PARAM:{}:SP".format(param_name),
                                  new_position)
        self.ca.assert_that_pv_is("PARAM:{}:SP_NO_ACTION".format(param_name),
                                  new_position)
        self.ca.assert_that_pv_is("PARAM:{}:CHANGED".format(param_name), "NO")
        self.ca.assert_that_pv_is("PARAM:THETA", 0)
        self.ca.assert_that_pv_is("PARAM:THETA:SP", 0)
        self.ca.assert_that_pv_is("PARAM:THETA:SP:RBV", 0)

    def test_GIVEN_jaws_not_at_zero_WHEN_define_motor_position_for_jaw_gaps_THEN_jaws_position_are_changed_without_move(
            self):
        param_name = "S1VG"
        jaw_motors = ["MTR0201", "MTR0202"]
        initial_gap = 1.0
        initial_centre = 2.0
        new_gap = 4.0
        expected_pos = {
            "MTR0201": new_gap / 2.0 - initial_centre,
            "MTR0202": new_gap / 2.0 + initial_centre
        }
        self.ca.assert_setting_setpoint_sets_readback(initial_gap,
                                                      "PARAM:S1VG",
                                                      expected_alarm=None,
                                                      timeout=30)
        self.ca.assert_setting_setpoint_sets_readback(initial_centre,
                                                      "PARAM:S1VC",
                                                      expected_alarm=None,
                                                      timeout=30)
        for motor_name in jaw_motors:
            self.ca_galil.set_pv_value("{}.FOFF".format(motor_name), "Frozen")
            self.ca_galil.set_pv_value("{}.OFF".format(motor_name), 0)
        for motor_name in jaw_motors:
            self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                            1,
                                            timeout=30)

        with ManagerMode(self.ca_no_prefix):
            self.ca.set_pv_value(
                "PARAM:{}:DEFINE_POSITION_AS".format(param_name), new_gap)

        # soon after change there should be no movement, ie a move is triggered but the motor itself does not move so it
        # is very quick
        for motor_name in jaw_motors:
            self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                            1,
                                            timeout=1)

        for motor_name in jaw_motors:
            # jaws are open to half the gap
            self.ca_galil.assert_that_pv_is("{}.RBV".format(motor_name),
                                            expected_pos[motor_name])
            self.ca_galil.assert_that_pv_is("{}.VAL".format(motor_name),
                                            expected_pos[motor_name])
            self.ca_galil.assert_that_pv_is("{}.SET".format(motor_name), "Use")
            self.ca_galil.assert_that_pv_is("{}.FOFF".format(motor_name),
                                            "Frozen")
            self.ca_galil.assert_that_pv_is_number("{}.OFF".format(motor_name),
                                                   0.0,
                                                   tolerance=MOTOR_TOLERANCE)

        self.ca.assert_that_pv_is("PARAM:{}".format(param_name), new_gap)
        self.ca.assert_that_pv_is("PARAM:{}:SP".format(param_name), new_gap)
        self.ca.assert_that_pv_is("PARAM:{}:SP_NO_ACTION".format(param_name),
                                  new_gap)
        self.ca.assert_that_pv_is("PARAM:{}:CHANGED".format(param_name), "NO")

    def test_GIVEN_jaws_not_at_zero_WHEN_define_motor_position_for_jaw_centres_THEN_jaws_position_are_changed_without_move(
            self):
        param_name = "S1HC"
        jaw_motors = ["MTR0203", "MTR0204"]
        initial_gap = 1.0
        initial_centre = 2.0
        new_centre = 4.0
        expected_pos = {
            "MTR0203": initial_gap / 2.0 + new_centre,
            "MTR0204": initial_gap / 2.0 - new_centre
        }
        self.ca.assert_setting_setpoint_sets_readback(initial_gap,
                                                      "PARAM:S1HG",
                                                      expected_alarm=None,
                                                      timeout=30)
        self.ca.assert_setting_setpoint_sets_readback(initial_centre,
                                                      "PARAM:S1HC",
                                                      expected_alarm=None,
                                                      timeout=30)
        for motor_name in jaw_motors:
            self.ca_galil.set_pv_value("{}.FOFF".format(motor_name), "Frozen")
            self.ca_galil.set_pv_value("{}.OFF".format(motor_name), 0)
        for motor_name in jaw_motors:
            self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                            1,
                                            timeout=30)

        with ManagerMode(self.ca_no_prefix):
            self.ca.set_pv_value(
                "PARAM:{}:DEFINE_POSITION_AS".format(param_name), new_centre)

        # soon after change there should be no movement, ie a move is triggered but the motor itself does not move so it
        # is very quick
        for motor_name in jaw_motors:
            self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                            1,
                                            timeout=1)

        for motor_name in jaw_motors:
            # jaws are open to half the gap
            self.ca_galil.assert_that_pv_is("{}.RBV".format(motor_name),
                                            expected_pos[motor_name])
            self.ca_galil.assert_that_pv_is("{}.VAL".format(motor_name),
                                            expected_pos[motor_name])
            self.ca_galil.assert_that_pv_is("{}.SET".format(motor_name), "Use")
            self.ca_galil.assert_that_pv_is("{}.FOFF".format(motor_name),
                                            "Frozen")
            self.ca_galil.assert_that_pv_is_number("{}.OFF".format(motor_name),
                                                   0.0,
                                                   tolerance=MOTOR_TOLERANCE)

        self.ca.assert_that_pv_is("PARAM:{}".format(param_name), new_centre)
        self.ca.assert_that_pv_is("PARAM:{}:SP".format(param_name), new_centre)
        self.ca.assert_that_pv_is("PARAM:{}:SP_NO_ACTION".format(param_name),
                                  new_centre)
        self.ca.assert_that_pv_is("PARAM:{}:CHANGED".format(param_name), "NO")

    def test_GIVEN_theta_THEN_define_position_as_does_not_exist(self):
        param_name = "THETA"
        self.ca.assert_that_pv_exists("PARAM:{}".format(param_name))
        self.ca.assert_that_pv_does_not_exist(
            "PARAM:{}:DEFINE_POSITION_AS".format(param_name))

    def test_GIVEN_motors_at_zero_WHEN_define_motor_position_to_and_back_multiple_times_THEN_motor_position_is_changed_without_move(
            self):
        param_name = "DET_POS"
        motor_name = "MTR0104"
        initial_foff = "Frozen"
        self.ca.set_pv_value("PARAM:{}:SP".format(param_name), 0)
        self.ca_galil.set_pv_value("MTR0104.FOFF", initial_foff)
        self.ca_galil.set_pv_value("MTR0104.OFF", 0)
        self.ca.assert_that_pv_is_number("PARAM:{}".format(param_name),
                                         0,
                                         tolerance=MOTOR_TOLERANCE,
                                         timeout=30)
        self.ca_galil.assert_that_pv_is("MTR0104.DMOV", 1, timeout=30)

        for i in range(20):
            new_position = i - 5
            with ManagerMode(self.ca_no_prefix):
                self.ca.set_pv_value(
                    "PARAM:{}:DEFINE_POSITION_AS".format(param_name),
                    new_position)

            # soon after change there should be no movement, ie a move is triggered but the motor itself does not move so it
            # is very quick
            self.ca_galil.assert_that_pv_is("{}.DMOV".format(motor_name),
                                            1,
                                            timeout=1)
            self.ca_galil.assert_that_pv_is("{}.RBV".format(motor_name),
                                            new_position)
            self.ca_galil.assert_that_pv_is("{}.VAL".format(motor_name),
                                            new_position)
            self.ca_galil.assert_that_pv_is("{}.SET".format(motor_name), "Use")
            self.ca_galil.assert_that_pv_is("{}.FOFF".format(motor_name),
                                            initial_foff)
            self.ca_galil.assert_that_pv_is_number("{}.OFF".format(motor_name),
                                                   0.0,
                                                   tolerance=MOTOR_TOLERANCE)

            self.ca.assert_that_pv_is("PARAM:{}".format(param_name),
                                      new_position)
            self.ca.assert_that_pv_is("PARAM:{}:SP".format(param_name),
                                      new_position)
            self.ca.assert_that_pv_is(
                "PARAM:{}:SP_NO_ACTION".format(param_name), new_position)
            self.ca.assert_that_pv_is("PARAM:{}:CHANGED".format(param_name),
                                      "NO")

    def test_GIVEN_parameter_not_in_manager_mode_WHEN_define_position_THEN_position_is_not_defined(
            self):
        new_position = 10

        param_pv = "PARAM:{}:DEFINE_POSITION_AS".format("DET_POS")
        self.assertRaises(IOError, self.ca.set_pv_value, param_pv,
                          new_position)

        self.ca.assert_that_pv_is_not(param_pv, new_position)

    def test_GIVEN_value_parameter_WHEN_read_THEN_value_returned(self):

        param_pv = "CONST:TEN"

        self.ca.assert_that_pv_is(param_pv, 10)
        self.ca.assert_that_pv_is("{}.DESC".format(param_pv), "The value 10")

    def test_GIVEN_bool_parameter_WHEN_read_THEN_value_returned(self):

        param_pv = "CONST:YES"

        self.ca.assert_that_pv_is(param_pv, "YES")

    def test_GIVEN_string_constant_parameter_WHEN_read_THEN_value_returned(
            self):

        param_pv = "CONST:STRING"

        self.ca.assert_that_pv_is(param_pv, "Test String")

    def test_GIVEN_PNR_mode_with_SM_angle_WHEN_move_in_disable_mode_and_into_PNR_THEN_beamline_is_updated_on_mode_change_and_value_of_pd_offsets_correct(
            self):

        self.ca.set_pv_value("BL:MODE:SP", "POLARISED")
        self.ca.set_pv_value("PARAM:SMANGLE:SP_NO_ACTION", 0.2)
        self.ca.set_pv_value("BL:MOVE", 1)
        self.ca.assert_that_pv_is_number("PARAM:SMANGLE", 0.2, tolerance=1e-2)

        self.ca.set_pv_value("BL:MODE:SP", "DISABLED")
        self.ca.set_pv_value("PARAM:SMANGLE:SP", 0)
        self.ca.assert_that_pv_is_number("PARAM:SMANGLE", 0.0, tolerance=1e-2)

        self.ca.assert_that_pv_is_number("PARAM:S3", 0.0, tolerance=1e-2)
        self.ca.assert_that_pv_is_number("PARAM:DET_POS", 0.0, tolerance=1e-2)

        self.ca.set_pv_value("BL:MODE:SP", "POLARISED")

        # In polarised mode the sm angle will now make everything appear to be in the wrong place.
        # This test will also check that on changing modes the beamline is updated
        self.ca.assert_that_pv_is_not_number("PARAM:S3", 0.0, tolerance=1e-2)
        self.ca.assert_that_pv_is_not_number("PARAM:DET_POS",
                                             0.0,
                                             tolerance=1e-2)

    @parameterized.expand([(0, OUT_POSITION_HIGH), (22.5, OUT_POSITION_LOW)])
    def test_GIVEN_component_with_multiple_parked_positions_WHEN_moving_out_of_beam_THEN_driver_moves_to_correct_out_of_beam_position_based_on_beam_interception(
            self, theta_sp, expected_out_of_beam_position):
        self.ca.assert_that_pv_is("PARAM:S3INBEAM", "IN")

        self.ca.set_pv_value("PARAM:THETA:SP_NO_ACTION", theta_sp, wait=True)
        self.ca.set_pv_value("PARAM:S3INBEAM:SP_NO_ACTION", "OUT", wait=True)
        self.ca.set_pv_value("BL:MOVE", 1, wait=True)

        self.ca_galil.assert_that_pv_is_number("MTR0102.VAL",
                                               expected_out_of_beam_position,
                                               timeout=5)

    def test_GIVEN_component_with_multiple_out_of_beam_positions_is_out_of_beam_WHEN_beam_intercept_moves_above_threshold_THEN_driver_moves_to_correct_out_of_beam_position(
            self):
        self.ca.assert_that_pv_is("PARAM:S3INBEAM", "IN")
        self.ca.set_pv_value("PARAM:S3INBEAM:SP", "OUT", wait=True)
        self.ca.assert_that_pv_is("PARAM:S3INBEAM:CHANGING", "NO", timeout=30)
        self.ca_galil.assert_that_pv_is_number("MTR0102.RBV",
                                               OUT_POSITION_HIGH,
                                               timeout=20)

        self.ca.set_pv_value("PARAM:THETA:SP", 22.5, wait=True)

        self.ca_galil.assert_that_pv_is_number("MTR0102.VAL",
                                               OUT_POSITION_LOW,
                                               timeout=5)

    @parameterized.expand([(0, OUT_POSITION_HIGH, "OUT"),
                           (0, OUT_POSITION_LOW, "IN"),
                           (22.5, OUT_POSITION_HIGH, "IN"),
                           (22.5, OUT_POSITION_LOW, "OUT")])
    def test_GIVEN_component_with_multiple_parked_positions_WHEN_moving_axis_to_sp_THEN_inbeam_param_reports_correct_rbv(
            self, theta_sp, axis_sp, expected_inbeam_status):
        self.ca.assert_that_pv_is("PARAM:S3INBEAM", "IN")

        self.ca.set_pv_value("PARAM:THETA:SP", theta_sp, wait=True)
        self.ca.assert_that_pv_is("PARAM:THETA:CHANGING", "NO", timeout=30)

        self.ca_galil.set_pv_value("MTR0102.VAL", axis_sp, wait=True)

        self.ca.assert_that_pv_is("PARAM:S3INBEAM:CHANGING", "NO", timeout=30)
        self.ca.assert_that_pv_is("PARAM:S3INBEAM", expected_inbeam_status)

    def test_GIVEN_driver_with_param_value_dependent_axis_WHEN_set_value_THEN_correct_axis_drives_to_position_and_read_back_is_correct(
            self):
        expected_offset1 = 0.3
        expected_offset2 = 0.2
        for expected_offset, choice, mot0205, mot0207 in [
            (expected_offset1, "MTR0207", 0, expected_offset1),
            (expected_offset2, "MTR0205", expected_offset2, expected_offset1)
        ]:
            self.ca.assert_setting_setpoint_sets_readback(
                choice, "PARAM:CHOICE")
            self.ca.set_pv_value("PARAM:NOTINMODE:SP", expected_offset)

            self.ca.assert_that_pv_is("PARAM:NOTINMODE",
                                      expected_offset,
                                      timeout=20)
            self.ca_galil.assert_that_pv_is_number("MTR0205.RBV",
                                                   mot0205,
                                                   timeout=20)
            self.ca_galil.assert_that_pv_is_number("MTR0207.RBV",
                                                   mot0207,
                                                   timeout=20)

    def test_GIVEN_theta_WHEN_detector_long_axis_changes_THEN_detector_tracks(
            self):
        theta_angle = 2
        long_axis_addition = 1
        self.ca.set_pv_value("PARAM:THETA:SP", theta_angle)
        self.ca.set_pv_value("BL:MOVE", 1)

        # theta set
        self._check_param_pvs("THETA", theta_angle)

        self.ca.set_pv_value("PARAM:DET_LONG:SP", long_axis_addition)
        self.ca.set_pv_value("BL:MOVE", 1)

        expected_det_value = (2 * SPACING + long_axis_addition) * tan(
            radians(theta_angle * 2.0))

        self._check_param_pvs("DET_LONG", long_axis_addition)
        self._check_param_pvs("DET_POS", 0.0)
        self.ca_galil.assert_that_pv_is_number("MTR0104", expected_det_value,
                                               0.01)
class RikenChangeover(object):
    """
    Tests for a riken changeover.

    This class is inherited by the riken port changeover tests and also the RB2 mode change tests as they are very
    similar (just the PSUs that they look at / control are different)
    """
    @abstractmethod
    def get_input_pv(self):
        return ""

    @abstractmethod
    def get_acknowledgement_pv(self):
        return ""

    @abstractmethod
    def get_power_supplies(self):
        return []

    @abstractmethod
    def get_coord_prefix(self):
        return ""

    @abstractmethod
    def get_prefix(self):
        return ""

    def _set_input_pv(self, ok_to_run_psus):
        self.ca.set_pv_value("{}:SIM".format(self.get_input_pv()), 1 if ok_to_run_psus else 0)
        self.ca.assert_that_pv_is("{}:SIM".format(self.get_input_pv()), 1 if ok_to_run_psus else 0)
        self.ca.assert_that_pv_alarm_is("{}".format(self.get_input_pv()), self.ca.Alarms.NONE)
        self.ca.assert_that_pv_is("{}".format(self.get_input_pv()), 1 if ok_to_run_psus else 0)

    def _set_power_supply_state(self, supply, on):
        self.ca.set_pv_value("{}:POWER:SP".format(supply), 1 if on else 0)
        self.ca.assert_that_pv_is("{}:POWER".format(supply), "On" if on else "Off")

    def _assert_power_supply_disabled(self, supply, disabled):
        self.ca.assert_that_pv_is_number("{}:POWER:SP.DISP".format(supply), 1 if disabled else 0)

    def _set_all_power_supply_states(self, on):
        for supply in self.get_power_supplies():
            self._set_power_supply_state(supply, on)

    def _assert_all_power_supplies_disabled(self, disabled):
        for supply in self.get_power_supplies():
            self._assert_power_supply_disabled(supply, disabled)

    def _assert_necessary_pvs_exist(self):
        self.ca.assert_that_pv_exists("{}:PSUS:DISABLE".format(self.get_coord_prefix()))
        self.ca.assert_that_pv_exists(self.get_input_pv())
        self.ca.assert_that_pv_exists(self.get_acknowledgement_pv())
        for id in self.get_power_supplies():
            self.ca.assert_that_pv_exists("{}:POWER".format(id))

    def setUp(self):
        self.ca = ChannelAccess(device_prefix=self.get_prefix(), default_timeout=10)

        self._assert_necessary_pvs_exist()

        self._set_input_pv(False)  # Set it to false so that the sequencer see the change,
                                   # probably to do with it being in RECSIM
        self._set_input_pv(True)
        self._assert_all_power_supplies_disabled(False)
        self._set_all_power_supply_states(False)

    def test_GIVEN_value_on_input_ioc_changes_THEN_coord_psus_disable_pv_updates_with_the_same_value(self):
        def _set_and_check(ok_to_run_psus):
            self._set_input_pv(ok_to_run_psus)
            self.ca.assert_that_pv_is("{}:PSUS:DISABLE".format(self.get_coord_prefix()),
                                      "ENABLED" if ok_to_run_psus else "DISABLED")

        for ok_to_run_psus in [True, False, True]:  # Check both transitions
            _set_and_check(ok_to_run_psus)

    def test_GIVEN_all_power_supplies_off_WHEN_value_on_input_ioc_changes_THEN_power_supplies_have_their_disp_field_set(self):
        def _set_and_check_disabled_status(ok_to_run_psus):
            self._set_input_pv(ok_to_run_psus)
            self._assert_all_power_supplies_disabled(not ok_to_run_psus)

        for ok_to_run_psus in [True, False, True]:  # Check both transitions
            _set_and_check_disabled_status(ok_to_run_psus)

    def test_WHEN_any_power_supply_is_on_THEN_power_all_pv_is_high(self):

        self._set_all_power_supply_states(False)

        self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0)

        for psu in self.get_power_supplies():
            self._set_power_supply_state(psu, True)
            self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 1)

            self._set_power_supply_state(psu, False)
            self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0)

    def test_GIVEN_power_supplies_on_WHEN_value_on_input_ioc_changes_THEN_power_supplies_are_not_disabled_until_they_are_switched_off(self):
        self._set_all_power_supply_states(True)
        self._set_input_pv(False)
        self._assert_all_power_supplies_disabled(False)
        self._set_all_power_supply_states(False)
        self._assert_all_power_supplies_disabled(True)

    def test_GIVEN_plc_cancels_changeover_before_psus_are_all_switched_off_WHEN_psus_become_switched_off_THEN_they_do_not_get_disabled(self):
        self._set_all_power_supply_states(True)
        self._set_input_pv(False)
        self._assert_all_power_supplies_disabled(False)  # Power supplies not disabled because still powered on
        self._set_input_pv(True)  # PLC now cancels request to do a changeover
        self._set_all_power_supply_states(False)
        self._assert_all_power_supplies_disabled(False)

    def test_GIVEN_a_power_supply_is_in_alarm_THEN_the_power_any_pv_is_also_in_alarm(self):
        for supply in self.get_power_supplies():
            with self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID):
                self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.INVALID)
            self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.NONE)

    def test_GIVEN_all_power_supply_are_in_alarm_THEN_the_power_any_pv_is_also_in_alarm(self):
        with ExitStack() as stack:
            for supply in self.get_power_supplies():
                stack.enter_context(
                    self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID)
                )
            self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.INVALID)
        self.ca.assert_that_pv_alarm_is("{}:PSUS:POWER".format(self.get_coord_prefix()), self.ca.Alarms.NONE)

    def test_GIVEN_a_power_supply_is_in_alarm_THEN_the_power_any_pv_reports_that_psus_are_active(self):
        for supply in self.get_power_supplies():
            with self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID):
                self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 1)
            self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0)

    def test_GIVEN_all_power_supply_are_in_alarm_THEN_the_power_any_pv_reports_that_psus_are_active(self):
        with ExitStack() as stack:
            for supply in self.get_power_supplies():
                stack.enter_context(
                    self.ca.put_simulated_record_into_alarm("{}:POWER".format(supply), self.ca.Alarms.INVALID)
                )
            self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 1)
        self.ca.assert_that_pv_is_number("{}:PSUS:POWER".format(self.get_coord_prefix()), 0)

    def test_GIVEN_changeover_initiated_WHEN_power_supplies_off_THEN_acknowledgement_pv_true(self):
        self._set_all_power_supply_states(False)
        self._set_input_pv(False)

        self.ca.assert_that_pv_is_not_number(self.get_acknowledgement_pv(), 0)

        self._set_input_pv(True)  # Some time later the PLC sends signal to say it has finished the changeover sequence
        self.ca.assert_that_pv_is_number(self.get_acknowledgement_pv(), 0)

    def test_GIVEN_changeover_sequence_completes_THEN_power_supplies_are_reenabled_after_sequence(self):
        self._set_all_power_supply_states(True)
        self._set_input_pv(False)
        self._assert_all_power_supplies_disabled(False)  # Power supplies not disabled because still powered on
        self._set_all_power_supply_states(False)  # Power supplies now switched off so changeover can continue
        self._assert_all_power_supplies_disabled(True)  # All power supplies are now disabled
        self.ca.assert_that_pv_is_not_number(self.get_acknowledgement_pv(), 0)
        self._set_input_pv(True)  # Some time later, changeover is finished
        self._assert_all_power_supplies_disabled(False)  # Power supplies should now be reenabled
        self.ca.assert_that_pv_is(self.get_acknowledgement_pv(), 0)  # And "ok to run changeover" line should be cleared
예제 #3
0
class ZeroFieldMagFieldTests(unittest.TestCase):
    def setUp(self):
        self._ioc = IOCRegister.get_running(DEVICE_PREFIX)
        self.ca = ChannelAccess(device_prefix=DEVICE_PREFIX)
        self.ca.assert_that_pv_exists("DISABLE", timeout=30)
        self.write_offset(0)
        self.ca.set_pv_value("RANGE", 1.0, sleep_after_set=0.0)
        self.write_simulated_field_values(ZERO_FIELD)
        self.write_simulated_alarm_level(self.ca.Alarms.NONE)
        self.ca.process_pv("TAKEDATA")

    def write_offset(self, offset):
        """
        Writes offset values for all three IOC components
        Args:
            offset: float, the offset to be written to the IOC

        Returns:
            None

        """
        for axis in AXES.keys():
            self.ca.set_pv_value("OFFSET:{}".format(axis), offset, sleep_after_set=0.0)

    def write_sensor_matrix(self, sensor_matrix):
        """
        Writes the provided sensor matrix to the relevant PVs

        Args:
            sensor_matrix: 3x3 numpy ndarray containing the values to use as the fixed sensor matrix.

        Returns:
            None
        """
        assert sensor_matrix.shape == (SENSOR_MATRIX_SIZE, SENSOR_MATRIX_SIZE)

        for i in range(SENSOR_MATRIX_SIZE):
            for j in range(SENSOR_MATRIX_SIZE):
                self.ca.set_pv_value(SENSOR_MATRIX_PVS.format(row=i+1, column=j+1),
                                     sensor_matrix[i, j], sleep_after_set=0.0)

    def apply_offset_to_field(self, simulated_field, offset):
        """
        Applies offset to the simulated measured field

        Args:
            simulated_field: dict with 'X', 'Y' and 'Z' keys. Values are the corresponding simulated field values
            offset: float, The offset to subtract from the input data. Applies same offset to all fields

        Returns:
            offset_applied_field: dict with 'X', 'Y', 'Z' keys. Values are offset-subtracted simulated_field values

        """

        offset_applied_field = {}
        for axis in AXES.keys():
            offset_applied_field[axis] = simulated_field[axis] - offset

        return offset_applied_field

    def write_simulated_field_values(self, simulated_field):
        """
        Writes the given simulated field values to the IOC.

        Also asserts that the value has been taken up by the '_RAW' PV. We need to do this because the '_RAW' PVs are
        on SCAN = .1 second in RECSIM, so some time is taken between writing the SIM field and it being available in the
        '_RAW' PV.

        Args:
            simulated_field: dict with 'X', 'Y' and 'Z' keys. Values are the corresponding simulated field values

        Returns:
            None

        """

        for component in AXES.keys():
            self.ca.set_pv_value("SIM:DAQ:{}".format(component), simulated_field[component], sleep_after_set=0.0)
            self.ca.assert_that_pv_is_number("DAQ:{}:_RAW".format(component), simulated_field[component])

    def apply_offset_and_matrix_multiplication(self, simulated_field, offset, sensor_matrix):
        """
        Applies trasformation between raw or 'measured' field to 'corrected' field.

        Subtracts the offset from the input (raw) data, then matrix multiplies by the sensor matrix.

        Args:
            simulated_field: dict with keys matching AXES (X, Y and Z). Values are the simulated field values
            offset: float, The Offset to subtract from the input data. Applies same offset to all fields
            sensor_matrix: 3x3 numpy ndarray containing the values to use as the fixed sensor matrix.

        Returns:
            corrected_field_vals: 3-element array containing corrected X, Y and Z field values

        """

        offset_input_field = self.apply_offset_to_field(simulated_field, offset)

        corrected_field_vals = np.matmul(sensor_matrix, np.array([offset_input_field["X"],
                                                                  offset_input_field["Y"],
                                                                  offset_input_field["Z"]]))

        return corrected_field_vals

    def get_overload_range_value(self):
        """
        Returns the maximum value an input field can have before the magnetometer is overloaded
        """

        return self.ca.get_pv_value("RANGE") * 4.5

    def write_simulated_alarm_level(self, level):
        """
        Writes to the SIML field of the RAW data pvs. This sets the severity level of the three pvs to level.
        Waits for the SEVR fields of the RAW data pvs to update before returning.

        Args:
            level: Class attribute of ChannelAccess.Alarms (e.g. ca.Alarms.NONE). The severity level to set to the PV

        """
        for axis in AXES.keys():
            self.ca.set_pv_value("DAQ:{}:_RAW.SIMS".format(axis), level, sleep_after_set=0.0)

        # Wait for the raw PVs to process
        for axis in AXES.keys():
            self.ca.assert_that_pv_alarm_is("DAQ:{}:_RAW".format(axis), level)

    @parameterized.expand(parameterized_list(itertools.product(AXES.keys(), FIELD_STRENGTHS)))
    def test_GIVEN_field_offset_THEN_field_strength_read_back_with_offset_applied(self, _, hw_axis, field_strength):
        # GIVEN
        self.write_offset(OFFSET)

        field = {"X": 0,
                 "Y": 0,
                 "Z": 0}

        field[hw_axis] = field_strength

        self.write_simulated_field_values(field)
        self.ca.set_pv_value("SIM:DAQ:{}".format(hw_axis), field_strength, sleep_after_set=0.0)

        # WHEN
        self.ca.process_pv("TAKEDATA")

        # THEN
        self.ca.assert_that_pv_is_number("APPLYOFFSET:{}".format(hw_axis), field_strength-OFFSET)

    def test_GIVEN_offset_corrected_field_WHEN_sensor_matrix_is_identity_THEN_input_field_returned_by_matrix_multiplier(self):
        offset_corrected_field = {"X": 1.1,
                                  "Y": 2.2,
                                  "Z": 3.3}

        # GIVEN
        self.write_simulated_field_values(offset_corrected_field)

        # WHEN
        self.write_sensor_matrix(np.identity(3))
        self.ca.process_pv("TAKEDATA")

        # THEN
        for hw_axis in AXES.keys():
            expected_value = offset_corrected_field[hw_axis]
            self.ca.assert_that_pv_is_number("CORRECTEDFIELD:{}".format(hw_axis),
                                             expected_value,
                                             tolerance=0.1*abs(expected_value))

            self.ca.assert_that_pv_alarm_is("CORRECTEDFIELD:{}".format(hw_axis), self.ca.Alarms.NONE)

    @parameterized.expand(parameterized_list(['X', 'Y', 'Z']))
    def test_GIVEN_sensor_matrix_with_only_one_nonzero_row_THEN_corrected_field_has_component_in_correct_dimension(self, _, hw_axis):

        input_field = {"X": 1.1,
                       "Y": 2.2,
                       "Z": 3.3}

        self.write_simulated_field_values(input_field)

        # GIVEN
        sensor_matrix = np.zeros((3, 3))

        # Set one row to one
        if hw_axis == "X":
            sensor_matrix[0, :] = 1
        elif hw_axis == "Y":
            sensor_matrix[1, :] = 1
        elif hw_axis == "Z":
            sensor_matrix[2, :] = 1

        # WHEN
        self.write_sensor_matrix(sensor_matrix)
        self.ca.process_pv("TAKEDATA")

        # THEN
        for component in AXES.keys():
            if component == hw_axis:
                expected_value = sum(input_field.values())
            else:
                expected_value = 0

            self.ca.assert_that_pv_is_number("CORRECTEDFIELD:{}".format(component), expected_value)

    def test_GIVEN_test_input_field_strengths_WHEN_corrections_applied_THEN_corrected_fields_agree_with_labview(self):
        # GIVEN
        input_field = {"X": 11.1,
                       "Y": 22.2,
                       "Z": 33.3}

        input_offsets = {"X": -8.19e-1,
                         "Y": 3.45e-1,
                         "Z": -6.7e-1}

        sensor_matrix = np.array([-1.17e-1, 7.36e-2, -2e-1,
                                  -3.41e-1, -2.15e-1, -3e-1,
                                  -2.3e-1, -4e-2, 1e-1]).reshape(3, 3)

        self.write_simulated_field_values(input_field)
        self.write_sensor_matrix(sensor_matrix)

        for axis in input_offsets.keys():
            self.ca.set_pv_value("OFFSET:{}".format(axis), input_offsets[axis], sleep_after_set=0.0)

        # WHEN
        self.ca.process_pv("TAKEDATA")

        # THEN
        labview_result = {"X": -6.58,
                          "Y": -18.9542,
                          "Z": -0.21857}

        for component in AXES.keys():
            self.ca.assert_that_pv_is_number("CORRECTEDFIELD:{}".format(component),
                                             labview_result[component],
                                             tolerance=1e-4)

    def test_GIVEN_measured_data_WHEN_corrections_applied_THEN_field_magnitude_read_back(self):
        # GIVEN
        input_field = {"X": 2.2,
                       "Y": 3.3,
                       "Z": 4.4}

        sensor_matrix = np.array([-1.17e-1, 7.36e-2, -2e-1,
                                  -3.41e-1, -2.15e-1, -3e-1,
                                  -2.3e-1, -4e-2, 1e-1]).reshape(3, 3)

        self.write_simulated_field_values(input_field)
        self.write_offset(OFFSET)
        self.write_sensor_matrix(sensor_matrix)

        # WHEN
        self.ca.process_pv("TAKEDATA")

        # THEN
        expected_field_vals = self.apply_offset_and_matrix_multiplication(input_field, OFFSET, sensor_matrix)

        expected_magnitude = np.linalg.norm(expected_field_vals)

        self.ca.assert_that_pv_is_number("FIELDSTRENGTH", expected_magnitude, tolerance=0.1*expected_magnitude, timeout=30)

    def test_WHEN_takedata_alias_processed_THEN_all_magnetometer_axes_read_and_processed(self):
        # GIVEN
        test_field = {"X": 1.1,
                      "Y": 2.2,
                      "Z": 3.3}

        self.write_simulated_field_values(test_field)

        for component in AXES.keys():
            self.ca.assert_that_pv_is_not_number("DAQ:{}".format(component), test_field[component])

        # WHEN
        self.ca.process_pv("TAKEDATA")

        # THEN
        for component in AXES.keys():
            self.ca.assert_that_pv_is_number("DAQ:{}".format(component),
                                             test_field[component],
                                             tolerance=0.1*test_field[component])

    @parameterized.expand(parameterized_list(FIELD_STRENGTHS))
    def test_GIVEN_magnetometer_scaling_factor_WHEN_data_read_THEN_inputs_scaled_by_factor(self, _, factor):
        # GIVEN
        self.ca.set_pv_value("RANGE", factor, sleep_after_set=0.0)

        test_field = {"X": 1.1,
                      "Y": 2.2,
                      "Z": 3.3}

        self.write_simulated_field_values(test_field)

        self.ca.process_pv("TAKEDATA")

        # THEN
        for component in AXES.keys():
            self.ca.assert_that_pv_is_number("MEASURED:{}".format(component),
                                             test_field[component]*factor)

    @parameterized.expand(parameterized_list(AXES.keys()))
    def test_GIVEN_measured_field_too_high_THEN_overload_pv_reads_true_and_is_in_alarm(self, _, axis):
        # GIVEN
        test_field = {
            "X": 1.1,
            "Y": 1.1,
            "Z": 1.1
        }

        test_field[axis] = self.ca.get_pv_value("RANGE") * 4.5 + 1.0

        # WHEN
        self.write_simulated_field_values(test_field)
        self.ca.process_pv("TAKEDATA")

        # THEN
        self.ca.assert_that_pv_is("OVERLOAD", "OVERLOADED")
        self.ca.assert_that_pv_alarm_is("OVERLOAD", self.ca.Alarms.MAJOR)

    def test_GIVEN_measured_field_in_range_THEN_overload_pv_reads_false_and_not_in_alarm(self):
        # GIVEN
        test_value = self.get_overload_range_value() - 1.0

        test_field = {
            "X": test_value,
            "Y": test_value,
            "Z": test_value
        }

        # WHEN
        self.write_simulated_field_values(test_field)
        self.ca.process_pv("TAKEDATA")

        # THEN
        self.ca.assert_that_pv_is("OVERLOAD", "NORMAL")
        self.ca.assert_that_pv_alarm_is("OVERLOAD", self.ca.Alarms.NONE)

    def test_GIVEN_field_overloaded_THEN_output_PVs_in_major_alarm(self):
        # GIVEN
        overload_value = self.get_overload_range_value() + 1.0

        test_field = {
            "X": overload_value,
            "Y": overload_value,
            "Z": overload_value
        }

        self.write_simulated_field_values(test_field)

        self.ca.process_pv("TAKEDATA")

        # THEN
        self.ca.assert_that_pv_alarm_is("FIELDSTRENGTH", self.ca.Alarms.MAJOR)
        for axis in AXES.keys():
            self.ca.assert_that_pv_alarm_is("CORRECTEDFIELD:{}".format(axis), self.ca.Alarms.MAJOR)

    @parameterized.expand(parameterized_list(itertools.product([ChannelAccess.Alarms.INVALID,
                                              ChannelAccess.Alarms.MAJOR,
                                              ChannelAccess.Alarms.MAJOR], PVS_WHICH_USE_DAQ_DATA)))
    def test_GIVEN_raw_daq_pvs_in_alarm_WHEN_PVs_processed_THEN_alarm_copied_to_downstream_pvs(self, _, alarm, pv):
        # GIVEN
        self.ca.assert_that_pv_alarm_is("{}.SEVR".format(pv), self.ca.Alarms.NONE)

        self.write_simulated_alarm_level(alarm)

        self.ca.process_pv("TAKEDATA")

        # THEN
        self.ca.assert_that_pv_alarm_is("{}.SEVR".format(pv), alarm)

    @parameterized.expand(parameterized_list(AXES.keys()))
    def test_GIVEN_smoothing_samples_WHEN_setting_field_THEN_average_field_is_given(self, _, axis):
        number_samples = 10
        field_number = 100
        pv = "DAQ:{}".format(axis)
        with self._ioc.start_with_macros({"NUM_SAMPLES": number_samples}, pv_to_wait_for=pv):
            field = {"X": 0,
                     "Y": 0,
                     "Z": 0}
            self.write_simulated_field_values(field)

            for i in range(1, number_samples+1):
                self.ca.process_pv("TAKEDATA")
            self.ca.process_pv("TAKEDATA")
            # make sure the field is 0
            self.ca.assert_that_pv_is_number(pv, 0)

            # Change the field number
            field[axis] = field_number
            self.write_simulated_field_values(field)

            # every sample check the average has been processed correctly
            for i in range(1, number_samples+1):
                self.ca.process_pv("TAKEDATA")
                # assert that after every TAKEDATA the average has gone up by the field_number divided by the sample
                # number
                self.ca.assert_that_pv_is_number(pv, (field_number // number_samples) * i)
            
            # Check the final value stays the same
            self.ca.process_pv("TAKEDATA")
            self.ca.assert_that_pv_is_number(pv, field_number)
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
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)
예제 #6
0
class TiZrTests(unittest.TestCase):
    """
    Tests for the TiZr cell monitoring logic.
    """
    def setUp(self):
        self._ioc = IOCRegister.get_running(IOC_PREFIX)
        self.ca = ChannelAccess(default_timeout=20)

        for pv in [SIMPLE_VALUE_ONE, SIMPLE_VALUE_TWO]:
            self.ca.assert_that_pv_exists(pv)

        self.set_safe_values()
        self.ca.set_pv_value(MONITORING_ON_PV, "No")
        self.ca.assert_that_pv_is(MONITORING_ON_PV, "No")

    @contextmanager
    def monitoring_on(self):
        try:
            self.ca.set_pv_value(MONITORING_ON_PV, "Yes")
            self.ca.assert_that_pv_is(MONITORING_ON_PV, "Yes")
            yield
        finally:
            self.ca.set_pv_value(MONITORING_ON_PV, "No")
            self.ca.assert_that_pv_is(MONITORING_ON_PV, "No")

    def set_safe_values(self):
        self.ca.set_pv_value(SIMPLE_VALUE_ONE, IN_RANGE_PVONE_VAL)
        self.ca.set_pv_value(SIMPLE_VALUE_TWO, IN_RANGE_PVTWO_VAL)
        self.ca.set_pv_value(WARNING_PV, 0)

    def test_GIVEN_monitor_on_AND_PVONE_above_max_WHEN_PVTWO_goes_out_of_range_THEN_alarm_and_safe_value_written_to_PVONE(
            self):
        with self.monitoring_on():
            self.ca.set_pv_value(SIMPLE_VALUE_ONE, OUT_OF_RANGE_PVONE_VAL)
            self.ca.assert_that_pv_is_number(SIMPLE_VALUE_ONE,
                                             OUT_OF_RANGE_PVONE_VAL,
                                             tolerance=1e-4)

            self.ca.set_pv_value(SIMPLE_VALUE_TWO, OUT_OF_RANGE_PVTWO_VAL)

            self.ca.assert_that_pv_is_not_number(SIMPLE_VALUE_ONE,
                                                 OUT_OF_RANGE_PVONE_VAL,
                                                 tolerance=1e-4)
            self.ca.assert_that_pv_is_number(SIMPLE_VALUE_ONE,
                                             SAFE_VALUE,
                                             tolerance=1e-4)

            self.ca.assert_that_pv_alarm_is(WARNING_PV, self.ca.Alarms.MAJOR)

    def test_GIVEN_monitor_off_AND_PVONE_above_max_WHEN_PVTWO_goes_out_of_range_THEN_no_alarm_and_no_safe_value_written_to_PVONE(
            self):
        self.ca.set_pv_value(SIMPLE_VALUE_ONE, OUT_OF_RANGE_PVONE_VAL)
        self.ca.assert_that_pv_is_number(SIMPLE_VALUE_ONE,
                                         OUT_OF_RANGE_PVONE_VAL,
                                         tolerance=1e-4)

        self.ca.set_pv_value(SIMPLE_VALUE_TWO, OUT_OF_RANGE_PVTWO_VAL)

        self.ca.assert_that_pv_is_not_number(SIMPLE_VALUE_ONE,
                                             SAFE_VALUE,
                                             tolerance=1e-4)
        self.ca.assert_that_pv_is_number(SIMPLE_VALUE_ONE,
                                         OUT_OF_RANGE_PVONE_VAL,
                                         tolerance=1e-4)

        self.ca.assert_that_pv_alarm_is_not(WARNING_PV, self.ca.Alarms.MAJOR)

    def test_GIVEN_monitor_on_AND_PVTWO_above_max_WHEN_PVONE_out_of_range_THEN_alarm_and_safe_value_written_to_PVONE(
            self):
        with self.monitoring_on():
            self.ca.set_pv_value(SIMPLE_VALUE_TWO, OUT_OF_RANGE_PVTWO_VAL)
            self.ca.assert_that_pv_is_number(SIMPLE_VALUE_TWO,
                                             OUT_OF_RANGE_PVTWO_VAL,
                                             tolerance=1e-4)

            self.ca.set_pv_value(SIMPLE_VALUE_ONE, OUT_OF_RANGE_PVONE_VAL)

            self.ca.assert_that_pv_alarm_is(WARNING_PV, self.ca.Alarms.MAJOR)

            self.ca.assert_that_pv_is_not_number(SIMPLE_VALUE_ONE,
                                                 OUT_OF_RANGE_PVONE_VAL,
                                                 tolerance=1e-4)
            self.ca.assert_that_pv_is_number(SIMPLE_VALUE_ONE,
                                             SAFE_VALUE,
                                             tolerance=1e-4)

    def test_GIVEN_monitor_off_AND_PVTWO_above_max_WHEN_PVONE_out_of_range_THEN_no_alarm_and_no_safe_value_written_to_PVONE(
            self):
        self.ca.set_pv_value(SIMPLE_VALUE_TWO, OUT_OF_RANGE_PVTWO_VAL)
        self.ca.assert_that_pv_is_number(SIMPLE_VALUE_TWO,
                                         OUT_OF_RANGE_PVTWO_VAL,
                                         tolerance=1e-4)

        self.ca.set_pv_value(SIMPLE_VALUE_ONE, OUT_OF_RANGE_PVONE_VAL)

        self.ca.assert_that_pv_alarm_is_not(WARNING_PV, self.ca.Alarms.MAJOR)

        self.ca.assert_that_pv_is_not_number(SIMPLE_VALUE_ONE,
                                             SAFE_VALUE,
                                             tolerance=1e-4)
        self.ca.assert_that_pv_is_number(SIMPLE_VALUE_ONE,
                                         OUT_OF_RANGE_PVONE_VAL,
                                         tolerance=1e-4)

    def test_GIVEN_monitor_on_AND_PVONE_and_PVTWO_in_range_WHEN_in_range_value_written_THEN_no_alarm_and_value_written(
            self):
        with self.monitoring_on():
            self.ca.set_pv_value(SIMPLE_VALUE_ONE, IN_RANGE_PVONE_VAL)
            self.ca.set_pv_value(SIMPLE_VALUE_TWO, IN_RANGE_PVTWO_VAL)

            self.ca.assert_that_pv_is_number(SIMPLE_VALUE_ONE,
                                             IN_RANGE_PVONE_VAL)
            self.ca.assert_that_pv_is_number(SIMPLE_VALUE_TWO,
                                             IN_RANGE_PVTWO_VAL)

            self.ca.assert_that_pv_alarm_is(WARNING_PV, self.ca.Alarms.NONE)

            self.ca.assert_that_pv_alarm_is(SIMPLE_VALUE_ONE,
                                            self.ca.Alarms.NONE)
            self.ca.assert_that_pv_alarm_is(SIMPLE_VALUE_TWO,
                                            self.ca.Alarms.NONE)

    @parameterized.expand(
        parameterized_list([(SIMPLE_VALUE_ONE, IN_RANGE_PVONE_VAL),
                            (SIMPLE_VALUE_TWO, IN_RANGE_PVTWO_VAL)]))
    def test_GIVEN_monitor_on_AND_pvs_in_range_WHEN_one_goes_out_of_range_THEN_no_alarm_and_value_written(
            self, _, pv, in_range_value):
        with self.monitoring_on():
            self.ca.set_pv_value(pv, in_range_value)
            self.ca.assert_that_pv_is_number(pv, in_range_value)
            self.ca.assert_that_pv_alarm_is(WARNING_PV, self.ca.Alarms.NONE)
            self.ca.assert_that_pv_alarm_is(pv, self.ca.Alarms.NONE)

    def test_GIVEN_monitor_on_AND_safe_value_is_out_of_range_WHEN_safe_value_set_THEN_alarm_persisted(
            self):
        macros = tizr_macros
        macros["SAFE_VALUE"] = OUT_OF_RANGE_PVONE_VAL
        macros["MONITORING_ON"] = "1"
        with self._ioc.start_with_macros(macros, "TIZRWARNING"):
            self.ca.assert_that_pv_is(MONITORING_ON_PV, "Yes")
            self.ca.set_pv_value(SIMPLE_VALUE_ONE, OUT_OF_RANGE_PVONE_VAL)
            self.ca.assert_that_pv_is_number(SIMPLE_VALUE_ONE,
                                             OUT_OF_RANGE_PVONE_VAL,
                                             tolerance=1e-4)

            self.ca.set_pv_value(SIMPLE_VALUE_TWO, OUT_OF_RANGE_PVTWO_VAL)
            self.ca.assert_that_pv_is_number(SIMPLE_VALUE_TWO,
                                             OUT_OF_RANGE_PVTWO_VAL,
                                             tolerance=1e-4)

            self.ca.assert_that_pv_alarm_is(WARNING_PV, self.ca.Alarms.MAJOR)
            time.sleep(10)
            self.ca.assert_that_pv_alarm_is(WARNING_PV, self.ca.Alarms.MAJOR)