File: EditAndContinue\EditSessionActiveStatementsTests.cs
Web Access
Project: src\src\Features\Test\Microsoft.CodeAnalysis.Features.UnitTests.csproj (Microsoft.CodeAnalysis.Features.UnitTests)
// Licensed to the .NET Foundation under one or more agreements.
// The .NET Foundation licenses this file to you under the MIT license.
// See the LICENSE file in the project root for more information.
 
#nullable disable
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Microsoft.CodeAnalysis.Contracts.EditAndContinue;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Test.Utilities;
using Microsoft.CodeAnalysis.Text;
using Microsoft.CodeAnalysis.UnitTests;
using Roslyn.Test.Utilities;
using Roslyn.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.EditAndContinue.UnitTests;
 
using static ActiveStatementTestHelpers;
 
[UseExportProvider]
public sealed class EditSessionActiveStatementsTests : TestBase
{
    private static readonly TestComposition s_composition = FeaturesTestCompositions.Features.AddParts(typeof(NoCompilationLanguageService));
 
    private static EditSession CreateEditSession(
        Solution solution,
        ImmutableArray<ManagedActiveStatementDebugInfo> activeStatements,
        ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>> nonRemappableRegions = null,
        CommittedSolution.DocumentState initialState = CommittedSolution.DocumentState.MatchesBuildOutput)
    {
        var mockDebuggerService = new MockManagedEditAndContinueDebuggerService()
        {
            GetActiveStatementsImpl = () => activeStatements,
        };
 
        var mockCompilationOutputsProvider = new Func<Project, CompilationOutputs>(_ => new MockCompilationOutputs(Guid.NewGuid()));
 
        var log = new TraceLog("Test");
 
        var debuggingSession = new DebuggingSession(
            new DebuggingSessionId(1),
            solution,
            mockDebuggerService,
            mockCompilationOutputsProvider,
            NullPdbMatchingSourceTextProvider.Instance,
            initialDocumentStates: [],
            log,
            log,
            reportDiagnostics: true);
 
        if (initialState != CommittedSolution.DocumentState.None)
        {
            EditAndContinueTestVerifier.SetDocumentsState(debuggingSession, solution, initialState);
        }
 
        debuggingSession.RestartEditSession(nonRemappableRegions ?? ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>>.Empty, inBreakState: true);
        return debuggingSession.EditSession;
    }
 
    private static async Task<Solution> AddDefaultTestSolutionAsync(TestWorkspace workspace, string[] markedSources)
    {
        var solution = workspace.CurrentSolution;
 
        var project = solution
            .AddProject("proj", "proj", LanguageNames.CSharp)
            .WithMetadataReferences(TargetFrameworkUtil.GetReferences(TargetFramework.Standard));
 
        solution = project.Solution;
 
        for (var i = 0; i < markedSources.Length; i++)
        {
            var name = $"test{i + 1}.cs";
            var text = SourceText.From(SourceMarkers.Clear(markedSources[i]), Encoding.UTF8);
            var id = DocumentId.CreateNewId(project.Id, name);
            solution = solution.AddDocument(id, name, text, filePath: Path.Combine(TempRoot.Root, name));
        }
 
        await workspace.ChangeSolutionAsync(solution);
        return solution;
    }
 
