Skip to content

Commit 39d26ee

Browse files
Merge pull request #1664 from PowerShell/andschwa/set-var
Fix `DebuggerSetsVariablesWithConversion` test
2 parents fdd8806 + ccddeba commit 39d26ee

File tree

4 files changed

+41
-47
lines changed

4 files changed

+41
-47
lines changed

src/PowerShellEditorServices/Services/DebugAdapter/DebugService.cs

Lines changed: 25 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,14 @@ public async Task<string> SetVariableAsync(int variableContainerReferenceId, str
400400
}
401401

402402
VariableDetailsBase variable = variableContainer.Children[name];
403-
// Determine scope in which the variable lives so we can pass it to `Get-Variable -Scope`.
404-
string scope = null; // TODO: Can this use a fancy pattern matcher?
403+
404+
// Determine scope in which the variable lives so we can pass it to `Get-Variable
405+
// -Scope`. The default is scope 0 which is safe because if a user is able to see a
406+
// variable in the debugger and so change it through this interface, it's either in the
407+
// top-most scope or in one of the following named scopes. The default scope is most
408+
// likely in the case of changing from the "auto variables" container.
409+
string scope = "0";
410+
// NOTE: This can't use a switch because the IDs aren't constant.
405411
if (variableContainerReferenceId == localScopeVariables.Id)
406412
{
407413
scope = VariableContainerDetails.LocalScopeName;
@@ -414,11 +420,6 @@ public async Task<string> SetVariableAsync(int variableContainerReferenceId, str
414420
{
415421
scope = VariableContainerDetails.GlobalScopeName;
416422
}
417-
else
418-
{
419-
// Hmm, this would be unexpected. No scope means do not pass GO, do not collect $200.
420-
throw new Exception("Could not find the scope for this variable.");
421-
}
422423

423424
// Now that we have the scope, get the associated PSVariable object for the variable to be set.
424425
var getVariableCommand = new PSCommand()
@@ -456,22 +457,25 @@ public async Task<string> SetVariableAsync(int variableContainerReferenceId, str
456457

457458
if (argTypeConverterAttr is not null)
458459
{
460+
// PSVariable *is* strongly typed, so we have to convert it.
459461
_logger.LogTrace($"Setting variable '{name}' using conversion to value: {expressionResult ?? "<null>"}");
460462

461-
// TODO: This is throwing a 'PSInvalidOperationException' thus causing
462-
// 'DebuggerSetsVariablesWithConversion' to fail.
463-
psVariable.Value = await _executionService.ExecuteDelegateAsync(
464-
"PS debugger argument converter",
465-
ExecutionOptions.Default,
466-
(pwsh, _) =>
467-
{
468-
var engineIntrinsics = (EngineIntrinsics)pwsh.Runspace.SessionStateProxy.GetVariable("ExecutionContext");
469-
470-
// TODO: This is almost (but not quite) the same as LanguagePrimitives.Convert(), which does not require the pipeline thread.
471-
// We should investigate changing it.
472-
return argTypeConverterAttr.Transform(engineIntrinsics, expressionResult);
473-
},
463+
// NOTE: We use 'Get-Variable' here instead of 'SessionStateProxy.GetVariable()'
464+
// because we already have a pipeline running (the debugger) and the latter cannot
465+
// run concurrently (threw 'NoSessionStateProxyWhenPipelineInProgress').
466+
IReadOnlyList<EngineIntrinsics> results = await _executionService.ExecutePSCommandAsync<EngineIntrinsics>(
467+
new PSCommand()
468+
.AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable")
469+
.AddParameter("Name", "ExecutionContext")
470+
.AddParameter("ValueOnly"),
474471
CancellationToken.None).ConfigureAwait(false);
472+
EngineIntrinsics engineIntrinsics = results.Count > 0
473+
? results[0]
474+
: throw new Exception("Couldn't get EngineIntrinsics!");
475+
476+
// TODO: This is almost (but not quite) the same as 'LanguagePrimitives.Convert()',
477+
// which does not require the pipeline thread. We should investigate changing it.
478+
psVariable.Value = argTypeConverterAttr.Transform(engineIntrinsics, expressionResult);
475479
}
476480
else
477481
{
@@ -641,7 +645,7 @@ private Task<VariableContainerDetails> FetchVariableContainerAsync(string scope)
641645

642646
private async Task<VariableContainerDetails> FetchVariableContainerAsync(string scope, bool autoVarsOnly)
643647
{
644-
PSCommand psCommand = new PSCommand().AddCommand("Get-Variable").AddParameter("Scope", scope);
648+
PSCommand psCommand = new PSCommand().AddCommand(@"Microsoft.PowerShell.Utility\Get-Variable").AddParameter("Scope", scope);
645649

646650
var scopeVariableContainer = new VariableContainerDetails(nextVariableId++, "Scope: " + scope);
647651
variables.Add(scopeVariableContainer);

test/PowerShellEditorServices.Test/Debugging/DebugServiceTests.cs

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -613,7 +613,7 @@ await debugService.SetLineBreakpointsAsync(
613613
Assert.Equal(newGlobalIntValue, intGlobalVar.ValueString);
614614
}
615615

616-
[Fact(Skip = "Variable conversion is broken")]
616+
[Fact]
617617
public async Task DebuggerSetsVariablesWithConversion()
618618
{
619619
await debugService.SetLineBreakpointsAsync(
@@ -628,7 +628,7 @@ await debugService.SetLineBreakpointsAsync(
628628
VariableDetailsBase[] variables = GetVariables(VariableContainerDetails.LocalScopeName);
629629

630630
// Test set of a local string variable (not strongly typed but force conversion)
631-
const string newStrValue = "False";
631+
const string newStrValue = "\"False\"";
632632
const string newStrExpr = "$false";
633633
VariableScope localScope = Array.Find(scopes, s => s.Name == VariableContainerDetails.LocalScopeName);
634634
string setStrValue = await debugService.SetVariableAsync(localScope.Id, "$strVar2", newStrExpr).ConfigureAwait(true);
@@ -658,8 +658,6 @@ await debugService.SetLineBreakpointsAsync(
658658
var strVar = Array.Find(variables, v => v.Name == "$strVar2");
659659
Assert.Equal(newStrValue, strVar.ValueString);
660660

661-
scopes = debugService.GetVariableScopes(0);
662-
663661
// Test set of script scope bool variable (strongly typed)
664662
variables = GetVariables(VariableContainerDetails.ScriptScopeName);
665663
var boolVar = Array.Find(variables, v => v.Name == "$scriptBool");

test/PowerShellEditorServices.Test/PsesHostFactory.cs

Lines changed: 13 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -20,21 +20,13 @@ internal static class PsesHostFactory
2020
// NOTE: These paths are arbitrarily chosen just to verify that the profile paths can be set
2121
// to whatever they need to be for the given host.
2222

23-
public static readonly ProfilePathInfo TestProfilePaths =
24-
new(
25-
Path.GetFullPath(
26-
TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Profile/Test.PowerShellEditorServices_profile.ps1")),
27-
Path.GetFullPath(
28-
TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Profile/ProfileTest.ps1")),
29-
Path.GetFullPath(
30-
TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Test.PowerShellEditorServices_profile.ps1")),
31-
Path.GetFullPath(
32-
TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/ProfileTest.ps1")));
23+
public static readonly ProfilePathInfo TestProfilePaths = new(
24+
Path.GetFullPath(TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Profile/Test.PowerShellEditorServices_profile.ps1")),
25+
Path.GetFullPath(TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Profile/ProfileTest.ps1")),
26+
Path.GetFullPath(TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/Test.PowerShellEditorServices_profile.ps1")),
27+
Path.GetFullPath(TestUtilities.NormalizePath("../../../../PowerShellEditorServices.Test.Shared/ProfileTest.ps1")));
3328

34-
public static readonly string BundledModulePath = Path.GetFullPath(
35-
TestUtilities.NormalizePath("../../../../../module"));
36-
37-
public static System.Management.Automation.Runspaces.Runspace InitialRunspace;
29+
public static readonly string BundledModulePath = Path.GetFullPath(TestUtilities.NormalizePath("../../../../../module"));
3830

3931
public static PsesInternalHost Create(ILoggerFactory loggerFactory)
4032
{
@@ -53,25 +45,24 @@ public static PsesInternalHost Create(ILoggerFactory loggerFactory)
5345
}
5446

5547
HostStartupInfo testHostDetails = new(
56-
"PowerShell Editor Services Test Host",
57-
"Test.PowerShellEditorServices",
58-
new Version("1.0.0"),
48+
name: "PowerShell Editor Services Test Host",
49+
profileId: "Test.PowerShellEditorServices",
50+
version: new Version("1.0.0"),
5951
psHost: new NullPSHost(),
60-
TestProfilePaths,
52+
profilePaths: TestProfilePaths,
6153
featureFlags: Array.Empty<string>(),
6254
additionalModules: Array.Empty<string>(),
63-
initialSessionState,
55+
initialSessionState: initialSessionState,
6456
logPath: null,
65-
(int)LogLevel.None,
57+
logLevel: (int)LogLevel.None,
6658
consoleReplEnabled: false,
6759
usesLegacyReadLine: false,
6860
bundledModulePath: BundledModulePath);
6961

7062
var psesHost = new PsesInternalHost(loggerFactory, null, testHostDetails);
7163

7264
// NOTE: Because this is used by constructors it can't use await.
73-
// TODO: Should we actually load profiles here?
74-
if (psesHost.TryStartAsync(new HostStartOptions { LoadProfiles = true }, CancellationToken.None).GetAwaiter().GetResult())
65+
if (psesHost.TryStartAsync(new HostStartOptions { LoadProfiles = false }, CancellationToken.None).GetAwaiter().GetResult())
7566
{
7667
return psesHost;
7768
}

test/PowerShellEditorServices.Test/Session/PsesInternalHostTests.cs

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -94,6 +94,7 @@ await Assert.ThrowsAsync<TaskCanceledException>(() =>
9494
}
9595

9696
[Fact]
97+
[System.Diagnostics.CodeAnalysis.SuppressMessage("Usage", "VSTHRD003:Avoid awaiting foreign Tasks", Justification = "Explicitly checking task cancellation status.")]
9798
public async Task CanCancelExecutionWithMethod()
9899
{
99100
var executeTask = psesHost.ExecutePSCommandAsync(

0 commit comments

Comments
 (0)