using System; using System.Collections.Generic; using System.IO; using System.Reflection; #nullable enable namespace TestCompiler; public interface IProgram { int Main( StringWriter output ); } internal record BuildResult( Assembly Assembly, Assembly MethodBodyAssembly, bool ILHotloadSupported, bool HasSupportedAttribute, (MethodBase Old, MethodBase New)[] ChangedMethods ) { public IProgram CreateProgram( string typeName = "TestPackage.Program" ) { var type = Assembly.GetType( typeName ); Assert.IsNotNull( type ); return (IProgram)Activator.CreateInstance( type )!; } } internal class FastPathTestCompiler : IDisposable { public string Name { get; } public ILHotload Hotload { get; } public CompileGroup Group { get; } public Compiler Compiler { get; } public BaseFileSystem FileSystem { get; } public BuildResult? LastResult { get; private set; } public Compiler.Configuration Config { get => Compiler.GetConfiguration(); set => Compiler.SetConfiguration( value ); } public FastPathTestCompiler( string sourceName ) { Name = Path.GetFileNameWithoutExtension( sourceName ); Hotload = new ILHotload( "Test" ); Group = new CompileGroup( "Test" ) { AllowFastHotload = true }; Compiler = new Compiler( Group, Name ); Config = new Compiler.Configuration(); FileSystem = new MemoryFileSystem(); Compiler.AddSourceLocation( FileSystem ); Compiler.AddReference( "Sandbox.Compiling.Test" ); } public async Task BuildAsync( int version = 1, bool expectNotSupported = false ) { var compileSuccessCallback = false; Group.OnCompileSuccess = () => compileSuccessCallback = true; var srcFile = new FileInfo( Path.Combine( "data", "code", "fastpath", $"{Name}.{version}.cs" ) ); await using ( var dstStream = FileSystem.OpenWrite( $"{Name}.cs" ) ) await using ( var srcStream = srcFile.OpenRead() ) { await srcStream.CopyToAsync( dstStream ); } Compiler.MarkForRecompile(); Assert.IsTrue( Compiler.NeedsBuild ); Assert.IsFalse( compileSuccessCallback ); await Group.BuildAsync(); Assert.IsTrue( compileSuccessCallback ); Assert.IsFalse( Compiler.NeedsBuild ); Assert.IsNotNull( Group.BuildResult ); Assert.IsTrue( Group.BuildResult.Success, Group.BuildResult.BuildDiagnosticsString() ); Assert.AreEqual( Group.BuildResult.Output.Count(), 1 ); var output = Group.BuildResult.Output.First(); var asm = Assembly.Load( output.AssemblyData ); var supported = ILHotload.TryFindChangedMethods( LastResult?.Assembly, LastResult?.MethodBodyAssembly, asm, out var changes, out var unexpected, out var hasAttribute ); var result = new BuildResult( asm, asm, supported, hasAttribute, changes ); Console.WriteLine( $"Built VERSION {version}:" ); Console.WriteLine( $" ILHotloadSupported: {result.ILHotloadSupported}" ); Console.WriteLine( $" HasSupportedAttribute: {result.HasSupportedAttribute}" ); Console.WriteLine( $" ChangedMethods: {(result.ChangedMethods.Length == 0 ? "None" : "")}" ); foreach ( var (oldMethod, newMethod) in result.ChangedMethods ) { Console.WriteLine( $" {newMethod.ToSimpleString()}" ); } Console.WriteLine( $" UnexpectedChanges: {(unexpected.Length == 0 ? "None" : "")}" ); foreach ( var change in unexpected ) { Console.WriteLine( $" {change.ToSimpleString()}" ); } ILHotload.IgnoreAttachedDebugger = true; try { if ( LastResult is null ) { return result; } if ( Hotload.Replace( LastResult.Assembly, LastResult.MethodBodyAssembly, result.Assembly ) ) { Assert.IsTrue( result.ILHotloadSupported ); Assert.IsFalse( expectNotSupported ); result = result with { Assembly = LastResult.Assembly }; } else { if ( result.ILHotloadSupported && System.Diagnostics.Debugger.IsAttached ) { throw new Exception( "Can't perform ILHotload while debugging" ); } if ( !expectNotSupported ) { Assert.IsFalse( result.ILHotloadSupported ); } } return result; } finally { LastResult = result; } } public void Dispose() { Compiler.Dispose(); Group.Dispose(); } } public partial class FastPathTest { private static int TestProgram( IProgram program, IEnumerable expectedOutputLines ) { return TestProgram( program, string.Join( "", expectedOutputLines.Select( x => $"{x}{Environment.NewLine}" ) ) ); } private static int TestProgram( IProgram program, params string[] expectedOutputLines ) { return TestProgram( program, expectedOutputLines.AsEnumerable() ); } private static int TestProgram( IProgram program, string expectedOutput = "" ) { var writer = new StringWriter(); try { var exitCode = program.Main( writer ); Console.WriteLine( $"Exit code: {exitCode}" ); Assert.AreEqual( expectedOutput, writer.ToString() ); return exitCode; } finally { Console.WriteLine( "Output:" ); foreach ( var line in writer.ToString().Split( Environment.NewLine ) ) { Console.WriteLine( $" {line}" ); } } } }