    [Fact]
    public async Task BaseActiveStatementsAndExceptionRegions1()
    {
        var markedSources = new[]
        {
@"class Test1
{
    static void M1()
    {
        try { } finally { <AS:1>F1();</AS:1> }
    }
 
    static void F1()
    {
        <AS:0>Console.WriteLine(1);</AS:0>
    }
}",
@"class Test2
{
    static void M2()
    {
        try
        {
          try
          {
              <AS:3>F2();</AS:3>
          }
          catch (Exception1 e1)
          {
          }
        }
        catch (Exception2 e2)
        {
        }
    }
 
    static void F2()
    {
        <AS:2>Test1.M1()</AS:2>
    }
 
    static void Main()
    {
        try { <AS:4>M2();</AS:4> } finally { }
    }
}
"
        };
 
        var module1 = new Guid("11111111-1111-1111-1111-111111111111");
        var module2 = new Guid("22222222-2222-2222-2222-222222222222");
        var module3 = new Guid("33333333-3333-3333-3333-333333333333");
        var module4 = new Guid("44444444-4444-4444-4444-444444444444");
 
        var activeStatements = GetActiveStatementDebugInfosCSharp(
            markedSources,
            methodRowIds: [1, 2, 3, 4, 5],
            ilOffsets: [1, 1, 1, 2, 3],
            modules: [module1, module1, module2, module2, module2]);
 
        // add an extra active statement that has no location, it should be ignored:
        activeStatements = activeStatements.Add(
            new ManagedActiveStatementDebugInfo(
                new ManagedInstructionId(new ManagedMethodId(module: Guid.NewGuid(), token: 0x06000005, version: 1), ilOffset: 10),
                documentName: null,
                sourceSpan: default,
                ActiveStatementFlags.MethodUpToDate | ActiveStatementFlags.NonLeafFrame));
 
        // add an extra active statement from project not belonging to the solution, it should be ignored:
        activeStatements = activeStatements.Add(
            new ManagedActiveStatementDebugInfo(
                new ManagedInstructionId(new ManagedMethodId(module: module3, token: 0x06000005, version: 1), ilOffset: 10),
                "NonRoslynDocument.mcpp",
                new SourceSpan(1, 1, 1, 10),
                ActiveStatementFlags.MethodUpToDate | ActiveStatementFlags.NonLeafFrame));
 
        // Add an extra active statement from language that doesn't support Roslyn EnC should be ignored:
        // See https://github.com/dotnet/roslyn/issues/24408 for test scenario.
        activeStatements = activeStatements.Add(
            new ManagedActiveStatementDebugInfo(
                new ManagedInstructionId(new ManagedMethodId(module: module4, token: 0x06000005, version: 1), ilOffset: 10),
                "a.dummy",
                new SourceSpan(2, 1, 2, 10),
                ActiveStatementFlags.MethodUpToDate | ActiveStatementFlags.NonLeafFrame));
 
        using var workspace = new TestWorkspace(composition: s_composition);
 
        var solution = await AddDefaultTestSolutionAsync(workspace, markedSources);
        var projectId = solution.ProjectIds.Single();
        var dummyProject = solution.AddProject("dummy_proj", "dummy_proj", NoCompilationConstants.LanguageName);
        solution = dummyProject.Solution.AddDocument(DocumentId.CreateNewId(dummyProject.Id, NoCompilationConstants.LanguageName), "a.dummy", "");
        var project = solution.GetProject(projectId);
        var document1 = project.Documents.Single(d => d.Name == "test1.cs");
        var document2 = project.Documents.Single(d => d.Name == "test2.cs");
 
        var editSession = CreateEditSession(solution, activeStatements);
        var baseActiveStatementsMap = await editSession.BaseActiveStatements.GetValueAsync(CancellationToken.None).ConfigureAwait(false);
 
        // Active Statements
 
        var statements = baseActiveStatementsMap.InstructionMap.Values.OrderBy(v => v.Id.Ordinal).ToArray();
        AssertEx.Equal(
        [
            $"0: {document1.FilePath}: (9,14)-(9,35) flags=[LeafFrame, MethodUpToDate] mvid=11111111-1111-1111-1111-111111111111 0x06000001 v1 IL_0001",
            $"1: {document1.FilePath}: (4,32)-(4,37) flags=[MethodUpToDate, NonLeafFrame] mvid=11111111-1111-1111-1111-111111111111 0x06000002 v1 IL_0001",
            $"2: {document2.FilePath}: (21,14)-(21,24) flags=[MethodUpToDate, NonLeafFrame] mvid=22222222-2222-2222-2222-222222222222 0x06000003 v1 IL_0001",   // [|Test1.M1()|] in F2
            $"3: {document2.FilePath}: (8,20)-(8,25) flags=[MethodUpToDate, NonLeafFrame] mvid=22222222-2222-2222-2222-222222222222 0x06000004 v1 IL_0002",     // [|F2();|] in M2
            $"4: {document2.FilePath}: (26,20)-(26,25) flags=[MethodUpToDate, NonLeafFrame] mvid=22222222-2222-2222-2222-222222222222 0x06000005 v1 IL_0003",   // [|M2();|] in Main
            $"5: NonRoslynDocument.mcpp: (1,1)-(1,10) flags=[MethodUpToDate, NonLeafFrame] mvid={module3} 0x06000005 v1 IL_000A",
            $"6: a.dummy: (2,1)-(2,10) flags=[MethodUpToDate, NonLeafFrame] mvid={module4} 0x06000005 v1 IL_000A"
        ], statements.Select(InspectActiveStatementAndInstruction));
 
        // Active Statements per document
 
        Assert.Equal(4, baseActiveStatementsMap.DocumentPathMap.Count);
 
        AssertEx.Equal(
        [
            $"1: {document1.FilePath}: (4,32)-(4,37) flags=[MethodUpToDate, NonLeafFrame]",
            $"0: {document1.FilePath}: (9,14)-(9,35) flags=[LeafFrame, MethodUpToDate]"
        ], baseActiveStatementsMap.DocumentPathMap[document1.FilePath].Select(InspectActiveStatement));
 
        AssertEx.Equal(
        [
            $"3: {document2.FilePath}: (8,20)-(8,25) flags=[MethodUpToDate, NonLeafFrame]",            // [|F2();|] in M2
            $"2: {document2.FilePath}: (21,14)-(21,24) flags=[MethodUpToDate, NonLeafFrame]",          // [|Test1.M1()|] in F2
            $"4: {document2.FilePath}: (26,20)-(26,25) flags=[MethodUpToDate, NonLeafFrame]"           // [|M2();|] in Main
        ], baseActiveStatementsMap.DocumentPathMap[document2.FilePath].Select(InspectActiveStatement));
 
        AssertEx.Equal(
        [
            $"5: NonRoslynDocument.mcpp: (1,1)-(1,10) flags=[MethodUpToDate, NonLeafFrame]",
        ], baseActiveStatementsMap.DocumentPathMap["NonRoslynDocument.mcpp"].Select(InspectActiveStatement));
 
        AssertEx.Equal(
        [
            $"6: a.dummy: (2,1)-(2,10) flags=[MethodUpToDate, NonLeafFrame]",
        ], baseActiveStatementsMap.DocumentPathMap["a.dummy"].Select(InspectActiveStatement));
 
        // Exception Regions
 
        var analyzer = solution.GetProject(projectId).Services.GetRequiredService<IEditAndContinueAnalyzer>();
        var oldActiveStatements1 = await baseActiveStatementsMap.GetOldActiveStatementsAsync(analyzer, document1, CancellationToken.None).ConfigureAwait(false);
 
        AssertEx.Equal(
        [
            $"[{document1.FilePath}: (4,8)-(4,46)]",
            "[]",
        ], oldActiveStatements1.Select(s => "[" + string.Join(", ", s.ExceptionRegions.Spans) + "]"));
 
        var oldActiveStatements2 = await baseActiveStatementsMap.GetOldActiveStatementsAsync(analyzer, document2, CancellationToken.None).ConfigureAwait(false);
        AssertEx.Equal(
        [
            $"[{document2.FilePath}: (14,8)-(16,9), {document2.FilePath}: (10,10)-(12,11)]",
            "[]",
            $"[{document2.FilePath}: (26,35)-(26,46)]",
        ], oldActiveStatements2.Select(s => "[" + string.Join(", ", s.ExceptionRegions.Spans) + "]"));
 
        // GetActiveStatementAndExceptionRegionSpans
 
        // Assume 2 updates in Document2:
        //   Test2.M2: adding a line in front of try-catch.
        //   Test2.F2: moving the entire method 2 lines down.
 
        var newActiveStatementsInChangedDocuments = ImmutableArray.Create(
            new DocumentActiveStatementChanges(
                oldSpans: oldActiveStatements2,
                newStatements:
                [
                    statements[3].WithFileSpan(statements[3].FileSpan.AddLineDelta(+1)),
                    statements[2].WithFileSpan(statements[2].FileSpan.AddLineDelta(+2)),
                    statements[4],
                ],
                newExceptionRegions:
                [
                    oldActiveStatements2[0].ExceptionRegions.Spans.SelectAsArray(es => es.AddLineDelta(+1)),
                    oldActiveStatements2[1].ExceptionRegions.Spans,
                    oldActiveStatements2[2].ExceptionRegions.Spans,
                ]));
 
        EditSession.GetActiveStatementAndExceptionRegionSpans(
            module2,
            baseActiveStatementsMap,
            updatedMethodTokens: [0x06000004], // contains only recompiled methods in the project we are interested in (module2)
            previousNonRemappableRegions: ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>>.Empty,
            newActiveStatementsInChangedDocuments,
            out var activeStatementsInUpdatedMethods,
            out var nonRemappableRegions,
            out var exceptionRegionUpdates);
 
        AssertEx.Equal(
        [
            $"0x06000004 v1 | AS {document2.FilePath}: (8,20)-(8,25) => (9,20)-(9,25)",
            $"0x06000004 v1 | ER {document2.FilePath}: (14,8)-(16,9) => (15,8)-(17,9)",
            $"0x06000004 v1 | ER {document2.FilePath}: (10,10)-(12,11) => (11,10)-(13,11)",
            $"0x06000003 v1 | AS {document2.FilePath}: (21,14)-(21,24) => (21,14)-(21,24)",
            $"0x06000005 v1 | AS {document2.FilePath}: (26,20)-(26,25) => (26,20)-(26,25)"
        ], nonRemappableRegions.Select(r => $"{r.Method.GetDebuggerDisplay()} | {r.Region.GetDebuggerDisplay()}"));
 
        AssertEx.Equal(
        [
            $"0x06000004 v1 | (15,8)-(17,9) Delta=-1",
            $"0x06000004 v1 | (11,10)-(13,11) Delta=-1"
        ], exceptionRegionUpdates.Select(InspectExceptionRegionUpdate));
 
        AssertEx.Equal(
        [
            $"0x06000004 v1 IL_0002: (9,20)-(9,25)"
        ], activeStatementsInUpdatedMethods.Select(InspectActiveStatementUpdate));
    }
 
