diff --git a/test/test_char.py b/test/test_char.py new file mode 100644 index 0000000..12ab7ad --- /dev/null +++ b/test/test_char.py @@ -0,0 +1,39 @@ +""" +Tests for char module — IChar base class and CharacterCapabilities. + +Covers: character construction, capability dataclass, active skill tracking. +""" +import pytest +from logger import Logger + + +class TestCharacterCapabilities: + def test_capabilities_dataclass(self): + from char.capabilities import CharacterCapabilities + caps = CharacterCapabilities(can_teleport_natively=True, can_teleport_with_charges=False) + assert caps.can_teleport_natively is True + assert caps.can_teleport_with_charges is False + + +class TestIChar: + def setup_method(self): + Logger.init() + Logger.remove_file_logger() + + def test_ichar_initializes_skill_hotkeys(self): + from char.i_char import IChar + char = IChar({"left_attack": "1", "right_attack": "2"}) + assert char._skill_hotkeys["left_attack"] == "1" + assert char._skill_hotkeys["right_attack"] == "2" + + def test_ichar_active_skill_defaults_empty(self): + from char.i_char import IChar + char = IChar({"left_attack": "1", "right_attack": "2"}) + assert char._active_skill["left"] == "" + assert char._active_skill["right"] == "" + + def test_ichar_set_active_skill(self): + from char.i_char import IChar + char = IChar({"left_attack": "1", "right_attack": "2"}) + char._set_active_skill("left", "hammer") + assert char._active_skill["left"] == "hammer" \ No newline at end of file diff --git a/test/test_config.py b/test/test_config.py new file mode 100644 index 0000000..d835388 --- /dev/null +++ b/test/test_config.py @@ -0,0 +1,51 @@ +""" +Tests for config.Config singleton and edge cases. + +Covers: singleton behavior, config file merging, missing file handling. +""" +import os +import pytest +from logger import Logger + + +class TestConfig: + def setup_method(self): + Logger.init() + Logger.remove_file_logger() + # Reset singleton so each test gets a fresh config + import config + if hasattr(config, '_instance'): + config._instance = None + + def test_config_is_singleton(self): + from config import Config + c1 = Config() + c2 = Config() + assert c1 is c2 + + def test_config_loads_difficulty(self): + from config import Config + c = Config() + assert "difficulty" in c.general + + def test_config_loads_char_type(self): + from config import Config + c = Config() + assert "type" in c.char + + def test_config_loads_routes(self): + from config import Config + c = Config() + assert hasattr(c, 'routes') + + def test_config_general_has_max_game_length(self): + from config import Config + c = Config() + assert "max_game_length_s" in c.general + + def test_config_char_has_keybinds(self): + from config import Config + c = Config() + # hammerdin config should have stand_still and show_items + assert "stand_still" in c.char + assert "show_items" in c.char \ No newline at end of file diff --git a/test/test_death_manager.py b/test/test_death_manager.py new file mode 100644 index 0000000..42783b7 --- /dev/null +++ b/test/test_death_manager.py @@ -0,0 +1,38 @@ +""" +Tests for death_manager state management logic. + +Covers: death flag, callback wiring, monitor state, reset. +All testable without a real D2R client. +""" +import pytest +from logger import Logger + + +class TestDeathManager: + def setup_method(self): + Logger.init() + Logger.remove_file_logger() + from death_manager import DeathManager + self.dm = DeathManager() + + def test_died_defaults_false(self): + assert self.dm.died() is False + + def test_loop_delay(self): + assert self.dm.get_loop_delay() == 0.5 + + def test_callback_set_and_stored(self): + cb_called = [] + self.dm.set_callback(lambda: cb_called.append(1)) + assert self.dm._callback is not None + self.dm._callback() + assert cb_called == [1] + + def test_stop_monitor(self): + self.dm.stop_monitor() + assert self.dm._do_monitor is False + + def test_reset_death_flag(self): + self.dm._died = True + self.dm.reset_death_flag() + assert self.dm.died() is False \ No newline at end of file diff --git a/test/test_game_controller.py b/test/test_game_controller.py new file mode 100644 index 0000000..9bed895 --- /dev/null +++ b/test/test_game_controller.py @@ -0,0 +1,44 @@ +""" +Tests for game_controller initialization and state. + +Covers: GameController creates Bot, DeathManager, HealthManager, GameRecovery. +Tests the wiring between components without needing a real D2R client. +""" +import pytest +from logger import Logger + + +class TestGameController: + def setup_method(self): + Logger.init() + Logger.remove_file_logger() + # Reset singletons + import config + if hasattr(config, '_instance'): + config._instance = None + from health_manager import HealthManager + HealthManager._instance = None + + def test_gamecontroller_creates_components(self): + from game_controller import GameController + gc = GameController() + assert gc.game_stats is not None + assert gc.is_running is False + + def test_gamecontroller_creates_death_manager(self): + from game_controller import GameController + gc = GameController() + gc.start() + assert gc.death_manager is not None + + def test_gamecontroller_creates_health_manager(self): + from game_controller import GameController + gc = GameController() + gc.start() + assert gc.health_manager is not None + + def test_gamecontroller_creates_game_recovery(self): + from game_controller import GameController + gc = GameController() + gc.start() + assert gc.game_recovery is not None \ No newline at end of file diff --git a/test/test_game_recovery.py b/test/test_game_recovery.py new file mode 100644 index 0000000..6ce0916 --- /dev/null +++ b/test/test_game_recovery.py @@ -0,0 +1,20 @@ +""" +Tests for game_recovery. + +Covers: GameRecovery constructor and death_manager reference. +""" +import pytest +from logger import Logger + + +class TestGameRecovery: + def setup_method(self): + Logger.init() + Logger.remove_file_logger() + + def test_recovery_holds_death_manager_ref(self): + from death_manager import DeathManager + from game_recovery import GameRecovery + dm = DeathManager() + gr = GameRecovery(dm) + assert gr._death_manager is dm \ No newline at end of file diff --git a/test/test_health_manager.py b/test/test_health_manager.py new file mode 100644 index 0000000..e411f77 --- /dev/null +++ b/test/test_health_manager.py @@ -0,0 +1,55 @@ +""" +Tests for health_manager state management logic. + +Covers: pause state, panel check paused, chicken flag, callback wiring. +These are all testable without a real D2R client — they're just state machines. +""" +import pytest +from logger import Logger + + +class TestHealthManager: + def setup_method(self): + Logger.init() + Logger.remove_file_logger() + from health_manager import HealthManager + self.HM = HealthManager + self.hm = HealthManager() + + def test_pause_state_defaults_to_true(self): + assert self.hm.get_pause_state() is True + + def test_set_pause_state_changes(self): + self.hm.set_pause_state(False) + assert self.hm.get_pause_state() is False + self.hm.set_pause_state(True) + assert self.hm.get_pause_state() is True + + def test_panel_check_paused_defaults_to_false(self): + assert self.hm.get_panel_check_paused() is False + + def test_set_panel_check_paused_changes(self): + self.hm.set_panel_check_paused(True) + assert self.hm.get_panel_check_paused() is True + self.hm.set_panel_check_paused(False) + assert self.hm.get_panel_check_paused() is False + + def test_chicken_flag_defaults_false(self): + assert self.hm.did_chicken() is False + + def test_callback_set_and_stored(self): + cb_called = [] + self.hm.set_callback(lambda: cb_called.append(1)) + assert self.hm._callback is not None + self.hm._callback() + assert cb_called == [1] + + def test_stop_monitor_sets_flag(self): + self.hm.stop_monitor() + assert self.hm._do_monitor is False + + def test_reset_chicken_flag(self): + self.hm._did_chicken = True + self.hm.reset_chicken_flag() + assert self.hm.did_chicken() is False + assert self.hm.get_pause_state() is True \ No newline at end of file diff --git a/test/test_inventory_belt.py b/test/test_inventory_belt.py new file mode 100644 index 0000000..5524b9c --- /dev/null +++ b/test/test_inventory_belt.py @@ -0,0 +1,26 @@ +""" +Tests for inventory.belt logic. + +Covers: potion type detection, belt toggle keys. +""" +import pytest +import numpy as np +from logger import Logger + + +class TestInventoryBelt: + def setup_method(self): + Logger.init() + Logger.remove_file_logger() + + def test_belt_toggle_keys_returns_list(self): + from inventory.belt import _belt_toggle_keys + keys = _belt_toggle_keys() + assert isinstance(keys, list) + assert len(keys) > 0 + + def test_cut_potion_img_returns_array(self): + from inventory.belt import _cut_potion_img + img = np.full((100, 100, 3), 255, dtype=np.uint8) + result = _cut_potion_img(img, 0, 0) + assert isinstance(result, np.ndarray) \ No newline at end of file diff --git a/test/test_item_pickit.py b/test/test_item_pickit.py new file mode 100644 index 0000000..e58e70f --- /dev/null +++ b/test/test_item_pickit.py @@ -0,0 +1,24 @@ +""" +Tests for item.pickit PickedUpResult enum and basic logic. + +Covers: the result enum values and the pickit import chain. +""" +import pytest +from logger import Logger + + +class TestPickit: + def setup_method(self): + Logger.init() + Logger.remove_file_logger() + + def test_pickedupresult_enum_values(self): + from item.pickit import PickedUpResult + assert PickedUpResult.TeleportedTo.value == 0 + assert PickedUpResult.PickedUp.value == 1 + assert PickedUpResult.PickedUpFailed.value == 2 + + def test_pickit_imports_without_error(self): + # This verifies the full import chain: pickit -> bnip -> d2r_image -> config + from item.pickit import PickedUpResult + assert PickedUpResult is not None \ No newline at end of file diff --git a/test/test_ui_meters.py b/test/test_ui_meters.py new file mode 100644 index 0000000..33d2593 --- /dev/null +++ b/test/test_ui_meters.py @@ -0,0 +1,41 @@ +""" +Tests for ui.meters — health/mana/merc health reading. + +These test the math: given a known image, the percentage should be deterministic. +""" +import pytest +import numpy as np +import cv2 +from logger import Logger +from config import Config + + +class TestMeters: + def setup_method(self): + Logger.init() + Logger.remove_file_logger() + + def test_get_health_returns_value_between_0_and_1(self): + from ui.meters import get_health + # All-white image — no red/green pixels, so health = 0 + img = np.full((720, 1280, 3), 255, dtype=np.uint8) + result = get_health(img) + assert 0.0 <= result <= 1.0 + + def test_get_mana_returns_value_between_0_and_1(self): + from ui.meters import get_mana + img = np.full((720, 1280, 3), 255, dtype=np.uint8) + result = get_mana(img) + assert 0.0 <= result <= 1.0 + + def test_get_merc_health_returns_value_between_0_and_1(self): + from ui.meters import get_merc_health + img = np.full((720, 1280, 3), 255, dtype=np.uint8) + result = get_merc_health(img) + assert 0.0 <= result <= 1.0 + + def test_get_merc_health_black_image_is_zero(self): + from ui.meters import get_merc_health + img = np.zeros((720, 1280, 3), dtype=np.uint8) + result = get_merc_health(img) + assert result == 0.0 \ No newline at end of file