From 3b6be553767b1d81aabffcf0dab64a186806d8a6 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Sat, 8 Jun 2024 16:21:37 +1000 Subject: [PATCH] Add break option #1508 (#1851) --- README.md | 2 + docs/CHANGELOG-v3.md | 7 + .../PSRule/en-US/about_PSRule_Options.md | 65 +++++ schemas/PSRule-options.schema.json | 13 + src/PSRule.CommandLine/Commands/RunCommand.cs | 2 +- src/PSRule.Types/Options/BreakLevel.cs | 36 +++ src/PSRule.Types/Options/ExecutionOption.cs | 129 ++-------- src/PSRule.Types/Options/IExecutionOption.cs | 117 +++++++++ src/PSRule/PSRule.psm1 | 17 ++ src/PSRule/Pipeline/AssertPipelineBuilder.cs | 20 +- src/PSRule/Pipeline/DefaultPipelineResult.cs | 67 +++++ .../Pipeline/Formatters/AssertFormatter.cs | 2 +- src/PSRule/Pipeline/GetBaselinePipeline.cs | 2 +- src/PSRule/Pipeline/GetRulePipeline.cs | 2 +- src/PSRule/Pipeline/IPipeline.cs | 34 +++ src/PSRule/Pipeline/IPipelineResult.cs | 25 ++ src/PSRule/Pipeline/IPipelineWriter.cs | 104 ++++++++ src/PSRule/Pipeline/InvokeRulePipeline.cs | 8 +- .../Pipeline/Output/JobSummaryWriter.cs | 4 +- src/PSRule/Pipeline/PipelineBuilder.cs | 90 +------ src/PSRule/Pipeline/PipelineLogger.cs | 2 +- src/PSRule/Pipeline/PipelineWriter.cs | 209 +-------------- src/PSRule/Pipeline/ResultOutputWriter.cs | 55 ++++ src/PSRule/Pipeline/RulePipeline.cs | 5 +- .../Pipeline/SerializationOutputWriter.cs | 49 ++++ tests/PSRule.Tests/AssertFormatterTests.cs | 55 ++-- tests/PSRule.Tests/OutputWriterTests.cs | 14 +- tests/PSRule.Tests/PSRule.Options.Tests.ps1 | 240 +++++++++++------- tests/PSRule.Tests/PSRule.Tests16.yml | 5 + tests/PSRule.Tests/PipelineTests.cs | 1 - 30 files changed, 832 insertions(+), 549 deletions(-) create mode 100644 src/PSRule.Types/Options/BreakLevel.cs create mode 100644 src/PSRule.Types/Options/IExecutionOption.cs create mode 100644 src/PSRule/Pipeline/DefaultPipelineResult.cs create mode 100644 src/PSRule/Pipeline/IPipeline.cs create mode 100644 src/PSRule/Pipeline/IPipelineResult.cs create mode 100644 src/PSRule/Pipeline/IPipelineWriter.cs create mode 100644 src/PSRule/Pipeline/ResultOutputWriter.cs create mode 100644 src/PSRule/Pipeline/SerializationOutputWriter.cs create mode 100644 tests/PSRule.Tests/PSRule.Tests16.yml diff --git a/README.md b/README.md index f18b4186c..304ee9514 100644 --- a/README.md +++ b/README.md @@ -292,6 +292,7 @@ The following conceptual topics exist in the `PSRule` module: - [Configuration](https://aka.ms/ps-rule/options#configuration) - [Convention.Include](https://aka.ms/ps-rule/options#conventioninclude) - [Execution.AliasReference](https://aka.ms/ps-rule/options#executionaliasreference) + - [Execution.Break](https://aka.ms/ps-rule/options#executionbreak) - [Execution.DuplicateResourceId](https://aka.ms/ps-rule/options#executionduplicateresourceid) - [Execution.HashAlgorithm](https://aka.ms/ps-rule/options#executionhashalgorithm) - [Execution.LanguageMode](https://aka.ms/ps-rule/options#executionlanguagemode) @@ -303,6 +304,7 @@ The following conceptual topics exist in the `PSRule` module: - [Execution.UnprocessedObject](https://aka.ms/ps-rule/options#executionunprocessedobject) - [Include.Module](https://aka.ms/ps-rule/options#includemodule) - [Include.Path](https://aka.ms/ps-rule/options#includepath) + - [Input.FileObjects](https://aka.ms/ps-rule/options#inputfileobjects) - [Input.Format](https://aka.ms/ps-rule/options#inputformat) - [Input.IgnoreGitPath](https://aka.ms/ps-rule/options#inputignoregitpath) - [Input.IgnoreObjectSource](https://aka.ms/ps-rule/options#inputignoreobjectsource) diff --git a/docs/CHANGELOG-v3.md b/docs/CHANGELOG-v3.md index 3e5fee00c..c164c3b7e 100644 --- a/docs/CHANGELOG-v3.md +++ b/docs/CHANGELOG-v3.md @@ -29,6 +29,13 @@ See [upgrade notes][1] for helpful information when upgrading from previous vers What's changed since pre-release v3.0.0-B0203: +- New features: + - Added option to configure the severity level that PSRule will break the pipeline at by @BernieWhite. + [#1508](https://github.com/microsoft/PSRule/issues/1508) + - Previously only rules with the severity level `Error` would break the pipeline. + - With this update rules with the severity level `Error` that fail will break the pipeline by default. + - The `Execution.Break` option can be set to `Never`, `OnError`, `OnWarning`, or `OnInformation`. + - If a rule fails with a severity level equal or higher than the configured level the pipeline will break. - Engineering: - Bump xunit to v2.8.1. [#1840](https://github.com/microsoft/PSRule/pull/1840) diff --git a/docs/concepts/PSRule/en-US/about_PSRule_Options.md b/docs/concepts/PSRule/en-US/about_PSRule_Options.md index c8f0402f9..af6db68f5 100644 --- a/docs/concepts/PSRule/en-US/about_PSRule_Options.md +++ b/docs/concepts/PSRule/en-US/about_PSRule_Options.md @@ -16,6 +16,7 @@ The following workspace options are available for use: - [Baseline.Group](#baselinegroup) - [Convention.Include](#conventioninclude) - [Execution.AliasReference](#executionaliasreference) +- [Execution.Break](#executionbreak) - [Execution.DuplicateResourceId](#executionduplicateresourceid) - [Execution.HashAlgorithm](#executionhashalgorithm) - [Execution.LanguageMode](#executionlanguagemode) @@ -791,6 +792,68 @@ variables: value: Error ``` +### Execution.Break + +:octicons-milestone-24: v3.0.0 + +Determines the minimum rule severity level that breaks the pipeline. +By default, the pipeline will break if a rule of error severity level fails. + +For this to take effect the rule must execute successfully and return a failure. +This does not affect the pipeline if other errors or exceptions occurs. + +The following preferences are available: + +- `None` (0) - No preference. + Inherits the default of `Error`. +- `Never` = (1) - Never break the pipeline if a rule fails regardless of level. + The pipeline will still break if other errors occur. +- `OnError` = (2) - Break the pipeline if a rule of error severity level fails. + This is the default. +- `OnWarning` = (3) - Break the pipeline if a rule of warning or error severity level fails. +- `OnInformation` = (4) - Break the pipeline if a rule of information, warning, or error severity level fails. + +This option can be specified using: + +```powershell +# PowerShell: Using the Break parameter +$option = New-PSRuleOption -ExecutionBreak 'Never'; +``` + +```powershell +# PowerShell: Using the Execution.Break hashtable key +$option = New-PSRuleOption -Option @{ 'Execution.Break' = 'Never' }; +``` + +```powershell +# PowerShell: Using the ExecutionBreak parameter to set YAML +Set-PSRuleOption -ExecutionBreak 'Never'; +``` + +```yaml +# YAML: Using the execution/break property +execution: + break: Never +``` + +```bash +# Bash: Using environment variable +export PSRULE_EXECUTION_BREAK=Never +``` + +```yaml +# GitHub Actions: Using environment variable +env: + PSRULE_EXECUTION_BREAK: Never +``` + +```yaml +# Azure Pipelines: Using environment variable +variables: +- name: PSRULE_EXECUTION_BREAK + value: Never +``` + ### Execution.DuplicateResourceId :octicons-milestone-24: v2.4.0 @@ -1517,6 +1580,8 @@ variables: ### Input.FileObjects +:octicons-milestone-24: v3.0.0 + Determines if file objects are processed by rules. This option is for backwards compatibility with PSRule v2.x in cases where file objects are used as input. diff --git a/schemas/PSRule-options.schema.json b/schemas/PSRule-options.schema.json index 953957e58..1d93d78e5 100644 --- a/schemas/PSRule-options.schema.json +++ b/schemas/PSRule-options.schema.json @@ -356,6 +356,19 @@ "description": "Options that affect execution.", "markdownDescription": "Options that affect execution. [See help](https://aka.ms/ps-rule/options)", "properties": { + "break": { + "type": "string", + "title": "Break", + "description": "Determines the minimum rule severity level that breaks the pipeline. By default, the pipeline will break if a rule of error severity fails.", + "markdownDescription": "Determines the minimum rule severity level that breaks the pipeline. By default, the pipeline will break if a rule of error severity fails.\n\n[See help](https://microsoft.github.io/PSRule/v3/concepts/PSRule/en-US/about_PSRule_Options/#executionbreak)", + "enum": [ + "Never", + "OnError", + "OnWarning", + "OnInformation" + ], + "default": "OnError" + }, "duplicateResourceId": { "type": "string", "title": "Duplicate resource identifiers", diff --git a/src/PSRule.CommandLine/Commands/RunCommand.cs b/src/PSRule.CommandLine/Commands/RunCommand.cs index b4657ad75..ed0998848 100644 --- a/src/PSRule.CommandLine/Commands/RunCommand.cs +++ b/src/PSRule.CommandLine/Commands/RunCommand.cs @@ -53,7 +53,7 @@ public sealed class RunCommand pipeline.Begin(); pipeline.Process(null); pipeline.End(); - if (pipeline.Result.HadFailures) + if (pipeline.Result.ShouldBreakFromFailure) exitCode = ERROR_BREAK_ON_FAILURE; } return clientContext.Host.HadErrors || pipeline == null ? ERROR_GENERIC : exitCode; diff --git a/src/PSRule.Types/Options/BreakLevel.cs b/src/PSRule.Types/Options/BreakLevel.cs new file mode 100644 index 000000000..3b66c6d8e --- /dev/null +++ b/src/PSRule.Types/Options/BreakLevel.cs @@ -0,0 +1,36 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Options; + +/// +/// Determine the rule severity level at which to break the pipeline. +/// +public enum BreakLevel +{ + /// + /// No preference. + /// Inherits the default of OnError. + /// + None = 0, + + /// + /// Continue even if a rule fails regardless of rule severity. + /// + Never = 1, + + /// + /// Only break on error. + /// + OnError = 2, + + /// + /// Break if any rule of warning or error severity fails. + /// + OnWarning = 3, + + /// + /// Break if any rule fails. + /// + OnInformation = 4 +} diff --git a/src/PSRule.Types/Options/ExecutionOption.cs b/src/PSRule.Types/Options/ExecutionOption.cs index 50271829e..0da23205b 100644 --- a/src/PSRule.Types/Options/ExecutionOption.cs +++ b/src/PSRule.Types/Options/ExecutionOption.cs @@ -5,113 +5,6 @@ using System.ComponentModel; namespace PSRule.Options; -/// -/// Options that configure the execution sandbox. -/// -/// -/// See . -/// -public interface IExecutionOption : IOption -{ - /// - /// Determines how to handle duplicate resources identifiers during execution. - /// Regardless of the value, only the first resource will be used. - /// By defaut, an error is thrown. - /// When set to Warn, a warning is generated. - /// When set to Debug, a message is written to the debug log. - /// When set to Ignore, no output will be displayed. - /// - ExecutionActionPreference DuplicateResourceId { get; } - - /// - /// Configures the hashing algorithm used by the PSRule runtime. - /// The default is . - /// - HashAlgorithm HashAlgorithm { get; } - - /// - /// The language mode to execute PowerShell code with. - /// The default is . - /// - LanguageMode LanguageMode { get; } - - /// - /// Determines how the initial session state for executing PowerShell code is created. - /// The default is . - /// - SessionState InitialSessionState { get; } - - /// - /// Configures where to allow PowerShell language features (such as rules and conventions) to run from. - /// The default is . - /// - RestrictScriptSource RestrictScriptSource { get; } - - /// - /// Determines how to handle expired suppression groups. - /// Regardless of the value, an expired suppression group will be ignored. - /// By default, a warning is generated. - /// When set to Error, an error is thrown. - /// When set to Debug, a message is written to the debug log. - /// When set to Ignore, no output will be displayed. - /// - ExecutionActionPreference SuppressionGroupExpired { get; } - - /// - /// Determines how to handle rules that are excluded. - /// By default, excluded rules do not generated any output. - /// When set to Error, an error is thrown. - /// When set to Warn, a warning is generated. - /// When set to Debug, a message is written to the debug log. - /// - ExecutionActionPreference RuleExcluded { get; } - - /// - /// Determines how to handle rules that are suppressed. - /// By default, a warning is generated. - /// When set to Error, an error is thrown. - /// When set to Debug, a message is written to the debug log. - /// When set to Ignore, no output will be displayed. - /// - ExecutionActionPreference RuleSuppressed { get; } - - /// - /// Determines how to handle when an alias to a resource is used. - /// By default, a warning is generated. - /// When set to Error, an error is thrown. - /// When set to Debug, a message is written to the debug log. - /// When set to Ignore, no output will be displayed. - /// - ExecutionActionPreference AliasReference { get; } - - /// - /// Determines how to handle rules that generate inconclusive results. - /// By default, a warning is generated. - /// When set to Error, an error is thrown. - /// When set to Debug, a message is written to the debug log. - /// When set to Ignore, no output will be displayed. - /// - ExecutionActionPreference RuleInconclusive { get; } - - /// - /// Determines how to report when an invariant culture is used. - /// By default, a warning is generated. - /// When set to Error, an error is thrown. - /// When set to Debug, a message is written to the debug log. - /// When set to Ignore, no output will be displayed. - /// - ExecutionActionPreference InvariantCulture { get; } - - /// - /// Determines how to report objects that are not processed by any rule. - /// By default, a warning is generated. - /// When set to Error, an error is thrown. - /// When set to Debug, a message is written to the debug log. - /// When set to Ignore, no output will be displayed. - /// - ExecutionActionPreference UnprocessedObject { get; } -} - /// /// Options that configure the execution sandbox. /// @@ -120,6 +13,7 @@ public interface IExecutionOption : IOption /// public sealed class ExecutionOption : IEquatable, IExecutionOption { + private const BreakLevel DEFAULT_BREAK = BreakLevel.OnError; private const LanguageMode DEFAULT_LANGUAGEMODE = Options.LanguageMode.FullLanguage; private const ExecutionActionPreference DEFAULT_DUPLICATERESOURCEID = ExecutionActionPreference.Error; private const SessionState DEFAULT_INITIALSESSIONSTATE = SessionState.BuiltIn; @@ -135,6 +29,7 @@ public sealed class ExecutionOption : IEquatable, IExecutionOpt internal static readonly ExecutionOption Default = new() { + Break = DEFAULT_BREAK, DuplicateResourceId = DEFAULT_DUPLICATERESOURCEID, HashAlgorithm = DEFAULT_HASHALGORITHM, LanguageMode = DEFAULT_LANGUAGEMODE, @@ -154,6 +49,7 @@ public sealed class ExecutionOption : IEquatable, IExecutionOpt /// public ExecutionOption() { + Break = null; DuplicateResourceId = null; HashAlgorithm = null; LanguageMode = null; @@ -177,6 +73,7 @@ public sealed class ExecutionOption : IEquatable, IExecutionOpt if (option == null) return; + Break = option.Break; DuplicateResourceId = option.DuplicateResourceId; HashAlgorithm = option.HashAlgorithm; LanguageMode = option.LanguageMode; @@ -201,6 +98,7 @@ public sealed class ExecutionOption : IEquatable, IExecutionOpt public bool Equals(ExecutionOption other) { return other != null && + Break == other.Break && DuplicateResourceId == other.DuplicateResourceId && HashAlgorithm == other.HashAlgorithm && LanguageMode == other.LanguageMode && @@ -221,6 +119,7 @@ public sealed class ExecutionOption : IEquatable, IExecutionOpt unchecked // Overflow is fine { var hash = 17; + hash = hash * 23 + (Break.HasValue ? Break.Value.GetHashCode() : 0); hash = hash * 23 + (DuplicateResourceId.HasValue ? DuplicateResourceId.Value.GetHashCode() : 0); hash = hash * 23 + (HashAlgorithm.HasValue ? HashAlgorithm.Value.GetHashCode() : 0); hash = hash * 23 + (LanguageMode.HasValue ? LanguageMode.Value.GetHashCode() : 0); @@ -245,6 +144,7 @@ public sealed class ExecutionOption : IEquatable, IExecutionOpt { var result = new ExecutionOption(o1) { + Break = o1?.Break ?? o2?.Break, DuplicateResourceId = o1?.DuplicateResourceId ?? o2?.DuplicateResourceId, HashAlgorithm = o1?.HashAlgorithm ?? o2?.HashAlgorithm, LanguageMode = o1?.LanguageMode ?? o2?.LanguageMode, @@ -261,6 +161,13 @@ public sealed class ExecutionOption : IEquatable, IExecutionOpt return result; } + /// + /// Determines the minimum rule severity level that breaks the pipeline. + /// By default, the pipeline will break if a rule of error severity fails. + /// + [DefaultValue(null)] + public BreakLevel? Break { get; set; } + /// /// Determines how to handle duplicate resources identifiers during execution. /// Regardless of the value, only the first resource will be used. @@ -371,6 +278,8 @@ public sealed class ExecutionOption : IEquatable, IExecutionOpt #region IExecutionOption + BreakLevel IExecutionOption.Break => Break ?? DEFAULT_BREAK; + ExecutionActionPreference IExecutionOption.DuplicateResourceId => DuplicateResourceId ?? DEFAULT_DUPLICATERESOURCEID; HashAlgorithm IExecutionOption.HashAlgorithm => HashAlgorithm ?? DEFAULT_HASHALGORITHM; @@ -402,6 +311,9 @@ public sealed class ExecutionOption : IEquatable, IExecutionOpt /// internal void Load() { + if (Environment.TryEnum("PSRULE_EXECUTION_BREAK", out BreakLevel @break)) + Break = @break; + if (Environment.TryEnum("PSRULE_EXECUTION_HASHALGORITHM", out HashAlgorithm hashAlgorithm)) HashAlgorithm = hashAlgorithm; @@ -444,6 +356,9 @@ public sealed class ExecutionOption : IEquatable, IExecutionOpt /// internal void Load(Dictionary index) { + if (index.TryPopEnum("Execution.Break", out BreakLevel @break)) + Break = @break; + if (index.TryPopEnum("Execution.HashAlgorithm", out HashAlgorithm hashAlgorithm)) HashAlgorithm = hashAlgorithm; diff --git a/src/PSRule.Types/Options/IExecutionOption.cs b/src/PSRule.Types/Options/IExecutionOption.cs new file mode 100644 index 000000000..ae439e826 --- /dev/null +++ b/src/PSRule.Types/Options/IExecutionOption.cs @@ -0,0 +1,117 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Options; + +/// +/// Options that configure the execution sandbox. +/// +/// +/// See . +/// +public interface IExecutionOption : IOption +{ + /// + /// Determines the minimum rule severity level that breaks the pipeline. + /// By default, the pipeline will break if a rule of error severity fails. + /// + BreakLevel Break { get; } + + /// + /// Determines how to handle duplicate resources identifiers during execution. + /// Regardless of the value, only the first resource will be used. + /// By default, an error is thrown. + /// When set to Warn, a warning is generated. + /// When set to Debug, a message is written to the debug log. + /// When set to Ignore, no output will be displayed. + /// + ExecutionActionPreference DuplicateResourceId { get; } + + /// + /// Configures the hashing algorithm used by the PSRule runtime. + /// The default is . + /// + HashAlgorithm HashAlgorithm { get; } + + /// + /// The language mode to execute PowerShell code with. + /// The default is . + /// + LanguageMode LanguageMode { get; } + + /// + /// Determines how the initial session state for executing PowerShell code is created. + /// The default is . + /// + SessionState InitialSessionState { get; } + + /// + /// Configures where to allow PowerShell language features (such as rules and conventions) to run from. + /// The default is . + /// + RestrictScriptSource RestrictScriptSource { get; } + + /// + /// Determines how to handle expired suppression groups. + /// Regardless of the value, an expired suppression group will be ignored. + /// By default, a warning is generated. + /// When set to Error, an error is thrown. + /// When set to Debug, a message is written to the debug log. + /// When set to Ignore, no output will be displayed. + /// + ExecutionActionPreference SuppressionGroupExpired { get; } + + /// + /// Determines how to handle rules that are excluded. + /// By default, excluded rules do not generated any output. + /// When set to Error, an error is thrown. + /// When set to Warn, a warning is generated. + /// When set to Debug, a message is written to the debug log. + /// + ExecutionActionPreference RuleExcluded { get; } + + /// + /// Determines how to handle rules that are suppressed. + /// By default, a warning is generated. + /// When set to Error, an error is thrown. + /// When set to Debug, a message is written to the debug log. + /// When set to Ignore, no output will be displayed. + /// + ExecutionActionPreference RuleSuppressed { get; } + + /// + /// Determines how to handle when an alias to a resource is used. + /// By default, a warning is generated. + /// When set to Error, an error is thrown. + /// When set to Debug, a message is written to the debug log. + /// When set to Ignore, no output will be displayed. + /// + ExecutionActionPreference AliasReference { get; } + + /// + /// Determines how to handle rules that generate inconclusive results. + /// By default, a warning is generated. + /// When set to Error, an error is thrown. + /// When set to Debug, a message is written to the debug log. + /// When set to Ignore, no output will be displayed. + /// + ExecutionActionPreference RuleInconclusive { get; } + + /// + /// Determines how to report when an invariant culture is used. + /// By default, a warning is generated. + /// When set to Error, an error is thrown. + /// When set to Debug, a message is written to the debug log. + /// When set to Ignore, no output will be displayed. + /// + ExecutionActionPreference InvariantCulture { get; } + + /// + /// Determines how to report objects that are not processed by any rule. + /// By default, a warning is generated. + /// When set to Error, an error is thrown. + /// When set to Debug, a message is written to the debug log. + /// When set to Ignore, no output will be displayed. + /// + ExecutionActionPreference UnprocessedObject { get; } +} diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index a1989d9f0..7d2494f9a 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -1163,6 +1163,10 @@ function New-PSRuleOption { [Alias('ConventionInclude')] [String[]]$Convention, + # Sets the Execution.Break option + [Parameter(Mandatory = $False)] + [PSRule.Options.BreakLevel]$ExecutionBreak = [PSRule.Options.BreakLevel]::OnError, + # Sets the Execution.DuplicateResourceId option [Parameter(Mandatory = $False)] [Alias('ExecutionDuplicateResourceId')] @@ -1476,6 +1480,10 @@ function Set-PSRuleOption { [Alias('ConventionInclude')] [String[]]$Convention, + # Sets the Execution.Break option + [Parameter(Mandatory = $False)] + [PSRule.Options.BreakLevel]$ExecutionBreak = [PSRule.Options.BreakLevel]::OnError, + # Sets the Execution.DuplicateResourceId option [Parameter(Mandatory = $False)] [Alias('ExecutionDuplicateResourceId')] @@ -2236,6 +2244,10 @@ function SetOptions { [Alias('ConventionInclude')] [String[]]$Convention, + # Sets the Execution.Break option + [Parameter(Mandatory = $False)] + [PSRule.Options.BreakLevel]$ExecutionBreak = [PSRule.Options.BreakLevel]::OnError, + # Sets the Execution.DuplicateResourceId option [Parameter(Mandatory = $False)] [Alias('ExecutionDuplicateResourceId')] @@ -2461,6 +2473,11 @@ function SetOptions { $Option.Convention.Include = $Convention; } + # Sets option Execution.Break + if ($PSBoundParameters.ContainsKey('ExecutionBreak')) { + $Option.Execution.Break = $ExecutionBreak; + } + # Sets option Execution.DuplicateResourceId if ($PSBoundParameters.ContainsKey('DuplicateResourceId')) { $Option.Execution.DuplicateResourceId = $DuplicateResourceId; diff --git a/src/PSRule/Pipeline/AssertPipelineBuilder.cs b/src/PSRule/Pipeline/AssertPipelineBuilder.cs index 49775737b..de0a7999a 100644 --- a/src/PSRule/Pipeline/AssertPipelineBuilder.cs +++ b/src/PSRule/Pipeline/AssertPipelineBuilder.cs @@ -44,7 +44,7 @@ internal sealed class AssertPipelineBuilder : InvokePipelineBuilderBase _ResultVariableName = resultVariableName; _HostContext = hostContext; if (!string.IsNullOrEmpty(resultVariableName)) - _Results = new List(); + _Results = []; _Formatter = GetFormatter(style, source, inner, option); } @@ -61,7 +61,7 @@ internal sealed class AssertPipelineBuilder : InvokePipelineBuilderBase return new VisualStudioCodeFormatter(source, inner, option); return style == OutputStyle.Plain ? - (IAssertFormatter)new PlainFormatter(source, inner, option) : + new PlainFormatter(source, inner, option) : new ClientFormatter(source, inner, option); } @@ -100,10 +100,10 @@ internal sealed class AssertPipelineBuilder : InvokePipelineBuilderBase _Formatter.Begin(); } - public override void End() + public override void End(IPipelineResult result) { _Formatter.End(_TotalCount, _FailCount, _ErrorCount); - base.End(); + base.End(result); try { if (_ErrorCount > 0) @@ -115,7 +115,7 @@ internal sealed class AssertPipelineBuilder : InvokePipelineBuilderBase ErrorCategory.InvalidOperation, null)); } - else if (_FailCount > 0 && _Level == SeverityLevel.Error) + else if (result.ShouldBreakFromFailure) { HadFailures = true; base.WriteError(new ErrorRecord( @@ -133,13 +133,7 @@ internal sealed class AssertPipelineBuilder : InvokePipelineBuilderBase ErrorCategory.InvalidOperation, null)); } - if (_FailCount > 0 && _Level == SeverityLevel.Warning) - { - HadFailures = true; - base.WriteWarning(PSRuleResources.RuleFailPipelineException); - } - - if (_FailCount > 0 && _Level == SeverityLevel.Information) + else if (_FailCount > 0) { HadFailures = true; base.WriteHost(new HostInformationMessage() { Message = PSRuleResources.RuleFailPipelineException }); @@ -150,7 +144,7 @@ internal sealed class AssertPipelineBuilder : InvokePipelineBuilderBase } finally { - _InnerWriter?.End(); + _InnerWriter?.End(result); } } diff --git a/src/PSRule/Pipeline/DefaultPipelineResult.cs b/src/PSRule/Pipeline/DefaultPipelineResult.cs new file mode 100644 index 000000000..d2a4cfe32 --- /dev/null +++ b/src/PSRule/Pipeline/DefaultPipelineResult.cs @@ -0,0 +1,67 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Definitions.Rules; +using PSRule.Options; + +namespace PSRule.Pipeline; + +internal sealed class DefaultPipelineResult(IPipelineWriter writer, BreakLevel breakLevel) : IPipelineResult +{ + private readonly IPipelineWriter _Writer = writer; + private readonly BreakLevel _BreakLevel = breakLevel == BreakLevel.None ? ExecutionOption.Default.Break.Value : breakLevel; + private bool _HadErrors; + private bool _HadFailures; + private SeverityLevel _WorstCase = SeverityLevel.None; + + /// + public bool HadErrors + { + get + { + return _HadErrors || (_Writer != null && _Writer.HadErrors); + } + set + { + _HadErrors = value; + } + } + + /// + public bool HadFailures + { + get + { + return _HadFailures || (_Writer != null && _Writer.HadFailures); + } + set + { + _HadFailures = value; + } + } + + /// + public bool ShouldBreakFromFailure { get; private set; } + + public void Fail(SeverityLevel level) + { + _WorstCase = _WorstCase.GetWorstCase(level); + _HadFailures = true; + + if (ShouldBreakFromFailure || _BreakLevel == BreakLevel.Never || _WorstCase == SeverityLevel.None) + return; + + if (_BreakLevel == BreakLevel.OnInformation && (_WorstCase == SeverityLevel.Information || _WorstCase == SeverityLevel.Warning || _WorstCase == SeverityLevel.Error)) + { + ShouldBreakFromFailure = true; + } + else if (_BreakLevel == BreakLevel.OnWarning && (_WorstCase == SeverityLevel.Warning || _WorstCase == SeverityLevel.Error)) + { + ShouldBreakFromFailure = true; + } + else if (_BreakLevel == BreakLevel.OnError && _WorstCase == SeverityLevel.Error) + { + ShouldBreakFromFailure = true; + } + } +} diff --git a/src/PSRule/Pipeline/Formatters/AssertFormatter.cs b/src/PSRule/Pipeline/Formatters/AssertFormatter.cs index 06e927f24..9f9188a2c 100644 --- a/src/PSRule/Pipeline/Formatters/AssertFormatter.cs +++ b/src/PSRule/Pipeline/Formatters/AssertFormatter.cs @@ -176,7 +176,7 @@ internal abstract class AssertFormatterBase : PipelineLoggerBase, IAssertFormatt public void End(int total, int fail, int error) { if (Option.Output.Footer.GetValueOrDefault(FooterFormat.Default) != FooterFormat.None) - LineBreak(); + BreakIfUnbrokenInfo(); FooterRuleCount(total, fail, error); FooterRunInfo(); diff --git a/src/PSRule/Pipeline/GetBaselinePipeline.cs b/src/PSRule/Pipeline/GetBaselinePipeline.cs index 27cb9a24c..7da5af5a0 100644 --- a/src/PSRule/Pipeline/GetBaselinePipeline.cs +++ b/src/PSRule/Pipeline/GetBaselinePipeline.cs @@ -26,7 +26,7 @@ internal sealed class GetBaselinePipeline : RulePipeline public override void End() { Writer.WriteObject(HostHelper.GetBaseline(Source, Context).Where(Match), true); - Writer.End(); + Writer.End(Result); } private bool Match(Baseline baseline) diff --git a/src/PSRule/Pipeline/GetRulePipeline.cs b/src/PSRule/Pipeline/GetRulePipeline.cs index cc9bdb32d..cb491edfd 100644 --- a/src/PSRule/Pipeline/GetRulePipeline.cs +++ b/src/PSRule/Pipeline/GetRulePipeline.cs @@ -24,6 +24,6 @@ internal sealed class GetRulePipeline : RulePipeline, IPipeline public override void End() { Writer.WriteObject(HostHelper.GetRule(Source, Context, _IncludeDependencies), true); - Writer.End(); + Writer.End(Result); } } diff --git a/src/PSRule/Pipeline/IPipeline.cs b/src/PSRule/Pipeline/IPipeline.cs new file mode 100644 index 000000000..c0de63371 --- /dev/null +++ b/src/PSRule/Pipeline/IPipeline.cs @@ -0,0 +1,34 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSRule.Pipeline; + +/// +/// An instance of a PSRule pipeline. +/// +public interface IPipeline : IDisposable +{ + /// + /// Get the pipeline result. + /// + IPipelineResult Result { get; } + + /// + /// Initialize the pipeline and results. Call this method once prior to calling Process. + /// + void Begin(); + + /// + /// Process an object through the pipeline. Each object will be processed by rules that apply based on pre-conditions. + /// + /// The object to process. + void Process(PSObject sourceObject); + + /// + /// Clean up and flush pipeline results. Call this method once after processing any objects through the pipeline. + /// + [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Matches PowerShell pipeline.")] + void End(); +} diff --git a/src/PSRule/Pipeline/IPipelineResult.cs b/src/PSRule/Pipeline/IPipelineResult.cs new file mode 100644 index 000000000..fe0ff10d9 --- /dev/null +++ b/src/PSRule/Pipeline/IPipelineResult.cs @@ -0,0 +1,25 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +namespace PSRule.Pipeline; + +/// +/// A result from the pipeline. +/// +public interface IPipelineResult +{ + /// + /// Determines if any errors were reported. + /// + public bool HadErrors { get; } + + /// + /// Determines if an failures were reported. + /// + public bool HadFailures { get; } + + /// + /// Determines if the pipeline should break from rules that failed. + /// + public bool ShouldBreakFromFailure { get; } +} diff --git a/src/PSRule/Pipeline/IPipelineWriter.cs b/src/PSRule/Pipeline/IPipelineWriter.cs new file mode 100644 index 000000000..6ab6a4cba --- /dev/null +++ b/src/PSRule/Pipeline/IPipelineWriter.cs @@ -0,0 +1,104 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using System.Management.Automation; + +namespace PSRule.Pipeline; + +/// +/// An writer which receives output from PSRule. +/// +public interface IPipelineWriter : IDisposable +{ + /// + /// Determines if any errors were reported. + /// + bool HadErrors { get; } + + /// + /// Determines if any failures were reported. + /// + bool HadFailures { get; } + + /// + /// Write a verbose message. + /// + void WriteVerbose(string message); + + /// + /// Determines if a verbose message should be written to output. + /// + bool ShouldWriteVerbose(); + + /// + /// Write a warning message. + /// + void WriteWarning(string message); + + /// + /// Determines if a warning message should be written to output. + /// + bool ShouldWriteWarning(); + + /// + /// Write an error message. + /// + void WriteError(ErrorRecord errorRecord); + + /// + /// Determines if an error message should be written to output. + /// + bool ShouldWriteError(); + + /// + /// Write an informational message. + /// + void WriteInformation(InformationRecord informationRecord); + + /// + /// Write a message to the host process. + /// + void WriteHost(HostInformationMessage info); + + /// + /// Determines if an informational message should be written to output. + /// + bool ShouldWriteInformation(); + + /// + /// Write a debug message. + /// + void WriteDebug(string text, params object[] args); + + /// + /// Determines if a debug message should be written to output. + /// + bool ShouldWriteDebug(); + + /// + /// Write an object to output. + /// + /// The object to write to the pipeline. + /// Determines when the object is enumerable if it should be enumerated as more then one object. + void WriteObject(object sendToPipeline, bool enumerateCollection); + + /// + /// Enter a logging scope. + /// + void EnterScope(string scopeName); + + /// + /// Exit a logging scope. + /// + void ExitScope(); + + /// + /// Start and initialize the writer. + /// + void Begin(); + + /// + /// Stop and finalized the writer. + /// + void End(IPipelineResult result); +} diff --git a/src/PSRule/Pipeline/InvokeRulePipeline.cs b/src/PSRule/Pipeline/InvokeRulePipeline.cs index a7924c3ec..85f7252f9 100644 --- a/src/PSRule/Pipeline/InvokeRulePipeline.cs +++ b/src/PSRule/Pipeline/InvokeRulePipeline.cs @@ -35,13 +35,13 @@ internal sealed class InvokeRulePipeline : RulePipeline, IPipeline _Outcome = outcome; _IsSummary = context.Option.Output.As.Value == ResultFormat.Summary; - _Summary = _IsSummary ? new Dictionary() : null; + _Summary = _IsSummary ? [] : null; var allRuleBlocks = _RuleGraph.GetAll(); var resourceIndex = new ResourceIndex(allRuleBlocks); _SuppressionFilter = new SuppressionFilter(Context, context.Option.Suppression, resourceIndex); _SuppressionGroupFilter = new SuppressionFilter(Pipeline.SuppressionGroup, resourceIndex); - _Completed = new List(); + _Completed = []; } public int RuleCount { get; private set; } @@ -84,7 +84,7 @@ internal sealed class InvokeRulePipeline : RulePipeline, IPipeline if (_IsSummary) Writer.WriteObject(_Summary.Values.Where(r => _Outcome == RuleOutcome.All || (r.Outcome & _Outcome) > 0).ToArray(), true); - Writer.End(); + Writer.End(Result); } private InvokeResult ProcessTargetObject(TargetObject targetObject) @@ -148,7 +148,7 @@ internal sealed class InvokeRulePipeline : RulePipeline, IPipeline } else if (ruleRecord.Outcome == RuleOutcome.Fail) { - Result.HadFailures = true; + Result.Fail(ruleRecord.Level); ruleBlockTarget.Fail(); Context.Fail(); } diff --git a/src/PSRule/Pipeline/Output/JobSummaryWriter.cs b/src/PSRule/Pipeline/Output/JobSummaryWriter.cs index 38e668216..f84d48542 100644 --- a/src/PSRule/Pipeline/Output/JobSummaryWriter.cs +++ b/src/PSRule/Pipeline/Output/JobSummaryWriter.cs @@ -47,10 +47,10 @@ internal sealed class JobSummaryWriter : ResultOutputWriter base.Begin(); } - public sealed override void End() + public sealed override void End(IPipelineResult result) { Flush(); - base.End(); + base.End(result); } #region Helper methods diff --git a/src/PSRule/Pipeline/PipelineBuilder.cs b/src/PSRule/Pipeline/PipelineBuilder.cs index a8044e469..f637b788c 100644 --- a/src/PSRule/Pipeline/PipelineBuilder.cs +++ b/src/PSRule/Pipeline/PipelineBuilder.cs @@ -3,7 +3,6 @@ using System.Collections; using System.Globalization; -using System.Management.Automation; using PSRule.Configuration; using PSRule.Data; using PSRule.Definitions; @@ -189,88 +188,6 @@ public interface IPipelineBuilder IPipeline Build(IPipelineWriter writer = null); } -/// -/// An instance of a PSRule pipeline. -/// -public interface IPipeline : IDisposable -{ - /// - /// Get the pipeline result. - /// - IPipelineResult Result { get; } - - /// - /// Initialize the pipeline and results. Call this method once prior to calling Process. - /// - void Begin(); - - /// - /// Process an object through the pipeline. Each object will be processed by rules that apply based on pre-conditions. - /// - /// The object to process. - void Process(PSObject sourceObject); - - /// - /// Clean up and flush pipeline results. Call this method once after processing any objects through the pipeline. - /// - [System.Diagnostics.CodeAnalysis.SuppressMessage("Naming", "CA1716:Identifiers should not match keywords", Justification = "Matches PowerShell pipeline.")] - void End(); -} - -/// -/// A result from the pipeline. -/// -public interface IPipelineResult -{ - /// - /// Determines if any errors were reported. - /// - public bool HadErrors { get; } - - /// - /// Determines if an failures were reported. - /// - public bool HadFailures { get; } -} - -internal sealed class DefaultPipelineResult : IPipelineResult -{ - private readonly IPipelineWriter _Writer; - private bool _HadErrors; - private bool _HadFailures; - - public DefaultPipelineResult(IPipelineWriter writer) - { - _Writer = writer; - } - - /// - public bool HadErrors - { - get - { - return _HadErrors || (_Writer != null && _Writer.HadErrors); - } - set - { - _HadErrors = value; - } - } - - /// - public bool HadFailures - { - get - { - return _HadFailures || (_Writer != null && _Writer.HadFailures); - } - set - { - _HadFailures = value; - } - } -} - internal abstract class PipelineBuilderBase : IPipelineBuilder { private const string ENGINE_MODULE_NAME = "PSRule"; @@ -413,7 +330,7 @@ internal abstract class PipelineBuilderBase : IPipelineBuilder { var writer = PrepareWriter(); writer.ErrorRequiredVersionMismatch(moduleName, moduleVersion, requiredVersion); - writer.End(); + writer.End(new DefaultPipelineResult(null, BreakLevel.None) { HadErrors = true }); return true; } return false; @@ -505,7 +422,7 @@ internal abstract class PipelineBuilderBase : IPipelineBuilder shouldProcess: HostContext.ShouldProcess, writeHost: writeHost ) - : (PipelineWriter)_Output; + : _Output; } protected static string[] GetCulture(string[] culture) @@ -580,8 +497,7 @@ internal abstract class PipelineBuilderBase : IPipelineBuilder var ignoreGitPath = Option.Input.IgnoreGitPath ?? InputOption.Default.IgnoreGitPath.Value; var ignoreRepositoryCommon = Option.Input.IgnoreRepositoryCommon ?? InputOption.Default.IgnoreRepositoryCommon.Value; var builder = PathFilterBuilder.Create(basePath, Option.Input.PathIgnore, ignoreGitPath, ignoreRepositoryCommon); - //if (Option.Input.Format == InputFormat.File) - builder.UseGitIgnore(); + builder.UseGitIgnore(); _InputFilter = builder.Build(); } diff --git a/src/PSRule/Pipeline/PipelineLogger.cs b/src/PSRule/Pipeline/PipelineLogger.cs index a4a7e20fc..ecaa8acd3 100644 --- a/src/PSRule/Pipeline/PipelineLogger.cs +++ b/src/PSRule/Pipeline/PipelineLogger.cs @@ -123,7 +123,7 @@ internal abstract class PipelineLoggerBase : IPipelineWriter } - public virtual void End() + public virtual void End(IPipelineResult result) { } diff --git a/src/PSRule/Pipeline/PipelineWriter.cs b/src/PSRule/Pipeline/PipelineWriter.cs index 708ee0be5..37e078f5f 100644 --- a/src/PSRule/Pipeline/PipelineWriter.cs +++ b/src/PSRule/Pipeline/PipelineWriter.cs @@ -8,108 +8,10 @@ using PSRule.Rules; namespace PSRule.Pipeline; -/// -/// An writer which recieves output from PSRule. -/// -public interface IPipelineWriter : IDisposable -{ - /// - /// Determines if any errors were reported. - /// - bool HadErrors { get; } - - /// - /// Determines if an failures were reported. - /// - bool HadFailures { get; } - - /// - /// Write a verbose message. - /// - void WriteVerbose(string message); - - /// - /// Determines if a verbose message should be written to output. - /// - bool ShouldWriteVerbose(); - - /// - /// Write a warning message. - /// - void WriteWarning(string message); - - /// - /// Determines if a warning message should be written to output. - /// - bool ShouldWriteWarning(); - - /// - /// Write an error message. - /// - void WriteError(ErrorRecord errorRecord); - - /// - /// Determines if an error message should be written to output. - /// - bool ShouldWriteError(); - - /// - /// Write an informational message. - /// - void WriteInformation(InformationRecord informationRecord); - - /// - /// Write a message to the host process. - /// - void WriteHost(HostInformationMessage info); - - /// - /// Determines if an informational message should be written to output. - /// - bool ShouldWriteInformation(); - - /// - /// Write a debug message. - /// - void WriteDebug(string text, params object[] args); - - /// - /// Determines if a debug message should be written to output. - /// - bool ShouldWriteDebug(); - - /// - /// Write an object to output. - /// - /// The object to write to the pipeline. - /// Determines when the object is enumerable if it should be enumerated as more then one object. - void WriteObject(object sendToPipeline, bool enumerateCollection); - - /// - /// Enter a logging scope. - /// - void EnterScope(string scopeName); - - /// - /// Exit a logging scope. - /// - void ExitScope(); - - /// - /// Start and initialize the writer. - /// - void Begin(); - - /// - /// Stop and finalized the writer. - /// - void End(); -} - /// /// A base class for writers. /// -internal abstract class PipelineWriter : IPipelineWriter +internal abstract class PipelineWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) : IPipelineWriter { protected const string ErrorPreference = "ErrorActionPreference"; protected const string WarningPreference = "WarningPreference"; @@ -117,22 +19,15 @@ internal abstract class PipelineWriter : IPipelineWriter protected const string InformationPreference = "InformationPreference"; protected const string DebugPreference = "DebugPreference"; - private readonly IPipelineWriter _Writer; - private readonly ShouldProcess _ShouldProcess; + private readonly IPipelineWriter _Writer = inner; + private readonly ShouldProcess _ShouldProcess = shouldProcess; - protected readonly PSRuleOption Option; + protected readonly PSRuleOption Option = option; private bool _IsDisposed; private bool _HadErrors; private bool _HadFailures; - protected PipelineWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) - { - _Writer = inner; - _ShouldProcess = shouldProcess; - Option = option; - } - bool IPipelineWriter.HadErrors => HadErrors; bool IPipelineWriter.HadFailures => HadFailures; @@ -182,12 +77,12 @@ internal abstract class PipelineWriter : IPipelineWriter } /// - public virtual void End() + public virtual void End(IPipelineResult result) { if (_Writer == null) return; - _Writer.End(); + _Writer.End(result); } /// @@ -363,98 +258,8 @@ internal abstract class PipelineWriter : IPipelineWriter /// /// Get the value of a preference variable. /// - protected static ActionPreference GetPreferenceVariable(System.Management.Automation.SessionState sessionState, string variableName) + protected static ActionPreference GetPreferenceVariable(SessionState sessionState, string variableName) { return (ActionPreference)sessionState.PSVariable.GetValue(variableName); } } - -internal abstract class ResultOutputWriter : PipelineWriter -{ - private readonly List _Result; - - protected ResultOutputWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) - : base(inner, option, shouldProcess) - { - _Result = new List(); - } - - public override void WriteObject(object sendToPipeline, bool enumerateCollection) - { - if (sendToPipeline is InvokeResult && Option.Output.As == ResultFormat.Summary) - { - base.WriteObject(sendToPipeline, enumerateCollection); - return; - } - - if (sendToPipeline is InvokeResult result) - { - Add(typeof(T) == typeof(RuleRecord) ? result.AsRecord() : result); - } - else - { - Add(sendToPipeline); - } - base.WriteObject(sendToPipeline, enumerateCollection); - } - - protected void Add(object o) - { - if (o is T[] collection) - _Result.AddRange(collection); - else if (o is T item) - _Result.Add(item); - } - - /// - /// Clear any buffers from the writer. - /// - protected virtual void Flush() { } - - protected T[] GetResults() - { - return _Result.ToArray(); - } -} - -internal abstract class SerializationOutputWriter : ResultOutputWriter -{ - protected SerializationOutputWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) - : base(inner, option, shouldProcess) { } - - public sealed override void End() - { - var results = GetResults(); - base.WriteObject(Serialize(results), false); - ProcessError(results); - Flush(); - base.End(); - } - - public override void WriteObject(object sendToPipeline, bool enumerateCollection) - { - if (sendToPipeline is InvokeResult && Option.Output.As == ResultFormat.Summary) - { - base.WriteObject(sendToPipeline, enumerateCollection); - return; - } - - if (sendToPipeline is InvokeResult result) - { - Add(result.AsRecord()); - return; - } - Add(sendToPipeline); - } - - protected abstract string Serialize(T[] o); - - private void ProcessError(T[] results) - { - for (var i = 0; i < results.Length; i++) - { - if (results[i] is RuleRecord record) - WriteErrorInfo(record); - } - } -} diff --git a/src/PSRule/Pipeline/ResultOutputWriter.cs b/src/PSRule/Pipeline/ResultOutputWriter.cs new file mode 100644 index 000000000..7714b1488 --- /dev/null +++ b/src/PSRule/Pipeline/ResultOutputWriter.cs @@ -0,0 +1,55 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Configuration; +using PSRule.Rules; + +namespace PSRule.Pipeline; + +internal abstract class ResultOutputWriter : PipelineWriter +{ + private readonly List _Result; + + protected ResultOutputWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) + : base(inner, option, shouldProcess) + { + _Result = new List(); + } + + public override void WriteObject(object sendToPipeline, bool enumerateCollection) + { + if (sendToPipeline is InvokeResult && Option.Output.As == ResultFormat.Summary) + { + base.WriteObject(sendToPipeline, enumerateCollection); + return; + } + + if (sendToPipeline is InvokeResult result) + { + Add(typeof(T) == typeof(RuleRecord) ? result.AsRecord() : result); + } + else + { + Add(sendToPipeline); + } + base.WriteObject(sendToPipeline, enumerateCollection); + } + + protected void Add(object o) + { + if (o is T[] collection) + _Result.AddRange(collection); + else if (o is T item) + _Result.Add(item); + } + + /// + /// Clear any buffers from the writer. + /// + protected virtual void Flush() { } + + protected T[] GetResults() + { + return _Result.ToArray(); + } +} diff --git a/src/PSRule/Pipeline/RulePipeline.cs b/src/PSRule/Pipeline/RulePipeline.cs index 653c61ea5..afc93a931 100644 --- a/src/PSRule/Pipeline/RulePipeline.cs +++ b/src/PSRule/Pipeline/RulePipeline.cs @@ -2,6 +2,7 @@ // Licensed under the MIT License. using System.Management.Automation; +using PSRule.Options; using PSRule.Runtime; namespace PSRule.Pipeline; @@ -19,7 +20,7 @@ internal abstract class RulePipeline : IPipeline protected RulePipeline(PipelineContext context, Source[] source, PipelineInputStream reader, IPipelineWriter writer) { - Result = new DefaultPipelineResult(writer); + Result = new DefaultPipelineResult(writer, context.Option.Execution.Break.GetValueOrDefault(ExecutionOption.Default.Break.Value)); Pipeline = context; Context = new RunspaceContext(Pipeline, writer); Source = source; @@ -55,7 +56,7 @@ internal abstract class RulePipeline : IPipeline /// public virtual void End() { - Writer.End(); + Writer.End(Result); } #endregion IPipeline diff --git a/src/PSRule/Pipeline/SerializationOutputWriter.cs b/src/PSRule/Pipeline/SerializationOutputWriter.cs new file mode 100644 index 000000000..d270febf0 --- /dev/null +++ b/src/PSRule/Pipeline/SerializationOutputWriter.cs @@ -0,0 +1,49 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +using PSRule.Configuration; +using PSRule.Rules; + +namespace PSRule.Pipeline; + +internal abstract class SerializationOutputWriter : ResultOutputWriter +{ + protected SerializationOutputWriter(IPipelineWriter inner, PSRuleOption option, ShouldProcess shouldProcess) + : base(inner, option, shouldProcess) { } + + public sealed override void End(IPipelineResult result) + { + var results = GetResults(); + base.WriteObject(Serialize(results), false); + ProcessError(results); + Flush(); + base.End(result); + } + + public override void WriteObject(object sendToPipeline, bool enumerateCollection) + { + if (sendToPipeline is InvokeResult && Option.Output.As == ResultFormat.Summary) + { + base.WriteObject(sendToPipeline, enumerateCollection); + return; + } + + if (sendToPipeline is InvokeResult result) + { + Add(result.AsRecord()); + return; + } + Add(sendToPipeline); + } + + protected abstract string Serialize(T[] o); + + private void ProcessError(T[] results) + { + for (var i = 0; i < results.Length; i++) + { + if (results[i] is RuleRecord record) + WriteErrorInfo(record); + } + } +} diff --git a/tests/PSRule.Tests/AssertFormatterTests.cs b/tests/PSRule.Tests/AssertFormatterTests.cs index 1f9e4737c..995c421ab 100644 --- a/tests/PSRule.Tests/AssertFormatterTests.cs +++ b/tests/PSRule.Tests/AssertFormatterTests.cs @@ -21,12 +21,13 @@ public sealed class AssertFormatterTests { var option = GetOption(); option.Output.Banner = BannerFormat.None; + option.Output.Footer = FooterFormat.None; var writer = GetWriter(); // Check output is empty var formatter = new PlainFormatter(null, writer, option); formatter.Begin(); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal("", writer.Output); // Check pass output @@ -34,7 +35,7 @@ public sealed class AssertFormatterTests formatter = new PlainFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetPassResult()); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [1/1] [PASS] Test @@ -45,7 +46,7 @@ public sealed class AssertFormatterTests formatter = new PlainFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult()); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -59,7 +60,7 @@ public sealed class AssertFormatterTests formatter = new PlainFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult(SeverityLevel.Warning)); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -73,7 +74,7 @@ public sealed class AssertFormatterTests formatter = new PlainFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult(SeverityLevel.Information)); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -88,12 +89,13 @@ public sealed class AssertFormatterTests { var option = GetOption(); option.Output.Banner = BannerFormat.None; + option.Output.Footer = FooterFormat.None; var writer = GetWriter(); // Check output is empty var formatter = new ClientFormatter(null, writer, option); formatter.Begin(); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal("", writer.Output); // Check pass output @@ -101,7 +103,7 @@ public sealed class AssertFormatterTests formatter = new ClientFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetPassResult()); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [1/1] [PASS] Test @@ -112,7 +114,7 @@ public sealed class AssertFormatterTests formatter = new ClientFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult()); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -126,7 +128,7 @@ public sealed class AssertFormatterTests formatter = new ClientFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult(SeverityLevel.Warning)); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -140,7 +142,7 @@ public sealed class AssertFormatterTests formatter = new ClientFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult(SeverityLevel.Information)); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -155,12 +157,13 @@ public sealed class AssertFormatterTests { var option = GetOption(); option.Output.Banner = BannerFormat.None; + option.Output.Footer = FooterFormat.None; var writer = GetWriter(); // Check output is empty var formatter = new AzurePipelinesFormatter(null, writer, option); formatter.Begin(); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal("", writer.Output); // Check pass output @@ -168,7 +171,7 @@ public sealed class AssertFormatterTests formatter = new AzurePipelinesFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetPassResult()); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [1/1] [PASS] Test @@ -179,7 +182,7 @@ public sealed class AssertFormatterTests formatter = new AzurePipelinesFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult()); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -197,7 +200,7 @@ public sealed class AssertFormatterTests formatter = new AzurePipelinesFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult(SeverityLevel.Warning)); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -215,7 +218,7 @@ public sealed class AssertFormatterTests formatter = new AzurePipelinesFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult(SeverityLevel.Information)); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -230,12 +233,13 @@ public sealed class AssertFormatterTests { var option = GetOption(); option.Output.Banner = BannerFormat.None; + option.Output.Footer = FooterFormat.None; var writer = GetWriter(); // Check output is empty var formatter = new GitHubActionsFormatter(null, writer, option); formatter.Begin(); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal("", writer.Output); // Check pass output @@ -243,7 +247,7 @@ public sealed class AssertFormatterTests formatter = new GitHubActionsFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetPassResult()); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [1/1] [PASS] Test @@ -254,7 +258,7 @@ public sealed class AssertFormatterTests formatter = new GitHubActionsFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult()); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -272,7 +276,7 @@ public sealed class AssertFormatterTests formatter = new GitHubActionsFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult(SeverityLevel.Warning)); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -290,7 +294,7 @@ public sealed class AssertFormatterTests formatter = new GitHubActionsFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult(SeverityLevel.Information)); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@" -> TestObject : TestType [0/2] [FAIL] Test1 @@ -309,12 +313,13 @@ public sealed class AssertFormatterTests { var option = GetOption(); option.Output.Banner = BannerFormat.None; + option.Output.Footer = FooterFormat.None; var writer = GetWriter(); // Check output is empty var formatter = new VisualStudioCodeFormatter(null, writer, option); formatter.Begin(); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal("", writer.Output); // Check pass output @@ -322,7 +327,7 @@ public sealed class AssertFormatterTests formatter = new VisualStudioCodeFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetPassResult()); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@"> TestObject : TestType [1/1] PASS Test @@ -333,7 +338,7 @@ public sealed class AssertFormatterTests formatter = new VisualStudioCodeFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult()); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@"> TestObject : TestType [0/2] FAIL Test1 @@ -347,7 +352,7 @@ public sealed class AssertFormatterTests formatter = new VisualStudioCodeFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult(SeverityLevel.Warning)); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@"> TestObject : TestType [0/2] FAIL Test1 @@ -361,7 +366,7 @@ public sealed class AssertFormatterTests formatter = new VisualStudioCodeFormatter(null, writer, option); formatter.Begin(); formatter.Result(GetFailResult(SeverityLevel.Information)); - formatter.End(); + formatter.End(0, 0, 0); Assert.Equal(@"> TestObject : TestType [0/2] FAIL Test1 diff --git a/tests/PSRule.Tests/OutputWriterTests.cs b/tests/PSRule.Tests/OutputWriterTests.cs index 828484547..08edfca1a 100644 --- a/tests/PSRule.Tests/OutputWriterTests.cs +++ b/tests/PSRule.Tests/OutputWriterTests.cs @@ -34,7 +34,7 @@ public sealed class OutputWriterTests var writer = new SarifOutputWriter(null, output, option, null); writer.Begin(); writer.WriteObject(result, false); - writer.End(); + writer.End(new DefaultPipelineResult(null, Options.BreakLevel.None)); var actual = JsonConvert.DeserializeObject(output.Output.OfType().FirstOrDefault()); Assert.NotNull(actual); @@ -78,7 +78,7 @@ public sealed class OutputWriterTests var writer = new SarifOutputWriter(null, output, option, null); writer.Begin(); writer.WriteObject(result, false); - writer.End(); + writer.End(new DefaultPipelineResult(null, Options.BreakLevel.None)); var actual = JsonConvert.DeserializeObject(output.Output.OfType().FirstOrDefault()); Assert.NotNull(actual); @@ -112,7 +112,7 @@ public sealed class OutputWriterTests var writer = new YamlOutputWriter(output, option, null); writer.Begin(); writer.WriteObject(result, false); - writer.End(); + writer.End(new DefaultPipelineResult(null, Options.BreakLevel.None)); Assert.Equal(@"- detail: reason: [] @@ -210,7 +210,7 @@ public sealed class OutputWriterTests var writer = new JsonOutputWriter(output, option, null); writer.Begin(); writer.WriteObject(result, false); - writer.End(); + writer.End(new DefaultPipelineResult(null, Options.BreakLevel.None)); Assert.Equal(@"[ { @@ -331,7 +331,7 @@ public sealed class OutputWriterTests var writer = new NUnit3OutputWriter(output, option, null); writer.Begin(); writer.WriteObject(result, false); - writer.End(); + writer.End(new DefaultPipelineResult(null, Options.BreakLevel.None)); var s = output.Output.OfType().FirstOrDefault(); var doc = new XmlDocument(); @@ -357,7 +357,7 @@ public sealed class OutputWriterTests var writer = new CsvOutputWriter(output, option, null); writer.Begin(); writer.WriteObject(result, false); - writer.End(); + writer.End(new DefaultPipelineResult(null, Options.BreakLevel.None)); var actual = output.Output.OfType().FirstOrDefault(); @@ -384,7 +384,7 @@ public sealed class OutputWriterTests writer.Begin(); writer.WriteObject(result, false); context.RunTime.Stop(); - writer.End(); + writer.End(new DefaultPipelineResult(null, Options.BreakLevel.None)); stream.Seek(0, SeekOrigin.Begin); using var reader = new StreamReader(stream); diff --git a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 index d9b977382..a53d030cb 100644 --- a/tests/PSRule.Tests/PSRule.Options.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Options.Tests.ps1 @@ -635,6 +635,145 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } } + Context 'Read Execution.AliasReference' { + It 'from default' { + $option = New-PSRuleOption -Default; + $option.Execution.AliasReference | Should -Be 'Warn'; + } + + It 'from Hashtable' { + $option = New-PSRuleOption -Option @{ 'Execution.AliasReference' = 'error' }; + $option.Execution.AliasReference | Should -Be 'Error'; + + $option = New-PSRuleOption -Option @{ 'Execution.AliasReference' = 'Error' }; + $option.Execution.AliasReference | Should -Be 'Error'; + } + + It 'from YAML' { + $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); + $option.Execution.AliasReference | Should -Be 'Ignore'; + } + + It 'from Environment' { + try { + # With enum + $Env:PSRULE_EXECUTION_ALIASREFERENCE = 'error'; + $option = New-PSRuleOption; + $option.Execution.AliasReference | Should -Be 'Error'; + + # With enum + $Env:PSRULE_EXECUTION_ALIASREFERENCE = 'Error'; + $option = New-PSRuleOption; + $option.Execution.AliasReference | Should -Be 'Error'; + + # With int + $Env:PSRULE_EXECUTION_ALIASREFERENCE = '3'; + $option = New-PSRuleOption; + $option.Execution.AliasReference | Should -Be 'Error'; + } + finally { + Remove-Item 'Env:PSRULE_EXECUTION_ALIASREFERENCE' -Force; + } + } + + It 'from parameter' { + $option = New-PSRuleOption -ExecutionAliasReference 'Error' -Path $emptyOptionsFilePath; + $option.Execution.AliasReference | Should -Be 'Error'; + } + } + + Context 'Read Execution.Break' { + It 'from default' { + $option = New-PSRuleOption -Default; + $option.Execution.Break | Should -Be 'OnError'; + } + + It 'from Hashtable' { + $option = New-PSRuleOption -Option @{ 'Execution.Break' = 'never' }; + $option.Execution.Break | Should -Be 'Never'; + + $option = New-PSRuleOption -Option @{ 'Execution.Break' = 'Never' }; + $option.Execution.Break | Should -Be 'Never'; + } + + It 'from YAML' { + $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests16.yml'); + $option.Execution.Break | Should -Be 'Never'; + } + + It 'from Environment' { + try { + # With enum + $Env:PSRULE_EXECUTION_BREAK = 'never'; + $option = New-PSRuleOption; + $option.Execution.Break | Should -Be 'Never'; + + $Env:PSRULE_EXECUTION_BREAK = 'never'; + $option = New-PSRuleOption; + $option.Execution.Break | Should -Be 'Never'; + + # With int + $Env:PSRULE_EXECUTION_BREAK = '1'; + $option = New-PSRuleOption; + $option.Execution.Break | Should -Be 'Never'; + } + finally { + Remove-Item 'Env:PSRULE_EXECUTION_BREAK' -Force; + } + } + + It 'from parameter' { + $option = New-PSRuleOption -ExecutionBreak 'Never' -Path $emptyOptionsFilePath; + $option.Execution.Break | Should -Be 'Never'; + } + } + + Context 'Read Execution.DuplicateResourceId' { + It 'from default' { + $option = New-PSRuleOption -Default; + $option.Execution.DuplicateResourceId | Should -Be 'Error' + } + + It 'from Hashtable' { + $option = New-PSRuleOption -Option @{ 'Execution.DuplicateResourceId' = 'warn' }; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + + $option = New-PSRuleOption -Option @{ 'Execution.DuplicateResourceId' = 'Warn' }; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + } + + It 'from YAML' { + $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + } + + It 'from Environment' { + try { + # With enum + $Env:PSRULE_EXECUTION_DUPLICATERESOURCEID = 'warn'; + $option = New-PSRuleOption; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + + $Env:PSRULE_EXECUTION_DUPLICATERESOURCEID = 'Warn'; + $option = New-PSRuleOption; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + + # With int + $Env:PSRULE_EXECUTION_DUPLICATERESOURCEID = '2'; + $option = New-PSRuleOption; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + } + finally { + Remove-Item 'Env:PSRULE_EXECUTION_DUPLICATERESOURCEID' -Force; + } + } + + It 'from parameter' { + $option = New-PSRuleOption -DuplicateResourceId 'Warn' -Path $emptyOptionsFilePath; + $option.Execution.DuplicateResourceId | Should -Be 'Warn'; + } + } + Context 'Read Execution.HashAlgorithm' { It 'from default' { $option = New-PSRuleOption -Default; @@ -827,53 +966,6 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } } - Context 'Read Execution.AliasReference' { - It 'from default' { - $option = New-PSRuleOption -Default; - $option.Execution.AliasReference | Should -Be 'Warn'; - } - - It 'from Hashtable' { - $option = New-PSRuleOption -Option @{ 'Execution.AliasReference' = 'error' }; - $option.Execution.AliasReference | Should -Be 'Error'; - - $option = New-PSRuleOption -Option @{ 'Execution.AliasReference' = 'Error' }; - $option.Execution.AliasReference | Should -Be 'Error'; - } - - It 'from YAML' { - $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); - $option.Execution.AliasReference | Should -Be 'Ignore'; - } - - It 'from Environment' { - try { - # With enum - $Env:PSRULE_EXECUTION_ALIASREFERENCE = 'error'; - $option = New-PSRuleOption; - $option.Execution.AliasReference | Should -Be 'Error'; - - # With enum - $Env:PSRULE_EXECUTION_ALIASREFERENCE = 'Error'; - $option = New-PSRuleOption; - $option.Execution.AliasReference | Should -Be 'Error'; - - # With int - $Env:PSRULE_EXECUTION_ALIASREFERENCE = '3'; - $option = New-PSRuleOption; - $option.Execution.AliasReference | Should -Be 'Error'; - } - finally { - Remove-Item 'Env:PSRULE_EXECUTION_ALIASREFERENCE' -Force; - } - } - - It 'from parameter' { - $option = New-PSRuleOption -ExecutionAliasReference 'Error' -Path $emptyOptionsFilePath; - $option.Execution.AliasReference | Should -Be 'Error'; - } - } - Context 'Read Execution.RuleInconclusive' { It 'from default' { $option = New-PSRuleOption -Default; @@ -1015,53 +1107,6 @@ Describe 'New-PSRuleOption' -Tag 'Option','New-PSRuleOption' { } } - Context 'Read Execution.DuplicateResourceId' { - It 'from default' { - $option = New-PSRuleOption -Default; - $option.Execution.DuplicateResourceId | Should -Be 'Error' - } - - It 'from Hashtable' { - $option = New-PSRuleOption -Option @{ 'Execution.DuplicateResourceId' = 'warn' }; - $option.Execution.DuplicateResourceId | Should -Be 'Warn'; - - $option = New-PSRuleOption -Option @{ 'Execution.DuplicateResourceId' = 'Warn' }; - $option.Execution.DuplicateResourceId | Should -Be 'Warn'; - } - - It 'from YAML' { - $option = New-PSRuleOption -Option (Join-Path -Path $here -ChildPath 'PSRule.Tests.yml'); - $option.Execution.DuplicateResourceId | Should -Be 'Warn'; - } - - It 'from Environment' { - try { - # With enum - $Env:PSRULE_EXECUTION_DUPLICATERESOURCEID = 'warn'; - $option = New-PSRuleOption; - $option.Execution.DuplicateResourceId | Should -Be 'Warn'; - - # With enum - $Env:PSRULE_EXECUTION_DUPLICATERESOURCEID = 'Warn'; - $option = New-PSRuleOption; - $option.Execution.DuplicateResourceId | Should -Be 'Warn'; - - # With int - $Env:PSRULE_EXECUTION_DUPLICATERESOURCEID = '2'; - $option = New-PSRuleOption; - $option.Execution.DuplicateResourceId | Should -Be 'Warn'; - } - finally { - Remove-Item 'Env:PSRULE_EXECUTION_DUPLICATERESOURCEID' -Force; - } - } - - It 'from parameter' { - $option = New-PSRuleOption -DuplicateResourceId 'Warn' -Path $emptyOptionsFilePath; - $option.Execution.DuplicateResourceId | Should -Be 'Warn'; - } - } - Context 'Read Execution.InitialSessionState' { It 'from default' { $option = New-PSRuleOption -Default; @@ -2341,6 +2386,13 @@ Describe 'Set-PSRuleOption' -Tag 'Option','Set-PSRuleOption' { } } + Context 'Read Execution.Break' { + It 'from parameter' { + $option = Set-PSRuleOption -ExecutionBreak 'Never' @optionParams; + $option.Execution.Break | Should -Be 'Never'; + } + } + Context 'Read Execution.SuppressionGroupExpired' { It 'from parameter' { $option = Set-PSRuleOption -SuppressionGroupExpired 'Error' @optionParams; diff --git a/tests/PSRule.Tests/PSRule.Tests16.yml b/tests/PSRule.Tests/PSRule.Tests16.yml new file mode 100644 index 000000000..aeeed6b5d --- /dev/null +++ b/tests/PSRule.Tests/PSRule.Tests16.yml @@ -0,0 +1,5 @@ +# These are options for unit tests + +# Configure execution options. +execution: + break: Never diff --git a/tests/PSRule.Tests/PipelineTests.cs b/tests/PSRule.Tests/PipelineTests.cs index fabbd62d2..058f77687 100644 --- a/tests/PSRule.Tests/PipelineTests.cs +++ b/tests/PSRule.Tests/PipelineTests.cs @@ -106,7 +106,6 @@ public sealed class PipelineTests var option = GetOption(); option.Rule.Include = ["WithPathPrefix"]; - //option.Input.Format = InputFormat.File; var builder = PipelineBuilder.Invoke(GetSource(), option, null); var writer = new TestWriter(option); var pipeline = builder.Build(writer);