Skip to content

Commit f6900aa

Browse files
committed
Apply existing cmdlet tests to library usage
1 parent 1ae7634 commit f6900aa

6 files changed

+261
-72
lines changed

Engine/Commands/InvokeScriptAnalyzerCommand.cs

Lines changed: 7 additions & 65 deletions
Original file line numberDiff line numberDiff line change
@@ -128,11 +128,10 @@ public SwitchParameter SuppressedOnly
128128
#region Private Members
129129

130130
Dictionary<string, List<string>> validationResults = new Dictionary<string, List<string>>();
131-
private IEnumerable<IRule> rules = null;
132131

133132
#endregion
134133

135-
#region Ovserrides
134+
#region Overrides
136135

137136
/// <summary>
138137
/// Imports all known rules and loggers.
@@ -146,19 +145,6 @@ protected override void BeginProcessing()
146145
this.excludeRule,
147146
this.severity,
148147
this.suppressedOnly);
149-
150-
#region Verify rules
151-
152-
rules = ScriptAnalyzer.Instance.ScriptRules.Union<IRule>(
153-
ScriptAnalyzer.Instance.TokenRules).Union<IRule>(
154-
ScriptAnalyzer.Instance.ExternalRules ?? Enumerable.Empty<IExternalRule>());
155-
156-
if (rules == null || rules.Count() == 0)
157-
{
158-
ThrowTerminatingError(new ErrorRecord(new Exception(), string.Format(CultureInfo.CurrentCulture, Strings.RulesNotFound), ErrorCategory.ResourceExists, this));
159-
}
160-
161-
#endregion
162148
}
163149

164150
/// <summary>
@@ -177,61 +163,17 @@ protected override void ProcessRecord()
177163

178164
private void ProcessPath(string path)
179165
{
180-
const string ps1Suffix = "ps1";
181-
const string psm1Suffix = "psm1";
182-
const string psd1Suffix = "psd1";
183-
184-
if (path == null)
185-
{
186-
ThrowTerminatingError(new ErrorRecord(new FileNotFoundException(),
187-
string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, path),
188-
ErrorCategory.InvalidArgument, this));
189-
}
166+
IEnumerable<DiagnosticRecord> diagnosticsList =
167+
ScriptAnalyzer.Instance.AnalyzePath(path, this.recurse);
190168

191-
if (Directory.Exists(path))
169+
//Output through loggers
170+
foreach (ILogger logger in ScriptAnalyzer.Instance.Loggers)
192171
{
193-
if (recurse)
172+
foreach (DiagnosticRecord diagnostic in diagnosticsList)
194173
{
195-
196-
foreach (string filePath in Directory.GetFiles(path))
197-
{
198-
ProcessPath(filePath);
199-
}
200-
foreach (string filePath in Directory.GetDirectories(path))
201-
{
202-
ProcessPath(filePath);
203-
}
204-
}
205-
else
206-
{
207-
foreach (string filePath in Directory.GetFiles(path))
208-
{
209-
ProcessPath(filePath);
210-
}
211-
}
212-
}
213-
else if (File.Exists(path))
214-
{
215-
if ((path.Length > ps1Suffix.Length && path.Substring(path.Length - ps1Suffix.Length).Equals(ps1Suffix, StringComparison.OrdinalIgnoreCase)) ||
216-
(path.Length > psm1Suffix.Length && path.Substring(path.Length - psm1Suffix.Length).Equals(psm1Suffix, StringComparison.OrdinalIgnoreCase)) ||
217-
(path.Length > psd1Suffix.Length && path.Substring(path.Length - psd1Suffix.Length).Equals(psd1Suffix, StringComparison.OrdinalIgnoreCase)))
218-
{
219-
IEnumerable<DiagnosticRecord> diagnosticsList = ScriptAnalyzer.Instance.AnalyzeFile(path);
220-
221-
//Output through loggers
222-
foreach (ILogger logger in ScriptAnalyzer.Instance.Loggers)
223-
{
224-
foreach (DiagnosticRecord diagnostic in diagnosticsList)
225-
{
226-
logger.LogObject(diagnostic, this);
227-
}
228-
}
174+
logger.LogObject(diagnostic, this);
229175
}
230176
}
231-
else
232-
{
233-
WriteError(new ErrorRecord(new FileNotFoundException(), string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, path), ErrorCategory.InvalidArgument, this));
234-
}
235177
}
236178

