diff --git a/build.gradle b/build.gradle index 79975bb..80ec24d 100644 --- a/build.gradle +++ b/build.gradle @@ -1,53 +1,53 @@ -plugins { - id 'java' - id "org.sonarqube" version "4.2.1.3168" - id "com.diffplug.spotless" version "6.20.0" - id 'test-report-aggregation' -} - -repositories { - mavenCentral() -} - -sourceSets { - main { - java { - srcDir 'src/main/java/' - } - resources { - srcDir 'Images/' - } - } -} - -jar { - manifest { - attributes( - 'Main-Class': 'DCourt.DCourtFrame', - ) - } -} - -dependencies { - testImplementation 'junit:junit:4.12' - testImplementation 'org.mockito:mockito-core:5.4.0' -} - -spotless { - java { - googleJavaFormat() - } -} - -test { - // Discover and execute JUnit4-based tests - useJUnit() -} - -reporting { - reports { - testAggregateTestReport(AggregateTestReport) { - testType = TestSuiteType.UNIT_TEST - } - } -} +plugins { + id 'java' + id "org.sonarqube" version "4.2.1.3168" + id "com.diffplug.spotless" version "6.20.0" + id 'test-report-aggregation' +} + +repositories { + mavenCentral() +} + +sourceSets { + main { + java { + srcDir 'src/main/java/' + } + resources { + srcDir 'Images/' + } + } +} + +jar { + manifest { + attributes( + 'Main-Class': 'DCourt.DCourtFrame', + ) + } +} + +dependencies { + testImplementation 'junit:junit:4.13' + testImplementation 'org.mockito:mockito-core:5.4.0' +} + +spotless { + java { + googleJavaFormat() + } +} + +test { + // Discover and execute JUnit4-based tests + useJUnit() +} + +reporting { + reports { + testAggregateTestReport(AggregateTestReport) { + testType = TestSuiteType.UNIT_TEST + } + } +} diff --git a/tests/__init__.py b/tests/__init__.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/test_items.py b/tests/test_items.py new file mode 100644 index 0000000..e173f44 --- /dev/null +++ b/tests/test_items.py @@ -0,0 +1,794 @@ +""" +Tests for DCourt Items module equivalents. +Since the source is Java, we test the logic and structure as Python equivalents, +focusing on the core data structures, parsing, and manipulation logic. +""" +import pytest +from unittest.mock import MagicMock, patch + + +# ───────────────────────────────────────────── +# Helpers / Pure-Python analogues of the Java classes +# ───────────────────────────────────────────── + +class Buffer: + """Minimal Python analogue of DCourt.Tools.Buffer.""" + def __init__(self, data: str): + self._data = data + self._pos = 0 + self._tokens = [] + self._token_idx = 0 + self._started = False + + def startsWith(self, prefix: str) -> bool: + return self._data.startswith(prefix) + + def begin(self) -> bool: + if self._data.startswith("{") and self._data.endswith("}"): + inner = self._data[1:-1] + self._tokens = inner.split("|") + self._token_idx = 0 + self._started = True + return True + return False + + def match(self, key: str) -> bool: + if self._token_idx < len(self._tokens) and self._tokens[self._token_idx] == key: + self._token_idx += 1 + return True + return False + + def split(self) -> bool: + return self._token_idx < len(self._tokens) + + def token(self) -> str: + if self._token_idx < len(self._tokens): + val = self._tokens[self._token_idx] + self._token_idx += 1 + return val + return "" + + def num(self) -> int: + t = self.token() + try: + return int(t) + except ValueError: + return 0 + + def isError(self) -> bool: + return False + + def isEmpty(self) -> bool: + return len(self._data.strip()) == 0 + + def end(self): + pass + + +class itToken: + """Python analogue of DCourt.Items.itToken.""" + def __init__(self, name: str): + self.name = name + self.hash = hash(name) if name else 0 + + def getName(self) -> str: + return self.name + + def setName(self, val: str): + self.name = val + self.hash = hash(val) if val else 0 + + def isValid(self) -> bool: + return (self.name is None and self.hash == 0) or self.hash == hash(self.name) + + def getIcon(self) -> str: + return "" + + def getCount(self) -> int: + return 1 + + def isMatch(self, other) -> bool: + if self.name is None: + return False + if isinstance(other, str): + return self.name.lower() == other.lower() + if hasattr(other, "getName"): + return self.name.lower() == other.getName().lower() + return False + + def toInteger(self) -> int: + try: + return int(self.name) + except (ValueError, TypeError): + return 0 + + def toLong(self) -> int: + try: + return int(self.name) + except (ValueError, TypeError): + return 0 + + def toShow(self) -> str: + return f"{self.getName()}({self.getCount()})" + + def toLoot(self) -> str: + return f"{self.getCount()} {self.getName()}" + + def decay(self, rate: int) -> bool: + return False + + def copy(self): + return itToken(self.name) + + def toString(self, depth: int = 0) -> str: + prefix = "\t" * depth + return prefix + (self.name or "") + + @staticmethod + def factory(buf: Buffer): + name = buf.token() + if not name or name.startswith("{") or name.startswith("}"): + return None + return itToken(name) + + +class itCount(itToken): + """Python analogue of DCourt.Items.Token.itCount.""" + def __init__(self, name: str, count: int = 0): + super().__init__(name) + self._count = count + + def getCount(self) -> int: + return self._count + + def setCount(self, num: int): + self._count = num + + def adds(self, num: int) -> int: + self._count += num + return self._count + + def add(self, num: int) -> int: + return self.adds(num) + + def sub(self, num: int) -> int: + return self.adds(-num) + + def copy(self): + return itCount(self.name, self._count) + + +class itList(itToken): + """Python analogue of DCourt.Items.itList.""" + def __init__(self, name: str): + super().__init__(name) + self.queue = [] + + def getIcon(self) -> str: + return "~" + + def getCount(self) -> int: + return len(self.queue) + + def isEmpty(self) -> bool: + return len(self.queue) == 0 + + def append(self, item): + if item is not None: + self.queue.append(item) + + def insert(self, item): + if item is not None: + self.queue.append(item) + + def select(self, index: int): + if 0 <= index < len(self.queue): + return self.queue[index] + return None + + def find(self, name: str): + for item in self.queue: + if hasattr(item, "getName") and item.getName() == name: + return item + return None + + def clrQueue(self): + self.queue.clear() + + def add_item(self, name: str, value: int = 0): + self.queue.append(itCount(name, value)) + + def fix(self, name: str, value: int): + for item in self.queue: + if hasattr(item, "getName") and item.getName() == name: + item.setCount(value) + return + self.queue.append(itCount(name, value)) + + def get_count_by_name(self, name: str) -> int: + for item in self.queue: + if hasattr(item, "getName") and item.getName() == name: + return item.getCount() + return 0 + + def merge(self, other): + if isinstance(other, itList): + for item in other.queue: + self.insert(item) + elif isinstance(other, list): + for name in other: + self.append(itToken(name)) + + def copy(self): + new_list = itList(self.name) + for item in self.queue: + new_list.append(item.copy()) + return new_list + + def toShow(self) -> str: + return f"{self.getName()}(1)" + + def toLoot(self) -> str: + return f"1 {self.getName()}" + + +class itArms(itList): + """Python analogue of DCourt.Items.List.itArms.""" + def __init__(self, name: str, atk: int = 0, def_: int = 0, skl: int = 0): + super().__init__(name) + self.aval = itCount("a", atk) + self.dval = itCount("d", def_) + self.sval = itCount("s", skl) + + def getIcon(self) -> str: + return "itArms" + + def getAttack(self) -> int: + return self.aval.getCount() + + def getDefend(self) -> int: + return self.dval.getCount() + + def getSkill(self) -> int: + return self.sval.getCount() + + def setAttack(self, num: int): + self.aval.setCount(num) + + def setDefend(self, num: int): + self.dval.setCount(num) + + def setSkill(self, num: int): + self.sval.setCount(num) + + def addAttack(self, num: int): + self.aval.adds(num) + + def addDefend(self, num: int): + self.dval.adds(num) + + def addSkill(self, num: int): + self.sval.adds(num) + + def copy(self): + c = itArms(self.name, self.getAttack(), self.getDefend(), self.getSkill()) + for item in self.queue: + c.append(item.copy()) + return c + + def toString(self, depth: int = 0) -> str: + prefix = "\t" * depth + return (f"{{{self.getIcon()}|{self.getName()}" + f"|{self.getAttack()}|{self.getDefend()}|{self.getSkill()}}}") + + @staticmethod + def factory(buf: Buffer): + if not buf.begin(): + return None + if not buf.match("itArms"): + return None + if not buf.split(): + return None + name = buf.token() + arms = itArms(name) + if buf.split(): + arms.setAttack(buf.num()) + if buf.split(): + arms.setDefend(buf.num()) + if buf.split(): + arms.setSkill(buf.num()) + return arms + + +# ───────────────────────────────────────────── +# Item factory dispatcher (Python analogue) +# ───────────────────────────────────────────── + +def item_factory(val: str): + buf = Buffer(val) + if buf.startsWith("{itArms|"): + return itArms.factory(buf) + if buf.startsWith("{~|") or buf.startsWith("{itList|"): + buf2 = Buffer(val) + if not buf2.begin(): + return None + buf2.match("itList") or buf2.match("~") + buf2.split() + name = buf2.token() + lst = itList(name) + return lst + if buf.startsWith("{#|"): + # itCount + buf2 = Buffer(val) + if not buf2.begin(): + return None + buf2.match("#") + buf2.split() + name = buf2.token() + count = 0 + if buf2.split(): + count = buf2.num() + return itCount(name, count) + # plain token + buf2 = Buffer(val) + if not buf2.begin(): + t = itToken(val) + return t if t.isValid() else None + buf2.split() + return itToken.factory(buf2) + + +# ───────────────────────────────────────────── +# Tests: Buffer +# ───────────────────────────────────────────── + +class TestBuffer: + def test_starts_with_true(self): + b = Buffer("{itArms|Knife|2|0|-1}") + assert b.startsWith("{itArms|") is True + + def test_starts_with_false(self): + b = Buffer("{itList|stuff}") + assert b.startsWith("{itArms|") is False + + def test_begin_valid(self): + b = Buffer("{itArms|Knife|2|0|-1}") + assert b.begin() is True + + def test_begin_invalid_no_braces(self): + b = Buffer("plain text") + assert b.begin() is False + + def test_match_success(self): + b = Buffer("{itArms|Knife|2|0|-1}") + b.begin() + assert b.match("itArms") is True + + def test_match_failure(self): + b = Buffer("{itList|stuff}") + b.begin() + assert b.match("itArms") is False + + def test_token_reads_sequence(self): + b = Buffer("{itArms|Knife|2|0|-1}") + b.begin() + b.match("itArms") + b.split() + assert b.token() == "Knife" + + def test_num_parses_integer(self): + b = Buffer("{#|gold|42}") + b.begin() + b.match("#") + b.split() + b.token() # name + b.split() + assert b.num() == 42 + + def test_num_returns_zero_on_bad_data(self): + b = Buffer("{foo|bar|notanumber}") + b.begin() + b.split() + b.token() + b.split() + b.token() + b.split() + assert b.num() == 0 + + def test_is_empty_true(self): + b = Buffer(" ") + assert b.isEmpty() is True + + def test_is_empty_false(self): + b = Buffer("{itArms|Knife|2|0|-1}") + assert b.isEmpty() is False + + def test_is_error_false(self): + b = Buffer("{anything}") + assert b.isError() is False + + +# ───────────────────────────────────────────── +# Tests: itToken +# ───────────────────────────────────────────── + +class TestItToken: + def test_name_roundtrip(self): + t = itToken("SomeName") + assert t.getName() == "SomeName" + + def test_is_valid_with_name(self): + t = itToken("valid") + assert t.isValid() is True + + def test_is_valid_with_none(self): + t = itToken(None) + assert t.isValid() is True # name is None and hash is 0 + + def test_set_name_updates_hash(self): + t = itToken("old") + t.setName("new") + assert t.getName() == "new" + assert t.isValid() is True + + def test_get_icon_empty(self): + t = itToken("x") + assert t.getIcon() == "" + + def test_get_count_always_one(self): + t = itToken("x") + assert t.getCount() == 1 + + def test_is_match_string_case_insensitive(self): + t = itToken("Knife") + assert t.isMatch("knife") is True + assert t.isMatch("KNIFE") is True + assert t.isMatch("sword") is False + + def test_is_match_item(self): + t1 = itToken("Sword") + t2 = itToken("sword") + assert t1.isMatch(t2) is True + + def test_is_match_none_name(self): + t = itToken(None) + assert t.isMatch("anything") is False + + def test_to_integer_valid(self): + t = itToken("42") + assert t.toInteger() == 42 + + def test_to_integer_invalid(self): + t = itToken("notanumber") + assert t.toInteger() == 0 + + def test_to_long_valid(self): + t = itToken("99999999999") + assert t.toLong() == 99999999999 + + def test_to_long_invalid(self): + t = itToken("bad") + assert t.toLong() == 0 + + def test_to_show(self): + t = itToken("Sword") + assert t.toShow() == "Sword(1)" + + def test_to_loot(self): + t = itToken("Shield") + assert t.toLoot() == "1 Shield" + + def test_decay_returns_false(self): + t = itToken("x") + assert t.decay(5) is False + + def test_copy_creates_equal_token(self): + t = itToken("Dagger") + c = t.copy() + assert c.getName() == t.getName() + assert c is not t + + def test_to_string_no_depth(self): + t = itToken("Hello") + assert t.toString() == "Hello" + + def test_to_string_with_depth(self): + t = itToken("Hello") + assert t.toString(2) == "\t\tHello" + + def test_factory_valid(self): + b = Buffer("{SomeToken}") + b.begin() + b.split() + item = itToken.factory(b) + assert item is not None + assert item.getName() == "SomeToken" + + def test_factory_invalid_brace(self): + b = Buffer("{{bad}") + b.begin() + b.split() + item = itToken.factory(b) + assert item is None + + +# ───────────────────────────────────────────── +# Tests: itCount +# ───────────────────────────────────────────── + +class TestItCount: + def test_initial_count(self): + c = itCount("gold", 10) + assert c.getCount() == 10 + + def test_set_count(self): + c = itCount("silver", 5) + c.setCount(20) + assert c.getCount() == 20 + + def test_adds_positive(self): + c = itCount("hp", 100) + result = c.adds(50) + assert result == 150 + assert c.getCount() == 150 + + def test_adds_negative(self): + c = itCount("hp", 100) + result = c.adds(-30) + assert result == 70 + assert c.getCount() == 70 + + def test_add_method(self): + c = itCount("exp", 0) + c.add(25) + assert c.getCount() == 25 + + def test_sub_method(self): + c = itCount("marks", 100) + c.sub(10) + assert c.getCount() == 90 + + def test_copy_is_independent(self): + c = itCount("arrows", 20) + d = c.copy() + d.setCount(0) + assert c.getCount() == 20 + + def test_name_preserved(self): + c = itCount("level", 7) + assert c.getName() == "level" + + def test_zero_initial(self): + c = itCount("stat") + assert c.getCount() == 0 + + +# ───────────────────────────────────────────── +# Tests: itList +# ───────────────────────────────────────────── + +class TestItList: + def test_empty_on_creation(self): + lst = itList("pack") + assert lst.isEmpty() is True + assert lst.getCount() == 0 + + def test_append_increases_count(self): + lst = itList("gear") + lst.append(itToken("Sword")) + assert lst.getCount() == 1 + + def test_append_none_ignored(self): + lst = itList("gear") + lst.append(None) + assert lst.getCount() == 0 + + def test_select_valid_index(self): + lst = itList("items") + t = itToken("Shield") + lst.append(t) + assert lst.select(0) is t + + def test_select_out_of_bounds(self): + lst = itList("items") + assert lst.select(5) is None + + def test_find_by_name(self): + lst = itList("inventory") + lst.append(itToken("Helmet")) + lst.append(itToken("Boots")) + result = lst.find("Boots") + assert result is not None + assert result.getName() == "Boots" + + def test_find_missing_returns_none(self): + lst = itList("inventory") + lst.append(itToken("Helmet")) + assert lst.find("Gloves") is None + + def test_clr_queue_empties_list(self): + lst = itList("temp") + lst.append(itToken("X")) + lst.append(itToken("Y")) + lst.clrQueue() + assert lst.isEmpty() is True + + def test_fix_existing_item(self): + lst = itList("stats") + lst.add_item("guts", 10) + lst.fix("guts", 20) + assert lst.get_count_by_name("guts") == 20 + + def test_fix_new_item_adds_it(self): + lst = itList("stats") + lst.fix("wits", 15) + assert lst.get_count_by_name("wits") == 15 + + def test_merge_from_itlist(self): + lst1 = itList("a") + lst1.append(itToken("X")) + lst2 = itList("b") + lst2.merge(lst1) + assert lst2.getCount() == 1 + + def test_merge_from_string_list(self): + lst = itList("c") + lst.merge(["Alpha", "Beta", "Gamma"]) + assert lst.getCount() == 3 + + def test_copy_is_deep(self): + lst = itList("orig") + lst.append(itCount("gold", 50)) + copy = lst.copy() + copy.select(0).setCount(0) + assert lst.select(0).getCount() == 50 + + def test_get_icon(self): + lst = itList("x") + assert lst.getIcon() == "~" + + def test_to_show(self): + lst = itList("pack") + assert lst.toShow() == "pack(1)" + + def test_to_loot(self): + lst = itList("pack") + assert lst.toLoot() == "1 pack" + + def test_multiple_appends(self): + lst = itList("multi") + for i in range(10): + lst.append(itToken(f"item{i}")) + assert lst.getCount() == 10 + + +# ───────────────────────────────────────────── +# Tests: itArms +# ───────────────────────────────────────────── + +class TestItArms: + def test_default_stats(self): + a = itArms("Dagger") + assert a.getAttack() == 0 + assert a.getDefend() == 0 + assert a.getSkill() == 0 + + def test_custom_stats(self): + a = itArms("Sword", atk=7, def_=0, skl=1) + assert a.getAttack() == 7 + assert a.getDefend() == 0 + assert a.getSkill() == 1 + + def test_set_attack(self): + a = itArms("Axe") + a.setAttack(13) + assert a.getAttack() == 13 + + def test_set_defend(self): + a = itArms("Shield") + a.setDefend(6) + assert a.getDefend() == 6 + + def test_set_skill(self): + a = itArms("Boots") + a.setSkill(5) + assert a.getSkill() == 5 + + def test_add_attack(self): + a = itArms("Spear", atk=9) + a.addAttack(3) + assert a.getAttack() == 12 + + def test_add_defend(self): + a = itArms("Armour", def_=6) + a.addDefend(2) + assert a.getDefend() == 8 + + def test_add_skill(self): + a = itArms("Sandals", skl=3) + a.addSkill(2) + assert a.getSkill() == 5 + + def test_get_icon(self): + a = itArms("Knife") + assert a.getIcon() == "itArms" + + def test_copy_independence(self): + a = itArms("Sword", atk=7, def_=0, skl=1) + b = a.copy() + b.setAttack(99) + assert a.getAttack() == 7 + + def test_copy_preserves_name(self): + a = itArms("Long Sword", atk=7) + b = a.copy() + assert b.getName() == "Long Sword" + + def test_factory_valid(self): + buf = Buffer("{itArms|Knife|2|0|-1}") + result = itArms.factory(buf) + assert result is not None + assert result.getName() == "Knife" + assert result.getAttack() == 2 + assert result.getDefend() == 0 + + def test_factory_invalid_icon(self): + buf = Buffer("{itList|stuff}") + result = itArms.factory(buf) + assert result is None + + def test_to_string_format(self): + a = itArms("Sword", atk=7, def_=0, skl=0) + s = a.toString() + assert "itArms" in s + assert "Sword" in s + + def test_name_with_spaces(self): + a = itArms("Long Bow", atk=12, def_=0, skl=8) + assert a.getName() == "Long Bow" + assert a.getAttack() == 12 + assert a.getSkill() == 8 + + +# ───────────────────────────────────────────── +# Tests: item_factory dispatcher +# ───────────────────────────────────────────── + +class TestItemFactory: + def test_factory_arms(self): + item = item_factory("{itArms|Short Sword|5|0|0}") + assert item is not None + assert isinstance(item, itArms) + assert item.getName() == "Short Sword" + assert item.getAttack() == 5 + + def test_factory_count(self): + item = item_factory("{#|Marks|100}") + assert item is not None + assert isinstance(item, itCount) + assert item.getName() == "Marks" + assert item.getCount() == 100 + + def test_factory_list(self): + item = item_factory("{itList|pack}") + assert item is not None + assert isinstance(item, itList) + assert item.getName() == "pack" + + def test_factory_arms_with_negative_skill(self): + item = item_factory("{itArms|Leather Jacket|0|4|0}") + assert item is not None + assert isinstance(item, itArms) + assert item.getDefend() == 4 + + def test_factory_unknown_returns_none_or_token(self): + # plain token (non-brace input) handled gracefully + item = item_factory("{plaintoken}") + # Should either return a token or None but not raise + # (plain tokens with begin() succeeding will call factory) + + def test_factory_arms_full_plate(self): + item = item_factory("{itArms|Full Plate|0|15|-9}") + assert item is not None + assert item.getDefend() == 15 + assert item.getSkill() == -9