Skip to content

Commit

Permalink
EnC: Fix document out-of-sync checks when module is not loaded when d… (
Browse files Browse the repository at this point in the history
#39836)

* EnC: Fix document out-of-sync checks when module is not loaded when debugging session starts

Previous change #39295 allowed us to rely on PDB source file checksums when distinguishing between design-time-only documents that should be ignored when applying changes and documents whose changes must be applied to the debuggee. The change did not handle the case when the PDB isn't loaded yet when this check is being performed. This bug resulted in changes not being applied correctly and documents getting out-of-sync in scenarios where the modules were not loaded to the debuggee fast enough.

Fixes https://devdiv.visualstudio.com/DevDiv/_workitems/edit/1013242
  • Loading branch information
tmat authored Nov 18, 2019
1 parent ff930de commit 82f2e25
Show file tree
Hide file tree
Showing 8 changed files with 375 additions and 100 deletions.
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
// Copyright (c) Microsoft. All Rights Reserved. Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information.
using System;
using System.IO;
using System.Linq;
using System.Reflection.Metadata;
using System.Reflection.Metadata.Ecma335;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
Expand Down Expand Up @@ -54,7 +55,7 @@ public static void Main()
}
}
";
var compilation = CSharpTestBase.CreateCompilationWithMscorlib40AndSystemCore(source, options: TestOptions.DebugDll);
var compilation = CSharpTestBase.CreateCompilationWithMscorlib40AndSystemCore(source, options: TestOptions.DebugDll, sourceFileName: "/a/c.cs");

var pdbStream = new MemoryStream();
compilation.EmitToArray(new EmitOptions(debugInformationFormat: format), pdbStream: pdbStream);
Expand Down Expand Up @@ -95,6 +96,15 @@ public static void Main()

localSig = reader.GetLocalSignature(MetadataTokens.MethodDefinitionHandle(1));
Assert.Equal(default, localSig);

// document checksums:
Assert.False(reader.TryGetDocumentChecksum("/b/c.cs", out _, out _));
Assert.False(reader.TryGetDocumentChecksum("/a/d.cs", out _, out _));
Assert.False(reader.TryGetDocumentChecksum("/A/C.cs", out _, out _));

Assert.True(reader.TryGetDocumentChecksum("/a/c.cs", out var actualChecksum, out var actualAlgorithm));
Assert.Equal("21-C8-B2-D7-A3-6B-49-C7-57-DF-67-B8-1F-75-DF-6A-64-FD-59-22", BitConverter.ToString(actualChecksum.ToArray()));
Assert.Equal(new Guid("ff1816ec-aa5e-4d10-87f7-6f4963833460"), actualAlgorithm);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -138,21 +138,42 @@ TService IDocumentServiceProvider.GetService<TService>()
}

private (DebuggeeModuleInfo, Guid) EmitAndLoadLibraryToDebuggee(string source, ProjectId projectId, string assemblyName = "", string sourceFilePath = "test1.cs")
{
var (debuggeeModuleInfo, moduleId) = EmitLibrary(source, projectId, assemblyName, sourceFilePath);
LoadLibraryToDebuggee(debuggeeModuleInfo);
return (debuggeeModuleInfo, moduleId);
}

private void LoadLibraryToDebuggee(DebuggeeModuleInfo debuggeeModuleInfo)
=> _mockDebugeeModuleMetadataProvider.TryGetBaselineModuleInfo = mvid => debuggeeModuleInfo;