237179
#endregion

Engine/ScriptAnalyzer.cs

Lines changed: 110 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -36,7 +36,6 @@ public sealed class ScriptAnalyzer
3636
private IOutputWriter outputWriter;
3737
private CompositionContainer container;
3838
Dictionary<string, List<string>> validationResults = new Dictionary<string, List<string>>();
39-
Dictionary<string, List<RuleSuppression>> ruleSuppressions;
4039
string[] includeRule;
4140
string[] excludeRule;
4241
string[] severity;
@@ -211,6 +210,28 @@ private void Initialize(
211210
}
212211

213212
#endregion
213+
214+
#region Verify rules
215+
216+
IEnumerable<IRule> rules =
217+
this.ScriptRules.Union<IRule>(
218+
this.TokenRules).Union<IRule>(
219+
this.ExternalRules ?? Enumerable.Empty<IExternalRule>());
220+
221+
// Ensure that rules were actually loaded
222+
if (rules == null || rules.Count() == 0)
223+
{
224+
this.outputWriter.ThrowTerminatingError(
225+
new ErrorRecord(
226+
new Exception(),
227+
string.Format(
228+
CultureInfo.CurrentCulture,
229+
Strings.RulesNotFound),
230+
ErrorCategory.ResourceExists,
231+
this));
232+
}
233+
234+
#endregion
214235
}
215236