    [Fact, WorkItem("https://github.com/dotnet/roslyn/issues/24439")]
    public async Task BaseActiveStatementsAndExceptionRegions2()
    {
        var baseSource =
@"class Test
{
    static void F1()
    {   
        try
        {
            <AS:0>F2();</AS:0>
        }
        catch (Exception) {
            Console.WriteLine(1);
            Console.WriteLine(2);
            Console.WriteLine(3);
        }
        /*insert1[1]*/
    }
 
    static void F2()
    {
        <AS:1>throw new Exception();</AS:1>
    }
}";
        var updatedSource = Update(baseSource, marker: "1");
 
        var module1 = new Guid("11111111-1111-1111-1111-111111111111");
        var baseText = SourceText.From(baseSource);
        var updatedText = SourceText.From(updatedSource);
 
        var baseActiveStatementInfos = GetActiveStatementDebugInfosCSharp(
            [baseSource],
            modules: [module1, module1],
            methodVersions: [1, 1],
            flags:
            [
                ActiveStatementFlags.MethodUpToDate | ActiveStatementFlags.NonLeafFrame, // F1
                ActiveStatementFlags.MethodUpToDate | ActiveStatementFlags.LeafFrame,    // F2
            ]);
 
        using var workspace = new TestWorkspace(composition: s_composition);
        var solution = await AddDefaultTestSolutionAsync(workspace, [baseSource]);
        var project = solution.Projects.Single();
        var document = project.Documents.Single();
 
        var editSession = CreateEditSession(solution, baseActiveStatementInfos);
        var baseActiveStatementMap = await editSession.BaseActiveStatements.GetValueAsync(CancellationToken.None).ConfigureAwait(false);
 
        // Active Statements
 
        var baseActiveStatements = baseActiveStatementMap.InstructionMap.Values.OrderBy(v => v.Id.Ordinal).ToArray();
 
        AssertEx.Equal(
        [
            $"0: {document.FilePath}: (6,18)-(6,23) flags=[MethodUpToDate, NonLeafFrame] mvid=11111111-1111-1111-1111-111111111111 0x06000001 v1 IL_0000 '<AS:0>F2();</AS:0>'",
            $"1: {document.FilePath}: (18,14)-(18,36) flags=[LeafFrame, MethodUpToDate] mvid=11111111-1111-1111-1111-111111111111 0x06000002 v1 IL_0000 '<AS:1>throw new Exception();</AS:1>'"
        ], baseActiveStatements.Select(s => InspectActiveStatementAndInstruction(s, baseText)));
 
        // Exception Regions
 
        var analyzer = solution.GetProject(project.Id).Services.GetRequiredService<IEditAndContinueAnalyzer>();
        var oldActiveStatements = await baseActiveStatementMap.GetOldActiveStatementsAsync(analyzer, document, CancellationToken.None).ConfigureAwait(false);
 
        // Note that the spans correspond to the base snapshot (V2). 
        AssertEx.Equal(
        [
            $"[{document.FilePath}: (8,8)-(12,9) 'catch (Exception) {{']",
            "[]",
        ], oldActiveStatements.Select(s => "[" + string.Join(", ", s.ExceptionRegions.Spans.Select(span => $"{span} '{GetFirstLineText(span.Span, baseText)}'")) + "]"));
 
        // GetActiveStatementAndExceptionRegionSpans
 
        var newActiveStatementsInChangedDocuments = ImmutableArray.Create(
            new DocumentActiveStatementChanges(
                oldSpans: oldActiveStatements,
                newStatements: [baseActiveStatements[0], baseActiveStatements[1].WithFileSpan(baseActiveStatements[1].FileSpan.AddLineDelta(+1))],
                newExceptionRegions: [oldActiveStatements[0].ExceptionRegions.Spans, oldActiveStatements[1].ExceptionRegions.Spans])
        );
 
        EditSession.GetActiveStatementAndExceptionRegionSpans(
            module1,
            baseActiveStatementMap,
            updatedMethodTokens: [0x06000001], // F1
            previousNonRemappableRegions: ImmutableDictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>>.Empty,
            newActiveStatementsInChangedDocuments,
            out var activeStatementsInUpdatedMethods,
            out var nonRemappableRegions,
            out var exceptionRegionUpdates);
 
        AssertEx.Equal(
        [
            $"0x06000001 v1 | AS {document.FilePath}: (6,18)-(6,23) => (6,18)-(6,23)",
            $"0x06000001 v1 | ER {document.FilePath}: (8,8)-(12,9) => (8,8)-(12,9)",
            $"0x06000002 v1 | AS {document.FilePath}: (18,14)-(18,36) => (18,14)-(18,36)",
        ], nonRemappableRegions.OrderBy(r => r.Region.OldSpan.Span.Start.Line).Select(r => $"{r.Method.GetDebuggerDisplay()} | {r.Region.GetDebuggerDisplay()}"));
 
        AssertEx.Equal(
        [
            "0x06000001 v1 | (8,8)-(12,9) Delta=0",
        ], exceptionRegionUpdates.Select(InspectExceptionRegionUpdate));
 
        AssertEx.Equal(
        [
            "0x06000001 v1 IL_0000: (6,18)-(6,23) '<AS:0>F2();</AS:0>'"
        ], activeStatementsInUpdatedMethods.Select(update => $"{InspectActiveStatementUpdate(update)} '{GetFirstLineText(update.NewSpan.ToLinePositionSpan(), updatedText)}'"));
    }
 
