From 6af9e05664481a0d1beeeb9ff6a5df1bdb91c971 Mon Sep 17 00:00:00 2001 From: Jon Kazama Date: Sat, 30 May 2026 08:40:51 +0200 Subject: [PATCH] test(selftest): add PayloadHandler and ChunkRenderer ctor smoke steps --- HellionChat/Plugin.cs | 20 ++++++ .../SelfTests/ChunkRendererCtorSmokeStep.cs | 37 ++++++++++ .../SelfTests/PayloadHandlerCtorSmokeStep.cs | 69 +++++++++++++++++++ 3 files changed, 126 insertions(+) create mode 100644 HellionChat/SelfTests/ChunkRendererCtorSmokeStep.cs create mode 100644 HellionChat/SelfTests/PayloadHandlerCtorSmokeStep.cs diff --git a/HellionChat/Plugin.cs b/HellionChat/Plugin.cs index 4a06ccc..60952aa 100755 --- a/HellionChat/Plugin.cs +++ b/HellionChat/Plugin.cs @@ -118,6 +118,14 @@ public sealed class Plugin : IAsyncDalamudPlugin internal Integrations.HonorificService HonorificService { get; private set; } = null!; internal Integrations.CustomAudioPlayer CustomAudioPlayer { get; private set; } = null!; + // Ctor-smoke anchors (B0-2). Exposed so the Payload/Chunk ctor-smoke steps + // can drive the real per-frame Lender path (Borrow()) and the eager + // singletons through the container, never via new(). Mirror of the + // FontManager property pattern — every SelfTest reaches services this way. + internal PayloadHandler PayloadHandler { get; private set; } = null!; + internal Util.Lender PayloadHandlerLender { get; private set; } = null!; + internal Ui.Components.ChunkRenderer ChunkRenderer { get; private set; } = null!; + // Platform indirection over Dalamud.Utility.Util. Wired in Phase-1 ctor so // any service allocated in LoadAsync can read Plugin.PlatformUtil. internal static IPlatformUtil PlatformUtil { get; private set; } = null!; @@ -304,6 +312,16 @@ public sealed class Plugin : IAsyncDalamudPlugin DebuggerWindow = _host.Services.GetRequiredService(); FirstRunWizard = _host.Services.GetRequiredService(); ChannelPopoutPool = _host.Services.GetRequiredService(); + + // Ctor-smoke anchors (B0-2). Resolved last, against the fully built + // container: every MakePayloadHandler dep (MainWindow, InputBar, + // ChunkRenderer, ...) is resolvable here, and the ChunkRenderer resolve + // below just reuses the same cached singleton. These are plain + // post-build container resolves (no new factory-lambda edge) — they add + // no DI cycle. See feedback_di_factory_callsite_cycles. + PayloadHandler = _host.Services.GetRequiredService(); + PayloadHandlerLender = _host.Services.GetRequiredService>(); + ChunkRenderer = _host.Services.GetRequiredService(); } public async Task LoadAsync(CancellationToken cancellationToken) @@ -340,6 +358,8 @@ public sealed class Plugin : IAsyncDalamudPlugin new SelfTests.ThemeSwitchSelfTestStep(this), new SelfTests.ThemeCrossfadeSelfTestStep(this), new SelfTests.FontManagerCtorSmokeStep(this), + new SelfTests.PayloadHandlerCtorSmokeStep(this), + new SelfTests.ChunkRendererCtorSmokeStep(this), new SelfTests.FontPushSmokeStep(this), new SelfTests.WizardStateSmokeStep(this), new SelfTests.FoxBannerTextureSmokeStep(this), diff --git a/HellionChat/SelfTests/ChunkRendererCtorSmokeStep.cs b/HellionChat/SelfTests/ChunkRendererCtorSmokeStep.cs new file mode 100644 index 0000000..304bcf4 --- /dev/null +++ b/HellionChat/SelfTests/ChunkRendererCtorSmokeStep.cs @@ -0,0 +1,37 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.SelfTest; + +namespace HellionChat.SelfTests; + +// ChunkRenderer is a plain singleton (PluginHostFactory.cs:247) consumed by the +// real render path (MainWindow/MessageList/InputPreview DrawChunks). One +// resolution path is enough — unlike PayloadHandler there is no Lender. The +// type exposes no post-ctor observables (no LoadException-style state), so the +// honest assertion is "the DI ctor resolved a non-null instance". If a +// dependency registration breaks, Plugin's eager resolve throws before this +// step; the step pins that the singleton is reachable through the real +// container property, not via new(). +internal sealed class ChunkRendererCtorSmokeStep : ISelfTestStep +{ + private readonly Plugin plugin; + + public ChunkRendererCtorSmokeStep(Plugin plugin) + { + this.plugin = plugin; + } + + public string Name => "Hellion Chat - ChunkRenderer ctor smoke"; + + public SelfTestStepResult RunStep() + { + if (this.plugin.ChunkRenderer is null) + { + ImGui.Text("Plugin.ChunkRenderer is null"); + return SelfTestStepResult.Fail; + } + + return SelfTestStepResult.Pass; + } + + public void CleanUp() { } +} diff --git a/HellionChat/SelfTests/PayloadHandlerCtorSmokeStep.cs b/HellionChat/SelfTests/PayloadHandlerCtorSmokeStep.cs new file mode 100644 index 0000000..b0af9de --- /dev/null +++ b/HellionChat/SelfTests/PayloadHandlerCtorSmokeStep.cs @@ -0,0 +1,69 @@ +using Dalamud.Bindings.ImGui; +using Dalamud.Plugin.SelfTest; + +namespace HellionChat.SelfTests; + +// Drives the per-frame Lender path the same way MainWindow.Draw +// and InputPreview do (Borrow() + ResetCounter()), NOT the eager singleton. +// PayloadHandler is registered twice (PluginHostFactory.cs:253/254): an eager +// singleton for the init HostedServices, and a Lender factory-lambda for +// per-frame isolation. MS.DI resolves factory lambdas lazily and does not +// detect cycles through them, so a Borrow() that throws is the only automated +// signal of a broken lazy ctor before the first real frame renders. A +// singleton-only smoke would resolve the eager instance and mask exactly that +// failure. Resolve through the container/Lender, never new(). +internal sealed class PayloadHandlerCtorSmokeStep : ISelfTestStep +{ + private readonly Plugin plugin; + + public PayloadHandlerCtorSmokeStep(Plugin plugin) + { + this.plugin = plugin; + } + + public string Name => "Hellion Chat - PayloadHandler ctor smoke"; + + public SelfTestStepResult RunStep() + { + var lender = this.plugin.PayloadHandlerLender; + if (lender is null) + { + ImGui.Text("Plugin.PayloadHandlerLender is null"); + return SelfTestStepResult.Fail; + } + + // Borrow() runs MakePayloadHandler's factory lambda on first use; a + // throw or null here means a broken lazy ctor. This is the real + // per-frame construction path, not the eager singleton. + var borrowed = lender.Borrow(); + + // Keep the probe idempotent and avoid perturbing the frame path: + // MainWindow.Draw resets this same shared Lender every frame, so + // resetting here leaves a closed-MainWindow /xlperf run clean too. + lender.ResetCounter(); + + if (borrowed is null) + { + ImGui.Text("Lender.Borrow() returned null"); + return SelfTestStepResult.Fail; + } + + // Second construction path: the eager singleton the init HostedServices + // consume (PluginHostFactory.cs:253, :356). Assert it resolved too. + if (this.plugin.PayloadHandler is null) + { + ImGui.Text("Plugin.PayloadHandler (singleton) is null"); + return SelfTestStepResult.Fail; + } + + // NOTE: we deliberately do NOT assert HandleTooltips == false / + // HoveredItem == 0u. MainWindow and InputPreview share this Lender, so a + // warm pool can hand back a reused instance whose hover state was set by + // a prior frame. The honest ctor-smoke assertion is "constructs through + // the real lazy path and is reachable" — a non-default warm value does + // not contradict that. + return SelfTestStepResult.Pass; + } + + public void CleanUp() { } +}