diff --git a/.gitignore b/.gitignore index 2b08146b3c..2a5a0154c2 100644 --- a/.gitignore +++ b/.gitignore @@ -9,3 +9,4 @@ /**/*.user /src/**/*-help.xml /src/**/*.help.txt +/BenchmarkDotNet.Artifacts/ diff --git a/.vscode/tasks.json b/.vscode/tasks.json index 0359609084..155776ba1a 100644 --- a/.vscode/tasks.json +++ b/.vscode/tasks.json @@ -56,6 +56,16 @@ "type": "shell", "command": "Invoke-Build Analyze", "problemMatcher": [] + }, + { + "label": "benchmark", + "type": "shell", + "command": "Invoke-Build Benchmark", + "problemMatcher": [], + "presentation": { + "clear": true, + "panel": "dedicated" + } } ] } diff --git a/CHANGELOG.md b/CHANGELOG.md index 96acccd4b0..94e21c06b2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,6 +3,7 @@ - Added rule tags to results to enable grouping and sorting [#14](https://github.com/BernieWhite/PSRule/issues/14) - Added support to check for rule tag existence. Use `*` for tag value on `-Tag` parameter with `Invoke-PSRule` and `Get-PSRule` +- Added option to report rule summary using `-As` parameter of `Invoke-PSRule` [#12](https://github.com/BernieWhite/PSRule/issues/12) ## v0.1.0-B181212 diff --git a/PSRule.build.ps1 b/PSRule.build.ps1 index be802d12aa..366dcfbd01 100644 --- a/PSRule.build.ps1 +++ b/PSRule.build.ps1 @@ -227,6 +227,10 @@ task TestModule Pester, PSScriptAnalyzer, { } } +task Benchmark { + dotnet run -p src/PSRule.Benchmark -f net472 -c Release -- benchmark --output $PWD; +} + # Synopsis: Run script analyzer task Analyze Build, PSScriptAnalyzer, { diff --git a/PSRule.sln b/PSRule.sln index 2693998217..f3db2c3a61 100644 --- a/PSRule.sln +++ b/PSRule.sln @@ -3,9 +3,19 @@ Microsoft Visual Studio Solution File, Format Version 12.00 # Visual Studio 15 VisualStudioVersion = 15.0.28307.106 MinimumVisualStudioVersion = 10.0.40219.1 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PSRule", "src\PSRule\PSRule.csproj", "{ACAFD790-980B-4B64-912F-9BAD91DFF749}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule", "src\PSRule\PSRule.csproj", "{ACAFD790-980B-4B64-912F-9BAD91DFF749}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "PSRule.Benchmark", "src\PSRule.Benchmark\PSRule.Benchmark.csproj", "{0693DC93-1F72-410A-B77F-A81A92391995}" +EndProject +Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{8FFA09C2-4E4A-4F9D-89E4-744E3ACD5280}" + ProjectSection(SolutionItems) = preProject + dotnet.psess = dotnet.psess + EndProjectSection EndProject Global + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU Release|Any CPU = Release|Any CPU @@ -15,6 +25,10 @@ Global {ACAFD790-980B-4B64-912F-9BAD91DFF749}.Debug|Any CPU.Build.0 = Debug|Any CPU {ACAFD790-980B-4B64-912F-9BAD91DFF749}.Release|Any CPU.ActiveCfg = Release|Any CPU {ACAFD790-980B-4B64-912F-9BAD91DFF749}.Release|Any CPU.Build.0 = Release|Any CPU + {0693DC93-1F72-410A-B77F-A81A92391995}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {0693DC93-1F72-410A-B77F-A81A92391995}.Debug|Any CPU.Build.0 = Debug|Any CPU + {0693DC93-1F72-410A-B77F-A81A92391995}.Release|Any CPU.ActiveCfg = Release|Any CPU + {0693DC93-1F72-410A-B77F-A81A92391995}.Release|Any CPU.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -22,4 +36,7 @@ Global GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {533491EB-BAE9-472E-B57F-A675ECD335B5} EndGlobalSection + GlobalSection(Performance) = preSolution + HasPerformanceSessions = true + EndGlobalSection EndGlobal diff --git a/README.md b/README.md index e08793646f..932daf0949 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,7 @@ You can download and install the PSRule module from the PowerShell Gallery. Module | Description | Downloads / instructions ------ | ----------- | ------------------------ -PSRule | A PowerShell rules engine | [latest][psg-psrule] / [instructions][install] +PSRule | Validate objects using PowerShell rules | [latest][psg-psrule] / [instructions][install] ## Getting started diff --git a/azure-pipelines.yml b/azure-pipelines.yml index ae2784e765..d9e11b521d 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -25,10 +25,10 @@ steps: displayName: 'Publish test results' inputs: testRunTitle: 'Pester unit tests' - testResultsFormat: NUnit + testRunner: NUnit testResultsFiles: 'reports/*.xml' mergeTestResults: true - buildConfiguration: $(buildConfiguration) + configuration: $(buildConfiguration) condition: succeededOrFailed() # Generate artifacts diff --git a/docs/commands/PSRule/en-US/Invoke-PSRule.md b/docs/commands/PSRule/en-US/Invoke-PSRule.md index 72b2aafbce..f4b620d687 100644 --- a/docs/commands/PSRule/en-US/Invoke-PSRule.md +++ b/docs/commands/PSRule/en-US/Invoke-PSRule.md @@ -15,7 +15,7 @@ Evaluate pipeline objects against matching rules. ```text Invoke-PSRule [[-Path] ] [-Name ] [-Tag ] -InputObject - [-Status ] [-Option ] [] + [-Status ] [-Option ] [-As ] [] ``` ## DESCRIPTION @@ -87,7 +87,7 @@ Accept wildcard characters: False Filter output to only show rules with a specific status. ```yaml -Type: RuleResultOutcome +Type: RuleOutcome Parameter Sets: (All) Aliases: Accepted values: Success, Failed @@ -133,6 +133,28 @@ Accept pipeline input: False Accept wildcard characters: False ``` +### -As + +The format to return results. Results are returned using detailed by default. + +The following result formats are available: + +- `Detail` - Returns pass/ fail results for each individual object +- `Summary` - Returns summarized results for the rule and an overall outcome +- `Default` - Same as `Detail`. + +```yaml +Type: ResultFormat +Parameter Sets: (All) +Aliases: + +Required: False +Position: Named +Default value: None +Accept pipeline input: False +Accept wildcard characters: False +``` + ### CommonParameters This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable, -InformationAction, -InformationVariable, -OutVariable, -OutBuffer, -PipelineVariable, -Verbose, -WarningAction, and -WarningVariable. For more information, see about_CommonParameters (http://go.microsoft.com/fwlink/?LinkID=113216). @@ -143,7 +165,9 @@ This cmdlet supports the common parameters: -Debug, -ErrorAction, -ErrorVariable ## OUTPUTS -### PSRule.Rules.RuleResult +### PSRule.Rules.RuleRecord + +### PSRule.Rules.RuleSummaryRecord ## NOTES diff --git a/src/PSRule.Benchmark/Benchmark.Rule.ps1 b/src/PSRule.Benchmark/Benchmark.Rule.ps1 new file mode 100644 index 0000000000..7f60346ede --- /dev/null +++ b/src/PSRule.Benchmark/Benchmark.Rule.ps1 @@ -0,0 +1,11 @@ +# +# A set of benchmark rules for testing PSRule performance +# + +Rule 'BenchmarkOdd' -If { ($TargetObject.Name % 2) -gt 0 } { + Hint 'Odd message' +} + +Rule 'BenchmarkEven' -If { ($TargetObject.Name % 2) -ge 0 } { + Hint 'Even message' +} \ No newline at end of file diff --git a/src/PSRule.Benchmark/PSRule.Benchmark.csproj b/src/PSRule.Benchmark/PSRule.Benchmark.csproj new file mode 100644 index 0000000000..cc96a1902e --- /dev/null +++ b/src/PSRule.Benchmark/PSRule.Benchmark.csproj @@ -0,0 +1,45 @@ + + + + Exe + netcoreapp2.1;net472 + + + + + + TRACE;BENCHMARK + + + + + + + + + + + + + + + + + + + + + + + PreserveNewest + + + + diff --git a/src/PSRule.Benchmark/PSRule.cs b/src/PSRule.Benchmark/PSRule.cs new file mode 100644 index 0000000000..9f3d493e27 --- /dev/null +++ b/src/PSRule.Benchmark/PSRule.cs @@ -0,0 +1,61 @@ +using BenchmarkDotNet.Attributes; +using PSRule.Pipeline; +using PSRule.Rules; +using System; +using System.Collections.Generic; +using System.Management.Automation; +using System.Linq; +using BenchmarkDotNet.Engines; +using System.IO; +using System.Reflection; + +namespace PSRule.Benchmark +{ + /// + /// Define a set of benchmarks for performance testing PSRule internals. + /// + [MemoryDiagnoser] + [MarkdownExporterAttribute.GitHub] + public class PSRule + { + private PSObject[] _TargetObject; + private InvokeRulePipeline _Invoke; + + public sealed class TargetObject + { + public TargetObject(string name, string message) + { + Name = name; + Message = message; + } + + public string Name { get; private set; } + + public string Message { get; private set; } + } + + [GlobalSetup] + public void Prepare() + { + var builder = PipelineBuilder.Invoke(); + + builder.Source(new string[] { Path.Combine(Path.GetDirectoryName(Assembly.GetEntryAssembly().Location), "Benchmark.Rule.ps1") }); + _Invoke = builder.Build(); + + var r = new Random(); + var randomBuffer = new byte[40]; + var targetObjects = new List(); + while (targetObjects.Count < 1000) + { + r.NextBytes(randomBuffer); + var o = new TargetObject(name: targetObjects.Count.ToString(), message: Convert.ToBase64String(randomBuffer)); + targetObjects.Add(PSObject.AsPSObject(o)); + } + + _TargetObject = targetObjects.ToArray(); + } + + [Benchmark] + public void Invoke() => _Invoke.Process(_TargetObject).Consume(new Consumer()); + } +} diff --git a/src/PSRule.Benchmark/Program.cs b/src/PSRule.Benchmark/Program.cs new file mode 100644 index 0000000000..e2e7edeb94 --- /dev/null +++ b/src/PSRule.Benchmark/Program.cs @@ -0,0 +1,87 @@ +using BenchmarkDotNet.Analysers; +using BenchmarkDotNet.Columns; +using BenchmarkDotNet.Configs; +using BenchmarkDotNet.Exporters; +using BenchmarkDotNet.Loggers; +using BenchmarkDotNet.Running; +using Microsoft.Extensions.CommandLineUtils; +using PSRule.Pipeline; +using System; + +namespace PSRule.Benchmark +{ + internal sealed class Program + { + private class BenchmarkConfig : ManualConfig + { + public BenchmarkConfig(string artifactsPath) + { + ArtifactsPath = artifactsPath; + } + } + + static void Main(string[] args) + { + + var app = new CommandLineApplication(); + app.Name = "PSRule Benchmark"; + app.Description = ""; + +#if !BENCHMARK + // Do profiling + DebugProfile(); +#endif + +#if BENCHMARK + RunProfile(app); + app.Execute(args); +#endif + } + + private static void RunProfile(CommandLineApplication app) + { + var config = ManualConfig.CreateEmpty() + .With(ConsoleLogger.Default) + .With(DefaultColumnProviders.Instance) + .With(EnvironmentAnalyser.Default) + .With(OutliersAnalyser.Default) + .With(MinIterationTimeAnalyser.Default) + .With(MultimodalDistributionAnalyzer.Default) + .With(RuntimeErrorAnalyser.Default) + .With(ZeroMeasurementAnalyser.Default); + + app.Command("benchmark", cmd => + { + var output = cmd.Option("-o | --output", "The path to store report output.", CommandOptionType.SingleValue); + + cmd.OnExecute(() => + { + if (output.HasValue()) + { + config.WithArtifactsPath(output.Value()); + } + + // Do benchmarks + var summary = BenchmarkRunner.Run(config); + + return 0; + }); + + cmd.HelpOption("-? | -h | --help"); + }); + + app.HelpOption("-? | -h | --help"); + } + + private static void DebugProfile() + { + var profile = new PSRule(); + profile.Prepare(); + + for (var i = 0; i < 1000; i++) + { + profile.Invoke(); + } + } + } +} diff --git a/src/PSRule.Benchmark/Properties/launchSettings.json b/src/PSRule.Benchmark/Properties/launchSettings.json new file mode 100644 index 0000000000..bad4c40873 --- /dev/null +++ b/src/PSRule.Benchmark/Properties/launchSettings.json @@ -0,0 +1,8 @@ +{ + "profiles": { + "PSRule.Benchmark": { + "commandName": "Project", + "commandLineArgs": "benchmark" + } + } +} \ No newline at end of file diff --git a/src/PSRule/Commands/RuleKeyword.cs b/src/PSRule/Commands/RuleKeyword.cs index cbe95c5518..abbec8a76d 100644 --- a/src/PSRule/Commands/RuleKeyword.cs +++ b/src/PSRule/Commands/RuleKeyword.cs @@ -10,9 +10,9 @@ namespace PSRule.Commands /// internal abstract class RuleKeyword : PSCmdlet { - protected RuleResult GetResult() + protected RuleRecord GetResult() { - return GetVariableValue("Rule") as RuleResult; + return GetVariableValue("Rule") as RuleRecord; } protected bool GetField(object obj, string name, out object value) diff --git a/src/PSRule/Configuration/ResultFormat.cs b/src/PSRule/Configuration/ResultFormat.cs new file mode 100644 index 0000000000..fb8b8e0892 --- /dev/null +++ b/src/PSRule/Configuration/ResultFormat.cs @@ -0,0 +1,14 @@ +namespace PSRule.Configuration +{ + /// + /// The format to return to the pipeline after executing rules. + /// + public enum ResultFormat + { + Default = 0, + + Detail = 1, + + Summary = 2 + } +} diff --git a/src/PSRule/Host/HostHelper.cs b/src/PSRule/Host/HostHelper.cs index a986535aec..6e8f915563 100644 --- a/src/PSRule/Host/HostHelper.cs +++ b/src/PSRule/Host/HostHelper.cs @@ -89,6 +89,11 @@ private static IEnumerable GetLanguageBlock(PSRuleOption option, var runspace = RunspaceFactory.CreateRunspace(state); runspace.ThreadOptions = PSThreadOptions.UseCurrentThread; + if (Runspace.DefaultRunspace == null) + { + Runspace.DefaultRunspace = runspace; + } + runspace.Open(); runspace.SessionStateProxy.PSVariable.Set(new RuleVariable("Rule")); runspace.SessionStateProxy.PSVariable.Set(new TargetObjectVariable("TargetObject")); @@ -108,6 +113,11 @@ private static IEnumerable GetLanguageBlock(PSRuleOption option, PipelineContext.WriteVerbose($"[PSRule][D] -- Scanning: {path}"); + if (!File.Exists(path)) + { + throw new FileNotFoundException("Can't find file", path); + } + ps.AddScript(path, true); var invokeResults = ps.Invoke(); @@ -131,16 +141,16 @@ private static IEnumerable GetLanguageBlock(PSRuleOption option, return results; } - public static RuleResult InvokeRuleBlock(PSRuleOption option, RuleBlock block, PSObject inputObject) + public static RuleRecord InvokeRuleBlock(PSRuleOption option, RuleBlock block, PSObject inputObject) { try { //PipelineContext.WriteVerbose($"[PSRule][R][{block.Id}]::BEGIN"); - var result = new RuleResult(block.Id) + var result = new RuleRecord(block.Id) { TargetObject = inputObject, - TargetName = BindName(inputObject), + TargetName = BindName(inputObject), // TODO: Move name binding outside of InvokeRuleBlock so that it is not called for every rule Tag = block.Tag?.ToHashtable() }; @@ -155,18 +165,18 @@ public static RuleResult InvokeRuleBlock(PSRuleOption option, RuleBlock block, P } } - result.Status = RuleResultOutcome.InProgress; + result.Status = RuleOutcome.InProgress; var invokeResults = block.Body.Invoke(); if (invokeResults == null) { - result.Status = RuleResultOutcome.Inconclusive; + result.Status = RuleOutcome.Inconclusive; } else { result.Success = invokeResults.Success; - result.Status = result.Success ? RuleResultOutcome.Passed : RuleResultOutcome.Failed; + result.Status = result.Success ? RuleOutcome.Passed : RuleOutcome.Failed; } PipelineContext.WriteVerbose($"[PSRule][R][{block.Id}] -- [{result.Status}]"); @@ -228,15 +238,18 @@ private static string BindName(PSObject targetObject) { string result = null; - var property = targetObject.Properties.ToDictionary(i => i.Name, StringComparer.OrdinalIgnoreCase); + var comparer = StringComparer.OrdinalIgnoreCase; - if (property.ContainsKey("TargetName")) + foreach (var p in targetObject.Properties) { - result = property["TargetName"].Value?.ToString(); - } - else if (property.ContainsKey("Name")) - { - result = property["Name"].Value?.ToString(); + if (comparer.Equals(p.Name, "TargetName")) + { + result = targetObject.Properties[p.Name].Value?.ToString(); + } + else if (comparer.Equals(p.Name, "Name") && result == null) + { + result = targetObject.Properties[p.Name].Value?.ToString(); + } } return result; diff --git a/src/PSRule/Host/LanguageContext.cs b/src/PSRule/Host/LanguageContext.cs index 8d57c94baa..f41a002382 100644 --- a/src/PSRule/Host/LanguageContext.cs +++ b/src/PSRule/Host/LanguageContext.cs @@ -6,6 +6,6 @@ namespace PSRule.Host internal sealed class LanguageContext { [ThreadStatic] - internal static RuleResult _Rule; + internal static RuleRecord _Rule; } } diff --git a/src/PSRule/PSRule.Format.ps1xml b/src/PSRule/PSRule.Format.ps1xml index 3baf73c110..6a889402b6 100644 --- a/src/PSRule/PSRule.Format.ps1xml +++ b/src/PSRule/PSRule.Format.ps1xml @@ -1,88 +1,131 @@ - - - PSRule.Message - - PSRule.Message - - - - - - - 18 - - - 24 - - - 4 - - - - - - - - - RuleName - - - Severity - - - ScriptName - - - Line - - - Message - - - - - - - - PSRule.Rules.RuleResult - - PSRule.Rules.RuleResult - - - TargetName - - - - - - 35 - - - - 10 - - - - - - - - - - RuleName - - - Status - - - Message - - - - - - - + + + PSRule.Message + + PSRule.Message + + + + + + + 18 + + + 24 + + + 4 + + + + + + + + + RuleName + + + Severity + + + ScriptName + + + Line + + + Message + + + + + + + + PSRule.Rules.RuleRecord + + PSRule.Rules.RuleRecord + + + TargetName + + + + + + 35 + + + + 10 + + + + + + + + + + RuleName + + + Status + + + Message + + + + + + + + PSRule.Rules.RuleSummaryRecord + + PSRule.Rules.RuleSummaryRecord + + + + + + 35 + + + + 5 + + + + 5 + + + + + + + + + + RuleId + + + Pass + + + Fail + + + Outcome + + + + + + + \ No newline at end of file diff --git a/src/PSRule/PSRule.csproj b/src/PSRule/PSRule.csproj index fbdd9f2b0d..ab8a8d3438 100644 --- a/src/PSRule/PSRule.csproj +++ b/src/PSRule/PSRule.csproj @@ -1,7 +1,7 @@ - netstandard2.0 + netstandard2.0;net472 Library portable false @@ -9,8 +9,8 @@ true - - + + diff --git a/src/PSRule/PSRule.psd1 b/src/PSRule/PSRule.psd1 index 11c12d1381..b4a9473451 100644 --- a/src/PSRule/PSRule.psd1 +++ b/src/PSRule/PSRule.psd1 @@ -26,7 +26,7 @@ CompanyName = 'Bernie White' Copyright = '(c) Bernie White. All rights reserved.' # Description of the functionality provided by this module -Description = 'A PowerShell rules engine. +Description = 'Validate objects using PowerShell rules. This project is to be considered a proof-of-concept and not a supported product.' diff --git a/src/PSRule/PSRule.psm1 b/src/PSRule/PSRule.psm1 index c34c77ca48..7d6b4b45fc 100644 --- a/src/PSRule/PSRule.psm1 +++ b/src/PSRule/PSRule.psm1 @@ -39,6 +39,8 @@ Import-LocalizedData -BindingVariable LocalizedData -FileName 'PSRule.Resources. function Invoke-PSRule { [CmdletBinding()] + [OutputType([PSRule.Rules.RuleRecord])] + [OutputType([PSRule.Rules.RuleSummaryRecord])] param ( [Parameter(Position = 0)] [String[]]$Path = $PWD, @@ -53,10 +55,13 @@ function Invoke-PSRule { [PSObject]$InputObject, [Parameter(Mandatory = $False)] - [PSRule.Rules.RuleResultOutcome]$Status = [PSRule.Rules.RuleResultOutcome]::Default, + [PSRule.Rules.RuleOutcome]$Status = [PSRule.Rules.RuleOutcome]::Default, [Parameter(Mandatory = $False)] - [PSRule.Configuration.PSRuleOption]$Option + [PSRule.Configuration.PSRuleOption]$Option, + + [Parameter(Mandatory = $False)] + [PSRule.Configuration.ResultFormat]$As = [PSRule.Configuration.ResultFormat]::Default ) begin { @@ -89,6 +94,7 @@ function Invoke-PSRule { $builder.Source($sourceFiles); $builder.Option($Option); $builder.Limit($Status); + $builder.As($As); $builder.UseCommandRuntime($PSCmdlet.CommandRuntime); $pipeline = $builder.Build(); } @@ -98,6 +104,10 @@ function Invoke-PSRule { } end { + if ($As -eq [PSRule.Configuration.ResultFormat]::Summary) { + $pipeline.GetSummary(); + } + Write-Verbose -Message "[PSRule] END::"; } } @@ -245,156 +255,6 @@ function Rule { # Helper functions # -function InvokeRule { - [CmdletBinding()] - [OutputType([PSRule.Rules.RuleResult])] - param ( - [Parameter(Mandatory = $True)] - [PSObject]$Rule - ) - - begin { - $RuleName = $Rule.RuleName; - $Body = $Rule.Body; - - # Create a progress record - $progressRecord = @{ - Activity = "Running rule $RuleName"; - Status = 'Running rule'; - CurrentOperation = ''; - PercentComplete = 0; - }; - - # Update progress display - Write-Progress @progressRecord; - } - - process { - - if ($Null -eq $Context) { - Write-Error -Message "Rule expression can only be used within a Rule engine. Please call with Invoke-RuleEngine"; - - return; - } - - Write-Verbose -Message "[Rule][$($Context.Index)][$RuleName]`tBEGIN::"; - Write-Verbose -Message "[Rule][$($Context.Index)][$RuleName]`t-- Setting context"; - - # Set Rule context variables - $Rule = @{ - Name = $RuleName; - Warning = @(); - Error = @(); - Output = @(); - Invocation = $PSCmdlet.MyInvocation; - TargetName = ''; - Message = ''; - Input = $This; - OnSuccess = @(); - OnFailure = @(); - }; - - $functionsToDefine = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,ScriptBlock]'([System.StringComparer]::OrdinalIgnoreCase); - $functionsToDefine.Add('AllOf', ${function:AssertAllOf}); - $functionsToDefine.Add('AnyOf', ${function:AssertAnyOf}); - $functionsToDefine.Add('Exists', ${function:AssertExists}); - $functionsToDefine.Add('Within', ${function:AssertWithin}); - $functionsToDefine.Add('Match', ${function:AssertMatch}); - $functionsToDefine.Add('When', ${function:WhenExpression}); - $functionsToDefine.Add('Warn', ${function:Write-RuleWarning}); - $functionsToDefine.Add('Hint', ${function:Set-RuleHint}); - $functionsToDefine.Add('TypeOf', ${function:AssertTypeOf}); - - $functionsToDefine.Add('OnSuccess', ${function:Add-RuleSuccessTrigger}); - $functionsToDefine.Add('OnFailure', ${function:Add-RuleFailureTrigger}); - - $variablesToDefine = New-Object -TypeName 'System.Collections.Generic.List[PSVariable]'; - $variablesToDefine.Add((Get-Variable -Name 'Rule')); - - # Add helper methods - Add-Member -InputObject $Rule -MemberType ScriptMethod -Name 'GetField' -Value { param([String]$Field) process { Get-ObjectField -InputObject $This.Input -Field $Field; } }; - - # Invoke body within the context of this Rule - $innerResult = ($Body.InvokeWithContext($functionsToDefine, $variablesToDefine)); - - # Count the results that are boolean $True - $numResult = $innerResult.Count; - $successCount = 0; - - foreach ($r in $innerResult) { - if ($r -eq $True) { - $successCount++; - } - } - - # Determine overall success of rule - $success = $successCount -eq $numResult; - - # Create a result object - $resultObject = NewRuleResult -Success $success; - - # Set Status to Success if all results are successful, otherwise default to Failed - if ($success) { - $resultObject.Status = 'Success' - } - - if ($numResult -eq 0) { - $resultObject.Status = 'Skipped' - } - - Write-Verbose -Message "[Rule][$($Context.Index)][$RuleName]`t-- Status = $($resultObject.Status)"; - - # Invoke triggers - if ($success -and $Rule.OnSuccess.Length -gt 0) { - # OnSuccess - - Write-Verbose -Message "[Rule][$($Context.Index)][$RuleName]`t-- Triggering OnSuccess"; - - # Run each trigger - foreach ($trigger in $Rule.OnSuccess) { - Write-Verbose -Message "[Rule][$($Context.Index)][$RuleName]`t[OnSuccess]::BEGIN"; - - $innerResult = $trigger.InvokeWithContext($functionsToDefine, $variablesToDefine); - - Write-Verbose -Message "[Rule][$($Context.Index)][$RuleName]`t[OnSuccess]::END"; - } - } elseif (!$success -and $Rule.OnFailure.Length -gt 0) { - # OnFailure - - Write-Verbose -Message "[Rule][$($Context.Index)][$RuleName]`t-- Triggering OnFailure"; - - # Run each trigger - foreach ($trigger in $Rule.OnFailure) { - Write-Verbose -Message "[Rule][$($Context.Index)][$RuleName]`t[OnFailure]::BEGIN"; - - $innerResult = $trigger.InvokeWithContext($functionsToDefine, $variablesToDefine); - - Write-Verbose -Message "[Rule][$($Context.Index)][$RuleName]`t[OnFailure]::END"; - } - } - - # Add output objects to result object - if ($Rule.Output.Length -gt 0) { - foreach ($a in $Rule.Output) { - $resultObject.Output += $a; - } - } - - Write-Verbose -Message "[Rule][$($Context.Index)][$RuleName]`t-- Output = $($resultObject.Output.Length)"; - - # Emit the result object to the pipeline - $resultObject; - - Write-Verbose -Message "[Rule][$($Context.Index)][$RuleName]`tEND:: [$successCount/$numResult]"; - } - - end { - # Update progress display - $progressRecord.PercentComplete = 100; - Write-Progress @progressRecord -Completed; - } -} - function AssertAllOf { [CmdletBinding()] [OutputType([System.Boolean])] @@ -961,52 +821,6 @@ function GetRuleScriptPath { } } -function NewEngine { - - param ( - - ) - - process { - - $result = New-Object -TypeName PSObject -Property @{ - Rule = New-Object -TypeName 'System.Collections.Generic.Dictionary[string,PSObject]'([System.StringComparer]::OrdinalIgnoreCase) - } - - $result; - } -} - -function NewContext { - - param ( - - ) - - process { - - } -} - -function NewRuleResult { - [CmdletBinding()] - [OutputType([PSRule.Rules.RuleResult])] - param ( - [System.Boolean]$Success, - - [String]$Status = 'Failed' - ) - - process { - # Create a result object - $result = New-Object -TypeName PSRule.Rules.RuleResult -Property @{ RuleName = $RuleName; Success = $Success; Status = $Status; Message = $Rule.Message; TargetName = $Rule.TargetName; Output = @(); } - - # $result.PSObject.TypeNames.Insert(0, 'PSRule.Rules.RuleResult'); - - $result; - } -} - function IsDeviceGuardEnabled { [CmdletBinding()] diff --git a/src/PSRule/Pipeline/InvokeRulePipeline.cs b/src/PSRule/Pipeline/InvokeRulePipeline.cs index 68ba4fd846..f37c05372d 100644 --- a/src/PSRule/Pipeline/InvokeRulePipeline.cs +++ b/src/PSRule/Pipeline/InvokeRulePipeline.cs @@ -8,54 +8,113 @@ namespace PSRule.Pipeline { public sealed class InvokeRulePipeline : RulePipeline { - private readonly RuleResultOutcome _Outcome; + private readonly RuleOutcome _Outcome; private readonly DependencyGraph _RuleGraph; + + // A per summary of rules being processes and outcome + private readonly Dictionary _Summary; + + private readonly ResultFormat _ResultFormat; private readonly PipelineContext _Context; - internal InvokeRulePipeline(PipelineLogger logger, PSRuleOption option, string[] path, RuleFilter filter, RuleResultOutcome outcome) + internal InvokeRulePipeline(PipelineLogger logger, PSRuleOption option, string[] path, RuleFilter filter, RuleOutcome outcome, ResultFormat resultFormat) : base(option, path, filter) { _Outcome = outcome; _Context = PipelineContext.New(logger); _RuleGraph = HostHelper.GetRuleBlockGraph(_Option, null, _Path, _Filter); + _Summary = new Dictionary(); + _ResultFormat = resultFormat; } - public IEnumerable Process(PSObject o) + public IEnumerable Process(PSObject o) { try { - var results = new List(); + return ProcessRule(o); + } + finally + { + + } + } + + public IEnumerable Process(PSObject[] targets) + { + var results = new List(); - foreach (var target in _RuleGraph.GetSingleTarget()) + foreach (var target in targets) + { + foreach (var result in Process(target)) { - var result = (target.Skipped) ? new RuleResult(target.Value.Id) : HostHelper.InvokeRuleBlock(_Option, target.Value, o); - - if (result.Status == RuleResultOutcome.Passed || result.Status == RuleResultOutcome.Inconclusive) - { - target.Pass(); - } - else if (result.Status == RuleResultOutcome.Failed || result.Status == RuleResultOutcome.Error) - { - target.Fail(); - } - - if (ShouldOutput(result.Status)) - { - results.Add(result); - } + results.Add(result); } - - return results; } - finally + + return results; + } + + public IEnumerable GetSummary() + { + return _Summary.Values; + } + + private IEnumerable ProcessRule(PSObject o) + { + var results = new List(); + + foreach (var target in _RuleGraph.GetSingleTarget()) { - + var result = (target.Skipped) ? new RuleRecord(target.Value.Id) : HostHelper.InvokeRuleBlock(_Option, target.Value, o); + + if (result.Status == RuleOutcome.Passed || result.Status == RuleOutcome.Inconclusive) + { + target.Pass(); + } + else if (result.Status == RuleOutcome.Failed || result.Status == RuleOutcome.Error) + { + target.Fail(); + } + + AddToSummary(ruleBlock: target.Value, targetName: result.TargetName, outcome: result.Status); + + if (ShouldOutput(result.Status)) + { + results.Add(result); + } } + + return results; + } + + private bool ShouldOutput(RuleOutcome outcome) + { + return _ResultFormat == ResultFormat.Detail && + (_Outcome == RuleOutcome.All | (outcome & _Outcome) > 0); } - private bool ShouldOutput(RuleResultOutcome outcome) + private void AddToSummary(RuleBlock ruleBlock, string targetName, RuleOutcome outcome) { - return _Outcome == RuleResultOutcome.All | (outcome & _Outcome) > 0; + if (!_Summary.TryGetValue(ruleBlock.Id, out RuleSummaryRecord s)) + { + s = new RuleSummaryRecord(ruleBlock.Id); + s.Tag = ruleBlock.Tag?.ToHashtable(); + + _Summary.Add(ruleBlock.Id, s); + } + + if (outcome == RuleOutcome.Passed) + { + s.Pass++; + } + else if (outcome == RuleOutcome.Failed) + { + s.Fail++; + } + else if (outcome == RuleOutcome.Error) + { + s.Error++; + } } } } diff --git a/src/PSRule/Pipeline/InvokeRulePipelineBuilder.cs b/src/PSRule/Pipeline/InvokeRulePipelineBuilder.cs index 15ae6bd41a..52d795ea3a 100644 --- a/src/PSRule/Pipeline/InvokeRulePipelineBuilder.cs +++ b/src/PSRule/Pipeline/InvokeRulePipelineBuilder.cs @@ -1,22 +1,27 @@ using PSRule.Configuration; using PSRule.Rules; -using System; using System.Collections; using System.Management.Automation; namespace PSRule.Pipeline { + /// + /// A helper to construct an invoke pipeline. + /// public sealed class InvokeRulePipelineBuilder { private string[] _Path; private PSRuleOption _Option; private RuleFilter _Filter; - private RuleResultOutcome _Outcome; + private RuleOutcome _Outcome; private PipelineLogger _Logger; + private ResultFormat _ResultFormat; internal InvokeRulePipelineBuilder() { _Logger = new PipelineLogger(); + _Option = new PSRuleOption(); + _ResultFormat = ResultFormat.Detail; } public void FilterBy(string[] name, Hashtable tag) @@ -31,28 +36,21 @@ public void Source(string[] path) public void Option(PSRuleOption option) { - _Option = option; + _Option = option.Clone(); } - public void Limit(RuleResultOutcome outcome) + public void Limit(RuleOutcome outcome) { _Outcome = outcome; } - //public void WriteVerbose(ActionPreference preference, Func callback) - //{ - - //} - - //public void WriteError(ActionPreference preference, Func callback) - //{ - - //} - - //public void WriteWarning(ActionPreference preference, Func callback) - //{ - - //} + public void As(ResultFormat resultFormat) + { + if (resultFormat != ResultFormat.Default) + { + _ResultFormat = resultFormat; + } + } public void UseCommandRuntime(ICommandRuntime commandRuntime) { @@ -70,7 +68,7 @@ public void UseCommandRuntime(ICommandRuntime2 commandRuntime) public InvokeRulePipeline Build() { - return new InvokeRulePipeline(_Logger, _Option, _Path, _Filter, _Outcome); + return new InvokeRulePipeline(_Logger, _Option, _Path, _Filter, _Outcome, _ResultFormat); } } } diff --git a/src/PSRule/Rules/IRuleResult.cs b/src/PSRule/Rules/IRuleResult.cs new file mode 100644 index 0000000000..7fce1cb79e --- /dev/null +++ b/src/PSRule/Rules/IRuleResult.cs @@ -0,0 +1,6 @@ +namespace PSRule.Rules +{ + public interface IRuleResult + { + } +} \ No newline at end of file diff --git a/src/PSRule/Rules/RuleFilter.cs b/src/PSRule/Rules/RuleFilter.cs index 383473649e..8ca4bba7ae 100644 --- a/src/PSRule/Rules/RuleFilter.cs +++ b/src/PSRule/Rules/RuleFilter.cs @@ -12,6 +12,11 @@ public sealed class RuleFilter private HashSet _RequiredName; private Hashtable _RequiredTag; + /// + /// Filter rules by name or tag. + /// + /// + /// public RuleFilter(IEnumerable name, Hashtable tag) { _RequiredName = name == null ? null : new HashSet(name, StringComparer.OrdinalIgnoreCase); diff --git a/src/PSRule/Rules/RuleResultOutcome.cs b/src/PSRule/Rules/RuleOutcome.cs similarity index 86% rename from src/PSRule/Rules/RuleResultOutcome.cs rename to src/PSRule/Rules/RuleOutcome.cs index 167757a1f7..263297fd6a 100644 --- a/src/PSRule/Rules/RuleResultOutcome.cs +++ b/src/PSRule/Rules/RuleOutcome.cs @@ -3,7 +3,7 @@ namespace PSRule.Rules { [Flags] - public enum RuleResultOutcome : byte + public enum RuleOutcome : byte { None = 0, diff --git a/src/PSRule/Rules/RuleRecord.cs b/src/PSRule/Rules/RuleRecord.cs new file mode 100644 index 0000000000..e28890421e --- /dev/null +++ b/src/PSRule/Rules/RuleRecord.cs @@ -0,0 +1,39 @@ +using System.Collections; +using System.Diagnostics; +using System.Management.Automation; + +namespace PSRule.Rules +{ + /// + /// A detailed format for rule results. + /// + [DebuggerDisplay("{RuleName")] + public sealed class RuleRecord : IRuleResult + { + internal RuleRecord(string ruleId) + { + RuleName = ruleId; + Status = RuleOutcome.None; + } + + public string RuleName { get; private set; } + + public bool Success { get; internal set; } + + /// + /// The outcome of the processing an object. + /// + public RuleOutcome Status { get; internal set; } + + public string Message { get; internal set; } + + /// + /// A name to identify the processed object. + /// + public string TargetName { get; internal set; } + + public PSObject TargetObject { get; internal set; } + + public Hashtable Tag { get; internal set; } + } +} diff --git a/src/PSRule/Rules/RuleResult.cs b/src/PSRule/Rules/RuleResult.cs deleted file mode 100644 index 7003db602d..0000000000 --- a/src/PSRule/Rules/RuleResult.cs +++ /dev/null @@ -1,28 +0,0 @@ -using System.Collections; -using System.Management.Automation; - -namespace PSRule.Rules -{ - public sealed class RuleResult - { - public RuleResult(string ruleId) - { - RuleName = ruleId; - Status = RuleResultOutcome.None; - } - - public string RuleName { get; private set; } - - public bool Success { get; internal set; } - - public RuleResultOutcome Status { get; internal set; } - - public string Message { get; internal set; } - - public string TargetName { get; internal set; } - - public PSObject TargetObject { get; internal set; } - - public Hashtable Tag { get; internal set; } - } -} diff --git a/src/PSRule/Rules/RuleSummaryRecord.cs b/src/PSRule/Rules/RuleSummaryRecord.cs new file mode 100644 index 0000000000..02ae3dcc5c --- /dev/null +++ b/src/PSRule/Rules/RuleSummaryRecord.cs @@ -0,0 +1,60 @@ +using System.Collections; +using System.Diagnostics; + +namespace PSRule.Rules +{ + /// + /// A summary format for rule results. + /// + [DebuggerDisplay("{RuleId")] + public sealed class RuleSummaryRecord : IRuleResult + { + internal RuleSummaryRecord(string ruleId) + { + RuleId = ruleId; + } + + /// + /// The number of rule passes. + /// + public int Pass { get; internal set; } + + /// + /// The number of rule failures. + /// + public int Fail { get; internal set; } + + /// + /// The number of rile errors. + /// + public int Error { get; internal set; } + + /// + /// The unique identifer for the rule. + /// + public string RuleId { get; private set; } + + public RuleOutcome Outcome + { + get + { + if (Error > 0) + { + return RuleOutcome.Error; + } + else if (Fail > 0) + { + return RuleOutcome.Failed; + } + else if (Pass > 0) + { + return RuleOutcome.Passed; + } + + return RuleOutcome.None; + } + } + + public Hashtable Tag { get; internal set; } + } +} diff --git a/tests/PSRule/PSRule.Common.Tests.ps1 b/tests/PSRule/PSRule.Common.Tests.ps1 index 2fc955371d..90a6967377 100644 --- a/tests/PSRule/PSRule.Common.Tests.ps1 +++ b/tests/PSRule/PSRule.Common.Tests.ps1 @@ -117,6 +117,39 @@ Describe 'Invoke-PSRule' { } } + Context 'Using -As' { + $testObject = @( + [PSCustomObject]@{ + Name = "TestObject1" + Value = 1 + } + [PSCustomObject]@{ + Name = "TestObject1" + Value = 1 + } + ); + + It 'Returns detail' { + $result = $testObject | Invoke-PSRule -Path (Join-Path -Path $here -ChildPath 'FromFile.Rule.ps1') -Tag @{ category = 'group1' } -As Detail; + $result | Should -Not -BeNullOrEmpty; + $result | Should -BeOfType PSRule.Rules.RuleRecord; + } + + It 'Returns summary' { + $result = $testObject | Invoke-PSRule -Path (Join-Path -Path $here -ChildPath 'FromFile.Rule.ps1') -Tag @{ category = 'group1' } -As Summary; + $result | Should -Not -BeNullOrEmpty; + $result.Count | Should -Be 3; + $result | Should -BeOfType PSRule.Rules.RuleSummaryRecord; + $result.RuleId | Should -BeIn 'FromFile1', 'FromFile2', 'FromFile3' + $result.Tag.category | Should -BeIn 'group1'; + + ($result | Where-Object { $_.RuleId -eq 'FromFile1'}).Outcome | Should -Be 'Passed'; + ($result | Where-Object { $_.RuleId -eq 'FromFile1'}).Pass | Should -Be 2; + ($result | Where-Object { $_.RuleId -eq 'FromFile2'}).Outcome | Should -Be 'Failed'; + ($result | Where-Object { $_.RuleId -eq 'FromFile2'}).Fail | Should -Be 2; + } + } + Context 'With constrained language' { $testObject = [PSCustomObject]@{