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
This commit is contained in:
Bernie White 2019-06-21 10:16:12 +08:00 коммит произвёл GitHub
Родитель 36df6b2ac3
Коммит b6de1c604b
Не найден ключ, соответствующий данной подписи
Идентификатор ключа GPG: 4AEE18F83AFDEB23
14 изменённых файлов: 165 добавлений и 41 удалений

3
.vscode/settings.json поставляемый
Просмотреть файл

@ -4,7 +4,8 @@
"out/": true, "out/": true,
"reports/": true, "reports/": true,
"**/bin/": true, "**/bin/": true,
"**/obj/": true "**/obj/": true,
"TestResults/": true
}, },
"search.exclude": { "search.exclude": {
"out/": true "out/": true

Просмотреть файл

@ -2,7 +2,9 @@
## Unreleased ## Unreleased
- Fix circular rule dependency issue. [#190](https://github.com/BernieWhite/PSRule/issues/190) - 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) - 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) ## v0.7.0-B190624 (pre-release)

Просмотреть файл

@ -1,9 +1,16 @@
# PSRule # 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] ![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 ## Disclaimer
This project is to be considered a **proof-of-concept** and **not a supported product**. This project is to be considered a **proof-of-concept** and **not a supported product**.

44
docs/features.md Normal file
Просмотреть файл

@ -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).

Просмотреть файл

@ -3,7 +3,7 @@
## Prerequisites ## Prerequisites
- Windows PowerShell 5.1 with .NET Framework 4.7.2+ or - 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). For a list of platforms that PowerShell Core is supported on [see](https://github.com/PowerShell/PowerShell#get-powershell).

Просмотреть файл

@ -1,10 +1,10 @@
using PSRule.Parser; using PSRule.Parser;
using PSRule.Pipeline; using PSRule.Pipeline;
using PSRule.Rules; using PSRule.Rules;
using System;
using System.Collections; using System.Collections;
using System.IO; using System.IO;
using System.Management.Automation; using System.Management.Automation;
using System.Runtime.Serialization;
namespace PSRule.Commands namespace PSRule.Commands
{ {
@ -23,6 +23,7 @@ namespace PSRule.Commands
/// The name of the rule. /// The name of the rule.
/// </summary> /// </summary>
[Parameter(Mandatory = true, Position = 0)] [Parameter(Mandatory = true, Position = 0)]
[ValidateNotNullOrEmpty()]
public string Name { get; set; } public string Name { get; set; }
/// <summary> /// <summary>
@ -53,6 +54,7 @@ namespace PSRule.Commands
/// Deployments that this deployment depends on. /// Deployments that this deployment depends on.
/// </summary> /// </summary>
[Parameter(Mandatory = false)] [Parameter(Mandatory = false)]
[ValidateNotNullOrEmpty()]
public string[] DependsOn { get; set; } public string[] DependsOn { get; set; }
/// <summary> /// <summary>
@ -81,6 +83,8 @@ namespace PSRule.Commands
} }
} }
CheckDependsOn();
var ps = PowerShell.Create(); var ps = PowerShell.Create();
ps.Runspace = context.GetRunspace(); ps.Runspace = context.GetRunspace();
ps.AddCommand(new CmdletInfo(InvokeBlockCmdletName, typeof(InvokeRuleBlockCommand))); ps.AddCommand(new CmdletInfo(InvokeBlockCmdletName, typeof(InvokeRuleBlockCommand)));
@ -111,6 +115,23 @@ namespace PSRule.Commands
WriteObject(block); 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) private RuleHelpInfo GetHelpInfo(PipelineContext context, string name)
{ {
if (context.Source.HelpPath == null || context.Source.HelpPath.Length == 0) if (context.Source.HelpPath == null || context.Source.HelpPath.Length == 0)

Просмотреть файл

@ -12,7 +12,7 @@ namespace PSRule.Pipeline
private readonly List<RuleRecord> _Record; private readonly List<RuleRecord> _Record;
private RuleOutcome _Outcome; private RuleOutcome _Outcome;
private float _Time; private long _Time;
private int _Total; private int _Total;
private int _Error; private int _Error;
private int _Fail; private int _Fail;
@ -21,13 +21,13 @@ namespace PSRule.Pipeline
{ {
TargetName = targetName; TargetName = targetName;
_Record = new List<RuleRecord>(); _Record = new List<RuleRecord>();
_Time = 0f; _Time = 0;
_Total = 0; _Total = 0;
_Error = 0; _Error = 0;
_Fail = 0; _Fail = 0;
} }
internal float Time internal long Time
{ {
get { return _Time; } get { return _Time; }
} }

Просмотреть файл

@ -18,12 +18,12 @@ namespace PSRule.Pipeline
{ {
_Builder.Append("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>"); _Builder.Append("<?xml version=\"1.0\" encoding=\"utf-8\" standalone=\"no\"?>");
var time = o.Sum(r => r.Time); float time = o.Sum(r => r.Time);
var total = o.Sum(r => r.Total); var total = o.Sum(r => r.Total);
var error = o.Sum(r => r.Error); var error = o.Sum(r => r.Error);
var fail = o.Sum(r => r.Fail); var fail = o.Sum(r => r.Fail);
_Builder.Append($"<test-results xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"nunit_schema_2.5.xsd\" name=\"PSRule\" total=\"{total}\" errors=\"{error}\" failures=\"{fail}\" not-run=\"0\" inconclusive=\"0\" ignored=\"0\" skipped=\"0\" invalid=\"0\" date=\"{DateTime.UtcNow.ToString("yyyy-MM-dd")}\" time=\"{TimeSpan.FromMilliseconds(time * 1000)}\">"); _Builder.Append($"<test-results xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:noNamespaceSchemaLocation=\"nunit_schema_2.5.xsd\" name=\"PSRule\" total=\"{total}\" errors=\"{error}\" failures=\"{fail}\" not-run=\"0\" inconclusive=\"0\" ignored=\"0\" skipped=\"0\" invalid=\"0\" date=\"{DateTime.UtcNow.ToString("yyyy-MM-dd")}\" time=\"{TimeSpan.FromMilliseconds(time).ToString()}\">");
_Builder.Append($"<environment user=\"{Environment.UserName}\" machine-name=\"{Environment.MachineName}\" cwd=\"{Configuration.PSRuleOption.GetWorkingPath()}\" user-domain=\"{Environment.UserDomainName}\" platform=\"{Environment.OSVersion.Platform}\" nunit-version=\"2.5.8.0\" os-version=\"{Environment.OSVersion.Version}\" clr-version=\"{Environment.Version.ToString()}\" />"); _Builder.Append($"<environment user=\"{Environment.UserName}\" machine-name=\"{Environment.MachineName}\" cwd=\"{Configuration.PSRuleOption.GetWorkingPath()}\" user-domain=\"{Environment.UserDomainName}\" platform=\"{Environment.OSVersion.Platform}\" nunit-version=\"2.5.8.0\" os-version=\"{Environment.OSVersion.Version}\" clr-version=\"{Environment.Version.ToString()}\" />");
_Builder.Append($"<culture-info current-culture=\"{System.Threading.Thread.CurrentThread.CurrentCulture.ToString()}\" current-uiculture=\"{System.Threading.Thread.CurrentThread.CurrentUICulture.ToString()}\" />"); _Builder.Append($"<culture-info current-culture=\"{System.Threading.Thread.CurrentThread.CurrentCulture.ToString()}\" current-uiculture=\"{System.Threading.Thread.CurrentThread.CurrentUICulture.ToString()}\" />");
@ -45,7 +45,7 @@ namespace PSRule.Pipeline
private void VisitFixture(TestFixture fixture) private void VisitFixture(TestFixture fixture)
{ {
_Builder.Append($"<test-suite type=\"TestFixture\" name=\"{fixture.Name}\" executed=\"{fixture.Executed}\" result=\"{(fixture.Success ? "Success" : "Failure")}\" success=\"{fixture.Success}\" time=\"{fixture.Time}\" asserts=\"{fixture.Asserts}\" description=\"{fixture.Description}\"><results>"); _Builder.Append($"<test-suite type=\"TestFixture\" name=\"{fixture.Name}\" executed=\"{fixture.Executed}\" result=\"{(fixture.Success ? "Success" : "Failure")}\" success=\"{fixture.Success}\" time=\"{fixture.Time.ToString()}\" asserts=\"{fixture.Asserts}\" description=\"{fixture.Description}\"><results>");
foreach (var testCase in fixture.Results) foreach (var testCase in fixture.Results)
{ {
@ -57,7 +57,7 @@ namespace PSRule.Pipeline
private void VisitTestCase(TestCase testCase) private void VisitTestCase(TestCase testCase)
{ {
_Builder.Append($"<test-case description=\"{testCase.Description}\" name=\"{testCase.Name}\" time=\"{testCase.Time}\" asserts=\"0\" success=\"{testCase.Success}\" result=\"{(testCase.Success ? "Success" : "Failure")}\" executed=\"{testCase.Executed}\" />"); _Builder.Append($"<test-case description=\"{testCase.Description}\" name=\"{testCase.Name}\" time=\"{testCase.Time.ToString()}\" asserts=\"0\" success=\"{testCase.Success}\" result=\"{(testCase.Success ? "Success" : "Failure")}\" executed=\"{testCase.Executed}\" />");
} }
private sealed class TestFixture private sealed class TestFixture
@ -70,13 +70,13 @@ namespace PSRule.Pipeline
public readonly int Asserts; public readonly int Asserts;
public readonly TestCase[] Results; 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; Name = name;
Description = description; Description = description;
Success = success; Success = success;
Executed = executed; Executed = executed;
Time = time; Time = time / 1000f;
Asserts = asserts; Asserts = asserts;
Results = testCases; Results = testCases;
} }
@ -90,13 +90,13 @@ namespace PSRule.Pipeline
public readonly bool Executed; public readonly bool Executed;
public readonly float Time; 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; Name = name;
Description = description; Description = description;
Success = success; Success = success;
Executed = executed; Executed = executed;
Time = time; Time = time / 1000f;
} }
} }
} }