private (DebuggeeModuleInfo, Guid) EmitLibrary(string source, ProjectId projectId, string assemblyName = "", string sourceFilePath = "test1.cs", DebugInformationFormat pdbFormat = DebugInformationFormat.PortablePdb)
{
var sourceText = SourceText.From(new MemoryStream(Encoding.UTF8.GetBytes(source)), encoding: Encoding.UTF8, checksumAlgorithm: SourceHashAlgorithm.Sha256);
var tree = SyntaxFactory.ParseSyntaxTree(sourceText, TestOptions.RegularPreview, sourceFilePath);
var compilation = CSharpTestBase.CreateCompilationWithMscorlib40(tree, options: TestOptions.DebugDll, assemblyName: assemblyName);
var (peImage, symReader) = SymReaderTestHelpers.EmitAndOpenDummySymReader(compilation, DebugInformationFormat.PortablePdb);

var (peImage, pdbImage) = compilation.EmitToArrays(new EmitOptions(debugInformationFormat: pdbFormat));
var symReader = SymReaderTestHelpers.OpenDummySymReader(pdbImage);

var moduleMetadata = ModuleMetadata.CreateFromImage(peImage);
var moduleId = moduleMetadata.GetModuleVersionId();
var debuggeeModuleInfo = new DebuggeeModuleInfo(moduleMetadata, symReader);

// "load" it to the debuggee:
_mockDebugeeModuleMetadataProvider.TryGetBaselineModuleInfo = mvid => debuggeeModuleInfo;

// associate the binaries with the project
_mockCompilationOutputsService.Outputs.Add(projectId, new MockCompilationOutputs(moduleId));
_mockCompilationOutputsService.Outputs.Add(projectId, new MockCompilationOutputs(moduleId)
{
OpenPdbStreamImpl = () =>
{
var pdbStream = new MemoryStream();
pdbImage.WriteToStream(pdbStream);
pdbStream.Position = 0;
return pdbStream;
}
});

// library not loaded yet:
_mockDebugeeModuleMetadataProvider.TryGetBaselineModuleInfo = mvid => null;

return (debuggeeModuleInfo, moduleId);
}
Expand All @@ -163,7 +184,6 @@ private SourceText CreateSourceTextFromFile(string path)
return SourceText.From(stream, Encoding.UTF8, SourceHashAlgorithm.Sha256);
}


[Fact]
public void ActiveStatementTracking()
{
Expand Down Expand Up @@ -577,23 +597,25 @@ public async Task BreakMode_DesignTimeOnlyDocument_Dynamic()
Assert.Empty(deltas);
}

[Fact]
public async Task BreakMode_DesignTimeOnlyDocument_Wpf()
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task BreakMode_DesignTimeOnlyDocument_Wpf(bool delayLoad)
{
var sourceA = "class A { public void M() { } }";
var sourceB = "class B { public void M() { } }";
var sourceC = "class C { public void M() { } }";

var dir = Temp.CreateDirectory();
var sourceFile = dir.CreateFile("a.cs").WriteAllText(sourceA);
var sourceFileA = dir.CreateFile("a.cs").WriteAllText(sourceA);

using var workspace = new TestWorkspace();

// the workspace starts with a version of the source that's not updated with the output of single file generator (or design-time build):
var documentA = workspace.CurrentSolution.
AddProject("test", "test", LanguageNames.CSharp).
AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)).
AddDocument("a.cs", SourceText.From(sourceA, Encoding.UTF8), filePath: sourceFile.Path);
AddDocument("a.cs", SourceText.From(sourceA, Encoding.UTF8), filePath: sourceFileA.Path);

var documentB = documentA.Project.
AddDocument("b.g.i.cs", SourceText.From(sourceB, Encoding.UTF8), filePath: "b.g.i.cs");
Expand All @@ -604,7 +626,12 @@ public async Task BreakMode_DesignTimeOnlyDocument_Wpf()
workspace.ChangeSolution(documentC.Project.Solution);

// only compile A; B and C are design-time-only:
var (_, moduleId) = EmitAndLoadLibraryToDebuggee(sourceA, documentA.Project.Id, sourceFilePath: sourceFile.Path);
var (moduleInfo, moduleId) = EmitLibrary(sourceA, documentA.Project.Id, sourceFilePath: sourceFileA.Path);

if (!delayLoad)
{
LoadLibraryToDebuggee(moduleInfo);
}

var service = CreateEditAndContinueService(workspace);

Expand All @@ -630,6 +657,19 @@ public async Task BreakMode_DesignTimeOnlyDocument_Wpf()
Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit);
Assert.Empty(_emitDiagnosticsUpdated);

if (delayLoad)
{
LoadLibraryToDebuggee(moduleInfo);

// validate solution update status and emit:
solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.None, solutionStatus);

(solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit);
Assert.Empty(_emitDiagnosticsUpdated);
}

service.EndEditSession();
service.EndDebuggingSession();
}
Expand Down Expand Up @@ -1021,6 +1061,70 @@ public async Task BreakMode_RudeEdits_DocumentWithoutSequencePoints()
service.EndDebuggingSession();
}

