Files
sbox-public/engine/Sandbox.Compiling.Test/Tests/CodeGen.cs
s&box team 71f266059a Open source release
This commit imports the C# engine code and game files, excluding C++ source code.

[Source-Commit: ceb3d758046e50faa6258bc3b658a30c97743268]
2025-11-24 09:05:18 +00:00

333 lines
15 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Collections.Generic;
using System.IO;
using System.Reflection;
namespace Generator
{
[TestClass]
public class CodeGen
{
private void AddTree( List<SyntaxTree> syntaxTree, string path )
{
var parseOptions = CSharpParseOptions.Default.WithLanguageVersion( LanguageVersion.Default );
var code = System.IO.File.ReadAllText( path );
var tree = CSharpSyntaxTree.ParseText( text: code, options: parseOptions, path: path, encoding: System.Text.Encoding.UTF8 );
syntaxTree.Add( tree );
}
CSharpCompilation Build( string assemblyName, params string[] files )
{
List<SyntaxTree> SyntaxTree = new List<SyntaxTree>();
foreach ( var file in files )
AddTree( SyntaxTree, $"data/codegen/{file}" );
var optn = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary )
.WithConcurrentBuild( true )
.WithOptimizationLevel( OptimizationLevel.Debug )
.WithGeneralDiagnosticOption( ReportDiagnostic.Info )
.WithPlatform( Microsoft.CodeAnalysis.Platform.AnyCpu )
.WithAllowUnsafe( false );
var refs = new List<MetadataReference>();
var path = System.IO.Path.GetDirectoryName( typeof( System.Object ).Assembly.Location );
refs.Add( MetadataReference.CreateFromFile( typeof( System.Object ).Assembly.Location ) );
refs.Add( MetadataReference.CreateFromFile( $"{path}\\System.Runtime.dll" ) );
refs.Add( MetadataReference.CreateFromFile( typeof( Networking ).Assembly.Location ) );
refs.Add( MetadataReference.CreateFromFile( typeof( ConCmdAttribute ).Assembly.Location ) ); // Sandbox.System
CSharpCompilation compiler = CSharpCompilation.Create( $"{assemblyName}.dll", SyntaxTree, refs, optn );
var processor = new Sandbox.Generator.Processor();
processor.AddonName = assemblyName;
processor.PackageAssetResolver = ( p ) => $"/{p}/model_mock.mdl";
processor.Run( compiler );
compiler = processor.Compilation;
foreach ( var tree in compiler.SyntaxTrees )
{
System.Console.WriteLine( tree.FilePath );
System.Console.WriteLine( tree.GetText().ToString() );
}
return compiler;
}
CSharpCompilation BuildString( string contents, string assemblyName = "assembly_name" )
{
List<SyntaxTree> SyntaxTree = new List<SyntaxTree>();
{
var parseOptions = CSharpParseOptions.Default.WithLanguageVersion( LanguageVersion.Default );
var code = contents;
var tree = CSharpSyntaxTree.ParseText( text: code, options: parseOptions, path: "Program.cs", encoding: System.Text.Encoding.UTF8 );
SyntaxTree.Add( tree );
}
var optn = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary )
.WithConcurrentBuild( true )
.WithOptimizationLevel( OptimizationLevel.Debug )
.WithGeneralDiagnosticOption( ReportDiagnostic.Info )
.WithPlatform( Microsoft.CodeAnalysis.Platform.AnyCpu )
.WithAllowUnsafe( false );
var refs = new List<MetadataReference>();
var path = System.IO.Path.GetDirectoryName( typeof( System.Object ).Assembly.Location );
refs.Add( MetadataReference.CreateFromFile( typeof( System.Object ).Assembly.Location ) );
refs.Add( MetadataReference.CreateFromFile( $"{path}\\System.Runtime.dll" ) );
refs.Add( MetadataReference.CreateFromFile( typeof( Networking ).Assembly.Location ) );
refs.Add( MetadataReference.CreateFromFile( typeof( ConCmdAttribute ).Assembly.Location ) ); // Sandbox.System
CSharpCompilation compiler = CSharpCompilation.Create( $"{assemblyName}.dll", SyntaxTree, refs, optn );
var processor = new Sandbox.Generator.Processor();
processor.AddonName = assemblyName;
processor.PackageAssetResolver = ( p ) => $"/{p}/model_mock.mdl";
processor.Run( compiler );
compiler = processor.Compilation;
foreach ( var tree in compiler.SyntaxTrees )
{
System.Console.WriteLine( tree.FilePath );
System.Console.WriteLine( tree.GetText().ToString() );
}
return compiler;
}
[TestMethod]
[DataRow( "TestVar.cs" )]
[DataRow( "TestRpc.cs" )]
[DataRow( "TestReplicate.cs" )]
[DataRow( "TestStackOverflow.cs" )]
[DataRow( "TestRpc.Entity.cs" )]
[DataRow( "TestReplicate.Entity.cs" )]
public void DoCodeGen( string filename )
{
Build( "do_code_gen", filename );
}
[TestMethod]
public void AdditionalFiles()
{
List<SyntaxTree> SyntaxTree = new List<SyntaxTree>();
var optn = new CSharpCompilationOptions( OutputKind.DynamicallyLinkedLibrary )
.WithConcurrentBuild( true )
.WithOptimizationLevel( OptimizationLevel.Debug )
.WithGeneralDiagnosticOption( ReportDiagnostic.Info )
.WithPlatform( Microsoft.CodeAnalysis.Platform.AnyCpu )
.WithAllowUnsafe( false );
var refs = new List<MetadataReference>();
var path = System.IO.Path.GetDirectoryName( typeof( System.Object ).Assembly.Location );
refs.Add( MetadataReference.CreateFromFile( typeof( System.Object ).Assembly.Location ) );
refs.Add( MetadataReference.CreateFromFile( $"{path}\\System.Runtime.dll" ) );
refs.Add( MetadataReference.CreateFromFile( typeof( Networking ).Assembly.Location ) );
refs.Add( MetadataReference.CreateFromFile( typeof( ConCmdAttribute ).Assembly.Location ) );
CSharpCompilation compiler = CSharpCompilation.Create( $"poopy.dll", SyntaxTree, refs, optn );
// Process Razor files using RazorProcessor before running the Processor
foreach ( var file in System.IO.Directory.EnumerateFiles( "data/codegen/", "*.razor" ) )
{
var razorText = System.IO.File.ReadAllText( file );
var generatedCode = Sandbox.Razor.RazorProcessor.GenerateFromSource( razorText, file );
var razorTree = CSharpSyntaxTree.ParseText( generatedCode, path: $"_gen_{System.IO.Path.GetFileName( file )}.cs", encoding: System.Text.Encoding.UTF8 );
compiler = compiler.AddSyntaxTrees( razorTree );
}
var processor = new Sandbox.Generator.Processor();
processor.AddonName = $"poopy";
processor.Run( compiler );
compiler = processor.Compilation;
foreach ( var tree in compiler.SyntaxTrees )
{
System.Console.WriteLine( tree.GetText().ToString() );
}
}
/// <summary>
/// Note that our aim here is to prevent 99% of accidental overflows. There are still going to be ways
/// to overflow and force quit.. but this should all stop the accidental stuff.
/// </summary>
// [TestMethod]
public void StackOverflowPrevention()
{
var compiler = Build( "do_code_gen", "TestStackOverflow.cs" );
// actually compile
using var stream = new MemoryStream();
var result = compiler.Emit( stream );
Assert.IsTrue( result.Success, "Failed to compile!" );
// Load it
var assemblyBytes = stream.ToArray();
var asm = System.Reflection.Assembly.Load( assemblyBytes );
// Call RecurseForever
var TestStackOverflow = asm.GetType( "TestStackOverflow" );
// Should successfully throw an exception due to stack overflowing
var RecurseForever = TestStackOverflow.GetMethod( "RecurseForever" );
Assert.ThrowsException<TargetInvocationException>( () => RecurseForever.Invoke( null, null ) );
var RecurseForeverInline = TestStackOverflow.GetMethod( "RecurseForeverInline" );
Assert.ThrowsException<TargetInvocationException>( () => RecurseForeverInline.Invoke( null, null ) );
}
[TestMethod]
public void TestCloudAssets()
{
var compiler = Build( "do_code_gen", "CloudAssetTest.cs" );
Assert.AreEqual( 2, compiler.SyntaxTrees.Count(), "Should have the original syntaxtree plus the generated one" );
var added = compiler.SyntaxTrees.First( x => x.FilePath.Contains( "_gen__AddedCode.cs" ) );
Assert.IsTrue( added.GetText().ToString().Contains( "[assembly:Sandbox.Cloud.Asset( " ), "Outputted text should contain the asset attribute" );
}
[TestMethod]
public void TestWrapCall()
{
var compiler = Build( "do_code_gen", "TestWrapCall.cs" );
var tree = compiler.SyntaxTrees.First();
System.Console.WriteLine( tree.GetText().ToString() );
Assert.IsTrue( tree.GetText().ToString().Contains( "WrapCall.OnMethodInvokedStatic( new global::Sandbox.WrappedMethod {Resume = () => {},Object = null,MethodIdentity = -1168963981,MethodName = \"TestWrappedStaticCall\",TypeName = \"TestWrapCall\",IsStatic = true,Attributes = __m_1168963981__Attrs}, arga );" ), "Generated code should wrap static method call" );
Assert.IsTrue( tree.GetText().ToString().Contains( "OnMethodInvoked( new global::Sandbox.WrappedMethod {Resume = () => {},Object = this,MethodIdentity = -446800946,MethodName = \"TestWrappedInstanceCall\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __m_446800946__Attrs}, arga );" ), "Generated code should wrap instance method call" );
Assert.IsTrue( tree.GetText().ToString().Contains( "WrapCall.OnMethodInvokedStatic( new global::Sandbox.WrappedMethod {Resume = () => {},Object = null,MethodIdentity = 1638661065,MethodName = \"TestWrappedStaticCallNoArg\",TypeName = \"TestWrapCall\",IsStatic = true,Attributes = __1638661065__Attrs} );" ), "Generated code should wrap static method call with no arg" );
Assert.IsTrue( tree.GetText().ToString().Contains( "OnMethodInvoked( new global::Sandbox.WrappedMethod {Resume = () => {},Object = this,MethodIdentity = -1769572979,MethodName = \"TestWrappedInstanceCallNoArg\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __m_1769572979__Attrs} );" ), "Generated code should wrap instance method call with no arg" );
Assert.IsTrue( tree.GetText().ToString().Contains( "public void ExpressionBodiedBroadcast() => OnMethodInvoked( new global::Sandbox.WrappedMethod {Resume = () => Log.Info( \"Test.\" ),Object = this,MethodIdentity = 1201362747,MethodName = \"ExpressionBodiedBroadcast\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __1201362747__Attrs} );" ), "Generated code should wrap expression bodied method" );
Assert.IsTrue( tree.GetText().ToString().Contains( "Object = this,MethodIdentity = -1316352073,MethodName = \"TestWrappedInstanceCallReturnType\",TypeName = \"TestWrapCall\",IsStatic = false,Attributes = __m_1316352073__Attrs}, arga );" ), "Generated code should wrap instance method call with return type" );
Assert.IsTrue( tree.GetText().ToString().Contains( "return WrapCall.OnMethodInvokedStatic( new global::Sandbox.WrappedMethod<Task<bool>> {Resume = async () =>" ), "Generated code should wrap async Task method call" );
}
[TestMethod]
public void TestPreprocessorWrappedCall()
{
var compiler = Build( "do_code_gen", "TestPreprocessorWrapCall.cs" );
var tree = compiler.SyntaxTrees.First();
var treeText = tree.GetText().ToString();
Assert.IsTrue( treeText.Contains( "{\r\n#if true\r\n\t\tif ( true )\r\n\t\t{\r\n\t\t\t// Hello there!\r\n\t\t}\r\n#endif\r\n\t}" ), "Maintain preprocessor directive in body" );
Assert.IsFalse( treeText.Contains( $");}}#endif" ), "No trailing trivia outside of body" );
}
[TestMethod]
public void TestWrapGet()
{
var compiler = Build( "do_code_gen", "TestWrapGet.cs" );
var tree = compiler.SyntaxTrees.First();
System.Console.WriteLine( tree.GetText().ToString() );
Assert.IsTrue( tree.GetText().ToString().Contains( "return (bool)WrapGet.OnWrapGetStatic(new global::Sandbox.WrappedPropertyGet<bool> { Value = value, Object = null, IsStatic = true, TypeName = \"TestWrapGet\", PropertyName = \"StaticProperty\", MemberIdent = 2112852062, Attributes = __StaticProperty__Attrs });" ), "Generated code should wrap static property get accessor" );
Assert.IsTrue( tree.GetText().ToString().Contains( "return (bool)OnWrapGet(new global::Sandbox.WrappedPropertyGet<bool> { Value = value, Object = this, IsStatic = false, TypeName = \"TestWrapGet\", PropertyName = \"InstanceProperty\", MemberIdent = 1816497229, Attributes = __InstanceProperty__Attrs });" ), "Generated code should wrap instance property get accessor" );
Assert.IsTrue( tree.GetText().ToString().Contains( "_repback__InstanceProperty= true;" ), "Generated code should copy initializer" );
}
[TestMethod]
public void TestWrapSet()
{
var compiler = Build( "do_code_gen", "TestWrapSet.cs" );
var tree = compiler.SyntaxTrees.First();
System.Console.WriteLine( tree.GetText().ToString() );
Assert.IsTrue( tree.GetText().ToString().Contains( "WrapSet.OnWrapSetStatic(new global::Sandbox.WrappedPropertySet<bool> { Value = value, Object = null, Setter = (v) =>" ), "Generated code should wrap static property set accessor" );
Assert.IsTrue( tree.GetText().ToString().Contains( "OnWrapSet(new global::Sandbox.WrappedPropertySet<bool> { Value = value, Object = this, Setter = (v) =>" ), "Generated code should wrap instance property set accessor" );
}
[TestMethod]
public void CodeGenFullyQualified()
{
var compiler = BuildString( """
namespace CodeGenFullyQualified;
[Sandbox.CodeGenerator(Sandbox.CodeGeneratorFlags.WrapPropertyGet | Sandbox.CodeGeneratorFlags.Static | Sandbox.CodeGeneratorFlags.Instance, "CodeGenFullyQualified.TestCodeGenAttribute.GetWrapper")]
[System.AttributeUsage(System.AttributeTargets.Property)]
public class TestCodeGenAttribute : System.Attribute
{
internal static T GetWrapper<T>(Sandbox.WrappedPropertyGet<T> property)
{
return property.Value;
}
}
public class Program
{
[TestCodeGen] public bool TestProperty { get; set; }
public static void Main()
{
}
}
""" );
var tree = compiler.SyntaxTrees.First();
Assert.IsTrue( compiler.GetDiagnostics().Length == 0, "Should be no compile errors" );
}
[TestMethod]
public void StringTokenReplacement()
{
var compiler = BuildString( """"
using Sandbox;
public static class Program
{
public static void Main()
{
RenderAttributes ra = new ();
ra.Set( "PoopSock", Vector3.One );
}
}
"""" );
var tree = compiler.SyntaxTrees.First();
Assert.IsTrue( tree.GetText().ToString().Contains( "\"PoopSock\",1590150592U" ), "Should have replaced string token" );
}
[TestMethod]
public void ParameterDescriptionAttribute()
{
var compiler = Build( "do_code_gen", "Descriptions.cs" );
var tree = compiler.SyntaxTrees.First();
Assert.IsTrue( tree.GetText().ToString().Contains( "[DescriptionAttribute( \"This parameter has a description!\" )]int baz" ) );
}
[TestMethod]
public void ReturnsDescriptionAttribute()
{
var compiler = Build( "do_code_gen", "Descriptions.cs" );
var tree = compiler.SyntaxTrees.First();
Assert.IsTrue( tree.GetText().ToString().Contains( "[return:DescriptionAttribute( \"Here's a description of the return value!\" )]" ) );
}
}
}