Files
AIC-Project/Client/PackageLocal/com.unity.pipeline@0.4.0-exp.1/Tests/Editor/RoslynCompilationServiceTests.cs
T

348 lines
12 KiB
C#

using System.Linq;
using System.Threading.Tasks;
using NUnit.Framework;
using Unity.Pipeline.Compilation;
using UnityEngine;
namespace Unity.Pipeline.Tests.Editor
{
/// <summary>
/// Tests for the shared RoslynCompilationService to verify core compilation functionality.
/// This service is used by both EvalCodeCompiler and HotReloadCompiler.
/// </summary>
public class RoslynCompilationServiceTests
{
[Test]
public void Compile_ValidCode_ReturnsSuccessfulResult()
{
// Arrange
var request = new CompilationRequest
{
SourceCode = @"
using System;
using UnityEngine;
namespace TestNamespace
{
public static class TestClass
{
public static object Execute()
{
return 42;
}
}
}",
AssemblyName = "TestAssembly"
};
// Act
var result = RoslynCompilationService.Compile(request);
// Assert
Assert.IsTrue(result.Success, "Valid code should compile successfully");
Assert.IsNotNull(result.Assembly, "Should return compiled assembly");
Assert.IsNotNull(result.AssemblyBytes, "Should return assembly bytes");
Assert.IsNotEmpty(result.AssemblyBytes, "Assembly bytes should not be empty");
Assert.IsNotNull(result.Diagnostics, "Should return diagnostics list");
}
[Test]
public void Compile_InvalidSyntax_ReturnsCompilationErrors()
{
// Arrange
var request = new CompilationRequest
{
SourceCode = @"
using System;
public static class TestClass
{
public static object Execute()
{
return 42 +; // Syntax error
}
}",
AssemblyName = "InvalidTestAssembly"
};
// Act
var result = RoslynCompilationService.Compile(request);
// Assert
Assert.IsFalse(result.Success, "Invalid syntax should fail compilation");
Assert.IsNull(result.Assembly, "Should not return assembly on failure");
Assert.IsNotNull(result.Diagnostics, "Should return diagnostics");
Assert.Greater(result.Diagnostics.Count, 0, "Should have compilation errors");
var errorDiagnostic = result.Diagnostics.FirstOrDefault(d => d.Severity == "error");
Assert.IsNotNull(errorDiagnostic, "Should have at least one error diagnostic");
Assert.IsNotEmpty(errorDiagnostic.Message, "Error should have message");
Assert.IsNotEmpty(errorDiagnostic.Id, "Error should have Roslyn diagnostic ID");
}
[Test]
public async Task CompileAsync_ValidCode_WorksAsynchronously()
{
// Arrange
var request = new CompilationRequest
{
SourceCode = @"
using UnityEngine;
public static class AsyncTestClass
{
public static object Execute()
{
return Application.unityVersion;
}
}",
AssemblyName = "AsyncTestAssembly"
};
// Act
var result = await RoslynCompilationService.CompileAsync(request);
// Assert
Assert.IsTrue(result.Success, "Async compilation should succeed");
Assert.IsNotNull(result.Assembly, "Should return compiled assembly");
Assert.Contains("AsyncTestClass", result.Assembly.GetTypes().Select(t => t.Name).ToList(),
"Should contain expected class");
}
[Test]
public void Compile_WithLineNumberOffset_AdjustsDiagnosticLineNumbers()
{
// Arrange - Code with error and line offset to simulate wrapper code
var request = new CompilationRequest
{
SourceCode = @"using System;
using UnityEngine;
// Some wrapper code lines...
// More wrapper lines...
// Even more wrapper lines...
public static class TestClass
{
public static object Execute()
{
return undefined_variable; // Error on line ~10, but should be reported as line ~4-5 after offset
}
}",
AssemblyName = "LineOffsetTestAssembly",
LineNumberOffset = 6 // Simulate 6 lines of wrapper code
};
// Act
var result = RoslynCompilationService.Compile(request);
// Assert
Assert.IsFalse(result.Success, "Code with undefined variable should fail");
Assert.Greater(result.Diagnostics.Count, 0, "Should have diagnostics");
var errorDiagnostic = result.Diagnostics.FirstOrDefault(d => d.Severity == "error");
Assert.IsNotNull(errorDiagnostic, "Should have error diagnostic");
Assert.Less(errorDiagnostic.Line, 6, "Line number should be adjusted by offset");
}
[Test]
public void GetMetadataReferences_InEditor_ReturnsAllReferences()
{
// Act
var references = RoslynCompilationService.GetMetadataReferences();
// Assert
Assert.IsNotNull(references, "Should return references list");
Assert.Greater(references.Count, 0, "Should have at least some references");
// Should include Unity references
var hasUnityEngine = references.Any(r => r.Display?.Contains("UnityEngine") == true);
Assert.IsTrue(hasUnityEngine, "Should include UnityEngine references");
// Should include system references
var hasSystemReferences = references.Any(r => r.Display?.Contains("System") == true);
Assert.IsTrue(hasSystemReferences, "Should include System references");
}
[Test]
public void GetMetadataReferences_WithAdditionalPrefixes_IncludesSpecifiedAssemblies()
{
// Arrange
var additionalPrefixes = new[] { "Microsoft.CodeAnalysis", "Newtonsoft.Json" };
// Act
var references = RoslynCompilationService.GetMetadataReferences(additionalPrefixes);
// Assert
Assert.IsNotNull(references, "Should return references list");
Assert.Greater(references.Count, 0, "Should have references");
// Should include additional prefixes if those assemblies are loaded
var hasCodeAnalysis = references.Any(r => r.Display?.Contains("Microsoft.CodeAnalysis") == true);
var hasNewtonsoft = references.Any(r => r.Display?.Contains("Newtonsoft.Json") == true);
}
[Test]
public void Compile_WithHotReloadExample_CompilesCorrectly()
{
// Arrange - Test hot reload specific compilation
var request = new CompilationRequest
{
SourceCode = @"
using System;
using UnityEngine;
using Unity.Pipeline.HotReload;
public static class HotReloadTestClass
{
[HotReloadOverrideMethod(""TargetClass.TargetMethod"")]
public static void OverrideMethod(TargetClass instance)
{
Debug.Log(""Hot reload method executed"");
}
}
public class TargetClass
{
public void TargetMethod()
{
Debug.Log(""Original method"");
}
}",
AssemblyName = "HotReloadTestAssembly",
AdditionalAssemblyPrefixes = new[] { "Microsoft.CodeAnalysis" } // For hot reload attributes
};
// Act
var result = RoslynCompilationService.Compile(request);
// Assert
Assert.IsTrue(result.Success, $"Hot reload code should compile. Errors: {string.Join(", ", result.Diagnostics?.Select(d => d.Message) ?? new string[0])}");
Assert.IsNotNull(result.Assembly, "Should return compiled assembly");
var types = result.Assembly.GetTypes();
Assert.Contains("HotReloadTestClass", types.Select(t => t.Name).ToList(), "Should contain hot reload class");
Assert.Contains("TargetClass", types.Select(t => t.Name).ToList(), "Should contain target class");
}
[Test]
public void Compile_WithEvalExample_CompilesCorrectly()
{
// Arrange - Test eval-style compilation (Execute method wrapper)
var request = new CompilationRequest
{
SourceCode = @"
using System;
using UnityEngine;
namespace PipelineEvaluation
{
public static class PipelineEval_test123
{
public static object Execute()
{
var result = 2 + 2;
Debug.Log($""Eval calculation: {result}"");
return result;
}
}
}",
AssemblyName = "EvalTestAssembly",
LineNumberOffset = 11 // Simulate EvalCodeCompiler's wrapper offset
};
// Act
var result = RoslynCompilationService.Compile(request);
// Assert
Assert.IsTrue(result.Success, $"Eval code should compile. Errors: {string.Join(", ", result.Diagnostics?.Select(d => d.Message) ?? new string[0])}");
Assert.IsNotNull(result.Assembly, "Should return compiled assembly");
var evalType = result.Assembly.GetType("PipelineEvaluation.PipelineEval_test123");
Assert.IsNotNull(evalType, "Should find eval class");
var executeMethod = evalType.GetMethod("Execute");
Assert.IsNotNull(executeMethod, "Should find Execute method");
Assert.IsTrue(executeMethod.IsStatic, "Execute method should be static");
}
[Test]
public void Compile_WithEmitDebugInformation_ProducesPortablePdbAndLoadableAssembly()
{
// Arrange
var request = new CompilationRequest
{
SourceCode = @"
public static class DebugClass
{
public static int Execute()
{
return 7;
}
}",
AssemblyName = "PdbTestAssembly_" + System.Guid.NewGuid().ToString("N"),
EmitDebugInformation = true,
DocumentPath = "PdbTest.cs"
};
// Act
var result = RoslynCompilationService.Compile(request);
// Assert
Assert.IsTrue(result.Success, $"Should compile. Errors: {string.Join(", ", result.Diagnostics?.Select(d => d.Message) ?? new string[0])}");
Assert.IsNotNull(result.Assembly, "Should return loadable assembly");
Assert.IsNotNull(result.PdbBytes, "Should emit a PDB when EmitDebugInformation is set");
Assert.IsNotEmpty(result.PdbBytes, "PDB bytes should not be empty");
// Portable PDB blobs start with the "BSJB" metadata signature.
Assert.AreEqual((byte)'B', result.PdbBytes[0]);
Assert.AreEqual((byte)'S', result.PdbBytes[1]);
Assert.AreEqual((byte)'J', result.PdbBytes[2]);
Assert.AreEqual((byte)'B', result.PdbBytes[3]);
}
[Test]
public void Compile_WithoutEmitDebugInformation_ProducesNoPdb()
{
// Arrange
var request = new CompilationRequest
{
SourceCode = @"
public static class NoDebugClass
{
public static int Execute() { return 1; }
}",
AssemblyName = "NoPdbTestAssembly_" + System.Guid.NewGuid().ToString("N")
};
// Act
var result = RoslynCompilationService.Compile(request);
// Assert
Assert.IsTrue(result.Success, "Should compile");
Assert.IsNull(result.PdbBytes, "Default path must not emit a PDB (behavior unchanged)");
}
[Test]
public void Compile_ExceptionHandling_ReturnsGracefulError()
{
// Arrange - Request that might cause internal exception
var request = new CompilationRequest
{
SourceCode = null, // This might cause internal exception
AssemblyName = "ExceptionTestAssembly"
};
// Act
var result = RoslynCompilationService.Compile(request);
// Assert
Assert.IsFalse(result.Success, "Null source code should fail gracefully");
Assert.IsNotNull(result.Diagnostics, "Should return diagnostics even on exception");
Assert.Greater(result.Diagnostics.Count, 0, "Should have error diagnostic");
var errorDiagnostic = result.Diagnostics.First();
Assert.AreEqual("error", errorDiagnostic.Severity, "Should be marked as error");
Assert.That(errorDiagnostic.Message, Does.Contain("exception"), "Should mention exception");
}
}
}