Skip to content

Commit 10cff26

Browse files
Merge pull request #1830 from PowerShell/andschwa/untitled-tests
Add regression test for untitled scripts in Windows PowerShell
2 parents 5f08df6 + bb9cdfe commit 10cff26

File tree

11 files changed

+90
-44
lines changed

11 files changed

+90
-44
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/Handlers/ConfigurationDoneHandler.cs

+10-4
Original file line numberDiff line numberDiff line change
@@ -105,10 +105,12 @@ public Task<ConfigurationDoneResponse> Handle(ConfigurationDoneArguments request
105105
private async Task LaunchScriptAsync(string scriptToLaunch)
106106
{
107107
PSCommand command;
108-
if (ScriptFile.IsUntitledPath(scriptToLaunch))
108+
// Script could an actual script, or a URI to a script file (or untitled document).
109+
if (!System.Uri.IsWellFormedUriString(scriptToLaunch, System.UriKind.RelativeOrAbsolute)
110+
|| ScriptFile.IsUntitledPath(scriptToLaunch))
109111
{
110-
ScriptFile untitledScript = _workspaceService.GetFile(scriptToLaunch);
111-
if (BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace))
112+
bool isScriptFile = _workspaceService.TryGetFile(scriptToLaunch, out ScriptFile untitledScript);
113+
if (isScriptFile && BreakpointApiUtils.SupportsBreakpointApis(_runspaceContext.CurrentRunspace))
112114
{
113115
// Parse untitled files with their `Untitled:` URI as the filename which will
114116
// cache the URI and contents within the PowerShell parser. By doing this, we
@@ -138,7 +140,11 @@ private async Task LaunchScriptAsync(string scriptToLaunch)
138140
// Command breakpoints and `Wait-Debugger` will work. We must wrap the script
139141
// with newlines so that any included comments don't break the command.
140142
command = PSCommandHelpers.BuildDotSourceCommandWithArguments(
141-
string.Concat("{\n", untitledScript.Contents, "\n}"), _debugStateService.Arguments);
143+
string.Concat(
144+
"{" + System.Environment.NewLine,
145+
isScriptFile ? untitledScript.Contents : scriptToLaunch,
146+
System.Environment.NewLine + "}"),
147+
_debugStateService.Arguments);
142148
}
143149
}
144150
else

src/PowerShellEditorServices/Services/Symbols/SymbolsService.cs