Просмотреть файл

@ -441,6 +441,7 @@ namespace PSRule.Pipeline
RuleBlock = ruleBlock; RuleBlock = ruleBlock;
// Starts rule execution timer
_RuleTimer.Restart(); _RuleTimer.Restart();
return RuleRecord; return RuleRecord;
@ -451,9 +452,9 @@ namespace PSRule.Pipeline
/// </summary> /// </summary>
public void ExitRuleBlock() public void ExitRuleBlock()
{ {
// Stop rule execution time
_RuleTimer.Stop(); _RuleTimer.Stop();
var time = _RuleTimer.ElapsedMilliseconds; RuleRecord.Time = _RuleTimer.ElapsedMilliseconds;
RuleRecord.Time = time > 0 ? time / 1000 : 0f;
_LogPrefix = null; _LogPrefix = null;
RuleRecord = null; RuleRecord = null;

Просмотреть файл

@ -84,7 +84,7 @@ namespace PSRule.Rules
[DefaultValue(0f)] [DefaultValue(0f)]
[JsonProperty(PropertyName = "time")] [JsonProperty(PropertyName = "time")]
public float Time { get; internal set; } public long Time { get; internal set; }
public bool IsSuccess() public bool IsSuccess()
{ {

Просмотреть файл

@ -140,6 +140,11 @@ Rule 'WithCsv' {
$True; $True;
} }
Rule 'WithSleep' {
Start-Sleep -Milliseconds 50;
$True;
}
# Synopsis: Test for Recommend keyword # Synopsis: Test for Recommend keyword
Rule 'RecommendTest' { Rule 'RecommendTest' {
Recommend 'This is a recommendation' Recommend 'This is a recommendation'

Просмотреть файл

@ -0,0 +1,10 @@
# Synopsis: Null DependsOn is invalid.
Rule 'InvalidRule1' -DependsOn $Null {
}
# Synopsis: Empty DependsOn collection is invalid.
Rule 'InvalidRule2' -DependsOn @($Null) {
}

Просмотреть файл

@ -19,3 +19,7 @@ Rule 'WithDependency2' -DependsOn 'WithDependency3' {
Rule 'WithDependency3' -DependsOn 'WithDependency1' { Rule 'WithDependency3' -DependsOn 'WithDependency1' {
$True; $True;
} }
Rule 'WithDependency4' -DependsOn 'WithDependency5' {
$True;
}

Просмотреть файл

@ -65,6 +65,13 @@ Describe 'Invoke-PSRule' -Tag 'Invoke-PSRule','Common' {
$result.OutcomeReason | Should -Be 'Inconclusive'; $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' { It 'Propagates PowerShell logging' {
$withLoggingRulePath = (Join-Path -Path $here -ChildPath 'FromFileWithLogging.Rule.ps1'); $withLoggingRulePath = (Join-Path -Path $here -ChildPath 'FromFileWithLogging.Rule.ps1');
$loggingParams = @{ $loggingParams = @{
@ -1093,22 +1100,33 @@ Describe 'Get-PSRuleHelp' -Tag 'Get-PSRuleHelp', 'Common' {
#endregion Get-PSRuleHelp #endregion Get-PSRuleHelp
#region Rule processing #region Rules
Describe 'Rule processing' -Tag 'Common', 'RuleProcessing' { Describe 'Rules' -Tag 'Common', 'Rules' {
Context 'Error handling' { $testObject = [PSCustomObject]@{
$ruleFilePath = (Join-Path -Path $here -ChildPath 'FromFileWithError.Rule.ps1'); Name = 'TestObject1'
$testObject = [PSCustomObject]@{ Value = 1
Name = 'TestObject1' }
Value = 1 $testParams = @{
} ErrorVariable = 'outError'
$testParams = @{ ErrorAction = 'SilentlyContinue'
ErrorVariable = 'outError' WarningAction = 'SilentlyContinue'
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' { It 'Error on non-boolean results' {
$ruleFilePath = (Join-Path -Path $here -ChildPath 'FromFileWithError.Rule.ps1');
$result = $testObject | Invoke-PSRule @testParams -Path $ruleFilePath -Name WithNonBoolean; $result = $testObject | Invoke-PSRule @testParams -Path $ruleFilePath -Name WithNonBoolean;
$messages = @($outError); $messages = @($outError);
$result | Should -Not -BeNullOrEmpty; $result | Should -Not -BeNullOrEmpty;
@ -1117,23 +1135,34 @@ Describe 'Rule processing' -Tag 'Common', 'RuleProcessing' {
$messages.Exception | Should -BeOfType PSRule.Pipeline.RuleRuntimeException; $messages.Exception | Should -BeOfType PSRule.Pipeline.RuleRuntimeException;
$messages.Exception.Message | Should -BeLike 'An invalid rule result was returned for *'; $messages.Exception.Message | Should -BeLike 'An invalid rule result was returned for *';
} }
}
It 'Error on nested rules' { Context 'Dependencies' {
$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 *';
}
It 'Error on circular dependency' { 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 = @({ $Null = $testObject | Invoke-PSRule @testParams -Path $ruleFilePath -Name WithDependency1; $outError; } | Should -Throw -PassThru);
$messages.Length | Should -BeGreaterThan 0; $messages.Length | Should -BeGreaterThan 0;
$messages.Exception | Should -BeOfType PSRule.Pipeline.RuleRuntimeException; $messages.Exception | Should -BeOfType PSRule.Pipeline.RuleRuntimeException;
$messages.Exception.Message | Should -BeLike 'A circular rule dependency was detected.*'; $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