< Summary - Kestrun — Combined Coverage

Information
Class: Kestrun.Languages.PowerShellDelegateBuilder
Assembly: Kestrun
File(s): /home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/PowerShellDelegateBuilder.cs
Tag: Kestrun/Kestrun@2d87023b37eb91155071c91dd3d6a2eeb3004705
Line coverage
87%
Covered lines: 91
Uncovered lines: 13
Coverable lines: 104
Total lines: 194
Line coverage: 87.5%
Branch coverage
70%
Covered branches: 21
Total branches: 30
Branch coverage: 70%
Method coverage

Feature is only available for sponsors

Upgrade to PRO version

Coverage history

Coverage history 0 25 50 75 100 08/26/2025 - 01:25:22 Line coverage: 75.2% (85/113) Branch coverage: 52.7% (19/36) Total lines: 194 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/06/2025 - 18:30:33 Line coverage: 80.5% (91/113) Branch coverage: 63.8% (23/36) Total lines: 194 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 87.5% (91/104) Branch coverage: 70% (21/30) Total lines: 194 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e 08/26/2025 - 01:25:22 Line coverage: 75.2% (85/113) Branch coverage: 52.7% (19/36) Total lines: 194 Tag: Kestrun/Kestrun@07f821172e5dc3657f1be7e6818f18d6721cf38a09/06/2025 - 18:30:33 Line coverage: 80.5% (91/113) Branch coverage: 63.8% (23/36) Total lines: 194 Tag: Kestrun/Kestrun@aeddbedb8a96e9137aac94c2d5edd011b57ac87110/13/2025 - 16:52:37 Line coverage: 87.5% (91/104) Branch coverage: 70% (21/30) Total lines: 194 Tag: Kestrun/Kestrun@10d476bee71c71ad215bb8ab59f219887b5b4a5e

Metrics

MethodBranch coverage Crap Score Cyclomatic complexity Line coverage
Build(...)100%2287.14%
GetPowerShellFromContext(...)66.66%66100%
GetKestrunContext(...)50%22100%
LogTopResults(...)66.66%7675%
HandleErrorsIfAnyAsync()100%44100%
LogSideChannelMessagesIfAny(...)50%12860%
HandleRedirectIfAny(...)100%22100%
ApplyResponseAsync(...)100%11100%

File(s)

/home/runner/work/Kestrun/Kestrun/src/CSharp/Kestrun/Languages/PowerShellDelegateBuilder.cs

