Files
sbox-public/engine/Tests/Sandbox.Test.Unit/Compiling/FastPathTest.Helpers.cs
James King 6e71079a04 Fix ILHotload falling apart when doing incremental compiles (#5376)
* Simplify by removing BeforeILHotloadProcessingTrees
* Strip ILHotload attribs when comparing syntax trees
* MethodBodyChanged attribute is versioned to support incremental compiles
* Possible fix for Facepunch/sbox-public#11275
2026-07-21 15:18:18 +02:00

209 lines
5.3 KiB
C#

using System;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
#nullable enable
namespace CompilingTests;
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 IReadOnlyList<string> SourceNames { 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( params IEnumerable<string> sourceNames )
{
SourceNames = [.. sourceNames.Select( Path.GetFileNameWithoutExtension )!];
Hotload = new ILHotload( "Test" );
Group = new CompileGroup( "Test" ) { AllowFastHotload = true };
Compiler = new Compiler( Group, SourceNames[0] );
Config = new Compiler.Configuration();
FileSystem = new MemoryFileSystem();
Compiler.AddSourceLocation( FileSystem );
Compiler.AddReference( "Sandbox.Test.Unit" );
}
public async Task<BuildResult> BuildAsync( int version = 1, bool expectNotSupported = false )
{
var compileSuccessCallback = false;
Group.OnCompileSuccess = () => compileSuccessCallback = true;
foreach ( string sourceName in SourceNames )
{
// Look for the most recent version of the file before or including the specified one
for ( var fileVersion = version; fileVersion >= 1; fileVersion-- )
{
var srcFile = new FileInfo( Path.Combine( "data", "code", "fastpath", $"{sourceName}.{fileVersion}.cs" ) );
if ( !srcFile.Exists ) continue;
await using var dstStream = FileSystem.OpenWrite( $"{sourceName}.cs" );
await using var srcStream = srcFile.OpenRead();
await srcStream.CopyToAsync( dstStream );
break;
}
}
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<string> 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 = "" )
{
using 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}" );
}
}
}
}