[Fact]
public async Task BreakMode_RudeEdits_DelayLoadedModule()
{
var source1 = "class C { public void M() { } }";
var dir = Temp.CreateDirectory();
var sourceFile = dir.CreateFile("a.cs").WriteAllText(source1);

using var workspace = new TestWorkspace();

// the workspace starts with a version of the source that's not updated with the output of single file generator (or design-time build):
var document1 = workspace.CurrentSolution.
AddProject("test", "test", LanguageNames.CSharp).
AddMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Mscorlib40)).
AddDocument("test.cs", SourceText.From(source1, Encoding.UTF8), filePath: sourceFile.Path);

var project = document1.Project;
workspace.ChangeSolution(project.Solution);

var (debuggeeModuleInfo, _) = EmitLibrary(source1, project.Id, sourceFilePath: sourceFile.Path);

var service = CreateEditAndContinueService(workspace);

// do not initialize the document state - we will detect the state based on the PDB content.
var debuggingSession = StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None);

service.StartEditSession();

// change the source (rude edit) before the library is loaded:
workspace.ChangeDocument(document1.Id, SourceText.From("class C { public void Renamed() { } }"));
var document2 = workspace.CurrentSolution.Projects.Single().Documents.Single();

// Rude Edits reported:
var diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
AssertEx.Equal(
new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_will_prevent_the_debug_session_from_continuing, FeaturesResources.method) },
diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}"));

var solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus);

var (solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
Assert.Empty(deltas);

// load library to the debuggee:
LoadLibraryToDebuggee(debuggeeModuleInfo);

// Rude Edits still reported:
diagnostics = await service.GetDocumentDiagnosticsAsync(document2, CancellationToken.None).ConfigureAwait(false);
AssertEx.Equal(
new[] { "ENC0020: " + string.Format(FeaturesResources.Renaming_0_will_prevent_the_debug_session_from_continuing, FeaturesResources.method) },
diagnostics.Select(d => $"{d.Id}: {d.GetMessage()}"));

solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatus);

(solutionStatusEmit, deltas) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.Blocked, solutionStatusEmit);
Assert.Empty(deltas);

service.EndEditSession();
service.EndDebuggingSession();
}

[Fact]
public async Task BreakMode_SyntaxError()
{
Expand Down Expand Up @@ -1413,8 +1517,10 @@ public async Task BreakMode_ValidSignificantChange_FileUpdateBeforeDebuggingSess
service.EndDebuggingSession();
}

[Fact]
public async Task BreakMode_ValidSignificantChange_DocumentOutOfSync2()
[Theory]
[InlineData(true)]
[InlineData(false)]
public async Task BreakMode_ValidSignificantChange_DocumentOutOfSync(bool delayLoad)
{
var sourceOnDisk = "class C1 { void M() { System.Console.WriteLine(1); } }";

Expand All @@ -1432,11 +1538,16 @@ public async Task BreakMode_ValidSignificantChange_DocumentOutOfSync2()
var project = document1.Project;
workspace.ChangeSolution(project.Solution);

var (_, moduleId) = EmitAndLoadLibraryToDebuggee(sourceOnDisk, project.Id, sourceFilePath: sourceFile.Path);
var (moduleInfo, moduleId) = EmitLibrary(sourceOnDisk, project.Id, sourceFilePath: sourceFile.Path);

if (!delayLoad)
{
LoadLibraryToDebuggee(moduleInfo);
}

var service = CreateEditAndContinueService(workspace);

var debuggingSession = StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None);
StartDebuggingSession(service, initialState: CommittedSolution.DocumentState.None);

service.StartEditSession();
VerifyReanalyzeInvocation(workspace, null, ImmutableArray<DocumentId>.Empty, false);
Expand All @@ -1458,15 +1569,15 @@ public async Task BreakMode_ValidSignificantChange_DocumentOutOfSync2()
workspace.ChangeDocument(document1.Id, SourceText.From(sourceOnDisk, Encoding.UTF8));
var document3 = workspace.CurrentSolution.Projects.Single().Documents.Single();

