From b6de1c604bf237ba2d063e532ac7442703f7db57 Mon Sep 17 00:00:00 2001 From: Bernie White Date: Fri, 21 Jun 2019 10:16:12 +0800 Subject: [PATCH] Fix rule DependsOn parameter allows null and change rule timing to milliseconds #191 #192 (#205) * Fix null DependsOn parameter #191 * Exclude VS test results * Record rule time in milliseconds * Add documentation on PSRule features #68 * Update change log #192 --- .vscode/settings.json | 3 +- CHANGELOG.md | 2 + README.md | 9 ++- docs/features.md | 44 +++++++++++ docs/scenarios/install-instructions.md | 2 +- .../Commands/NewRuleDefinitionCommand.cs | 23 +++++- src/PSRule/Pipeline/InvokeResult.cs | 6 +- src/PSRule/Pipeline/NUnit3Serializer.cs | 16 ++-- src/PSRule/Pipeline/PipelineContext.cs | 5 +- src/PSRule/Rules/RuleRecord.cs | 2 +- tests/PSRule.Tests/FromFile.Rule.ps1 | 5 ++ tests/PSRule.Tests/FromFileInvalid.Rule.ps1 | 10 +++ tests/PSRule.Tests/FromFileWithError.Rule.ps1 | 4 + tests/PSRule.Tests/PSRule.Common.Tests.ps1 | 75 +++++++++++++------ 14 files changed, 165 insertions(+), 41 deletions(-) create mode 100644 docs/features.md create mode 100644 tests/PSRule.Tests/FromFileInvalid.Rule.ps1 diff --git a/.vscode/settings.json b/.vscode/settings.json index f3b0cf0ed..c5bc65317 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -4,7 +4,8 @@ "out/": true, "reports/": true, "**/bin/": true, - "**/obj/": true + "**/obj/": true, + "TestResults/": true }, "search.exclude": { "out/": true diff --git a/CHANGELOG.md b/CHANGELOG.md index b2f59a783..b8ac70b47 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,7 +2,9 @@ ## Unreleased - Fix circular rule dependency issue. [#190](https://github.com/BernieWhite/PSRule/issues/190) +- Fix rule `DependsOn` parameter allows null. [#191](https://github.com/BernieWhite/PSRule/issues/191) - Fix error message when attempting to use the rule keyword in a rule definition. [#189](https://github.com/BernieWhite/PSRule/issues/189) +- **Breaking change**: Rule time is recorded in milliseconds instead of seconds. [#192](https://github.com/BernieWhite/PSRule/issues/192) ## v0.7.0-B190624 (pre-release) diff --git a/README.md b/README.md index 373c3e0ef..ba46cb00b 100644 --- a/README.md +++ b/README.md @@ -1,9 +1,16 @@ # PSRule -A cross-platform PowerShell module (Windows, Linux, and macOS) with commands to validate objects on the pipeline using PowerShell syntax. +A cross-platform PowerShell module (Windows, Linux, and MacOS) with commands to validate objects on the pipeline using PowerShell syntax. ![ci-badge] +Features of PSRule include: + +- [Extensible](docs/features.md#extensible) - Use PowerShell, a flexible scripting language. +- [Cross-platform](docs/features.md#cross-platform) - Run on MacOS, Linux and Windows. +- [Reusable](docs/features.md#reusable) - Share rules across teams or organizations. +- [Recommendations](docs/features.md#recommendations) - Include detailed instructions to remediate issues. + ## Disclaimer This project is to be considered a **proof-of-concept** and **not a supported product**. diff --git a/docs/features.md b/docs/features.md new file mode 100644 index 000000000..26284800d --- /dev/null +++ b/docs/features.md @@ -0,0 +1,44 @@ +# PSRule features + +The following sections describe key features of PSRule. + +- [Extensible](#extensible) +- [Cross-platform](#cross-platform) +- [Reusable](#reusable) +- [Recommendations](#recommendations) + +## Extensible + +Authors define rules using PowerShell, a flexible scripting language. If you or your team already can write a basic PowerShell script, you can already define a rule. What's more, you can leverage a large world-wide community of PowerShell users with scripts and cmdlets to help you build out rules quickly. + +## Cross-platform + +PSRule uses modern PowerShell libraries at it's core, allowing it to go anywhere Windows PowerShell 5.1 or PowerShell Core 6.2 can go. PSRule runs on MacOS, Linux and Windows. + +To install PSRule use the `Install-Module` cmdlet within Windows PowerShell or PowerShell Core. + +```powershell +Install-Module -Name PSRule -Scope CurrentUser; +``` + +PSRule also has editor support for Visual Studio Code with the companion extension, which can be installed on MacOS, Linux and Windows. + +To install the extension: + +```text +code --install-extension bewhite.psrule-vscode-preview +``` + +For additional installation options see [install instructions](scenarios/install-instructions.md). + +## Reusable + +Define rules once then reuse and share rules across teams or organizations. Rules can be packaged up into a module then distributed. + +PSRule uses PowerShell modules as the standard way to distribute rules. Modules containing rules can be published on the PowerShell Gallery or network share using the same process as regular PowerShell modules. + +## Recommendations + +PSRule allows rule authors to define recommendations in markdown. This allows not only the cause of the issue to be identified but detailed instructions to be included to remediate issues. + +For more information see [about_PSRule_docs](concepts/PSRule/en-US/about_PSRule_Docs.md). diff --git a/docs/scenarios/install-instructions.md b/docs/scenarios/install-instructions.md index 32a14a111..5ee883538 100644 --- a/docs/scenarios/install-instructions.md +++ b/docs/scenarios/install-instructions.md @@ -3,7 +3,7 @@ ## Prerequisites - Windows PowerShell 5.1 with .NET Framework 4.7.2+ or -- PowerShell Core 6.0 or greater on Windows, macOS and Linux +- PowerShell Core 6.2 or greater on Windows, MacOS and Linux For a list of platforms that PowerShell Core is supported on [see](https://github.com/PowerShell/PowerShell#get-powershell). diff --git a/src/PSRule/Commands/NewRuleDefinitionCommand.cs b/src/PSRule/Commands/NewRuleDefinitionCommand.cs index bfe881ae8..2b874c238 100644 --- a/src/PSRule/Commands/NewRuleDefinitionCommand.cs +++ b/src/PSRule/Commands/NewRuleDefinitionCommand.cs @@ -1,10 +1,10 @@ using PSRule.Parser; using PSRule.Pipeline; using PSRule.Rules; +using System; using System.Collections; using System.IO; using System.Management.Automation; -using System.Runtime.Serialization; namespace PSRule.Commands { @@ -23,6 +23,7 @@ namespace PSRule.Commands /// The name of the rule. /// [Parameter(Mandatory = true, Position = 0)] + [ValidateNotNullOrEmpty()] public string Name { get; set; } /// @@ -53,6 +54,7 @@ namespace PSRule.Commands /// Deployments that this deployment depends on. /// [Parameter(Mandatory = false)] + [ValidateNotNullOrEmpty()] public string[] DependsOn { get; set; } /// @@ -81,6 +83,8 @@ namespace PSRule.Commands } } + CheckDependsOn(); + var ps = PowerShell.Create(); ps.Runspace = context.GetRunspace(); ps.AddCommand(new CmdletInfo(InvokeBlockCmdletName, typeof(InvokeRuleBlockCommand))); @@ -111,6 +115,23 @@ namespace PSRule.Commands WriteObject(block); } + private void CheckDependsOn() + { + if (MyInvocation.BoundParameters.ContainsKey(nameof(DependsOn))) + { + if (DependsOn == null || DependsOn.Length == 0) + { + WriteError(new ErrorRecord( + exception: new ArgumentNullException(paramName: nameof(DependsOn)), + errorId: "PSRule.Runtime.ArgumentNull", + errorCategory: ErrorCategory.InvalidArgument, + targetObject: null + )); + } + //else if (DependsOn.Length ) + } + } + private RuleHelpInfo GetHelpInfo(PipelineContext context, string name) { if (context.Source.HelpPath == null || context.Source.HelpPath.Length == 0) diff --git a/src/PSRule/Pipeline/InvokeResult.cs b/src/PSRule/Pipeline/InvokeResult.cs index cebd46b55..d2f25e9d2 100644 --- a/src/PSRule/Pipeline/InvokeResult.cs +++ b/src/PSRule/Pipeline/InvokeResult.cs @@ -12,7 +12,7 @@ namespace PSRule.Pipeline private readonly List _Record; private RuleOutcome _Outcome; - private float _Time; + private long _Time; private int _Total; private int _Error; private int _Fail; @@ -21,13 +21,13 @@ namespace PSRule.Pipeline { TargetName = targetName; _Record = new List(); - _Time = 0f; + _Time = 0; _Total = 0; _Error = 0; _Fail = 0; } - internal float Time + internal long Time { get { return _Time; } } diff --git a/src/PSRule/Pipeline/NUnit3Serializer.cs b/src/PSRule/Pipeline/NUnit3Serializer.cs index 1d8e9f0f9..5f2a9553a 100644 --- a/src/PSRule/Pipeline/NUnit3Serializer.cs +++ b/src/PSRule/Pipeline/NUnit3Serializer.cs @@ -18,12 +18,12 @@ namespace PSRule.Pipeline { _Builder.Append(""); - var time = o.Sum(r => r.Time); + float time = o.Sum(r => r.Time); var total = o.Sum(r => r.Total); var error = o.Sum(r => r.Error); var fail = o.Sum(r => r.Fail); - _Builder.Append($""); + _Builder.Append($""); _Builder.Append($""); _Builder.Append($""); @@ -45,7 +45,7 @@ namespace PSRule.Pipeline private void VisitFixture(TestFixture fixture) { - _Builder.Append($""); + _Builder.Append($""); foreach (var testCase in fixture.Results) { @@ -57,7 +57,7 @@ namespace PSRule.Pipeline private void VisitTestCase(TestCase testCase) { - _Builder.Append($""); + _Builder.Append($""); } private sealed class TestFixture @@ -70,13 +70,13 @@ namespace PSRule.Pipeline public readonly int Asserts; public readonly TestCase[] Results; - public TestFixture(string name, string description, bool success, bool executed, float time, int asserts, TestCase[] testCases) + public TestFixture(string name, string description, bool success, bool executed, long time, int asserts, TestCase[] testCases) { Name = name; Description = description; Success = success; Executed = executed; - Time = time; + Time = time / 1000f; Asserts = asserts; Results = testCases; } @@ -90,13 +90,13 @@ namespace PSRule.Pipeline public readonly bool Executed; public readonly float Time; - public TestCase(string name, string description, bool success, bool executed, float time) + public TestCase(string name, string description, bool success, bool executed, long time) { Name = name; Description = description; Success = success; Executed = executed; - Time = time; + Time = time / 1000f; } } } diff --git a/src/PSRule/Pipeline/PipelineContext.cs b/src/PSRule/Pipeline/PipelineContext.cs index 0b82ba39f..deea47ea1 100644 --- a/src/PSRule/Pipeline/PipelineContext.cs +++ b/src/PSRule/Pipeline/PipelineContext.cs @@ -441,6 +441,7 @@ namespace PSRule.Pipeline RuleBlock = ruleBlock; + // Starts rule execution timer _RuleTimer.Restart(); return RuleRecord; @@ -451,9 +452,9 @@ namespace PSRule.Pipeline /// public void ExitRuleBlock() { + // Stop rule execution time _RuleTimer.Stop(); - var time = _RuleTimer.ElapsedMilliseconds; - RuleRecord.Time = time > 0 ? time / 1000 : 0f; + RuleRecord.Time = _RuleTimer.ElapsedMilliseconds; _LogPrefix = null; RuleRecord = null; diff --git a/src/PSRule/Rules/RuleRecord.cs b/src/PSRule/Rules/RuleRecord.cs index 47f12fd2a..433659cef 100644 --- a/src/PSRule/Rules/RuleRecord.cs +++ b/src/PSRule/Rules/RuleRecord.cs @@ -84,7 +84,7 @@ namespace PSRule.Rules [DefaultValue(0f)] [JsonProperty(PropertyName = "time")] - public float Time { get; internal set; } + public long Time { get; internal set; } public bool IsSuccess() { diff --git a/tests/PSRule.Tests/FromFile.Rule.ps1 b/tests/PSRule.Tests/FromFile.Rule.ps1 index 12a7763b1..7e6534c2d 100644 --- a/tests/PSRule.Tests/FromFile.Rule.ps1 +++ b/tests/PSRule.Tests/FromFile.Rule.ps1 @@ -140,6 +140,11 @@ Rule 'WithCsv' { $True; } +Rule 'WithSleep' { + Start-Sleep -Milliseconds 50; + $True; +} + # Synopsis: Test for Recommend keyword Rule 'RecommendTest' { Recommend 'This is a recommendation' diff --git a/tests/PSRule.Tests/FromFileInvalid.Rule.ps1 b/tests/PSRule.Tests/FromFileInvalid.Rule.ps1 new file mode 100644 index 000000000..0225fe70d --- /dev/null +++ b/tests/PSRule.Tests/FromFileInvalid.Rule.ps1 @@ -0,0 +1,10 @@ + +# Synopsis: Null DependsOn is invalid. +Rule 'InvalidRule1' -DependsOn $Null { + +} + +# Synopsis: Empty DependsOn collection is invalid. +Rule 'InvalidRule2' -DependsOn @($Null) { + +} diff --git a/tests/PSRule.Tests/FromFileWithError.Rule.ps1 b/tests/PSRule.Tests/FromFileWithError.Rule.ps1 index 289382fe7..53c83fd17 100644 --- a/tests/PSRule.Tests/FromFileWithError.Rule.ps1 +++ b/tests/PSRule.Tests/FromFileWithError.Rule.ps1 @@ -19,3 +19,7 @@ Rule 'WithDependency2' -DependsOn 'WithDependency3' { Rule 'WithDependency3' -DependsOn 'WithDependency1' { $True; } + +Rule 'WithDependency4' -DependsOn 'WithDependency5' { + $True; +} diff --git a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 index 2f0719cd4..8981e7749 100644 --- a/tests/PSRule.Tests/PSRule.Common.Tests.ps1 +++ b/tests/PSRule.Tests/PSRule.Common.Tests.ps1 @@ -65,6 +65,13 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' { $result.OutcomeReason | Should -Be 'Inconclusive'; } + It 'Returns rule timing' { + $result = $testObject | Invoke-PSRule -Path $ruleFilePath -Name 'WithSleep'; + $result | Should -Not -BeNullOrEmpty; + $result.IsSuccess() | Should -Be $True; + $result.Time | Should -BeGreaterThan 0; + } + It 'Propagates PowerShell logging' { $withLoggingRulePath = (Join-Path -Path $here -ChildPath 'FromFileWithLogging.Rule.ps1'); $loggingParams = @{ @@ -1093,22 +1100,33 @@ Describe 'Get-PSRuleHelp' -Tag 'Get-PSRuleHelp', 'Common' { #endregion Get-PSRuleHelp -#region Rule processing +#region Rules -Describe 'Rule processing' -Tag 'Common', 'RuleProcessing' { - Context 'Error handling' { - $ruleFilePath = (Join-Path -Path $here -ChildPath 'FromFileWithError.Rule.ps1'); - $testObject = [PSCustomObject]@{ - Name = 'TestObject1' - Value = 1 - } - $testParams = @{ - ErrorVariable = 'outError' - ErrorAction = 'SilentlyContinue' - WarningAction = 'SilentlyContinue' - } +Describe 'Rules' -Tag 'Common', 'Rules' { + $testObject = [PSCustomObject]@{ + Name = 'TestObject1' + Value = 1 + } + $testParams = @{ + ErrorVariable = 'outError' + ErrorAction = 'SilentlyContinue' + WarningAction = 'SilentlyContinue' + } + Context 'Rule definition' { + It 'Error on nested rules' { + $ruleFilePath = (Join-Path -Path $here -ChildPath 'FromFileNested.Rule.ps1'); + $Null = $testObject | Invoke-PSRule @testParams -Path $ruleFilePath -Name WithNestedRule; + $messages = @($outError); + $messages.Length | Should -BeGreaterThan 0; + $messages.Exception | Should -BeOfType PSRule.Pipeline.RuleRuntimeException; + $messages.Exception.Message | Should -BeLike 'Rule nesting was detected in rule *'; + } + } + + Context 'Conditions' { It 'Error on non-boolean results' { + $ruleFilePath = (Join-Path -Path $here -ChildPath 'FromFileWithError.Rule.ps1'); $result = $testObject | Invoke-PSRule @testParams -Path $ruleFilePath -Name WithNonBoolean; $messages = @($outError); $result | Should -Not -BeNullOrEmpty; @@ -1117,23 +1135,34 @@ Describe 'Rule processing' -Tag 'Common', 'RuleProcessing' { $messages.Exception | Should -BeOfType PSRule.Pipeline.RuleRuntimeException; $messages.Exception.Message | Should -BeLike 'An invalid rule result was returned for *'; } + } - It 'Error on nested rules' { - $nestedRulePath = (Join-Path -Path $here -ChildPath 'FromFileNested.Rule.ps1'); - $Null = $testObject | Invoke-PSRule @testParams -Path $nestedRulePath -Name WithNestedRule; - $messages = @($outError); - $messages.Length | Should -BeGreaterThan 0; - $messages.Exception | Should -BeOfType PSRule.Pipeline.RuleRuntimeException; - $messages.Exception.Message | Should -BeLike 'Rule nesting was detected in rule *'; - } - + Context 'Dependencies' { It 'Error on circular dependency' { + $ruleFilePath = (Join-Path -Path $here -ChildPath 'FromFileWithError.Rule.ps1'); $messages = @({ $Null = $testObject | Invoke-PSRule @testParams -Path $ruleFilePath -Name WithDependency1; $outError; } | Should -Throw -PassThru); $messages.Length | Should -BeGreaterThan 0; $messages.Exception | Should -BeOfType PSRule.Pipeline.RuleRuntimeException; $messages.Exception.Message | Should -BeLike 'A circular rule dependency was detected.*'; } + + It 'Error on $null DependsOn' { + $ruleFilePath = (Join-Path -Path $here -ChildPath 'FromFileInvalid.Rule.ps1'); + $Null = $testObject | Invoke-PSRule @testParams -Path $ruleFilePath -Name InvalidRule1, InvalidRule2; + $messages = @($outError); + $messages.Length | Should -Be 2; + $messages.Exception | Should -BeOfType System.Management.Automation.ParameterBindingException; + $messages.Exception.Message | Should -BeLike '*The argument is null*'; + } + + It 'Error on missing dependency' { + $ruleFilePath = (Join-Path -Path $here -ChildPath 'FromFileWithError.Rule.ps1'); + $messages = @({ $Null = $testObject | Invoke-PSRule @testParams -Path $ruleFilePath -Name WithDependency4; $outError; } | Should -Throw -PassThru); + $messages.Length | Should -BeGreaterThan 0; + $messages.Exception | Should -BeOfType PSRule.Pipeline.RuleRuntimeException; + $messages.Exception.Message | Should -BeLike 'The dependency * for * could not be found.*'; + } } } -#endregion Rule processing +#endregion Rules