Skip to content

Commit 23fb7dc

Browse files
committed
add simple typetree test set
1 parent 75ca7ad commit 23fb7dc

File tree

3 files changed

+201
-2
lines changed

3 files changed

+201
-2
lines changed

.github/workflows/publish.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -57,7 +57,7 @@ jobs:
5757
# Disabled repair wheel since the fmob lib is not compatible
5858
CIBW_REPAIR_WHEEL_COMMAND: ''
5959
CIBW_REPAIR_WHEEL_COMMAND_LINUX: 'mv {wheel} {dest_dir}/"$(basename {wheel} | sed "s/-linux_/-manylinux_2_17_/")"'
60-
CIBW_TEST_REQUIRES: pytest
60+
CIBW_TEST_REQUIRES: pytest psutil
6161
CIBW_TEST_COMMAND: pytest -v -s {package}/tests
6262
CIBW_TEST_SKIP: "*-macosx* *-manylinux_i686 *-win32"
6363

.github/workflows/test.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -29,7 +29,7 @@ jobs:
2929

3030
- name: Install Dependencies
3131
run: |
32-
python -m pip install --upgrade pip pytest
32+
python -m pip install --upgrade pip pytest psutil
3333
3434
- name: Install UnityPy
3535
run: |

tests/test_typetree.py

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
import gc
2+
import math
3+
import os
4+
import random
5+
from typing import List, Literal, Tuple, TypeVar
6+
7+
import psutil
8+
9+
from UnityPy.classes.generated import GameObject
10+
from UnityPy.helpers.Tpk import get_typetree_node
11+
from UnityPy.helpers.TypeTreeHelper import read_typetree, write_typetree
12+
from UnityPy.helpers.TypeTreeNode import TypeTreeNode
13+
from UnityPy.streams import EndianBinaryReader, EndianBinaryWriter
14+
15+
PROCESS = psutil.Process(os.getpid())
16+
17+
18+
def get_memory():
19+
gc.collect()
20+
return PROCESS.memory_info().rss
21+
22+
23+
def check_leak(func):
24+
def wrapper(*args, **kwargs):
25+
mem_0 = get_memory()
26+
func(*args, **kwargs)
27+
mem_1 = get_memory()
28+
diff = mem_1 - mem_0
29+
if diff != 0:
30+
diff %= 4096
31+
assert diff == 0, f"Memory leak in {func.__name__}"
32+
33+
return wrapper
34+
35+
36+
TEST_NODE_STR = "TestNode"
37+
38+
39+
@check_leak
40+
def test_typetreenode():
41+
TypeTreeNode(
42+
m_Level=0, m_Type=TEST_NODE_STR, m_Name=TEST_NODE_STR, m_ByteSize=0, m_Version=0
43+
)
44+
45+
46+
def generate_dummy_node(typ: str, name: str = ""):
47+
return TypeTreeNode(m_Level=0, m_Type=typ, m_Name=name, m_ByteSize=0, m_Version=0)
48+
49+
50+
SIMPLE_NODE_SAMPLES = [
51+
(["SInt8"], int, (-(2**7), 2**7)),
52+
(["SInt16", "short"], int, (-(2**15), 2**15)),
53+
(["SInt32", "int"], int, (-(2**31), 2**31)),
54+
(["SInt64", "long long"], int, (-(2**63), 2**63)),
55+
(["UInt8", "char"], int, (0, 2**8)),
56+
(["UInt16", "unsigned short"], int, (0, 2**16)),
57+
(["UInt32", "unsigned int", "Type*"], int, (0, 2**32)),
58+
(["UInt64", "unsigned long long", "FileSize"], int, (0, 2**64)),
59+
(["float"], float, (-1, 1)),
60+
(["double"], float, (-1, 1)),
61+
(["bool"], bool, (False, True)),
62+
]
63+
64+
T = TypeVar("T")
65+
66+
INT_BYTESIZE_MAP = {
67+
1: "b",
68+
2: "h",
69+
4: "i",
70+
8: "q",
71+
}
72+
73+
74+
def generate_sample_data(
75+
u_type: List[str],
76+
py_typ: Literal[int, float, str],
77+
bounds: Tuple[T, T],
78+
count: int = 10,
79+
) -> List[T]:
80+
if py_typ is int:
81+
if bounds[0] < 0:
82+
# signed
83+
byte_size = math.log2(bounds[1]) + 1
84+
signed = True
85+
elif bounds[0] == 0:
86+
# unsigned
87+
byte_size = math.log2(bounds[1])
88+
signed = False
89+
90+
byte_size = round(byte_size / 8)
91+
char = INT_BYTESIZE_MAP[byte_size]
92+
if not signed:
93+
char = char.upper()
94+
95+
sample_values = [
96+
bounds[0],
97+
*[random.randint(bounds[0], bounds[1] - 1) for _ in range(count)],
98+
bounds[1] - 1,
99+
]
100+
# sample_data = pack(f"<{count+2}{char}", *sample_values)
101+
102+
elif py_typ is float:
103+
sample_values = [
104+
bounds[0],
105+
*[random.uniform(bounds[0], bounds[1]) for _ in range(count)],
106+
bounds[1],
107+
]
108+
char = "f" if u_type == "float" else "d"
109+
# sample_data = pack(f"<{count+2}f", *sample_values)
110+
111+
elif py_typ is bool:
112+
sample_values = [
113+
bounds[0],
114+
*[random.choice([True, False]) for _ in range(count)],
115+
bounds[1],
116+
]
117+
# sample_data = pack(f"<{count+2}?", *sample_values)
118+
119+
elif py_typ is str:
120+
raise NotImplementedError("String generation not implemented")
121+
122+
elif py_typ is bytes:
123+
raise NotImplementedError("Bytes generation not implemented")
124+
125+
return sample_values
126+
127+
128+
@check_leak
129+
def test_simple_nodes():
130+
for typs, py_typ, bounds in SIMPLE_NODE_SAMPLES:
131+
values = generate_sample_data(typs, py_typ, bounds)
132+
for typ in typs:
133+
node = generate_dummy_node(typ)
134+
for value in values:
135+
writer = EndianBinaryWriter(b"", "<")
136+
write_typetree(value, node, writer)
137+
raw = writer.bytes
138+
re_value = read_typetree(node, EndianBinaryReader(raw, "<"))
139+
assert (
140+
abs(value - re_value) < 1e-5
141+
), f"Failed on {typ}: {value} != {re_value}"
142+
143+
144+
@check_leak
145+
def test_simple_nodes_array():
146+
def generate_list_node(item_node: TypeTreeNode):
147+
root = generate_dummy_node("root", "root")
148+
array = generate_dummy_node("Array", "Array")
149+
array.m_Children = [None, item_node]
150+
root.m_Children = [array]
151+
return root
152+
153+
for typs, py_typ, bounds in SIMPLE_NODE_SAMPLES:
154+
values = generate_sample_data(typs, py_typ, bounds)
155+
for typ in typs:
156+
node = generate_dummy_node(typ)
157+
array_node = generate_list_node(node)
158+
writer = EndianBinaryWriter(b"", "<")
159+
write_typetree(values, array_node, writer)
160+
raw = writer.bytes
161+
re_values = read_typetree(array_node, EndianBinaryReader(raw, "<"))
162+
assert all(
163+
(abs(value - re_value) < 1e-5)
164+
for value, re_value in zip(values, re_values)
165+
), f"Failed on {typ}: {values} != {re_values}"
166+
167+
168+
TEST_CLASS_NODE = get_typetree_node(1, (5, 0, 0, 0))
169+
TEST_CLASS_NODE_OBJ = GameObject(
170+
m_Component=[], m_IsActive=True, m_Layer=0, m_Name="TestObject", m_Tag=0
171+
)
172+
TEST_CLASS_NODE_DICT = TEST_CLASS_NODE_OBJ.__dict__
173+
174+
175+
def test_class_node_dict():
176+
writer = EndianBinaryWriter(b"", "<")
177+
write_typetree(TEST_CLASS_NODE_DICT, TEST_CLASS_NODE, writer)
178+
raw = writer.bytes
179+
re_value = read_typetree(
180+
TEST_CLASS_NODE, EndianBinaryReader(raw, "<"), as_dict=True
181+
)
182+
assert re_value == TEST_CLASS_NODE_DICT
183+
184+
185+
def test_class_node_clz():
186+
writer = EndianBinaryWriter(b"", "<")
187+
write_typetree(TEST_CLASS_NODE_OBJ, TEST_CLASS_NODE, writer)
188+
raw = writer.bytes
189+
re_value = read_typetree(
190+
TEST_CLASS_NODE, EndianBinaryReader(raw, "<"), as_dict=False
191+
)
192+
assert re_value == TEST_CLASS_NODE_OBJ
193+
194+
195+
if __name__ == "__main__":
196+
for x in list(locals()):
197+
if str(x)[:4] == "test":
198+
locals()[x]()
199+
input("All Tests Passed")

0 commit comments

Comments
 (0)