var diagnostics2 = await service.GetDocumentDiagnosticsAsync(document3, CancellationToken.None).ConfigureAwait(false);
Assert.Empty(diagnostics2);
var diagnostics = await service.GetDocumentDiagnosticsAsync(document3, CancellationToken.None).ConfigureAwait(false);
Assert.Empty(diagnostics);

// the content of the file is now exactly the same as the compiled document, so there is no change to be applied:
var solutionStatus2 = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.None, solutionStatus2);
solutionStatus = await service.GetSolutionUpdateStatusAsync(sourceFilePath: null, CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.None, solutionStatus);

var (solutionStatusEmit2, deltas2) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit2);
(solutionStatusEmit, _) = await service.EmitSolutionUpdateAsync(CancellationToken.None).ConfigureAwait(false);
Assert.Equal(SolutionUpdateStatus.None, solutionStatusEmit);

service.EndEditSession();

Expand Down Expand Up @@ -1745,7 +1856,8 @@ public async Task TwoUpdatesWithLoadedAndUnloadedModule()
var compilationA = CSharpTestBase.CreateCompilationWithMscorlib40(source1, options: TestOptions.DebugDll, assemblyName: "A");
var compilationB = CSharpTestBase.CreateCompilationWithMscorlib45(source1, options: TestOptions.DebugDll, assemblyName: "B");

var (peImageA, symReaderA) = SymReaderTestHelpers.EmitAndOpenDummySymReader(compilationA, DebugInformationFormat.PortablePdb);
var (peImageA, pdbImageA) = compilationA.EmitToArrays(new EmitOptions(debugInformationFormat: DebugInformationFormat.PortablePdb));
var symReaderA = SymReaderTestHelpers.OpenDummySymReader(pdbImageA);

var moduleMetadataA = ModuleMetadata.CreateFromImage(peImageA);
var moduleFileA = Temp.CreateFile("A.dll").WriteAllBytes(peImageA);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
using System;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Reflection;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Test.Utilities;
Expand Down Expand Up @@ -30,32 +31,32 @@ public bool TryGetTypeReferenceInfo(int typeReferenceToken, out string namespace
=> throw new NotImplementedException();
}

public static (ImmutableArray<byte> PEImage, ISymUnmanagedReader5 SymReader) EmitAndOpenDummySymReader(Compilation compilation, DebugInformationFormat pdbFormat)
public static (ImmutableArray<byte> PEImage, ImmutableArray<byte> PdbImage) EmitToArrays(this Compilation compilation, EmitOptions options)
{
var pdbStream = new MemoryStream();
var peImage = compilation.EmitToArray(options, pdbStream: pdbStream);
return (peImage, pdbStream.ToImmutable());
}

public static ISymUnmanagedReader5 OpenDummySymReader(ImmutableArray<byte> pdbImage)
{
var symBinder = new SymBinder();
var metadataImportProvider = new DummyMetadataImportProvider();

var pdbStream = new MemoryStream();
var peImage = compilation.EmitToArray(new EmitOptions(debugInformationFormat: pdbFormat), pdbStream: pdbStream);
pdbStream.Position = 0;
pdbImage.WriteToStream(pdbStream);

var pdbStreamCom = SymUnmanagedStreamFactory.CreateStream(pdbStream);

ISymUnmanagedReader5 symReader5;
if (pdbFormat == DebugInformationFormat.PortablePdb)
if (pdbImage.Length > 4 && pdbImage[0] == 'B' && pdbImage[1] == 'S' && pdbImage[2] == 'J' && pdbImage[3] == 'B')
{
int hr = symBinder.GetReaderFromPdbStream(metadataImportProvider, pdbStreamCom, out var symReader);
Assert.Equal(0, hr);
symReader5 = (ISymUnmanagedReader5)symReader;
return (ISymUnmanagedReader5)symReader;
}
else
{
symReader5 = SymUnmanagedReaderFactory.CreateReader<ISymUnmanagedReader5>(pdbStream, new DummySymReaderMetadataProvider());
return SymUnmanagedReaderFactory.CreateReader<ISymUnmanagedReader5>(pdbStream, new DummySymReaderMetadataProvider());
}

return (peImage, symReader5);
}


}
}
Loading

0 comments on commit 82f2e25

Please sign in to comment.