using Dalamud.Bindings.ImGui; using Dalamud.Plugin.SelfTest; using HellionChat.Ui.Components.Settings; using HellionChat.Util; namespace HellionChat.SelfTests; // The gate has fourteen unit tests. None of them prove that the workers are // wired to it, and that is where the mistake actually happened: the metadata // refresh reached the store without taking the gate at all, and its flag was // missing from the tab's shared busy state, so a wipe could start while it held // the read lock. // // A unit test cannot see that. It needs the real singleton, the real flags and // the real settings tab, so it lives here. internal sealed class DbGateWiringStep : ISelfTestStep { private readonly Plugin _plugin; public DbGateWiringStep(Plugin plugin) => _plugin = plugin; public string Name => "Hellion Chat - DB gate wiring"; public SelfTestStepResult RunStep() { var failures = new List(); // Nothing may be running when the probe starts, or the assertions below // measure somebody else's operation. if (_plugin.DbOperations.IsBusy) failures.Add($"gate already held by {_plugin.DbOperations.Current} before the probe"); CheckEveryOperationRoundTrips(failures); CheckRefusalLeavesTheOwnerAlone(failures); CheckRevisionTracksMutations(failures); CheckTabSeesTheGate(failures); foreach (var f in failures) ImGui.Text(f); SelfTestReport.Append(Name, failures.Count == 0 ? "PASS" : "FAIL", failures); return failures.Count == 0 ? SelfTestStepResult.Pass : SelfTestStepResult.Fail; } private void CheckEveryOperationRoundTrips(List failures) { foreach (var op in EnumValues.All) { if (op == DbOperation.None) continue; if (!_plugin.DbOperations.TryBegin(op)) { failures.Add($"{op}: TryBegin refused on a free gate"); continue; } if (_plugin.DbOperations.Current != op) failures.Add($"{op}: gate reports {_plugin.DbOperations.Current} after TryBegin"); if (_plugin.DbOperations.TryBegin(op)) failures.Add($"{op}: a second TryBegin succeeded while it was held"); _plugin.DbOperations.End(op); if (_plugin.DbOperations.IsBusy) failures.Add($"{op}: still busy after End"); } } private void CheckRefusalLeavesTheOwnerAlone(List failures) { if (!_plugin.DbOperations.TryBegin(DbOperation.Export)) { failures.Add("could not take the gate for the refusal check"); return; } // A worker whose TryBegin was refused still runs its finally. Releasing // there must not hand away somebody else's lock. _plugin.DbOperations.End(DbOperation.Clear); if (_plugin.DbOperations.Current != DbOperation.Export) failures.Add("a foreign End released the gate"); _plugin.DbOperations.End(DbOperation.Export); } private void CheckRevisionTracksMutations(List failures) { var before = _plugin.DbOperations.Revision; _plugin.DbOperations.TryBegin(DbOperation.Export); _plugin.DbOperations.End(DbOperation.Export); if (_plugin.DbOperations.Revision != before) failures.Add("export moved the revision; it cannot change a row"); _plugin.DbOperations.TryBegin(DbOperation.Cleanup); _plugin.DbOperations.End(DbOperation.Cleanup); if (_plugin.DbOperations.Revision == before) failures.Add( "cleanup did not move the revision; a stale preview would pass as current" ); } // The settings tab decides whether the destructive buttons are live. If it // cannot see a held gate, two of them are clickable at once. private void CheckTabSeesTheGate(List failures) { if (!_plugin.DbOperations.TryBegin(DbOperation.Clear)) { failures.Add("could not take the gate for the tab check"); return; } try { if (!_plugin.DataPrivacyTab.AnythingRunningForSelfTest) failures.Add("the data and privacy tab does not see a held gate"); } finally { _plugin.DbOperations.End(DbOperation.Clear); } if (_plugin.DataPrivacyTab.AnythingRunningForSelfTest) failures.Add("the tab still reports busy after the gate was released"); } public void CleanUp() { } }