    [Fact]
    public async Task BaseActiveStatementsAndExceptionRegions_WithInitialNonRemappableRegions()
    {
        var markedSourceV1 =
@"class Test
{
    static void F1()
    {
        try
        {
            <AS:0>M();</AS:0>
        }
        <ER:0.0>catch
        {
        }</ER:0.0>
    }
 
    static void F2()
    {   /*delete2
      */try
        {
        }
        <ER:1.0>catch
        {
            <AS:1>M();</AS:1>
        }</ER:1.0>/*insert2[1]*/
    }
 
    static void F3()
    {   
        try
        {
            try 
            {   /*delete1
              */<AS:2>M();</AS:2>/*insert1[3]*/
            }
            <ER:2.0>finally
            {
            }</ER:2.0>
        }
        <ER:2.1>catch
        {
        }</ER:2.1>
/*delete1
 
*/  }
 
    static void F4()
    {   /*insert1[1]*//*insert2[2]*/
        try
        {
            try
            {
            }
            <ER:3.0>catch
            {
                <AS:3>M();</AS:3>
            }</ER:3.0>
        }
        <ER:3.1>catch
        {
        }</ER:3.1>
    }
}";
        var markedSourceV2 = Update(markedSourceV1, marker: "1");
        var markedSourceV3 = Update(markedSourceV2, marker: "2");
 
        var module1 = new Guid("11111111-1111-1111-1111-111111111111");
        var sourceTextV1 = SourceText.From(markedSourceV1);
        var sourceTextV2 = SourceText.From(markedSourceV2);
        var sourceTextV3 = SourceText.From(markedSourceV3);
 
        var activeStatementsPreRemap = GetActiveStatementDebugInfosCSharp([markedSourceV1],
            modules: [module1, module1, module1, module1],
            methodVersions: [2, 2, 1, 1], // method F3 and F4 were not remapped
            flags:
            [
                ActiveStatementFlags.MethodUpToDate | ActiveStatementFlags.NonLeafFrame, // F1
                ActiveStatementFlags.MethodUpToDate | ActiveStatementFlags.NonLeafFrame, // F2
                ActiveStatementFlags.None | ActiveStatementFlags.NonLeafFrame,           // F3
                ActiveStatementFlags.None | ActiveStatementFlags.NonLeafFrame,           // F4
            ]);
 
        var exceptionSpans = SourceMarkers.GetExceptionRegions(markedSourceV1);
 
        var filePath = activeStatementsPreRemap[0].DocumentName;
        var spanPreRemap2 = new SourceFileSpan(filePath, activeStatementsPreRemap[2].SourceSpan.ToLinePositionSpan());
        var erPreRemap20 = new SourceFileSpan(filePath, sourceTextV1.Lines.GetLinePositionSpan(exceptionSpans[2][0]));
        var erPreRemap21 = new SourceFileSpan(filePath, sourceTextV1.Lines.GetLinePositionSpan(exceptionSpans[2][1]));
        var spanPreRemap3 = new SourceFileSpan(filePath, activeStatementsPreRemap[3].SourceSpan.ToLinePositionSpan());
        var erPreRemap30 = new SourceFileSpan(filePath, sourceTextV1.Lines.GetLinePositionSpan(exceptionSpans[3][0]));
        var erPreRemap31 = new SourceFileSpan(filePath, sourceTextV1.Lines.GetLinePositionSpan(exceptionSpans[3][1]));
 
        // Assume that the following edits have been made to F3 and F4 and set up non-remappable regions mapping
        // from the pre-remap spans of AS:2 and AS:3 to their current location.
        var initialNonRemappableRegions = new Dictionary<ManagedMethodId, ImmutableArray<NonRemappableRegion>>
        {
            { new ManagedMethodId(module1, 0x06000003, 1), ImmutableArray.Create(
                // move AS:2 one line up:
                new NonRemappableRegion(spanPreRemap2, spanPreRemap2.AddLineDelta(-1), isExceptionRegion: false),
                // move ER:2.0 and ER:2.1 two lines down:
                new NonRemappableRegion(erPreRemap20, erPreRemap20.AddLineDelta(+2), isExceptionRegion: true),
                new NonRemappableRegion(erPreRemap21, erPreRemap21.AddLineDelta(+2), isExceptionRegion: true)) },
            { new ManagedMethodId(module1, 0x06000004, 1), ImmutableArray.Create(
                // move AS:3 one line down:
                new NonRemappableRegion(spanPreRemap3, spanPreRemap3.AddLineDelta(+1), isExceptionRegion: false),
                // move ER:3.0 and ER:3.1 one line down:
                new NonRemappableRegion(erPreRemap30, erPreRemap30.AddLineDelta(+1), isExceptionRegion: true),
                new NonRemappableRegion(erPreRemap31, erPreRemap31.AddLineDelta(+1), isExceptionRegion: true)) }
        }.ToImmutableDictionary();
 
        using var workspace = new TestWorkspace(composition: s_composition);
        var solution = await AddDefaultTestSolutionAsync(workspace, [markedSourceV2]);
        var project = solution.Projects.Single();
        var document = project.Documents.Single();
 
        var editSession = CreateEditSession(solution, activeStatementsPreRemap, initialNonRemappableRegions);
        var baseActiveStatementMap = await editSession.BaseActiveStatements.GetValueAsync(CancellationToken.None).ConfigureAwait(false);
 
        // Active Statements
 
        var baseActiveStatements = baseActiveStatementMap.InstructionMap.Values.OrderBy(v => v.Id.Ordinal).ToArray();
 
        // Note that the spans of AS:2 and AS:3 correspond to the base snapshot (V2).
        AssertEx.Equal(
        [
            $"0: {document.FilePath}: (6,18)-(6,22) flags=[MethodUpToDate, NonLeafFrame] mvid=11111111-1111-1111-1111-111111111111 0x06000001 v2 IL_0000 '<AS:0>M();</AS:0>'",
            $"1: {document.FilePath}: (20,18)-(20,22) flags=[MethodUpToDate, NonLeafFrame] mvid=11111111-1111-1111-1111-111111111111 0x06000002 v2 IL_0000 '<AS:1>M();</AS:1>'",
            $"2: {document.FilePath}: (29,22)-(29,26) flags=[NonLeafFrame] mvid=11111111-1111-1111-1111-111111111111 0x06000003 v1 IL_0000 '{{   <AS:2>M();</AS:2>'",
            $"3: {document.FilePath}: (53,22)-(53,26) flags=[NonLeafFrame] mvid=11111111-1111-1111-1111-111111111111 0x06000004 v1 IL_0000 '<AS:3>M();</AS:3>'"
        ], baseActiveStatements.Select(s => InspectActiveStatementAndInstruction(s, sourceTextV2)));
 
        // Exception Regions
 
        var analyzer = solution.GetProject(project.Id).Services.GetRequiredService<IEditAndContinueAnalyzer>();
        var oldActiveStatements = await baseActiveStatementMap.GetOldActiveStatementsAsync(analyzer, document, CancellationToken.None).ConfigureAwait(false);
 
        // Note that the spans correspond to the base snapshot (V2). 
        AssertEx.Equal(
        [
            $"[{document.FilePath}: (8,16)-(10,9) '<ER:0.0>catch']",
            $"[{document.FilePath}: (18,16)-(21,9) '<ER:1.0>catch']",
            $"[{document.FilePath}: (38,16)-(40,9) '<ER:2.1>catch', {document.FilePath}: (34,20)-(36,13) '<ER:2.0>finally']",
            $"[{document.FilePath}: (56,16)-(58,9) '<ER:3.1>catch', {document.FilePath}: (51,20)-(54,13) '<ER:3.0>catch']",
        ], oldActiveStatements.Select(s => "[" + string.Join(", ", s.ExceptionRegions.Spans.Select(span => $"{span} '{GetFirstLineText(span.Span, sourceTextV2)}'")) + "]"));
 
        // GetActiveStatementAndExceptionRegionSpans
 
        // Assume 2 more updates:
        //   F2: Move 'try' one line up (a new non-remappable entries will be added)
        //   F4: Insert 2 new lines before the first 'try' (an existing non-remappable entries will be updated)
        var newActiveStatementsInChangedDocuments = ImmutableArray.Create(
            new DocumentActiveStatementChanges(
                oldSpans: oldActiveStatements,
                newStatements:
                [
                    baseActiveStatements[0],
                    baseActiveStatements[1].WithFileSpan(baseActiveStatements[1].FileSpan.AddLineDelta(-1)),
                    baseActiveStatements[2],
                    baseActiveStatements[3].WithFileSpan(baseActiveStatements[3].FileSpan.AddLineDelta(+2)),
                ],
                newExceptionRegions:
                [
                    oldActiveStatements[0].ExceptionRegions.Spans,
                    oldActiveStatements[1].ExceptionRegions.Spans.SelectAsArray(es => es.AddLineDelta(-1)),
                    oldActiveStatements[2].ExceptionRegions.Spans,
                    oldActiveStatements[3].ExceptionRegions.Spans.SelectAsArray(es => es.AddLineDelta(+2)),
                ]));
 
        EditSession.GetActiveStatementAndExceptionRegionSpans(
            module1,
            baseActiveStatementMap,
            updatedMethodTokens: [0x06000002, 0x06000004], // F2, F4
            initialNonRemappableRegions,
            newActiveStatementsInChangedDocuments,
            out var activeStatementsInUpdatedMethods,
            out var nonRemappableRegions,
            out var exceptionRegionUpdates);
 
        // Note: Since no method have been remapped yet all the following spans are in their pre-remap locations: 
        AssertEx.Equal(
        [
            $"0x06000001 v2 | AS {document.FilePath}: (6,18)-(6,22) => (6,18)-(6,22)",
            $"0x06000002 v2 | ER {document.FilePath}: (18,16)-(21,9) => (17,16)-(20,9)",
            $"0x06000002 v2 | AS {document.FilePath}: (20,18)-(20,22) => (19,18)-(19,22)",
            $"0x06000003 v1 | AS {document.FilePath}: (30,22)-(30,26) => (29,22)-(29,26)",  // AS:2 moved -1 in first edit, 0 in second
            $"0x06000003 v1 | ER {document.FilePath}: (32,20)-(34,13) => (34,20)-(36,13)",  // ER:2.0 moved +2 in first edit, 0 in second
            $"0x06000003 v1 | ER {document.FilePath}: (36,16)-(38,9) => (38,16)-(40,9)",    // ER:2.0 moved +2 in first edit, 0 in second
            $"0x06000004 v1 | ER {document.FilePath}: (50,20)-(53,13) => (53,20)-(56,13)",  // ER:3.0 moved +1 in first edit, +2 in second              
            $"0x06000004 v1 | AS {document.FilePath}: (52,22)-(52,26) => (55,22)-(55,26)",  // AS:3 moved +1 in first edit, +2 in second
            $"0x06000004 v1 | ER {document.FilePath}: (55,16)-(57,9) => (58,16)-(60,9)",    // ER:3.1 moved +1 in first edit, +2 in second     
        ], nonRemappableRegions.OrderBy(r => r.Region.OldSpan.Span.Start.Line).Select(r => $"{r.Method.GetDebuggerDisplay()} | {r.Region.GetDebuggerDisplay()}"));
 
        AssertEx.Equal(
        [
            $"0x06000002 v2 | (17,16)-(20,9) Delta=1",
            $"0x06000003 v1 | (34,20)-(36,13) Delta=-2",
            $"0x06000003 v1 | (38,16)-(40,9) Delta=-2",
            $"0x06000004 v1 | (53,20)-(56,13) Delta=-3",
            $"0x06000004 v1 | (58,16)-(60,9) Delta=-3",
        ], exceptionRegionUpdates.OrderBy(r => r.NewSpan.StartLine).Select(InspectExceptionRegionUpdate));
 
        AssertEx.Equal(
        [
            $"0x06000002 v2 IL_0000: (19,18)-(19,22) '<AS:1>M();</AS:1>'",
            $"0x06000004 v1 IL_0000: (55,22)-(55,26) '<AS:3>M();</AS:3>'"
        ], activeStatementsInUpdatedMethods.Select(update => $"{InspectActiveStatementUpdate(update)} '{GetFirstLineText(update.NewSpan.ToLinePositionSpan(), sourceTextV3)}'"));
    }
 
