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 ) );
}
}