import sys from pathlib import Path sys.path.insert(0, str(Path("work/python-deps").resolve())) from lupa.lua51 import LuaRuntime PACKAGE=Path(__file__).resolve().parent.parent core=(PACKAGE/"LockPort.lua").read_text(encoding="utf-8") setup=r''' now=100; raidSize=5; partySize=0; moving=0; casting=nil; nativeOK=true function GetTime() return now end function GetRaidRosterInfo() return nil end function GetNumRaidMembers() return raidSize end function GetNumPartyMembers() return partySize end function GetLockPortVersion() return "1.2.1" end function LockPortNativeSummon(unit) nativeCalls=(nativeCalls or 0)+1; lastUnit=unit; return nativeOK end function GetUnitSpeed() return moving end function UnitName(u) if u=="player" then return "Tester" end return "Bob" end function UnitClass() return "Warlock","WARLOCK" end function UnitAffectingCombat() return false end function UnitIsConnected() return true end function UnitIsDeadOrGhost() return false end function CreateFrame() return {RegisterEvent=function() end, SetScript=function() end} end function SendAddonMessage() end function SendChatMessage() end function GetZoneText() return "Test" end function GetSubZoneText() return "" end function TargetUnit() end function CastSpellByName() manualCalls=(manualCalls or 0)+1 end C_Spell={UnitCastingInfo=function() return casting end,UnitChannelInfo=function() return nil end} SlashCmdList={} LockPortOptions={announceOpen=false,askHelpers=false,autoRemove=true} ''' def env(): L=LuaRuntime() L.execute(setup); L.execute(core) L.execute(r''' LP=LockPort; LP.mods={} LP.Print=function(self,text) lastPrint=text end LP.RefreshOptions=function() end; LP.UpdateUI=function() end LP.OpenAnnounce=function() openCalls=(openCalls or 0)+1 end LP.ReadyState=function() return true end LP.NextCandidate=function() if LP.autoRetryAfter and LP.autoRetryAfter.Bob and now0 or partySize>0 end LP.CountHelpers=function() return 2 end LockPortOptions.theme="classic" ''') L.execute((PACKAGE/"LockPort_UI.lua").read_text(encoding="utf-8")) L.execute(r''' LP:CreateUI() LockPortFrame:Show();LP:RefreshList() assert(LockPortFrame.raidAuto.visible) assert(LockPortFrame.raidAuto.width==95 and LockPortFrame.groupSummon.width==95) assert(LockPortFrame.groupSummon.visible and LockPortFrame.groupSummon.point[2]==LockPortFrame.raidAuto) LP:ToggleGroups();assert(LockPortGroupsFrame.visible) this=LockPortGroupsNone;this.scripts.OnClick();assert(LockPortOptions.summonGroups and not next(LockPortOptions.summonGroups)) this=LockPortGroupsFrame.groups[3];this.scripts.OnClick();assert(LockPortOptions.summonGroups[3]) this=LockPortGroupsAll;this.scripts.OnClick();assert(not LockPortOptions.summonGroups) assert(LockPortFrame.summon.width==95 and LockPortFrame.chatToggle.width==95) this=LockPortFrame.chatToggle;this.scripts.OnClick();assert(LockPortOptions.chatMuted) LP:RefreshList();assert(string.find(this.text,"Off")) this.scripts.OnClick();assert(not LockPortOptions.chatMuted) local nativeHeight=LockPortFrame.height assert(LockPortFrame.raidAuto.point[1]=="TOPLEFT") assert(LockPortFrame.raidAuto.point[3]=="BOTTOMLEFT" and LockPortFrame.raidAuto.point[5]==-2) GetLockPortVersion=nil;LP:RefreshList() assert(not LockPortFrame.raidAuto.visible and LockPortFrame.groupSummon.visible and LockPortFrame.height==nativeHeight) GetLockPortVersion=function() return "1.2.1" end;raidSize=0;LP:RefreshList() assert(LockPortFrame.raidAuto.visible and not LockPortFrame.raidAuto.enabled) assert(LockPortFrame.groupSummon.visible and not LockPortFrame.groupSummon.enabled and not LockPortGroupsFrame.visible) partySize=3;LP:RefreshList();assert(LockPortFrame.groupSummon.visible and not LockPortFrame.groupSummon.enabled) GetLockPortVersion=nil;LP:RefreshList();assert(LockPortFrame.groupSummon.visible and not LockPortFrame.groupSummon.enabled and LockPortFrame.height==nativeHeight) GetLockPortVersion=function() return "1.2.1" end;partySize=0 raidSize=5;LP:RefreshList();assert(LockPortFrame.raidAuto.enabled and LockPortFrame.groupSummon.enabled) this=LockPortFrame.raidAuto;this.checked=true this.scripts.OnClick() assert(LP.raidAutoSummon and LockPortOptions.announceOpen) ''') print("PASS UI native-only visibility, expanded layout, raid gating, and button toggle") check("party enables automatic casting",r'''raidSize=0;partySize=3;LP:SetRaidAutoSummon(true);assert(LP.raidAutoSummon and LockPortOptions.announceOpen);LP:CheckRaidAutoSummon(100);LP:CheckRaidAutoSummon(102);assert(casts==1 and wasAutomatic)''') check("raid to party conversion stays enabled",r'''LP:SetRaidAutoSummon(true);raidSize=0;partySize=3;LP:CheckRaidAutoSummon(100);assert(LP.raidAutoSummon);LP:CheckRaidAutoSummon(102);assert(casts==1)''') check("leaving party disables and restores announcements",r'''raidSize=0;partySize=3;LP:SetRaidAutoSummon(true);partySize=0;LP:CheckRaidAutoSummon(101);assert(not LP.raidAutoSummon and not LockPortOptions.announceOpen)''') check("old raid-only DLL is rejected",r'''GetLockPortVersion=function() return "1.0.0" end;assert(not LP:HasNativeMod());LP:SetRaidAutoSummon(true);assert(not LP.raidAutoSummon)''') check("live idle clears stale busy flag",r'''LP.castBusy=true;LP:RecoverCastTracking(100);assert(not LP.castBusy)''') check("missing stop event releases pending summon conservatively",r'''LP.pending={name="Bob",unit="party1",t=90,started=true};LP.FinishPending=function(self) assert(self.pending.interrupted);self.pending.done=true end;LP:RecoverCastTracking(100);assert(not LP.pending.done);LP:RecoverCastTracking(102);assert(LP.pending.done)''') check("active channel is never cleared by recovery",r'''C_Spell.UnitChannelInfo=function() return "Ritual of Summoning" end;LP.pending={name="Bob",t=90,started=true};LP:RecoverCastTracking(100);LP:RecoverCastTracking(105);assert(not LP.pending.done and LP.castBusy)''') check("unknown live state preserves tracking",r'''C_Spell=nil;LP.castBusy=true;LP.pending={name="Bob",t=90,started=true};LP:RecoverCastTracking(100);LP:RecoverCastTracking(105);assert(not LP.pending.done and LP.castBusy)''') check("brief idle gap during casting is tolerated",r'''LP.pending={name="Bob",t=90,started=true};LP:RecoverCastTracking(100);casting="Ritual of Summoning";LP:RecoverCastTracking(101);assert(not LP.pending.inactiveSince and not LP.pending.done)''') # Exercise the real scheduler, candidate checks, Summon, and start-event handler together. for unit in ("party2", "raid2"): L=env();L.execute(core) L.execute(r''' LP=LockPort raidSize='''+("0" if unit.startswith("party") else "5")+r''';partySize=3 LP.Print=function() end;LP.UpdateUI=function() end;LP.RefreshOptions=function() end LP.OpenAnnounce=function() end;LP.SendAddon=function() end LP.KnowsRitual=function() return true end LP.CountShards=function() return 5 end;LP.CountHelpers=function() return 2 end LP.TargetProblem=function() return nil end LP.roster={Bob={unit="'''+unit+r'''",online=true}} LP.ScanRoster=function(self) return self.roster end LP.SortedRequests=function(self) return {{name="Bob",info=self.roster.Bob}} end LP.requests={{name="Bob",t=90}} LockPortNativeSummon=function(unit) actualCasts=actualCasts+1;arg1=LP.SPELL;LP:OnEvent("SPELLCAST_START");return true,"spellbook request sent" end actualCasts=0 CastSpellByName=function(name) actualCasts=actualCasts+1;arg1=name;LP:OnEvent("SPELLCAST_START") end LP:SetRaidAutoSummon(true) LP:CheckRaidAutoSummon(100) now=102;LP:CheckRaidAutoSummon(now) assert(actualCasts==1 and LP.pending.started and LP.pending.automatic) assert(not LP.requests[1] and #LP.autoTrace>0) ''') print("PASS integrated automatic "+unit+" cast receives start event") check("off clears announcements even when already off",r'''LP.raidAutoSummon=false;LockPortOptions.announceOpen=true;LP:SetRaidAutoSummon(false);assert(not LP.raidAutoSummon and not LockPortOptions.announceOpen)''') # Simulate SavedVariables surviving a reload while addon runtime state is recreated. for enabled_by_auto in (True, False): before=env() if enabled_by_auto: before.execute("LP:SetRaidAutoSummon(true)") else: before.execute("LockPortOptions.announceOpen=true") after=env() after.globals().LockPortOptions.announceOpen=before.globals().LockPortOptions.announceOpen after.execute(r''' LP.DetectMods=function(self) self.mods={} end LP.CreateUI=function() end GetAddOnMetadata=function() return "test" end LP:Initialize() assert(not LP.raidAutoSummon and not LockPortOptions.announceOpen) sent=0;SendChatMessage=function() sent=sent+1 end ''') # env overrides OpenAnnounce; restore the production method before exercising its timer gate. after.execute(core) after.execute("LockPort:OpenAnnounce(1000); assert(sent==0)") print("PASS reload clears announcements", "auto" if enabled_by_auto else "manual") # Chat dispatch uses stable message/sender values and never replays an outer chat # event while handling a nested spell or addon-sync event. def chat_check(name, reader, globals_text="123", globals_sender="Bob"): L=env() L.globals().arg1=globals_text; L.globals().arg2=globals_sender L.execute(r''' LP.MatchesTrigger=function(self,text) return text=="123" end LP.ScanRoster=function() arg1="clobbered";arg2="Wrong";return {Bob={unit="raid2"}} end LP.OnTrigger=function(self,name,info,event) triggered=(triggered or 0)+1;assert(name=="Bob" and info.unit=="raid2");triggerEvent=event end ''') L.execute(reader) L.execute('LP:OnEvent("CHAT_MSG_RAID");assert(triggered==1 and triggerEvent=="CHAT_MSG_RAID")') print("PASS",name) chat_check("ClassicAPI chat payload survives overwritten globals",'GetCurrentChatEvent=function() return "CHAT_MSG_RAID","123","Bob" end',"wrong","Wrong") chat_check("legacy chat fallback snapshots sender before roster scan",'GetCurrentChatEvent=nil') chat_check("empty ClassicAPI chat result uses legacy fallback",'GetCurrentChatEvent=function() end') chat_check("ClassicAPI chat errors use legacy fallback",'GetCurrentChatEvent=function() error("unavailable") end') check("non-request actual payload does not use corrupted trigger globals",r'''GetCurrentChatEvent=function() return "CHAT_MSG_RAID","hello","Bob" end;arg1="123";arg2="Bob";LP.OnTrigger=function() error("false request") end;LP:OnEvent("CHAT_MSG_RAID")''') check("nested spell event does not replay outer chat",r'''GetCurrentChatEvent=function() return "CHAT_MSG_RAID","123","Bob" end;LP.OnTrigger=function() error("duplicate request") end;arg1="Shadow Bolt";LP:OnEvent("SPELLCAST_START");assert(LP.castBusy)''') check("addon sync payload is not replaced by chat reader",r'''GetCurrentChatEvent=function() error("must not read addon sync") end;LP.AddRequest=function(self,name) assert(name=="Bob");synced=true end;LP.ScanRoster=function() return {} end;arg1="RSAdd";arg2="Bob";arg4="Other";LockPortOptions.autoRemove=false;LP:OnEvent("CHAT_MSG_ADDON");assert(synced)''') check("mute blocks summon say and whisper then resumes",r'''LP.CountShards=function() return 5 end;LP.RemoveRequest=function() end;LP.SendAddon=function() end;sent=0;SendChatMessage=function() sent=sent+1 end;LockPortOptions.whisper=true;LP:SetChatMuted(true);LP.pending={name="Bob"};LP:OnRitualStarted();assert(sent==0 and LP.pending.started);LP:SetChatMuted(false);LP.pending={name="Bob"};LP:OnRitualStarted();assert(sent==2)''') check("mute clears pending status whispers and skips new replies",r'''LP.whisperQueue={{key="status:Bob",name="Bob",text="old",at=0}};LP:SetChatMuted(true);assert(table.getn(LP.whisperQueue)==0);LockPortOptions.whisperStatus=true;LP:QueueWhisper("Bob","status","new");assert(table.getn(LP.whisperQueue)==0);LP:SetChatMuted(false);LP:ProcessWhisperQueue(100);assert(table.getn(LP.whisperQueue)==0)''') L=env();L.execute(core);L.execute(r'''LP=LockPort;LockPortOptions.chatMuted=true;GetNumRaidMembers=function() error("must stop before sending") end;LP:OpenAnnounce(100,true)''');print("PASS mute blocks forced availability announcements") check("raid group selection defaults to all and supports none",r'''assert(LP:IsSelectedRaidGroup({subgroup=8}));LP:SetSummonGroups({});assert(not LP:IsSelectedRaidGroup({subgroup=1}));LP:SetSummonGroups(nil);assert(LP:IsSelectedRaidGroup({subgroup=1}))''') check("group selection ignores party roster",r'''raidSize=0;partySize=3;LP:SetSummonGroups({[8]=true});assert(LP:IsSelectedRaidGroup({unit="party1"}))''') check("group selection releases outstanding reservation",r'''LP.coordReservation={id=1,target="Bob",phase="reserve"};LP.autoReadyName="Bob";LP:SetSummonGroups({[2]=true});assert(not LP.coordReservation and not LP.autoReadyName);assert(LP:IsSelectedRaidGroup({subgroup=2}) and not LP:IsSelectedRaidGroup({subgroup=1}))''') # Parse the real raid roster's third field and immediately follow subgroup moves. L=env();L.execute(r'''raidSize=1;group=4;function GetRaidRosterInfo() return "Bob",0,group,60,"Warrior","WARRIOR","Test",true,false end;LP:SetSummonGroups({[4]=true});assert(LP:IsSelectedRaidGroup(LP:ScanRoster().Bob));group=5;assert(not LP:IsSelectedRaidGroup(LP:ScanRoster().Bob))''');print("PASS real raid roster subgroup parsing and subgroup changes") # Exercise real candidate selection and direct casts against an assignment. L=env();L.execute(core);L.execute(r''' LP=LockPort;LP.Print=function() end;LP.UpdateUI=function() end;LP.SendAddon=function() end LP.roster={Alice={unit="raid2",subgroup=1,online=true},Bob={unit="raid3",subgroup=2,online=true}} LP.ScanRoster=function(self) return self.roster end;LP.TargetProblem=function() return nil end LP.CountShards=function() return 5 end;LP.CountHelpers=function() return 2 end;LP.KnowsRitual=function() return true end LP.requests={{name="Alice",t=1},{name="Bob",t=2}} LP:SetSummonGroups({[2]=true});assert(LP:NextCandidate().name=="Bob") LP:Summon("Alice");assert(not LP.pending and not manualCalls) LP:SummonNext();assert(LP.pending.name=="Bob" and manualCalls==1) LP.pending=nil;LP:SetSummonGroups({});assert(not LP:NextCandidate()) ''');print("PASS Summon and direct row casts respect raid group selection") check("saved raid group selection survives initialization",r'''LockPortOptions.summonGroups={[2]=true,[7]=true};LP.DetectMods=function(self) self.mods={} end;LP.CreateUI=function() end;GetAddOnMetadata=function() return "3.9.0" end;LP:Initialize();assert(LockPortOptions.summonGroups[2] and LockPortOptions.summonGroups[7] and not LockPortOptions.summonGroups[1])''')