#LineLine coverage
 1using System.Management.Automation;
 2using Kestrun.Logging;
 3using Kestrun.Models;
 4using Kestrun.Utilities;
 5using Serilog.Events;
 6
 7namespace Kestrun.Languages;
 8
 9internal static class PowerShellDelegateBuilder
 10{
 11    public const string PS_INSTANCE_KEY = "PS_INSTANCE";
 12    public const string KR_CONTEXT_KEY = "KR_CONTEXT";
 13
 14    internal static RequestDelegate Build(string code, Serilog.ILogger log, Dictionary<string, object?>? arguments)
 15    {
 616        ArgumentNullException.ThrowIfNull(code);
 617        if (log.IsEnabled(LogEventLevel.Debug))
 18        {
 319            log.Debug("Building PowerShell delegate, script length={Length}", code.Length);
 20        }
 21
 622        return async context =>
 623        {
 624            // Log invocation
 525            if (log.IsEnabled(LogEventLevel.Debug))
 626            {
 227                log.DebugSanitized("PS delegate invoked for {Path}", context.Request.Path);
 628            }
 629            // Prepare for execution
 530            KestrunContext? krContext = null;
 631            // Get the PowerShell instance from the context (set by middleware)
 532            var ps = GetPowerShellFromContext(context, log);
 633
 634            // Ensure the runspace pool is open before executing the script
 635            try
 636            {
 437                PowerShellExecutionHelpers.SetVariables(ps, arguments, log);
 638
 439                log.Verbose("Setting PowerShell variables for Request and Response in the runspace.");
 440                krContext = GetKestrunContext(context);
 641
 442                PowerShellExecutionHelpers.AddScript(ps, code);
 443                var psResults = await PowerShellExecutionHelpers.InvokeAsync(ps, log, context.RequestAborted).ConfigureA
 444                LogTopResults(log, psResults);
 645
 446                if (await HandleErrorsIfAnyAsync(context, ps).ConfigureAwait(false))
 647                {
 148                    return;
 649                }
 650
 351                LogSideChannelMessagesIfAny(log, ps);
 652
 353                if (HandleRedirectIfAny(context, krContext, log))
 654                {
 155                    return;
 656                }
 657
 258                log.Verbose("Applying response to HttpResponse...");
 259                await ApplyResponseAsync(context, krContext).ConfigureAwait(false);
 260            }
 661            // optional: catch client cancellation to avoid noisy logs
 062            catch (OperationCanceledException) when (context.RequestAborted.IsCancellationRequested)
 663            {
 664                // client disconnected – nothing to send
 065            }
 066            catch (Exception ex)
 667            {
 668                // If we have exception options, set a 500 status code and generic message.
 669                // Otherwise rethrow to let higher-level middleware handle it (e.g., Developer Exception Page
 070                if (krContext?.Host?.ExceptionOptions is null)
 671                { // Log and handle script errors
 072                    log.Error(ex, "PowerShell script failed - {Preview}", code[..Math.Min(40, code.Length)]);
 073                    context.Response.StatusCode = 500; // Internal Server Error
 074                    context.Response.ContentType = "text/plain; charset=utf-8";
 075                    await context.Response.WriteAsync("An error occurred while processing your request.");
 676                }
 677                else
 678                {
 679                    // re-throw to let higher-level middleware handle it (e.g., Developer Exception Page)
 080                    throw;
 681                }
 682            }
 683            finally
 684            {
 685                // Do not call Response.CompleteAsync here; leaving the response open allows
 686                // downstream middleware like StatusCodePages to generate a body for status-only responses.
 687            }
 1088        };
 89    }
 90
 91    /// <summary>
 92    /// Retrieves the PowerShell instance from the HttpContext items.
 93    /// </summary>
 94    /// <param name="context">The HttpContext from which to retrieve the PowerShell instance.</param>
 95    /// <param name="log">The logger to use for logging.</param>
 96    /// <returns>The PowerShell instance associated with the current request.</returns>
 97    /// <exception cref="InvalidOperationException">Thrown if the PowerShell instance is not found in the context items.
 98    private static PowerShell GetPowerShellFromContext(HttpContext context, Serilog.ILogger log)
 99    {
 5100        if (!context.Items.ContainsKey(PS_INSTANCE_KEY))
 101        {
 1102            throw new InvalidOperationException("PowerShell runspace not found in context items. Ensure PowerShellRunspa
 103        }
 104
 4105        log.Verbose("Retrieving PowerShell instance from context items.");
 4106        var ps = context.Items[PS_INSTANCE_KEY] as PowerShell
 4107                 ?? throw new InvalidOperationException("PowerShell instance not found in context items.");
 4108        return ps.Runspace == null
 4109            ? throw new InvalidOperationException("PowerShell runspace is not set. Ensure PowerShellRunspaceMiddleware i
 4110            : ps;
 111    }
 112
 113    /// <summary>
 114    /// Retrieves the KestrunContext from the HttpContext items.
 115    /// </summary>
 116    /// <param name="context">The HttpContext from which to retrieve the KestrunContext.</param>
 117    /// <returns>The KestrunContext associated with the current request.</returns>
 118    /// <exception cref="InvalidOperationException">Thrown if the KestrunContext is not found in the context items.</exc
 119    private static KestrunContext GetKestrunContext(HttpContext context)
 4120        => context.Items[KR_CONTEXT_KEY] as KestrunContext
 4121           ?? throw new InvalidOperationException($"{KR_CONTEXT_KEY} key not found in context items.");
 122
 123
 124    ///<summary>
 125    /// Logs the top results from the PowerShell script output for debugging purposes.
 126    /// Only logs if the log level is set to Debug.
 127    /// </summary>
 128    /// <param name="log">The logger to use for logging.</param>
 129    /// <param name="psResults">The collection of PSObject results from the PowerShell script.</param>
 130    private static void LogTopResults(Serilog.ILogger log, PSDataCollection<PSObject> psResults)
 131    {
 4132        if (!log.IsEnabled(LogEventLevel.Debug))
 133        {
 3134            return;
 135        }
 136
 1137        log.Debug("PowerShell script output:");
 2138        foreach (var r in psResults.Take(10))
 139        {
 0140            log.Debug("   • {Result}", r);
 141        }
 1142        if (psResults.Count > 10)
 143        {
 0144            log.Debug("   … {Count} more", psResults.Count - 10);
 145        }
 1146    }
 147
 148    /// <summary>
 149    /// Handles any errors that occurred during the PowerShell script execution.
 150    /// </summary>
 151    /// <param name="context">The HttpContext for the current request.</param>
 152    /// <param name="ps">The PowerShell instance used for script execution.</param>
 153    /// <returns>True if errors were handled, false otherwise.</returns>
 154    private static async Task<bool> HandleErrorsIfAnyAsync(HttpContext context, PowerShell ps)
 155    {
 4156        if (ps.HadErrors || ps.Streams.Error.Count != 0)
 157        {
 1158            await BuildError.ResponseAsync(context, ps).ConfigureAwait(false);
 1159            return true;
 160        }
 3161        return false;
 4162    }
 163
 164    /// <summary>
 165    /// Logs any side-channel messages (Verbose, Debug, Warning, Information) produced by the PowerShell script.
 166    /// </summary>
 167    /// <param name="log">The logger to use for logging.</param>
 168    /// <param name="ps">The PowerShell instance used to invoke the script.</param>
 169    private static void LogSideChannelMessagesIfAny(Serilog.ILogger log, PowerShell ps)
 170    {
 3171        if (ps.Streams.Verbose.Count > 0 || ps.Streams.Debug.Count > 0 || ps.Streams.Warning.Count > 0 || ps.Streams.Inf
 172        {
 0173            log.Verbose("PowerShell script completed with verbose/debug/warning/info messages.");
 0174            log.Verbose(BuildError.Text(ps));
 175        }
 3176        log.Verbose("PowerShell script completed successfully.");
 3177    }
 178
 179    private static bool HandleRedirectIfAny(HttpContext context, KestrunContext krContext, Serilog.ILogger log)
 180    {
 3181        if (!string.IsNullOrEmpty(krContext.Response.RedirectUrl))
 182        {
 1183            log.Verbose($"Redirecting to {krContext.Response.RedirectUrl}");
 1184            context.Response.Redirect(krContext.Response.RedirectUrl);
 1185            return true;
 186        }
 2187        return false;
 188    }
 189
 190    private static Task ApplyResponseAsync(HttpContext context, KestrunContext krContext)
 2191        => krContext.Response.ApplyTo(context.Response);
 192
 193    // Removed explicit Response.CompleteAsync to allow StatusCodePages to run after endpoints when appropriate.
 194}