diff --git a/InvokePesterStub.ps1 b/InvokePesterStub.ps1 new file mode 100755 index 0000000000..8b6f204cce --- /dev/null +++ b/InvokePesterStub.ps1 @@ -0,0 +1,82 @@ +#!/usr/bin/env pwsh + +<# +.SYNOPSIS + Stub around Invoke-Pester command used by VSCode PowerShell extension. +.DESCRIPTION + The stub checks the version of Pester and if >= 4.6.0, invokes Pester + using the LineNumber parameter (if specified). Otherwise, it invokes + using the TestName parameter (if specified). If the All parameter + is specified, then all the tests are invoked in the specifed file. + Finally, if none of these three parameters are specified, all tests + are invoked and a warning is issued indicating what the user can do + to allow invocation of individual Describe blocks. +.EXAMPLE + PS C:\> .\InvokePesterStub.ps1 ~\project\test\foo.tests.ps1 -LineNumber 14 + Invokes a specific test by line number in the specified file. +.EXAMPLE + PS C:\> .\InvokePesterStub.ps1 ~\project\test\foo.tests.ps1 -TestName 'Foo Tests' + Invokes a specific test by test name in the specified file. +.EXAMPLE + PS C:\> .\InvokePesterStub.ps1 ~\project\test\foo.tests.ps1 -All + Invokes all tests in the specified file. +.INPUTS + None +.OUTPUTS + None +#> +param( + # Specifies the path to the test script. + [Parameter(Position=0, Mandatory)] + [ValidateNotNullOrEmpty()] + [string] + $ScriptPath, + + # Specifies the name of the test taken from the Describe block's name. + [Parameter()] + [string] + $TestName, + + # Specifies the starting line number of the DescribeBlock. This feature requires + # Pester 4.6.0 or higher. + [Parameter()] + [ValidatePattern('\d*')] + [string] + $LineNumber, + + # If specified, executes all the tests in the specified test script. + [Parameter()] + [switch] + $All +) + +$pesterModule = Microsoft.PowerShell.Core\Get-Module Pester +if (!$pesterModule) { + Write-Output "Importing Pester module..." + $pesterModule = Microsoft.PowerShell.Core\Import-Module Pester -ErrorAction Ignore -PassThru + if (!$pesterModule) { + # If we still don't have an imported Pester module, that is (most likely) because Pester is not installed. + Write-Warning "Failed to import the Pester module. You must install Pester to run or debug Pester tests." + Write-Warning "You can install Pester by executing: Install-Module Pester -Scope CurrentUser -Force" + return + } +} + +if ($All) { + Pester\Invoke-Pester -Script $ScriptPath -PesterOption @{IncludeVSCodeMarker=$true} +} +elseif ($TestName) { + Pester\Invoke-Pester -Script $ScriptPath -PesterOption @{IncludeVSCodeMarker=$true} -TestName $TestName +} +elseif (($LineNumber -match '\d+') -and ($pesterModule.Version -ge '4.6.0')) { + Pester\Invoke-Pester -Script $ScriptPath -PesterOption (New-PesterOption -ScriptBlockFilter @{ + IncludeVSCodeMarker=$true; Line=$LineNumber; Path=$ScriptPath}) +} +else { + # We get here when the TestName expression is of type ExpandableStringExpressionAst. + # PSES will not attempt to "evaluate" the expression so it returns null for the TestName. + Write-Warning "The Describe block's TestName cannot be evaluated. EXECUTING ALL TESTS instead." + Write-Warning "To avoid this, install Pester >= 4.6.0 or remove any expressions in the TestName." + + Pester\Invoke-Pester -Script $ScriptPath -PesterOption @{IncludeVSCodeMarker=$true} +} diff --git a/src/features/PesterTests.ts b/src/features/PesterTests.ts index ca950523cf..ea2c5fea9f 100644 --- a/src/features/PesterTests.ts +++ b/src/features/PesterTests.ts @@ -18,8 +18,11 @@ export class PesterTestsFeature implements IFeature { private command: vscode.Disposable; private languageClient: LanguageClient; + private invokePesterStubScriptPath: string; constructor(private sessionManager: SessionManager) { + this.invokePesterStubScriptPath = path.resolve(__dirname, "../../../InvokePesterStub.ps1"); + // File context-menu command - Run Pester Tests this.command = vscode.commands.registerCommand( "PowerShell.RunPesterTestsFromFile", @@ -35,8 +38,8 @@ export class PesterTestsFeature implements IFeature { // This command is provided for usage by PowerShellEditorServices (PSES) only this.command = vscode.commands.registerCommand( "PowerShell.RunPesterTests", - (uriString, runInDebugger, describeBlockName?) => { - this.launchTests(uriString, runInDebugger, describeBlockName); + (uriString, runInDebugger, describeBlockName?, describeBlockLineNumber?) => { + this.launchTests(uriString, runInDebugger, describeBlockName, describeBlockLineNumber); }); } @@ -51,38 +54,22 @@ export class PesterTestsFeature implements IFeature { private launchAllTestsInActiveEditor(launchType: LaunchType) { const uriString = vscode.window.activeTextEditor.document.uri.toString(); const launchConfig = this.createLaunchConfig(uriString, launchType); + launchConfig.args.push("-All"); this.launch(launchConfig); } - private async launchTests(uriString: string, runInDebugger: boolean, describeBlockName?: string) { - // PSES passes null for the describeBlockName to signal that it can't evaluate the TestName. - if (!describeBlockName) { - const answer = await vscode.window.showErrorMessage( - "This Describe block's TestName parameter cannot be evaluated. " + - `Would you like to ${runInDebugger ? "debug" : "run"} all the tests in this file?`, - "Yes", "No"); - - if (answer !== "Yes") { - return; - } - } + private async launchTests( + uriString: string, + runInDebugger: boolean, + describeBlockName?: string, + describeBlockLineNumber?: number) { const launchType = runInDebugger ? LaunchType.Debug : LaunchType.Run; - const launchConfig = this.createLaunchConfig(uriString, launchType); - - if (describeBlockName) { - launchConfig.args.push("-TestName"); - // Escape single quotes inside double quotes by doubling them up - if (describeBlockName.includes("'")) { - describeBlockName = describeBlockName.replace(/'/g, "''"); - } - launchConfig.args.push(`'${describeBlockName}'`); - } - + const launchConfig = this.createLaunchConfig(uriString, launchType, describeBlockName, describeBlockLineNumber); this.launch(launchConfig); } - private createLaunchConfig(uriString: string, launchType: LaunchType) { + private createLaunchConfig(uriString: string, launchType: LaunchType, testName?: string, lineNum?: number) { const uri = vscode.Uri.parse(uriString); const currentDocument = vscode.window.activeTextEditor.document; const settings = Settings.load(); @@ -95,12 +82,10 @@ export class PesterTestsFeature implements IFeature { request: "launch", type: "PowerShell", name: "PowerShell Launch Pester Tests", - script: "Invoke-Pester", + script: this.invokePesterStubScriptPath, args: [ - "-Script", + "-ScriptPath", `'${scriptPath}'`, - "-PesterOption", - "@{IncludeVSCodeMarker=$true}", ], internalConsoleOptions: "neverOpen", noDebug: (launchType === LaunchType.Run), @@ -111,6 +96,19 @@ export class PesterTestsFeature implements IFeature { : path.dirname(currentDocument.fileName), }; + if (lineNum) { + launchConfig.args.push("-LineNumber", `${lineNum}`); + } + + if (testName) { + // Escape single quotes inside double quotes by doubling them up + if (testName.includes("'")) { + testName = testName.replace(/'/g, "''"); + } + + launchConfig.args.push("-TestName", `'${testName}'`); + } + return launchConfig; }