216237
private List<string> GetValidCustomRulePaths(string[] customizedRulePath, PathIntrinsics path)
@@ -692,11 +713,95 @@ public Dictionary<string, List<string>> CheckRuleExtension(string[] path, PathIn
692713

693714

694715
/// <summary>
695-
/// Analyzes a single script file.
716+
/// Analyzes a script file or a directory containing script files.
696717
/// </summary>
697-
/// <param name="filePath">The path of the file to analyze.</param>
718+
/// <param name="path">The path of the file or directory to analyze.</param>
719+
/// <param name="searchRecursively">
720+
/// If true, recursively searches the given file path and analyzes any
721+
/// script files that are found.
722+
/// </param>
698723
/// <returns>An enumeration of DiagnosticRecords that were found by rules.</returns>
699-
public IEnumerable<DiagnosticRecord> AnalyzeFile(string filePath)
724+
public IEnumerable<DiagnosticRecord> AnalyzePath(string path, bool searchRecursively = false)
725+
{
726+
List<string> scriptFilePaths = new List<string>();
727+
728+
if (path == null)
729+
{
730+
this.outputWriter.ThrowTerminatingError(
731+
new ErrorRecord(
732+
new FileNotFoundException(),
733+
string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, path),
734+
ErrorCategory.InvalidArgument,
735+
this));
736+
}
737+
738+
// Precreate the list of script file paths to analyze. This
739+
// is an optimization over doing the whole operation at once
740+
// and calling .Concat on IEnumerables to join results.
741+
this.BuildScriptPathList(path, searchRecursively, scriptFilePaths);
742+
743+
foreach (string scriptFilePath in scriptFilePaths)
744+
{
745+
// Yield each record in the result so that the
746+
// caller can pull them one at a time
747+
foreach (var diagnosticRecord in this.AnalyzeFile(scriptFilePath))
748+
{
749+
yield return diagnosticRecord;
750+
}
751+
}
752+
}
753+
754+
private void BuildScriptPathList(
755+
string path,
756+
bool searchRecursively,
757+
IList<string> scriptFilePaths)
758+
{
759+
const string ps1Suffix = "ps1";
760+
const string psm1Suffix = "psm1";
761+
const string psd1Suffix = "psd1";
762+
763+
if (Directory.Exists(path))
764+
{
765+
if (searchRecursively)
766+
{
767+
foreach (string filePath in Directory.GetFiles(path))
768+
{
769+
this.BuildScriptPathList(filePath, searchRecursively, scriptFilePaths);
770+
}
771+
foreach (string filePath in Directory.GetDirectories(path))
772+
{
773+
this.BuildScriptPathList(filePath, searchRecursively, scriptFilePaths);
774+
}
775+
}
776+
else
777+
{
778+
foreach (string filePath in Directory.GetFiles(path))
779+
{
780+
this.BuildScriptPathList(filePath, searchRecursively, scriptFilePaths);
781+
}
782+
}
783+
}
784+
else if (File.Exists(path))
785+
{
786+
if ((path.Length > ps1Suffix.Length && path.Substring(path.Length - ps1Suffix.Length).Equals(ps1Suffix, StringComparison.OrdinalIgnoreCase)) ||
787+
(path.Length > psm1Suffix.Length && path.Substring(path.Length - psm1Suffix.Length).Equals(psm1Suffix, StringComparison.OrdinalIgnoreCase)) ||
788+
(path.Length > psd1Suffix.Length && path.Substring(path.Length - psd1Suffix.Length).Equals(psd1Suffix, StringComparison.OrdinalIgnoreCase)))
789+
{
790+
scriptFilePaths.Add(path);
791+
}
792+
}
793+
else
794+
{
795+
this.outputWriter.WriteError(
796+
new ErrorRecord(
797+
new FileNotFoundException(),
798+
string.Format(CultureInfo.CurrentCulture, Strings.FileNotFound, path),
799+
ErrorCategory.InvalidArgument,
800+
this));
801+
}
802+
}
803+
804+
private IEnumerable<DiagnosticRecord> AnalyzeFile(string filePath)
700805
{
701806
ScriptBlockAst scriptAst = null;
702807
Token[] scriptTokens = null;
@@ -750,6 +855,7 @@ public IEnumerable<DiagnosticRecord> AnalyzeSyntaxTree(
750855
Token[] scriptTokens,
751856
string filePath)
752857
{
858+
Dictionary<string, List<RuleSuppression>> ruleSuppressions;
753859
ConcurrentBag<DiagnosticRecord> diagnostics = new ConcurrentBag<DiagnosticRecord>();
754860
ConcurrentBag<SuppressedRecord> suppressed = new ConcurrentBag<SuppressedRecord>();
755861
BlockingCollection<List<object>> verboseOrErrors = new BlockingCollection<List<object>>();

Tests/Engine/CustomizedRule.tests.ps1

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
Import-Module PSScriptAnalyzer
1+
# Check if PSScriptAnalyzer is already loaded so we don't
2+
# overwrite a test version of Invoke-ScriptAnalyzer by
3+
# accident
4+
if (!(Get-Module PSScriptAnalyzer) -and !$testingLibraryUsage)
5+
{
6+
Import-Module PSScriptAnalyzer
7+
}
8+
29
$directory = Split-Path -Parent $MyInvocation.MyCommand.Path
310
$message = "this is help"
411
$measure = "Measure-RequiresRunAsAdministrator"

Tests/Engine/InvokeScriptAnalyzer.tests.ps1

Lines changed: 8 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,11 @@
1-
Import-Module PSScriptAnalyzer
1+
# Check if PSScriptAnalyzer is already loaded so we don't
2+
# overwrite a test version of Invoke-ScriptAnalyzer by
3+
# accident
4+
if (!(Get-Module PSScriptAnalyzer) -and !$testingLibraryUsage)
5+
{
6+
Import-Module PSScriptAnalyzer
7+
}
8+
29
$sa = Get-Command Invoke-ScriptAnalyzer
310
$directory = Split-Path -Parent $MyInvocation.MyCommand.Path
411
$singularNouns = "PSUseSingularNouns"

Tests/Engine/LibraryUsage.tests.ps1

Lines changed: 120 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,120 @@
1+
Import-Module PSScriptAnalyzer
2+
3+
$directory = Split-Path -Parent $MyInvocation.MyCommand.Path
4+
5+
# Overwrite Invoke-ScriptAnalyzer with a version that
6+
# wraps the usage of ScriptAnalyzer as a .NET library
7+
function Invoke-ScriptAnalyzer {
8+
param (
9+
[parameter(Mandatory = $true, Position = 0)]
10+
[Alias("PSPath")]
11+
[string] $Path,
12+
13+
[Parameter(Mandatory = $false)]
14+
[string[]] $CustomizedRulePath = $null,
15+
16+
[Parameter(Mandatory=$false)]
17+
[string[]] $ExcludeRule = $null,
18+
19+
[Parameter(Mandatory = $false)]
20+
[string[]] $IncludeRule = $null,
21+
22+
[ValidateSet("Warning", "Error", "Information", IgnoreCase = $true)]
23+
[Parameter(Mandatory = $false)]
24+
[string[]] $Severity = $null,
25+
26+
[Parameter(Mandatory = $false)]
27+
[switch] $Recurse,
28+
29+
[Parameter(Mandatory = $false)]
30+
[switch] $SuppressedOnly
31+
)
32+
33+
$scriptAnalyzer = New-Object "Microsoft.Windows.PowerShell.ScriptAnalyzer.ScriptAnalyzer"
34+
$scriptAnalyzer.Initialize(
35+
$runspace,
36+
$outputWriter,
37+
$CustomizedRulePath,
38+
$IncludeRule,
39+
$ExcludeRule,
40+
$Severity,
41+
$SuppressedOnly.IsPresent
42+
);
43+
44+
return $scriptAnalyzer.AnalyzePath($Path, $Recurse.IsPresent);
45+
}
46+
47+
# Define an implementation of the IOutputWriter interface
48+
Add-Type -Language CSharp @"
49+
using System.Management.Automation;
50+
using System.Management.Automation.Host;
51+
using Microsoft.Windows.PowerShell.ScriptAnalyzer;
52+
53+
public class PesterTestOutputWriter : IOutputWriter
54+
{
55+
private PSHost psHost;
56+
57+
public static PesterTestOutputWriter Create(PSHost psHost)
58+
{
59+
PesterTestOutputWriter outputWriter = new PesterTestOutputWriter();
60+
outputWriter.psHost = psHost;
61+
return outputWriter;
62+
}
63+
64+
// NOTE: We don't implement any Write methods to prevent console spew
65+
66+
public void WriteError(ErrorRecord error)
67+
{
68+
}
69+
70+
public void WriteWarning(string message)
71+
{
72+
// Some tests look for warning messages, so write those out
73+
psHost.UI.WriteWarningLine(message);
74+
}
75+
76+
public void WriteVerbose(string message)
77+
{
78+
}
79+
80+
public void WriteDebug(string message)
81+
{
82+
psHost.UI.WriteDebugLine(message);
83+
}
84+
85+
public void ThrowTerminatingError(ErrorRecord record)
86+
{
87+
throw new RuntimeException(
88+
"Test failed due to terminating error: \r\n" + record.ToString(),
89+
null,
90+
record);
91+
}
92+
}
93+
"@ -ReferencedAssemblies "Microsoft.Windows.PowerShell.ScriptAnalyzer" -ErrorAction SilentlyContinue
94+
95+
if ($outputWriter -eq $null)
96+
{
97+
$outputWriter = [PesterTestOutputWriter]::Create($Host);
98+
}
99+
100+
# Create a fresh runspace to pass into the ScriptAnalyzer class
101+
$initialSessionState = [System.Management.Automation.Runspaces.InitialSessionState]::CreateDefault2();
102+
$runspace = [System.Management.Automation.Runspaces.RunspaceFactory]::CreateRunspace([System.Management.Automation.Host.PSHost]$Host, [System.Management.Automation.Runspaces.InitialSessionState]$initialSessionState);
103+
$runspace.Open();
104+
105+
# Let other test scripts know we are testing library usage now
106+
$testingLibraryUsage = $true
107+
108+
# Invoke existing test files that use Invoke-ScriptAnalyzer
109+
. $directory\InvokeScriptAnalyzer.tests.ps1
110+
. $directory\RuleSuppression.tests.ps1
111+
. $directory\CustomizedRule.tests.ps1
112+
113+
# We're done testing library usage
114+
$testingLibraryUsage = $false
115+
116+
# Clean up the test runspace
117+
$runspace.Dispose();
118+
119+
# Re-import the PSScriptAnalyzer module to overwrite the library test cmdlet
120+
Import-Module PSScriptAnalyzer

0 commit comments

Comments
 (0)