зеркало из https://github.com/microsoft/PSRule.git
* 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:
Родитель
36df6b2ac3
Коммит
b6de1c604b
|
@ -4,7 +4,8 @@
|
|||
"out/": true,
|
||||
"reports/": true,
|
||||
"**/bin/": true,
|
||||
"**/obj/": true
|
||||
"**/obj/": true,
|
||||
"TestResults/": true
|
||||
},
|
||||
"search.exclude": {
|
||||
"out/": true
|
||||
|
|
|
@ -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)
|
||||
|
||||
|
|
|
@ -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**.
|
||||
|
|
|
@ -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
|
||||
|
||||
- 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).
|
||||
|
||||
|
|
|
@ -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.
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = true, Position = 0)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
public string Name { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
@ -53,6 +54,7 @@ namespace PSRule.Commands
|
|||
/// Deployments that this deployment depends on.
|
||||
/// </summary>
|
||||
[Parameter(Mandatory = false)]
|
||||
[ValidateNotNullOrEmpty()]
|
||||
public string[] DependsOn { get; set; }
|
||||
|
||||
/// <summary>
|
||||
|
@ -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)
|
||||
|
|
|
@ -12,7 +12,7 @@ namespace PSRule.Pipeline
|
|||
|
||||
private readonly List<RuleRecord> _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<RuleRecord>();
|
||||
_Time = 0f;
|
||||
_Time = 0;
|
||||
_Total = 0;
|
||||
_Error = 0;
|
||||
_Fail = 0;
|
||||
}
|
||||
|
||||
internal float Time
|
||||
internal long Time
|
||||
{
|
||||
get { return _Time; }
|
||||
}
|
||||
|
|
|
@ -18,12 +18,12 @@ namespace PSRule.Pipeline
|
|||
{
|
||||
_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 error = o.Sum(r => r.Error);
|
||||
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($"<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)
|
||||
{
|
||||
_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)
|
||||
{
|
||||
|
@ -57,7 +57,7 @@ namespace PSRule.Pipeline
|
|||
|
||||
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
|
||||
|
@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -441,6 +441,7 @@ namespace PSRule.Pipeline
|
|||
|
||||
RuleBlock = ruleBlock;
|
||||
|
||||
// Starts rule execution timer
|
||||
_RuleTimer.Restart();
|
||||
|
||||
return RuleRecord;
|
||||
|
@ -451,9 +452,9 @@ namespace PSRule.Pipeline
|
|||
/// </summary>
|
||||
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;
|
||||
|
|
|
@ -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()
|
||||
{
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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' {
|
||||
$True;
|
||||
}
|
||||
|
||||
Rule 'WithDependency4' -DependsOn 'WithDependency5' {
|
||||
$True;
|
||||
}
|
||||
|
|
|
@ -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,11 +1100,9 @@ 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');
|
||||
Describe 'Rules' -Tag 'Common', 'Rules' {
|
||||
$testObject = [PSCustomObject]@{
|
||||
Name = 'TestObject1'
|
||||
Value = 1
|
||||
|
@ -1108,7 +1113,20 @@ Describe 'Rule processing' -Tag 'Common', 'RuleProcessing' {
|
|||
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
|
||||
|
|
Загрузка…
Ссылка в новой задаче