using Dalamud.Bindings.ImGui; using Dalamud.Plugin.SelfTest; namespace HellionChat.SelfTests; // CurrentTab is coupled to MainWindow.ActiveTab (no longer the fixed index-0 // Tabs lookup). Asserts ReferenceEquals between the two, with false-green // defenses: (1) empty-config exercises the getter's fallback; (2) null ActiveTab // opens the window so the Draw-seed sets it and retries via Waiting (bounded so a // never-drawn window cannot hang a batch); (3) a victim tab at index 0 makes a // regressed index-0 getter return the victim (!= ActiveTab) and fail. Also checks // the ResetActiveTabIfRemoved reference no-op branch. internal sealed class CurrentTabCouplingStep : ISelfTestStep { private readonly Plugin _plugin; private bool _forcedOpen; private int _waitFrames; public CurrentTabCouplingStep(Plugin plugin) { _plugin = plugin; } public string Name => "Hellion Chat - CurrentTab couples to active tab"; public SelfTestStepResult RunStep() { // Empty-config edge: actually exercise the getter's empty-fallback (it must // return a fresh Tab, not null/throw) rather than an unconditional pass. if (Plugin.Config.Tabs.Count == 0) { if (_plugin.CurrentTab is null) { ImGui.Text("Empty-config getter returned null instead of a fallback Tab."); return SelfTestStepResult.Fail; } ImGui.Text("No tabs configured; getter returns the empty-fallback Tab."); return SelfTestStepResult.Pass; } // /xlperf usually runs without the window drawn, so ActiveTab can be null // on the first pass. Open the window so the Draw-seed sets it, retry next // frame, and assert unconditionally once it is non-null. Bounded so a // never-drawn window cannot hang a batch run. if (_plugin.MainWindow.ActiveTab is null) { if (!_plugin.MainWindow.IsOpen) { _plugin.MainWindow.Toggle(); _forcedOpen = true; } if (++_waitFrames > 300) { RestoreWindow(); ImGui.Text( "MainWindow never drew a seed within 300 frames; coupling not asserted." ); return SelfTestStepResult.Pass; } ImGui.Text("Opening window so the draw-seed can set ActiveTab; retrying..."); return SelfTestStepResult.Waiting; } try { // Insert a victim at index 0: a regressed index-0 getter would return // THIS instead of ActiveTab, so ReferenceEquals would catch it. var victim = new Tab { Name = "selftest-coupling-victim" }; // Insert shifts every index; the worker's DropOldestTempTab holds // TabsListLock across its index lookup and removal. lock (_plugin.TabsListLock) Plugin.Config.Tabs.Insert(0, victim); try { if (!ReferenceEquals(_plugin.CurrentTab, _plugin.MainWindow.ActiveTab)) { ImGui.Text("CurrentTab is not the same reference as ActiveTab"); return SelfTestStepResult.Fail; } if (ReferenceEquals(_plugin.CurrentTab, victim)) { ImGui.Text("CurrentTab returned the index-0 victim (getter still index-based)"); return SelfTestStepResult.Fail; } // Reference no-op: resetting against a tab that is NOT the active // one must leave the active reference untouched. var activeBefore = _plugin.MainWindow.ActiveTab; _plugin.MainWindow.ResetActiveTabIfRemoved(victim); if (!ReferenceEquals(_plugin.MainWindow.ActiveTab, activeBefore)) { ImGui.Text("ResetActiveTabIfRemoved changed the active tab on a non-match"); return SelfTestStepResult.Fail; } return SelfTestStepResult.Pass; } finally { lock (_plugin.TabsListLock) Plugin.Config.Tabs.Remove(victim); } } finally { RestoreWindow(); } } private void RestoreWindow() { if (_forcedOpen && _plugin.MainWindow.IsOpen) _plugin.MainWindow.Toggle(); _forcedOpen = false; } public void CleanUp() { RestoreWindow(); _waitFrames = 0; } }