    [Fact]
    public async Task BaseActiveStatementsAndExceptionRegions_Recursion()
    {
        var markedSources = new[]
        {
@"class C
{
    static void M()
    {
        try 
        {
            <AS:1>M();</AS:1>
        }
        catch (Exception e)
        {
        }
    }
 
    static void F()
    {
        <AS:0>M();</AS:0>
    }
}"
        };
 
        var thread1 = Guid.NewGuid();
        var thread2 = Guid.NewGuid();
 
        // Thread1 stack trace: F (AS:0), M (AS:1 leaf)
        // Thread2 stack trace: F (AS:0), M (AS:1), M (AS:1 leaf)
 
        var activeStatements = GetActiveStatementDebugInfosCSharp(
            markedSources,
            methodRowIds: [1, 2],
            ilOffsets: [1, 1],
            flags:
            [
                ActiveStatementFlags.NonLeafFrame | ActiveStatementFlags.NonUserCode | ActiveStatementFlags.PartiallyExecuted | ActiveStatementFlags.MethodUpToDate,
                ActiveStatementFlags.NonLeafFrame | ActiveStatementFlags.LeafFrame | ActiveStatementFlags.MethodUpToDate
            ]);
 
        using var workspace = new TestWorkspace(composition: s_composition);
        var solution = await AddDefaultTestSolutionAsync(workspace, markedSources);
        var project = solution.Projects.Single();
        var document = project.Documents.Single();
 
        var editSession = CreateEditSession(solution, activeStatements);
        var baseActiveStatementMap = await editSession.BaseActiveStatements.GetValueAsync(CancellationToken.None).ConfigureAwait(false);
 
        // Active Statements
 
        Assert.Equal(1, baseActiveStatementMap.DocumentPathMap.Count);
 
        AssertEx.Equal(
        [
            $"1: {document.FilePath}: (6,18)-(6,22) flags=[LeafFrame, MethodUpToDate, NonLeafFrame]",
            $"0: {document.FilePath}: (15,14)-(15,18) flags=[PartiallyExecuted, NonUserCode, MethodUpToDate, NonLeafFrame]",
        ], baseActiveStatementMap.DocumentPathMap[document.FilePath].Select(InspectActiveStatement));
 
        Assert.Equal(2, baseActiveStatementMap.InstructionMap.Count);
 
        var statements = baseActiveStatementMap.InstructionMap.Values.OrderBy(v => v.InstructionId.Method.Token).ToArray();
        var s = statements[0];
        Assert.Equal(0x06000001, s.InstructionId.Method.Token);
        Assert.Equal(document.FilePath, s.FilePath);
        Assert.True(s.IsNonLeaf);
 
        s = statements[1];
        Assert.Equal(0x06000002, s.InstructionId.Method.Token);
        Assert.Equal(document.FilePath, s.FilePath);
        Assert.True(s.IsLeaf);
        Assert.True(s.IsNonLeaf);
 
        // Exception Regions
 
        var analyzer = solution.GetProject(project.Id).Services.GetRequiredService<IEditAndContinueAnalyzer>();
        var oldActiveStatements = await baseActiveStatementMap.GetOldActiveStatementsAsync(analyzer, document, CancellationToken.None).ConfigureAwait(false);
 
        AssertEx.Equal(
        [
            $"[{document.FilePath}: (8,8)-(10,9)]",
            "[]"
        ], oldActiveStatements.Select(s => "[" + string.Join(",", s.ExceptionRegions.Spans) + "]"));
    }
}