+5-8
Original file line numberDiff line numberDiff line change
@@ -427,13 +427,10 @@ public async Task<SymbolReference> GetDefinitionOfSymbolAsync(
427427
SymbolReference foundDefinition = null;
428428
foreach (ScriptFile scriptFile in referencedFiles)
429429
{
430-
foundDefinition =
431-
AstOperations.FindDefinitionOfSymbol(
432-
scriptFile.ScriptAst,
433-
foundSymbol);
430+
foundDefinition = AstOperations.FindDefinitionOfSymbol(scriptFile.ScriptAst, foundSymbol);
434431

435432
filesSearched.Add(scriptFile.FilePath);
436-
if (foundDefinition != null)
433+
if (foundDefinition is not null)
437434
{
438435
foundDefinition.FilePath = scriptFile.FilePath;
439436
break;
@@ -453,7 +450,7 @@ public async Task<SymbolReference> GetDefinitionOfSymbolAsync(
453450

454451
// if the definition the not found in referenced files
455452
// look for it in all the files in the workspace
456-
if (foundDefinition == null)
453+
if (foundDefinition is null)
457454
{
458455
// Get a list of all powershell files in the workspace path
459456
foreach (string file in _workspaceService.EnumeratePSFiles())
@@ -469,7 +466,7 @@ public async Task<SymbolReference> GetDefinitionOfSymbolAsync(
469466
foundSymbol);
470467

471468
filesSearched.Add(file);
472-
if (foundDefinition != null)
469+
if (foundDefinition is not null)
473470
{
474471
foundDefinition.FilePath = file;
475472
break;
@@ -480,7 +477,7 @@ public async Task<SymbolReference> GetDefinitionOfSymbolAsync(
480477
// if the definition is not found in a file in the workspace
481478
// look for it in the builtin commands but only if the symbol
482479
// we are looking at is possibly a Function.
483-
if (foundDefinition == null
480+
if (foundDefinition is null
484481
&& (foundSymbol.SymbolType == SymbolType.Function
485482
|| foundSymbol.SymbolType == SymbolType.Unknown))
486483
{

src/PowerShellEditorServices/Services/Symbols/Vistors/AstOperations.cs

+1-4
Original file line numberDiff line numberDiff line change
@@ -215,11 +215,8 @@ public static SymbolReference FindDefinitionOfSymbol(
215215
Ast scriptAst,
216216
SymbolReference symbolReference)
217217
{
218-
FindDeclarationVisitor declarationVisitor =
219-
new(
220-
symbolReference);
218+
FindDeclarationVisitor declarationVisitor = new(symbolReference);
221219
scriptAst.Visit(declarationVisitor);
222-
223220
return declarationVisitor.FoundDeclaration;
224221
}
225222

src/PowerShellEditorServices/Services/TextDocument/ScriptFile.cs

+4-5
Original file line numberDiff line numberDiff line change
@@ -195,18 +195,17 @@ internal static List<string> GetLinesInternal(string text)
195195
}
196196

197197
/// <summary>
198-
/// Deterines whether the supplied path indicates the file is an "untitled:Unitled-X"
198+
/// Determines whether the supplied path indicates the file is an "untitled:Untitled-X"
199199
/// which has not been saved to file.
200200
/// </summary>
201201
/// <param name="path">The path to check.</param>
202202
/// <returns>True if the path is an untitled file, false otherwise.</returns>
203203
internal static bool IsUntitledPath(string path)
204204
{
205205
Validate.IsNotNull(nameof(path), path);
206-
return !string.Equals(
207-
DocumentUri.From(path).Scheme,
208-
Uri.UriSchemeFile,
209-
StringComparison.OrdinalIgnoreCase);
206+
// This may not have been given a URI, so return false instead of throwing.
207+
return Uri.IsWellFormedUriString(path, UriKind.RelativeOrAbsolute) &&
208+
!string.Equals(DocumentUri.From(path).Scheme, Uri.UriSchemeFile, StringComparison.OrdinalIgnoreCase);
210209
}
211210

212211
/// <summary>

src/PowerShellEditorServices/Services/Workspace/WorkspaceService.cs

+18-7
Original file line numberDiff line numberDiff line change
@@ -2,17 +2,17 @@
22
// Licensed under the MIT License.
33

44
using System;
5+
using System.Collections.Concurrent;
56
using System.Collections.Generic;
6-
using System.Linq;
77
using System.IO;
8+
using System.Linq;
89
using System.Security;
910
using System.Text;
1011
using Microsoft.Extensions.FileSystemGlobbing;
1112
using Microsoft.Extensions.Logging;
12-
using Microsoft.PowerShell.EditorServices.Utility;
13-
using Microsoft.PowerShell.EditorServices.Services.Workspace;
1413
using Microsoft.PowerShell.EditorServices.Services.TextDocument;
15-
using System.Collections.Concurrent;
14+
using Microsoft.PowerShell.EditorServices.Services.Workspace;
15+
using Microsoft.PowerShell.EditorServices.Utility;
1616
using OmniSharp.Extensions.LanguageServer.Protocol;
1717

1818
namespace Microsoft.PowerShell.EditorServices.Services
@@ -152,8 +152,19 @@ public ScriptFile GetFile(DocumentUri documentUri)
152152
/// </summary>
153153
/// <param name="filePath">The file path at which the script resides.</param>
154154
/// <param name="scriptFile">The out parameter that will contain the ScriptFile object.</param>
155-
public bool TryGetFile(string filePath, out ScriptFile scriptFile) =>
156-
TryGetFile(new Uri(filePath), out scriptFile);
155+
public bool TryGetFile(string filePath, out ScriptFile scriptFile)
156+
{
157+
// This might not have been given a file path, in which case the Uri constructor barfs.
158+
try
159+
{
160+
return TryGetFile(new Uri(filePath), out scriptFile);
161+
}
162+
catch (UriFormatException)
163+
{
164+
scriptFile = null;
165+
return false;
166+
}
167+
}
157168

158169
/// <summary>
159170
/// Tries to get an open file in the workspace. Returns true if it succeeds, false otherwise.
@@ -301,7 +312,7 @@ public ScriptFile[] ExpandScriptReferences(ScriptFile scriptFile)
301312
referencedScriptFiles.Add(scriptFile.Id, scriptFile);
302313
RecursivelyFindReferences(scriptFile, referencedScriptFiles);
303314

304-
// remove original file from referened file and add it as the first element of the
315+
// remove original file from referenced file and add it as the first element of the
305316
// expanded referenced list to maintain order so the original file is always first in the list
306317
referencedScriptFiles.Remove(scriptFile.Id);
307318
expandedReferences.Add(scriptFile);

test/PowerShellEditorServices.Test.E2E/DebugAdapterClientExtensions.cs

+2-2
Original file line numberDiff line numberDiff line change
@@ -12,13 +12,13 @@ namespace PowerShellEditorServices.Test.E2E
1212
{
1313
public static class DebugAdapterClientExtensions
1414
{
15-
public static async Task LaunchScript(this DebugAdapterClient debugAdapterClient, string filePath, TaskCompletionSource<object> started)
15+
public static async Task LaunchScript(this DebugAdapterClient debugAdapterClient, string script, TaskCompletionSource<object> started)
1616
{
1717
LaunchResponse launchResponse = await debugAdapterClient.Launch(
1818
new PsesLaunchRequestArguments
1919
{
2020
NoDebug = false,
21-
Script = filePath,
21+
Script = script,
2222
Cwd = "",
2323
CreateTemporaryIntegratedConsole = false
2424
}).ConfigureAwait(true);

test/PowerShellEditorServices.Test.E2E/DebugAdapterProtocolMessageTests.cs

+36-3
Original file line numberDiff line numberDiff line change
@@ -127,7 +127,13 @@ private string GenerateScriptFromLoggingStatements(params string[] logStatements
127127
throw new ArgumentNullException(nameof(logStatements), "Expected at least one argument.");
128128
}
129129

130-
// Have script create/overwrite file first with `>`.
130+
// Clean up side effects from other test runs.
131+
if (File.Exists(s_testOutputPath))
132+
{
133+
File.Delete(s_testOutputPath);
134+
}
135+
136+
// Have script create file first with `>` (but don't rely on overwriting).
131137
StringBuilder builder = new StringBuilder().Append('\'').Append(logStatements[0]).Append("' > '").Append(s_testOutputPath).AppendLine("'");
132138
for (int i = 1; i < logStatements.Length; i++)
133139
{
@@ -177,7 +183,7 @@ public async Task CanLaunchScriptWithNoBreakpointsAsync()
177183
public async Task CanSetBreakpointsAsync()
178184
{
179185
Skip.If(
180-
PsesStdioProcess.RunningInConstainedLanguageMode,
186+
PsesStdioProcess.RunningInConstrainedLanguageMode,
181187
"You can't set breakpoints in ConstrainedLanguage mode.");
182188

183189
string filePath = NewTestFile(GenerateScriptFromLoggingStatements(
@@ -254,7 +260,7 @@ public async Task CanSetBreakpointsAsync()
254260
public async Task CanStepPastSystemWindowsForms()
255261
{
256262
Skip.IfNot(PsesStdioProcess.IsWindowsPowerShell);
257-
Skip.If(PsesStdioProcess.RunningInConstainedLanguageMode);
263+
Skip.If(PsesStdioProcess.RunningInConstrainedLanguageMode);
258264

259265
string filePath = NewTestFile(string.Join(Environment.NewLine, new[]
260266
{
@@ -291,5 +297,32 @@ public async Task CanStepPastSystemWindowsForms()
291297
Assert.NotNull(form);
292298
Assert.Equal("System.Windows.Forms.Form, Text: ", form.Value);
293299
}
300+
301+
// This tests the edge-case where a raw script (or an untitled script) has the last line
302+
// commented. Since in some cases (such as Windows PowerShell, or the script not having a
303+
// backing ScriptFile) we just wrap the script with braces, we had a bug where the last
304+
// brace would be after the comment. We had to ensure we wrapped with newlines instead.
305+
[Trait("Category", "DAP")]
306+
[Fact]
307+
public async Task CanLaunchScriptWithCommentedLastLineAsync()
308+
{
309+
string script = GenerateScriptFromLoggingStatements("a log statement") + "# a comment at the end";
310+
Assert.Contains(Environment.NewLine + "# a comment", script);
311+
Assert.EndsWith("at the end", script);
312+
313+
// NOTE: This is horribly complicated, but the "script" parameter here is assigned to
314+
// PsesLaunchRequestArguments.Script, which is then assigned to
315+
// DebugStateService.ScriptToLaunch in that handler, and finally used by the
316+
// ConfigurationDoneHandler in LaunchScriptAsync.
317+
await PsesDebugAdapterClient.LaunchScript(script, Started).ConfigureAwait(false);
318+
319+
ConfigurationDoneResponse configDoneResponse = await PsesDebugAdapterClient.RequestConfigurationDone(new ConfigurationDoneArguments()).ConfigureAwait(false);
320+
Assert.NotNull(configDoneResponse);
321+
322+
// At this point the script should be running so lets give it time
323+
await Task.Delay(2000).ConfigureAwait(false);
324+
325+
Assert.Collection(GetLog(), (i) => Assert.Equal("a log statement", i));
326+
}
294327
}
295328
}

test/PowerShellEditorServices.Test.E2E/LanguageServerProtocolMessageTests.cs

+9-9
Original file line numberDiff line numberDiff line change
@@ -155,7 +155,7 @@ function CanSendWorkspaceSymbolRequest {
155155
public async Task CanReceiveDiagnosticsFromFileOpenAsync()
156156
{
157157
Skip.If(
158-
PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
158+
PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
159159
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
160160

161161
NewTestFile("$a = 4");
@@ -178,7 +178,7 @@ public async Task WontReceiveDiagnosticsFromFileOpenThatIsNotPowerShellAsync()
178178
public async Task CanReceiveDiagnosticsFromFileChangedAsync()
179179
{
180180
Skip.If(
181-
PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
181+
PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
182182
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
183183

184184
string filePath = NewTestFile("$a = 4");
@@ -230,7 +230,7 @@ public async Task CanReceiveDiagnosticsFromFileChangedAsync()
230230
public async Task CanReceiveDiagnosticsFromConfigurationChangeAsync()
231231
{
232232
Skip.If(
233-
PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
233+
PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
234234
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
235235

236236
NewTestFile("gci | % { $_ }");
@@ -331,7 +331,7 @@ await PsesLanguageClient
331331
public async Task CanSendFormattingRequestAsync()
332332
{
333333
Skip.If(
334-
PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
334+
PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
335335
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
336336

337337
string scriptPath = NewTestFile(@"
@@ -368,7 +368,7 @@ public async Task CanSendFormattingRequestAsync()
368368
public async Task CanSendRangeFormattingRequestAsync()
369369
{
370370
Skip.If(
371-
PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
371+
PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
372372
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
373373

374374
string scriptPath = NewTestFile(@"
@@ -892,7 +892,7 @@ function CanSendReferencesCodeLensRequest {
892892
public async Task CanSendCodeActionRequestAsync()
893893
{
894894
Skip.If(
895-
PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
895+
PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
896896
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
897897

898898
string filePath = NewTestFile("gci");
@@ -1090,7 +1090,7 @@ await PsesLanguageClient
10901090
[SkippableFact]
10911091
public async Task CanSendGetProjectTemplatesRequestAsync()
10921092
{
1093-
Skip.If(PsesStdioProcess.RunningInConstainedLanguageMode, "Plaster doesn't work in ConstrainedLanguage mode.");
1093+
Skip.If(PsesStdioProcess.RunningInConstrainedLanguageMode, "Plaster doesn't work in ConstrainedLanguage mode.");
10941094

10951095
GetProjectTemplatesResponse getProjectTemplatesResponse =
10961096
await PsesLanguageClient
@@ -1110,7 +1110,7 @@ await PsesLanguageClient
11101110
public async Task CanSendGetCommentHelpRequestAsync()
11111111
{
11121112
Skip.If(
1113-
PsesStdioProcess.RunningInConstainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
1113+
PsesStdioProcess.RunningInConstrainedLanguageMode && PsesStdioProcess.IsWindowsPowerShell,
11141114
"Windows PowerShell doesn't trust PSScriptAnalyzer by default so it won't load.");
11151115

11161116
string scriptPath = NewTestFile(@"
@@ -1183,7 +1183,7 @@ await PsesLanguageClient
11831183
public async Task CanSendExpandAliasRequestAsync()
11841184
{
11851185
Skip.If(
1186-
PsesStdioProcess.RunningInConstainedLanguageMode,
1186+
PsesStdioProcess.RunningInConstrainedLanguageMode,
11871187
"This feature currently doesn't support ConstrainedLanguage Mode.");
11881188

11891189
ExpandAliasResult expandAliasResult =

test/PowerShellEditorServices.Test.E2E/Processes/PsesStdioProcess.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -43,9 +43,10 @@ public class PsesStdioProcess : StdioServerProcess
4343

4444
#region public static properties
4545

46+
// NOTE: Just hard-code this to "powershell" when testing with the code lens.
4647
public static string PwshExe { get; } = Environment.GetEnvironmentVariable("PWSH_EXE_NAME") ?? "pwsh";
4748
public static bool IsWindowsPowerShell { get; } = PwshExe.Contains("powershell");
48-
public static bool RunningInConstainedLanguageMode { get; } =
49+
public static bool RunningInConstrainedLanguageMode { get; } =
4950
Environment.GetEnvironmentVariable("__PSLockdownPolicy", EnvironmentVariableTarget.Machine) != null;
5051

5152
#endregion

test/PowerShellEditorServices.Test/Language/SymbolsServiceTests.cs

+2-1
Original file line numberDiff line numberDiff line change
@@ -133,7 +133,7 @@ public async Task FindsFunctionDefinition()
133133
[Fact]
134134
public async Task FindsFunctionDefinitionForAlias()
135135
{
136-
// TODO: Eventually we should get the alises through the AST instead of relying on them
136+
// TODO: Eventually we should get the aliases through the AST instead of relying on them
137137
// being defined in the runspace.
138138
await psesHost.ExecutePSCommandAsync(
139139
new PSCommand().AddScript("Set-Alias -Name My-Alias -Value My-Function"),
@@ -184,6 +184,7 @@ public async Task FindsFunctionDefinitionInDotSourceReference()
184184
public async Task FindsDotSourcedFile()
185185
{
186186
SymbolReference definitionResult = await GetDefinition(FindsDotSourcedFileData.SourceDetails).ConfigureAwait(true);
187+
Assert.NotNull(definitionResult);
187188
Assert.True(
188189
definitionResult.FilePath.EndsWith(Path.Combine("References", "ReferenceFileE.ps1")),
189190
"Unexpected reference file: " + definitionResult.FilePath);

test/PowerShellEditorServices.Test/Session/ScriptFileTests.cs

+1
Original file line numberDiff line numberDiff line change
@@ -651,6 +651,7 @@ public void DocumentUriReturnsCorrectStringForAbsolutePath()
651651
[InlineData("vscode-notebook-cell:/Users/me/Documents/test.ps1#0001", true)]
652652
[InlineData("https://microsoft.com", true)]
653653
[InlineData("Untitled:Untitled-1", true)]
654+
[InlineData("'a log statement' > 'c:\\Users\\me\\Documents\\test.txt'\r\n", false)]
654655
public void IsUntitledFileIsCorrect(string path, bool expected) => Assert.Equal(expected, ScriptFile.IsUntitledPath(path));
655656
}
656657
}

0 commit comments

Comments
 (0)