namespace TestCompiler; /// /// /// Tests for method body changes that can use as a fast path. /// Each test compiles multiple versions of a .cs file in code/fastpath/, with the version /// number before the file extension (e.g. HelloWorld.1.cs). /// /// /// We couldn't use preprocessor directives because they aren't detected as per-file edits. /// /// [TestClass] public partial class FastPathTest { /// /// Adding a statement within a method body. /// [TestMethod] public async Task StatementAdded() { using var compiler = new FastPathTestCompiler( "HelloWorld.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 0, TestProgram( program, "Hello World!" ) ); } /// /// Removing a statement within a method body. /// [TestMethod] public async Task StatementRemoved() { using var compiler = new FastPathTestCompiler( "HelloWorld.cs" ); var result = await compiler.BuildAsync( version: 2 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, "Hello World!" ) ); result = await compiler.BuildAsync( version: 1 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 0, TestProgram( program ) ); } /// /// Adding and then removing a statement within a method body. /// [TestMethod] public async Task StatementAddedRemoved() { using var compiler = new FastPathTestCompiler( "HelloWorld.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 0, TestProgram( program, "Hello World!" ) ); result = await compiler.BuildAsync( version: 1 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 0, TestProgram( program ) ); } /// /// Tests multiple changed methods in the same file. /// [TestMethod] public async Task MultipleChanges() { using var compiler = new FastPathTestCompiler( "MultipleChanges.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, "Hello World!" ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 2, result.ChangedMethods.Length ); Assert.AreEqual( 1, TestProgram( program ) ); } /// /// Tests only a string changing in a method body, which doesn't cause any IL to change but /// we should still swap it. /// [TestMethod] public async Task MetadataChange() { using var compiler = new FastPathTestCompiler( "HelloWorld.cs" ); var result = await compiler.BuildAsync( version: 2 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, "Hello World!" ) ); result = await compiler.BuildAsync( version: 3 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 0, TestProgram( program, "Hello Blorld!" ) ); } /// /// Tests multiple methods having the same name, but only one changes. /// [TestMethod] public async Task OverloadMethod() { using var compiler = new FastPathTestCompiler( "Overloads.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 0, TestProgram( program, "Hello World!" ) ); result = await compiler.BuildAsync( version: 3 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 2, result.ChangedMethods.Length ); Assert.AreEqual( 0, TestProgram( program ) ); } /// /// Tests changing a define in project settings. /// [TestMethod] public async Task PreProcessorSymbolChange() { using var compiler = new FastPathTestCompiler( "HelloWorld.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); compiler.Config = compiler.Config with { DefineConstants = "TEST_DEFINE" }; result = await compiler.BuildAsync( version: 2 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); } /// /// Tests changing the return type of a method. /// [TestMethod] public async Task MethodSignatureChange() { using var compiler = new FastPathTestCompiler( "MethodSignatureChange.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); result = await compiler.BuildAsync( version: 2 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); } private async Task PropertyGetterSetterTest( string packageFileName ) { using var compiler = new FastPathTestCompiler( packageFileName ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, "Hello World!" ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 0, TestProgram( program, "Hello Blorld!" ) ); } /// /// Tests the block body of a property getter changing. /// [TestMethod] public Task PropertyGetterChange1() { return PropertyGetterSetterTest( "PropertyGetter1.cs" ); } /// /// Tests the expression body of a property getter changing. /// [TestMethod] public Task PropertyGetterChange2() { return PropertyGetterSetterTest( "PropertyGetter2.cs" ); } /// /// Tests the block body of a property setter changing. /// [TestMethod] public Task PropertySetterChange1() { return PropertyGetterSetterTest( "PropertySetter1.cs" ); } /// /// Tests the expression body of a property setter changing. /// // [TestMethod] public Task PropertySetterChange2() { // TODO: Edge case where text before a ; is added in an AccessorDeclaration return PropertyGetterSetterTest( "PropertySetter2.cs" ); } /// /// Modifying a statement inside a nested Block. /// [TestMethod] public async Task NestedBlock() { using var compiler = new FastPathTestCompiler( "NestedBlocks.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, Enumerable.Range( 0, 10 ).Select( x => $"Hello {x}" ) ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 0, TestProgram( program, Enumerable.Range( 0, 10 ).Select( x => $"Hello {x + 1}" ) ) ); } /// /// Stuff like typeof(T) should reference the original assembly, not the method body providing assembly. /// [TestMethod] public async Task Reflection() { using var compiler = new FastPathTestCompiler( "Reflection.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, result.Assembly.FullName ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreNotEqual( result.Assembly.FullName, result.MethodBodyAssembly.FullName ); Assert.AreEqual( 1, TestProgram( program, result.Assembly.FullName ) ); } /// /// References to static members in changed method bodies should be updated to point to the original assembly. /// [TestMethod] public async Task StaticReference() { using var compiler = new FastPathTestCompiler( "StaticReference.cs" ); var result = await compiler.BuildAsync( version: 2 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, "Hello World" ) ); result = await compiler.BuildAsync( version: 3 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 1, TestProgram( program, "Hello Blorld!" ) ); } /// /// Make sure changed methods with parameter / return types defined in the changed module are replaced without issues. /// [TestMethod] public async Task SignatureTypes() { using var compiler = new FastPathTestCompiler( "SignatureTypes.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 1, TestProgram( program ) ); } /// /// Changes that affect generated type definitions aren't supported. /// [TestMethod] public async Task CompilerGenerated1() { using var compiler = new FastPathTestCompiler( "CompilerGenerated.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsFalse( result.ILHotloadSupported ); } /// /// Changes inside lambda expression bodies should be supported, if they don't change which variables are captured. /// [TestMethod] public async Task CompilerGenerated2() { using var compiler = new FastPathTestCompiler( "CompilerGenerated" ); var result = await compiler.BuildAsync( version: 2 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, "0, 1, 2, 3, 4, 5, 6, 7, 8, 9" ) ); result = await compiler.BuildAsync( version: 3 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 0, TestProgram( program, "1, 2, 3, 4, 5, 6, 7, 8, 9, 10" ) ); } /// /// Changes inside async method bodies should be supported, if they don't change the generated state machine type. /// [TestMethod] public async Task CompilerGenerated3() { using var compiler = new FastPathTestCompiler( "CompilerGenerated.cs" ); var result = await compiler.BuildAsync( version: 4 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, "Hello World!" ) ); result = await compiler.BuildAsync( version: 5 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 0, TestProgram( program, "Hello Blorld!" ) ); } /// /// Make sure that references to generic types involving user types are updated correctly. /// [TestMethod] public async Task GenericReference1() { using var compiler = new FastPathTestCompiler( "GenericReference.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); var program = result.CreateProgram(); Assert.AreEqual( 123, TestProgram( program, "Count: 0" ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 456, TestProgram( program, "Count: 1" ) ); } [TestMethod] public async Task GenericReference2() { using var compiler = new FastPathTestCompiler( "GenericReference.cs" ); var result = await compiler.BuildAsync( version: 3 ); Assert.IsFalse( result.ILHotloadSupported ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, "Count: 0" ) ); result = await compiler.BuildAsync( version: 4 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, TestProgram( program, "Count: 1" ) ); result = await compiler.BuildAsync( version: 3 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 0, TestProgram( program, "Count: 1" ) ); } /// /// Calls to should return the original assembly. /// [TestMethod] public async Task ExecutingAssembly() { using var compiler = new FastPathTestCompiler( "ExecutingAssembly.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, result.Assembly.FullName ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, TestProgram( program, result.Assembly.FullName ) ); Assert.AreNotEqual( result.Assembly.FullName, result.MethodBodyAssembly.FullName ); } [TestMethod] public async Task GenericReadonlyStruct() { using var compiler = new FastPathTestCompiler( "GenericReadonlyStruct.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, "hello", "world" ) ); result = await compiler.BuildAsync( version: 2 ); if ( result.ILHotloadSupported ) { Assert.AreEqual( 0, TestProgram( program, "HELLO", "WORLD" ) ); } } [TestMethod] public async Task MixedGenericMethod() { using var compiler = new FastPathTestCompiler( "MixedGenericMethod.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, "Hello 53 World" ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 0, TestProgram( program, "Hello 35 Blorld" ) ); } /// /// Reference an array of a generic type. /// [TestMethod] public async Task ResolveGeneric() { using var compiler = new FastPathTestCompiler( "ResolveGeneric.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program ) ); result = await compiler.BuildAsync( version: 2 ); Assert.AreEqual( 1, TestProgram( program ) ); } /// /// Some compiler generated methods wouldn't resolve. /// [TestMethod] public async Task ResolveLambdaMethod() { using var compiler = new FastPathTestCompiler( "ResolveLambdaMethod.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsFalse( result.ILHotloadSupported ); } /// /// Some generic methods wouldn't resolve. /// [TestMethod] public async Task ResolveGenericTupleMethod() { using var compiler = new FastPathTestCompiler( "ResolveGenericTupleMethod.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, TestProgram( program ) ); } /// /// SourceLocation attribute parameters should be ignored. /// [TestMethod] public async Task SourceLocation() { using var compiler = new FastPathTestCompiler( "SourceLocation.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 1, TestProgram( program ) ); } /// /// Commenting out a line shouldn't count as a trivial change. /// [TestMethod] public async Task CommentOutLine() { using var compiler = new FastPathTestCompiler( "CommentLine.cs" ); var result = await compiler.BuildAsync( version: 1 ); Assert.IsFalse( result.ILHotloadSupported ); Assert.AreEqual( 0, result.ChangedMethods.Length ); var program = result.CreateProgram(); Assert.AreEqual( 0, TestProgram( program, "Hello, World!" ) ); result = await compiler.BuildAsync( version: 2 ); Assert.IsTrue( result.ILHotloadSupported ); Assert.AreEqual( 1, result.ChangedMethods.Length ); Assert.AreEqual( 0, TestProgram( program ) ); } }