using System; using System.Collections.Generic; using System.IO; using System.Diagnostics; namespace ShaderGraph; /// /// Tests for ShaderGraph compilation /// These tests verify that shader graphs from the tools addon can compile correctly /// [TestClass] public class ShaderGraphTests { private Sandbox.PackageLoader packageLoader; private Sandbox.PackageLoader.Enroller enroller; /// /// Initialize the package loader and load the tools addon /// private async Task InitializePackageLoader() { packageLoader = new Sandbox.PackageLoader( "shadergraph_test", GetType().Assembly ); packageLoader.ToolsMode = true; enroller = packageLoader.CreateEnroller( "shadergraph_test" ); var project = Project.AddFromFile( "addons/tools/.sbproj" ); await Project.SyncWithPackageManager(); await Project.CompileAsync(); enroller.LoadPackage( project.Package.FullIdent, true ); packageLoader.Tick(); var loadedAssemblies = enroller.GetLoadedAssemblies(); Assert.IsTrue( loadedAssemblies.Length > 0, "No assemblies were loaded from the tools addon" ); } /// /// Find and return the required ShaderGraph types from loaded assemblies /// private (Type shaderGraphType, Type resultNodeType, Type graphCompilerType) FindShaderGraphTypes() { var loadedAssemblies = enroller.GetLoadedAssemblies(); Type shaderGraphType = null; Type resultNodeType = null; Type graphCompilerType = null; foreach ( var loadedAssembly in loadedAssemblies ) { var types = loadedAssembly.Assembly.GetTypes(); foreach ( var type in types ) { if ( type.Name == "ShaderGraph" && type.Namespace == "Editor.ShaderGraph" ) shaderGraphType = type; else if ( type.Name == "Result" && type.Namespace == "Editor.ShaderGraph" ) resultNodeType = type; else if ( type.Name == "GraphCompiler" && type.Namespace == "Editor.ShaderGraph" ) graphCompilerType = type; } } Assert.IsNotNull( shaderGraphType, "ShaderGraph type not found" ); Assert.IsNotNull( resultNodeType, "Result node type not found" ); Assert.IsNotNull( graphCompilerType, "GraphCompiler type not found" ); return (shaderGraphType, resultNodeType, graphCompilerType); } /// /// Create a minimal shader graph with a result node /// private object CreateMinimalShaderGraph( Type shaderGraphType, Type resultNodeType, string description ) { var shaderGraph = Activator.CreateInstance( shaderGraphType ); Assert.IsNotNull( shaderGraph, "Failed to create ShaderGraph instance" ); // Set basic properties var descriptionProperty = shaderGraphType.GetProperty( "Description" ); descriptionProperty?.SetValue( shaderGraph, description ); // Create a Result node var resultNode = Activator.CreateInstance( resultNodeType ); Assert.IsNotNull( resultNode, "Failed to create Result node instance" ); // Add the result node to the graph var addNodeMethod = shaderGraphType.GetMethod( "AddNode" ); Assert.IsNotNull( addNodeMethod, "AddNode method not found" ); addNodeMethod.Invoke( shaderGraph, new[] { resultNode } ); return shaderGraph; } /// /// Generate shader code from a shader graph using the GraphCompiler /// private string GenerateShaderCode( object shaderGraph, Type shaderGraphType, Type graphCompilerType ) { // Create a compiler and generate shader code var compilerConstructor = graphCompilerType.GetConstructor( new[] { shaderGraphType, typeof( bool ) } ); Assert.IsNotNull( compilerConstructor, "GraphCompiler constructor not found" ); var compiler = compilerConstructor.Invoke( new[] { shaderGraph, false } ); Assert.IsNotNull( compiler, "Failed to create GraphCompiler instance" ); // Generate the shader source code var generateMethod = graphCompilerType.GetMethod( "Generate" ); Assert.IsNotNull( generateMethod, "Generate method not found" ); var shaderCode = generateMethod.Invoke( compiler, null ) as string; Assert.IsNotNull( shaderCode, "Shader compilation failed - returned null" ); Assert.IsTrue( shaderCode.Length > 0, "Shader compilation returned empty string" ); return shaderCode; } /// /// Test that we can create a minimal shader graph and compile it to /// [TestMethod] public async Task GraphToShader() { await InitializePackageLoader(); var (shaderGraphType, resultNodeType, graphCompilerType) = FindShaderGraphTypes(); var shaderGraph = CreateMinimalShaderGraph( shaderGraphType, resultNodeType, "Test shader graph" ); var shaderCode = GenerateShaderCode( shaderGraph, shaderGraphType, graphCompilerType ); // Basic validation that it looks like shader code Assert.IsTrue( shaderCode.Contains( "HEADER" ), "Generated shader doesn't contain expected HEADER section" ); Assert.IsTrue( shaderCode.Contains( "MainPs" ), "Generated shader doesn't contain expected MainPs function" ); Console.WriteLine( $"Generated shader code length: {shaderCode.Length} characters" ); Console.WriteLine( "First 500 characters of generated shader:" ); Console.WriteLine( shaderCode.Substring( 0, Math.Min( 500, shaderCode.Length ) ) ); } /// /// Test that we can create a shader graph, generate shader code, and compile it using shadercompiler.exe /// [TestMethod] public async Task ShaderCompilation() { await InitializePackageLoader(); var (shaderGraphType, resultNodeType, graphCompilerType) = FindShaderGraphTypes(); var shaderGraph = CreateMinimalShaderGraph( shaderGraphType, resultNodeType, "Test shader graph for unit testing" ); var shaderCode = GenerateShaderCode( shaderGraph, shaderGraphType, graphCompilerType ); // Write the shader code to a file in the game directory var gameDir = Directory.GetCurrentDirectory(); var shadersDir = Path.Combine( gameDir, "core", "shaders" ); Directory.CreateDirectory( shadersDir ); var shaderFileName = "test_shadergraph_unit_test.shader"; var shaderPath = Path.Combine( shadersDir, shaderFileName ); File.WriteAllText( shaderPath, shaderCode ); Console.WriteLine( $"Wrote shader to: {shaderPath}" ); // Find shadercompiler.exe var shaderCompilerPath = Path.Combine( gameDir, "bin", "managed", "shadercompiler.exe" ); Assert.IsTrue( File.Exists( shaderCompilerPath ), $"Shader compiler not found at: {shaderCompilerPath}" ); // Run shadercompiler.exe on this specific shadershader var success = await RunShaderCompiler( shaderCompilerPath, gameDir, shaderPath ); Assert.IsTrue( success, "Shader compilation with shadercompiler.exe failed" ); // Check if shader_c file was created var shaderCPath = shaderPath + "_c"; Assert.IsTrue( File.Exists( shaderCPath ), $"Compiled shader file not found at: {shaderCPath}" ); var shaderCSize = new FileInfo( shaderCPath ).Length; Assert.IsTrue( shaderCSize > 0, "Compiled shader file is empty" ); Console.WriteLine( $"Successfully compiled shader to {shaderCPath} ({shaderCSize} bytes)" ); // Clean up try { File.Delete( shaderPath ); File.Delete( shaderCPath ); } catch ( Exception ex ) { Console.WriteLine( $"Failed to clean up temp files: {ex.Message}" ); } } private async Task RunShaderCompiler( string shaderCompilerPath, string workingDirectory, string shaderFile ) { using ( var process = new Process() ) { process.StartInfo.FileName = shaderCompilerPath; process.StartInfo.Arguments = shaderFile; // Just compile this specific shader process.StartInfo.UseShellExecute = false; process.StartInfo.RedirectStandardOutput = true; process.StartInfo.RedirectStandardError = true; process.StartInfo.CreateNoWindow = true; process.StartInfo.WorkingDirectory = workingDirectory; var outputMessages = new List(); var errorMessages = new List(); process.OutputDataReceived += ( sender, e ) => { if ( e.Data != null ) { outputMessages.Add( e.Data ); Console.WriteLine( $"ShaderCompiler: {e.Data}" ); } }; process.ErrorDataReceived += ( sender, e ) => { if ( e.Data != null ) { errorMessages.Add( e.Data ); Console.WriteLine( $"ShaderCompiler ERROR: {e.Data}" ); } }; Console.WriteLine( $"Running: {shaderCompilerPath} {process.StartInfo.Arguments}" ); process.Start(); process.BeginOutputReadLine(); process.BeginErrorReadLine(); await process.WaitForExitAsync(); if ( process.ExitCode != 0 ) { Console.WriteLine( $"Shader compiler failed with exit code: {process.ExitCode}" ); foreach ( var error in errorMessages ) { Console.WriteLine( $"Error: {error}" ); } return false; } return true; } } }