def test_cancel_pending_tasks(self):
        # given
        task_done = Event()
        task_wait = Event()
        task_exec = False

        def task():
            nonlocal task_exec
            task_exec = True
            task_done.set()
            # the test will cancel tasks right before allowing
            # execution.
            task_wait.wait()

        cancelled_task_exec = False

        def cancelled_task():
            # this should not execute, due to cancel_pending_tasks
            nonlocal cancelled_task_exec
            cancelled_task_exec = True

        target = TaskLoop()
        target.add_task(task)
        target.add_task(cancelled_task)
        target.start()
        try:
            task_done.wait()
            # when
            target.cancel_pending_tasks()
            # then
            task_wait.set()
            # this future should execute after without
            # the cancelled task ever executing
            future = target.add_future(lambda: True)
            future.get()
            self.assertTrue(task_exec)
            self.assertFalse(cancelled_task_exec)
        finally:
            target.stop()
class EquipmentReader(ScreenReader):
    def __init__(self):
        super().__init__()
        self.task_loop = TaskLoop()

    def open(self):
        super().open()
        self.task_loop.start()

    def stop(self):
        super().close()
        self.task_loop.stop()

    def cancel_pending_futures(self):
        """Cancels pending future values for equipment status"""
        self.task_loop.cancel_pending_tasks()

    def _compare_screen_coords(self, coords: Tuple[int, int],
                               color_spec: Tuple[str, ...]):
        return ScreenReader.matches_screen(self, coords, color_spec)

    def matches_screen(self, coords, specs):
        if type(specs) == list or type(specs) == tuple:
            for animation_spec in specs:
                if self._compare_screen_coords(coords, animation_spec.colors):
                    return True
            return False
        else:
            return self._compare_screen_coords(coords, specs.colors)

    def get_equipment_status(
        self,
        emergency_action_amulet_cb: Callable[[str], None] = NOOP,
        emergency_action_ring_cb: Callable[[str], None] = NOOP,
        equipped_ring_cb: Callable[[str], None] = NOOP,
        equipped_amulet_cb: Callable[[str], None] = NOOP,
        magic_shield_status_cb: Callable[[str],
                                         None] = NOOP) -> EquipmentStatus:
        return FutureEquipmentStatus({
            'equipped_amulet':
            self.task_loop.add_future(
                self.get_equipped_amulet_name, equipped_amulet_cb,
                lambda e: equipped_amulet_cb('ERROR, check logs')),
            'equipped_ring':
            self.task_loop.add_future(
                self.get_equipped_ring_name, equipped_ring_cb,
                lambda e: equipped_ring_cb('ERROR, check logs')),
            'magic_shield_status':
            self.task_loop.add_future(
                self.get_magic_shield_status, magic_shield_status_cb,
                lambda e: magic_shield_status_cb('ERROR, check logs')),
            'emergency_action_amulet':
            self.task_loop.add_future(
                self.get_emergency_action_bar_amulet_name,
                emergency_action_amulet_cb,
                lambda e: emergency_action_amulet_cb('ERROR, check logs')),
            'emergency_action_ring':
            self.task_loop.add_future(
                self.get_emergency_action_bar_ring_name,
                emergency_action_ring_cb,
                lambda e: emergency_action_ring_cb('ERROR, check logs')),
        })

    def get_emergency_action_bar_amulet_name(self) -> AmuletName:
        color_spec = spec(*self.get_pixels(EMERGENCY_ACTION_BAR_AMULET_COORDS))
        return AMULET_REPOSITORY.get_action_name(color_spec)

    def get_equipped_amulet_name(self) -> AmuletName:
        color_spec = spec(*self.get_pixels(EQUIPPED_AMULET_COORDS))
        return AMULET_REPOSITORY.get_equipment_name(color_spec)

    def get_emergency_action_bar_ring_name(self) -> RingName:
        color_spec = spec(*self.get_pixels(EMERGENCY_ACTION_BAR_RING_COORDS))
        return RING_REPOSITORY.get_action_name(color_spec)

    def get_equipped_ring_name(self) -> RingName:
        color_spec = spec(*self.get_pixels(EQUIPPED_RING_COORDS))
        return RING_REPOSITORY.get_equipment_name(color_spec)

    def is_emergency_action_bar_amulet(self, name: ItemName):
        amulet = AMULET_REPOSITORY.get(name)
        return self.matches_screen(EMERGENCY_ACTION_BAR_AMULET_COORDS,
                                   amulet.action_color_specs)

    def is_emergency_action_bar_ring(self, name: ItemName):
        ring = RING_REPOSITORY.get(name)
        return self.matches_screen(EMERGENCY_ACTION_BAR_RING_COORDS,
                                   ring.action_color_specs)

    def is_amulet(self, name: ItemName):
        spec = AMULET_REPOSITORY.get(name).eq_color_specs
        return self.matches_screen(EQUIPPED_AMULET_COORDS, spec)

    def is_amulet_empty(self):
        return self.is_amulet(AmuletName.EMPTY)

    def is_ring(self, name: ItemName):
        spec = RING_REPOSITORY.get(name).eq_color_specs
        return self.matches_screen(EQUIPPED_RING_COORDS, spec)

    def is_ring_empty(self):
        return self.is_ring(RingName.EMPTY)

    def get_magic_shield_status(self) -> str:
        for name in MAGIC_SHIELD_SPEC:
            specs = []
            if isinstance(MAGIC_SHIELD_SPEC[name][0], list):
                specs = [spec(*_spec) for _spec in MAGIC_SHIELD_SPEC[name]]
            else:
                specs = spec(*MAGIC_SHIELD_SPEC[name])

            if self.matches_screen(MAGIC_SHIELD_COORDS, specs):
                return name
        # There are only 3 possible states: recently cast, off cooldown and
        # on cooldown.
        return MagicShieldStatus.ON_COOLDOWN
