File: Emit\EditAndContinue\EditAndContinueTest.cs
Web Access
Project: ..\..\..\src\Compilers\CSharp\Test\Emit\Microsoft.CodeAnalysis.CSharp.Emit.UnitTests.csproj (Microsoft.CodeAnalysis.CSharp.Emit.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.
 
using System;
using System.Collections.Generic;
using System.Collections.Immutable;
using System.Diagnostics;
using System.Linq;
using System.Reflection.Metadata;
using System.Runtime.InteropServices;
using System.Threading;
using Microsoft.CodeAnalysis.CSharp.Symbols;
using Microsoft.CodeAnalysis.CSharp.Test.Utilities;
using Microsoft.CodeAnalysis.Emit;
using Microsoft.CodeAnalysis.Test.Utilities;
using Roslyn.Test.Utilities;
using Xunit;
 
namespace Microsoft.CodeAnalysis.CSharp.EditAndContinue.UnitTests
{
    internal sealed partial class EditAndContinueTest : IDisposable
    {
        private readonly CSharpCompilationOptions? _options;
        private readonly CSharpParseOptions? _parseOptions;
        private readonly TargetFramework _targetFramework;
 
        private readonly List<IDisposable> _disposables = new();
        private readonly List<GenerationInfo> _generations = new();
        private readonly List<SourceWithMarkedNodes> _sources = new();
 
        private bool _hasVerified;
 
        public EditAndContinueTest(CSharpCompilationOptions? options = null, CSharpParseOptions? parseOptions = null, TargetFramework targetFramework = TargetFramework.Standard)
        {
            _options = options ?? TestOptions.DebugDll;
            _targetFramework = targetFramework;
            _parseOptions = parseOptions ?? TestOptions.Regular.WithNoRefSafetyRulesAttribute();
        }
 
        internal EditAndContinueTest AddBaseline(string source, Action<GenerationVerifier> validator)
            => AddBaseline(EditAndContinueTestBase.MarkedSource(source, options: _parseOptions), validator);
 
        internal EditAndContinueTest AddBaseline(SourceWithMarkedNodes source, Action<GenerationVerifier> validator)
        {
            _hasVerified = false;
 
            Assert.Empty(_generations);
 
            var compilation = CSharpTestBase.CreateCompilation(source.Tree, options: _options, targetFramework: _targetFramework);
 
            var verifier = new CompilationVerifier(compilation);
 
            verifier.Emit(
                expectedOutput: null,
                trimOutput: false,
                expectedReturnCode: null,
                args: null,
                manifestResources: null,
                emitOptions: EmitOptions.Default,
                peVerify: Verification.Passes,
                expectedSignatures: null);
 
            var md = ModuleMetadata.CreateFromImage(verifier.EmittedAssemblyData);
            _disposables.Add(md);
 
            var baseline = EmitBaseline.CreateInitialBaseline(md, EditAndContinueTestBase.EmptyLocalsProvider);
 
            _generations.Add(new GenerationInfo(compilation, md.MetadataReader, diff: null, verifier, baseline, validator));
            _sources.Add(source);
 
            return this;
        }
 
        internal EditAndContinueTest AddGeneration(string source, SemanticEditDescription[] edits, Action<GenerationVerifier> validator)
            => AddGeneration(EditAndContinueTestBase.MarkedSource(source), edits, validator);
 
        internal EditAndContinueTest AddGeneration(SourceWithMarkedNodes source, SemanticEditDescription[] edits, Action<GenerationVerifier> validator)
        {
            _hasVerified = false;
 
            Assert.NotEmpty(_generations);
            Assert.NotEmpty(_sources);
 
            var previousGeneration = _generations[^1];
            var previousSource = _sources[^1];
 
            Assert.Equal(previousSource.MarkedSpans.IsEmpty, source.MarkedSpans.IsEmpty);
 
            var compilation = previousGeneration.Compilation.WithSource(source.Tree);
 
            var unmappedNodes = new List<SyntaxNode>();
 
            var semanticEdits = GetSemanticEdits(edits, previousGeneration.Compilation, previousSource, compilation, source, unmappedNodes);
 
            CompilationDifference diff;
            try
            {
                diff = compilation.EmitDifference(previousGeneration.Baseline, semanticEdits);
            }
            catch (Exception ex)
            {
                throw new Exception($"Exception during generation #{_generations.Count}. See inner stack trace for details.", ex);
            }
 
            // EncVariableSlotAllocator attempted to map from current source to the previous one,
            // but the mapping failed for these nodes. Mark the nodes in sources with node markers <N:x>...</N:x>.
            Assert.Empty(unmappedNodes);
 
            var md = diff.GetMetadata();
            _disposables.Add(md);
 
            _generations.Add(new GenerationInfo(compilation, md.Reader, diff, compilationVerifier: null, diff.NextGeneration, validator));
            _sources.Add(source);
 
            return this;
        }
 
        internal EditAndContinueTest Verify()
        {
            _hasVerified = true;
 
            Assert.NotEmpty(_generations);
 
            var readers = new List<MetadataReader>();
            int index = 0;
            foreach (var generation in _generations)
            {
                if (readers.Count > 0)
                {
                    EncValidation.VerifyModuleMvid(index, readers[^1], generation.MetadataReader);
                }
 
                readers.Add(generation.MetadataReader);
                var verifier = new GenerationVerifier(index, generation, readers);
                generation.Verifier(verifier);
 
                index++;
            }
 
            return this;
        }
 
        private ImmutableArray<SemanticEdit> GetSemanticEdits(
            SemanticEditDescription[] edits,
            Compilation oldCompilation,
            SourceWithMarkedNodes oldSource,
            Compilation newCompilation,
            SourceWithMarkedNodes newSource,
            List<SyntaxNode> unmappedNodes)
        {
            var syntaxMapFromMarkers = oldSource.MarkedSpans.IsEmpty ? null : SourceWithMarkedNodes.GetSyntaxMap(oldSource, newSource, unmappedNodes);
 
            return ImmutableArray.CreateRange(edits.Select(e =>
            {
                var oldSymbol = e.SymbolProvider(oldCompilation);
                var newSymbol = e.NewSymbolProvider(newCompilation);
 
                Func<SyntaxNode, SyntaxNode?>? syntaxMap;
                if (e.PreserveLocalVariables)
                {
                    Assert.Equal(SemanticEditKind.Update, e.Kind);
                    syntaxMap = syntaxMapFromMarkers ?? EditAndContinueTestBase.GetEquivalentNodesMap((MethodSymbol)oldSymbol, (MethodSymbol)newSymbol);
                }
                else
                {
                    syntaxMap = null;
                }
 
                return new SemanticEdit(e.Kind, oldSymbol, newSymbol, syntaxMap, e.PreserveLocalVariables);
            }));
        }
 
        public void Dispose()
        {
            // If the test has thrown an exception, or the test host has crashed, we don't want to assert here
            // or we'll hide it, so we need to do this dodgy looking thing.
            var isInException = Marshal.GetExceptionPointers() != IntPtr.Zero;
 
            Assert.True(isInException || _hasVerified, "No Verify call since the last AddGeneration call.");
            foreach (var disposable in _disposables)
            {
                disposable.Dispose();
            }
        }
    }
}