import re import sys import types import unittest from pathlib import Path ROOT=Path(__file__).resolve().parents[2] def script(name):return (ROOT/'src/platform/ScriptLib'/name).read_text().split('R"PY(',1)[1].rsplit(')PY";',1)[0] class Widget(object): def __init__(self,*a,**k):self.shown=False;self.text='';self.pos=(0,0);self.size=(0,0);self.color=0 def __getattr__(self,name):return lambda *a,**k:None def Show(self):self.shown=True def Hide(self):self.shown=False def IsShow(self):return self.shown def SetText(self,text):self.text=text def SetPackedFontColor(self,color):self.color=color def SetPosition(self,x,y):self.pos=(x,y) def GetGlobalPosition(self):return self.pos def SetSize(self,w,h):self.size=(w,h) def GetWidth(self):return self.size[0] def GetHeight(self):return self.size[1] class Action(Widget): def __init__(self,parent,x,y,width,text,event): Widget.__init__(self);self.pos=(x,y);self.event=event;self.text=text;self.shown=True class Minimap(Widget): def GetGlobalPosition(self):return (700,10) class VipTests(unittest.TestCase): def setUp(self): self.time=100.;self.sent=[];self.chat=[] mods={n:types.ModuleType(n) for n in ('app','chat','game','grp','net','ui','mt_biolog')} mods['app'].GetTime=lambda:self.time mods['chat'].CHAT_TYPE_INFO=1;mods['chat'].AppendChat=lambda t,m:self.chat.append(m) mods['net'].SendChatPacket=self.sent.append mods['game'].GameWindow=type('GameWindow',(),dict(Open=lambda s:None,Close=lambda s:None, BINARY_ServerCommand_Run=lambda s,line:7)) grp=mods['grp'];grp.SetColor=grp.RenderBar=grp.RenderBox=lambda *a:None mods['ui'].Window=Widget mods['mt_biolog'].Action=Action;mods['mt_biolog'].L=lambda t:t mods['mt_biolog'].Text=lambda p,x,y,t='',c=0:self.text(t,c) self.previous={n:sys.modules.get(n) for n in mods} sys.modules.update(mods) self.ns={'__name__':'mt_vip'};exec(script('VipScript.inc'),self.ns) self.status=self.ns['Status'](self.sent.append,lambda:self.time) def text(self,t,c): w=Widget();w.text=t;w.color=c;return w def tearDown(self): for name,value in self.previous.items(): if value is None:sys.modules.pop(name,None) else:sys.modules[name]=value def test_query_parses_times_and_counts_down(self): self.assertTrue(self.status.Query());self.assertFalse(self.status.Query()) self.assertEqual(self.sent,['/mt_vip 1 status']) self.assertTrue(self.status.Receive('MTVIP1 times 604800 604800 0 604800 604800 604800 90')) self.assertIsNone(self.status.waiting);self.assertEqual(self.status.Left(2),0) self.time+=100;self.assertEqual(self.status.Left(0),604700);self.assertEqual(self.status.Left(6),0) self.time+=.5;self.assertTrue(self.status.Query()) # past the send gap def test_added_error_malformed_and_foreign(self): events=[];self.status.listener=events.append self.status.Receive('MTVIP1 added 38002 30');self.assertEqual(self.status.added,(38002,30)) self.status.Receive('MTVIP1 error full');self.assertEqual(self.status.error,'full') for bad in ('MTVIP1 times 1 2 3','MTVIP1 times 1 2 3 4 5 6 x','MTVIP1 times 1 2 3 4 5 6 -7','MTVIP1 added 38001 0', 'MTVIP1 added 38001 401','MTVIP1 error boom','MTVIP1 boom'): self.assertTrue(self.status.Receive(bad)) self.assertEqual(events,['added','error']);self.assertIsNone(self.status.times) self.assertFalse(self.status.Receive('MTCH1 error kind')) def test_timeout_marks_unsupported(self): events=[];self.status.listener=events.append self.status.Query();self.time+=3.1;self.status.Tick() self.assertTrue(self.status.unsupported);self.assertEqual(events,['unsupported']) self.status.Receive('MTVIP1 times 0 0 0 0 0 0 0');self.assertFalse(self.status.unsupported) def test_remaining_text(self): R=self.ns['Remaining'] self.assertEqual(R(0),'未开通');self.assertEqual(R(-5),'未开通') self.assertEqual(R(7*86400+3*3600+59),'剩余 7 天 3 小时') self.assertEqual(R(5*3600+120),'剩余 5 小时 2 分');self.assertEqual(R(30),'剩余 1 分') def test_window_entry_and_chat(self): game=sys.modules['game'];gw=game.GameWindow() gw.interface=types.SimpleNamespace(wndMiniMap=Minimap()) game.GameWindow.Open(gw);w=self.ns['g_window'];entry=self.ns['g_entry'] self.assertEqual(entry.pos,(632,418)) entry.event();self.assertTrue(w.IsShow());self.assertEqual(self.sent,['/mt_vip 1 status']) self.assertEqual(w.note.text,'正在查询……');self.assertEqual(w.values[0].text,'……') self.assertEqual(game.GameWindow.BINARY_ServerCommand_Run(gw,'MTVIP1 times 0 0 0 0 0 0 0'),1) self.assertEqual(w.note.text,'尚未开通 VIP');self.assertEqual(w.values[3].text,'未开通') self.assertEqual(game.GameWindow.BINARY_ServerCommand_Run(gw,'MTVIP1 added 38001 7'),1) self.assertEqual(self.chat,['VIP 会员已延长 7 天,全部特权立即生效']) game.GameWindow.BINARY_ServerCommand_Run(gw,'MTVIP1 times 604800 604800 604800 604800 604800 604800 604800') self.assertEqual(w.values[6].text,'剩余 7 天 0 小时');self.assertEqual(w.note.text,'VIP 生效中') game.GameWindow.BINARY_ServerCommand_Run(gw,'MTVIP1 error full') self.assertEqual(self.chat[-1],'VIP 剩余时间不能超过 400 天');self.assertIn('400',w.note.text) self.assertEqual(game.GameWindow.BINARY_ServerCommand_Run(gw,'other'),7) entry.event();self.assertFalse(w.IsShow()) self.time+=2;entry.event();self.time+=3.1;w.OnUpdate() self.assertEqual(w.note.text,'!服务器尚未启用 VIP 卡') game.GameWindow.Close(gw);self.assertIsNone(self.ns['g_window']);self.assertIsNone(self.ns['g_entry']) def test_server_constants_match(self): server=ROOT.parent/'metin2-server/src/server/game/src/port_premium.h' if not server.exists():self.skipTest('server checkout not installed') text=server.read_text(encoding='latin1') self.assertIn('kPortVipMaxDays=400',text);self.assertEqual(self.ns['MAX_DAYS'],400) self.assertIn('kPortVipGapMs=1000',text);self.assertEqual(self.ns['SEND_GAP'],1.) cards={int(v):int(d) for v,d in re.findall(r'\{(\d+),(\d+)\}',text.split('kPortVipCards',1)[1].split(';',1)[0])} self.assertEqual(cards,self.ns['CARDS']) self.assertEqual(text.count('_expire"'),len(self.ns['SERVICES'])) self.assertTrue(set(re.findall(r'MTVIP1 error ([a-z]+)',text))<=set(self.ns['ERRORS'])) if __name__=='__main__':unittest.main()