class EquipmentReader(ScreenReader):
    def __init__(self, tibia_wid: int, tibia_window_spec: TibiaWindowSpec):
        super().__init__(tibia_wid=tibia_wid)
        self.tibia_window_spec = tibia_window_spec
        self.item_repository = ItemRepositoryContainer(
            tibia_window_spec.item_repository
        )
        self.task_loop = TaskLoop()

    def __enter__(self, *args, **kwargs) -> 'EquipmentReader':
        self.open()
        return self

    def __exit__(self, *args, **kwargs):
        self.close()

    def open(self):
        super().open()
        self.task_loop.start()

    def close(self):
        super().close()
        self.task_loop.stop()

    def cancel_pending_futures(self):
        """Cancels pending future values for equipment status"""
        self.task_loop.cancel_pending_tasks()

    def get_equipment_status(
        self,
        emergency_action_amulet_cb: Callable[[str], None] = NOOP,
        emergency_action_ring_cb: Callable[[str], None] = NOOP,
        tank_action_amulet_cb: Callable[[str], None] = NOOP,
        tank_action_ring_cb: Callable[[str], None] = NOOP,
        equipped_ring_cb: Callable[[str], None] = NOOP,
        equipped_amulet_cb: Callable[[str], None] = NOOP,
        magic_shield_status_cb: Callable[[str], None] = NOOP,
        normal_action_amulet_cb: Callable[[str], None] = NOOP,
        normal_action_ring_cb: Callable[[str], None] = NOOP,

    ) -> EquipmentStatus:
        return FutureEquipmentStatus(
            {
                "equipped_amulet": self.task_loop.add_future(
                    self.get_equipped_amulet_name,
                    equipped_amulet_cb,
                    lambda e: equipped_amulet_cb("ERROR, check logs"),
                ),
                "equipped_ring": self.task_loop.add_future(
                    self.get_equipped_ring_name,
                    equipped_ring_cb,
                    lambda e: equipped_ring_cb("ERROR, check logs"),
                ),
                "magic_shield_status": self.task_loop.add_future(
                    self.get_magic_shield_status,
                    magic_shield_status_cb,
                    lambda e: magic_shield_status_cb("ERROR, check logs"),
                ),
                "emergency_action_amulet": self.task_loop.add_future(
                    self.get_emergency_action_bar_amulet_name,
                    emergency_action_amulet_cb,
                    lambda e: emergency_action_amulet_cb("ERROR, check logs"),
                ),
                "emergency_action_ring": self.task_loop.add_future(
                    self.get_emergency_action_bar_ring_name,
                    emergency_action_ring_cb,
                    lambda e: emergency_action_ring_cb("ERROR, check logs"),
                ),
                "tank_action_amulet": self.task_loop.add_future(
                    self.get_tank_action_bar_amulet_name,
                    tank_action_amulet_cb,
                    lambda e: tank_action_amulet_cb("ERROR, check logs"),
                ),
                "tank_action_ring": self.task_loop.add_future(
                    self.get_tank_action_bar_ring_name,
                    tank_action_ring_cb,
                    lambda e: tank_action_ring_cb("ERROR, check logs"),
                ),
                "normal_action_amulet": self.task_loop.add_future(
                    self.get_normal_action_bar_amulet_name,
                    normal_action_amulet_cb,
                    lambda e: normal_action_amulet_cb("ERROR, check logs"),
                ),
                "normal_action_ring": self.task_loop.add_future(
                    self.get_normal_action_bar_ring_name,
                    normal_action_ring_cb,
                    lambda e: normal_action_ring_cb("ERROR, check logs"),
                ),

            }
        )

    def read_equipment_colors(self, coords: EquipmentCoords) -> ItemColors:
        return ItemColors(
            north=self.get_coord_color(coords.north),
            south=self.get_coord_color(coords.south),
            left=self.get_coord_color(coords.left),
            right=self.get_coord_color(coords.right),
        )

    def gen_square_coords(self, center: Coord, delta: int) -> EquipmentCoords:
        return EquipmentCoords(
            north=Coord(center.x, center.y - delta),
            south=Coord(center.x, center.y + delta),
            left=Coord(center.x - delta, center.y),
            right=Coord(center.x + delta, center.y),
        )

    def matches_screen_item(
        self, coords: EquipmentCoords, color_specs: List[ItemColors]
    ) -> bool:
        actual_color_spec = self.read_equipment_colors(coords)
        for color_spec in color_specs:
            if actual_color_spec == color_spec:
                return True

        return False

    # start: item lookup methods

    def lookup_ring_by_name(self, name: Union[str, ItemName]) -> ItemEntry:
        return self.item_repository.lookup_ring_by_name(name)

    def lookup_amulet_by_name(self, name: Union[str, ItemName]) -> ItemEntry:
        return self.item_repository.lookup_amulet_by_name(name)

    def lookup_amulet_by_action_bar_colors(self, colors: ItemColors) -> ItemEntry:
        return self.item_repository.lookup_amulet_by_action_bar_colors(colors)

    def lookup_ring_by_action_bar_colors(self, colors: ItemColors) -> ItemEntry:
        return self.item_repository.lookup_ring_by_action_bar_colors(colors)

    def lookup_ring_by_equipped_colors(self, colors: ItemColors) -> ItemEntry:
        return self.item_repository.lookup_ring_by_equipped_colors(colors)

    def lookup_amulet_by_equipped_colors(self, colors: ItemColors) -> ItemEntry:
        return self.item_repository.lookup_amulet_by_equipped_colors(colors)

    # end: item lookup methods

    # start: read ring methods
    def gen_action_bar_ring_coords(self, center: Coord) -> EquipmentCoords:
        return self.gen_square_coords(center, 3)

    def get_normal_action_bar_ring_name(self) -> str:
        return self.lookup_ring_by_action_bar_colors(
            self.read_action_bar_normal_ring_colors()
        ).name

    def read_action_bar_normal_ring_colors(self) -> ItemColors:
        return self.read_equipment_colors(
            self.gen_action_bar_ring_coords(
                self.tibia_window_spec.action_bar.ring_center
            )
        )

    def get_emergency_action_bar_ring_name(self) -> str:
        return self.lookup_ring_by_action_bar_colors(
            self.read_action_bar_emergency_ring_colors()
        ).name

    def read_action_bar_emergency_ring_colors(self) -> ItemColors:
        return self.read_equipment_colors(
            self.gen_action_bar_ring_coords(
                self.tibia_window_spec.action_bar.emergency_ring_center
            )
        )

    def read_action_bar_tank_ring_colors(self) -> ItemColors:
        return self.read_equipment_colors(
            self.gen_action_bar_ring_coords(
                self.tibia_window_spec.action_bar.tank_ring_center
            )
        )

    def get_tank_action_bar_ring_name(self) -> str:
        if not self.tibia_window_spec.action_bar.tank_ring_center:
            return RingName.UNKNOWN.name

        return self.lookup_ring_by_action_bar_colors(
            self.read_action_bar_tank_ring_colors()
        ).name

    def read_equipped_ring_colors(self) -> ItemColors:
        return self.read_equipment_colors(self.tibia_window_spec.char_equipment.ring)

    def get_equipped_ring_name(self) -> str:
        return self.lookup_ring_by_equipped_colors(
            self.read_equipped_ring_colors()
        ).name

    def is_normal_action_bar_ring(self, name: ItemName):
        return self.matches_screen_item(
            self.gen_action_bar_ring_coords(
                self.tibia_window_spec.action_bar.ring_center
            ),
            self.lookup_ring_by_name(name).action_bar_colors,
        )

    def is_emergency_action_bar_ring(self, name: ItemName):
        return self.matches_screen_item(
            self.gen_action_bar_ring_coords(
                self.tibia_window_spec.action_bar.emergency_ring_center
            ),
            self.lookup_ring_by_name(name).action_bar_colors,
        )

    def is_tank_action_bar_ring(self, name: ItemName):
        if not self.tibia_window_spec.action_bar.tank_ring_center:
            return False

        return self.matches_screen_item(
            self.gen_action_bar_ring_coords(
                self.tibia_window_spec.action_bar.tank_ring_center
            ),
            self.lookup_ring_by_name(name).action_bar_colors,
        )

    def is_ring(self, name: ItemName) -> bool:
        return self.matches_screen_item(
            self.tibia_window_spec.char_equipment.ring,
            self.lookup_ring_by_name(name).equipped_colors,
        )

    def is_ring_empty(self):
        return self.is_ring(RingName.EMPTY)

    # end: read ring methods

    # start: read amulet methods

    def gen_action_bar_amulet_coords(self, center: Coord) -> EquipmentCoords:
        return self.gen_square_coords(center, 10)

    def read_action_bar_normal_amulet_colors(self) -> ItemColors:
        return self.read_equipment_colors(
            self.gen_action_bar_amulet_coords(
                self.tibia_window_spec.action_bar.amulet_center
            )
        )

    def get_normal_action_bar_amulet_name(self) -> str:
        return self.lookup_amulet_by_action_bar_colors(
            self.read_action_bar_normal_amulet_colors()
        ).name

    def read_action_bar_emergency_amulet_colors(self) -> ItemColors:
        return self.read_equipment_colors(
            self.gen_action_bar_amulet_coords(
                self.tibia_window_spec.action_bar.emergency_amulet_center
            )
        )

    def get_emergency_action_bar_amulet_name(self) -> str:
        return self.lookup_amulet_by_action_bar_colors(
            self.read_action_bar_emergency_amulet_colors()
        ).name

    def read_action_bar_tank_amulet_colors(self) -> ItemColors:
        if not self.tibia_window_spec.action_bar.tank_amulet_center:
            raise Exception("tibia_window_spec.action_bar.tank_amulet_center is not set")

        return self.read_equipment_colors(
            self.gen_action_bar_amulet_coords(
                self.tibia_window_spec.action_bar.tank_amulet_center
            )
        )

    def get_tank_action_bar_amulet_name(self) -> str:
        if not self.tibia_window_spec.action_bar.amulet_center:
            return AmuletName.UNKNOWN.name

        return self.lookup_amulet_by_action_bar_colors(
            self.read_action_bar_tank_amulet_colors()
        ).name

    def read_equipped_amulet_colors(self) -> ItemColors:
        return self.read_equipment_colors(self.tibia_window_spec.char_equipment.amulet)

    def get_equipped_amulet_name(self) -> str:
        return self.lookup_amulet_by_equipped_colors(
            self.read_equipped_amulet_colors()
        ).name

    def is_normal_action_bar_amulet(self, name: ItemName) -> bool:
        return self.matches_screen_item(
            self.gen_action_bar_amulet_coords(
                self.tibia_window_spec.action_bar.amulet_center
            ),
            self.lookup_amulet_by_name(name).action_bar_colors,
        )

    def is_emergency_action_bar_amulet(self, name: ItemName) -> bool:
        return self.matches_screen_item(
            self.gen_action_bar_amulet_coords(
                self.tibia_window_spec.action_bar.emergency_amulet_center
            ),
            self.lookup_amulet_by_name(name).action_bar_colors,
        )

    def is_tank_action_bar_amulet(self, name: ItemName) -> bool:
        if not self.tibia_window_spec.action_bar.tank_amulet_center:
            return False

        return self.matches_screen_item(
            self.gen_action_bar_amulet_coords(
                self.tibia_window_spec.action_bar.tank_amulet_center
            ),
            self.lookup_amulet_by_name(name).action_bar_colors,
        )

    def is_amulet(self, name: ItemName):
        return self.matches_screen_item(
            self.tibia_window_spec.char_equipment.amulet,
            self.lookup_amulet_by_name(name).equipped_colors,
        )

    def is_amulet_empty(self) -> bool:
        return self.is_amulet(AmuletName.EMPTY)

    # end: read amulet methods

    def get_magic_shield_status(self) -> str:
        magic_shield_spec = self.tibia_window_spec.action_bar.magic_shield
        color_str = self.get_coord_color(magic_shield_spec.coord)
        if color_str in magic_shield_spec.off_cooldown_color:
            return MagicShieldStatus.OFF_COOLDOWN
        if color_str in magic_shield_spec.recently_cast_color:
            return MagicShieldStatus.RECENTLY_CAST

        # There are only 3 possible states: recently cast, off cooldown and
        # on cooldown.
        return MagicShieldStatus.ON_COOLDOWN