diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs index 2e228d7205..7437031398 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/AutoInjectedNamespacesTests.cs @@ -1,11 +1,7 @@ using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using NUnit.Framework; using io.github.hatayama.UnityCliLoop.FirstPartyTools; -using io.github.hatayama.UnityCliLoop.ToolContracts; -using io.github.hatayama.UnityCliLoop.Domain; namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { @@ -77,193 +73,4 @@ public void Resolve_WhenAlreadyHasUsing_ShouldNotReportIt() } } - /// - /// Test fixture that verifies Auto Injected Namespaces Integration behavior. - /// - [TestFixture] - public class AutoInjectedNamespacesIntegrationTests - { - private IPreloadAssemblySecurityValidator _previousValidator; - - [SetUp] - public void SetUp() - { - _previousValidator = PreloadAssemblySecurityValidatorRegistry.SwapValidatorForTests( - new SystemReflectionMetadataPreloadValidator()); - } - - [TearDown] - public void TearDown() - { - PreloadAssemblySecurityValidatorRegistry.SwapValidatorForTests(_previousValidator); - } - - [Test] - public async Task CompileAsync_ScriptMode_MissingUsing_ShouldReportAutoInjectedNamespaces() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - StringBuilder builder = new(); - builder.Append(""hello""); - return builder.ToString(); - ", - ClassName = "AutoInjectedNsTestCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Should compile"); - Assert.That(result.AutoInjectedNamespaces, Does.Contain("System.Text")); - } - - [Test] - public async Task CompileAsync_ScriptMode_NoMissingUsing_ShouldReportEmptyAutoInjectedNamespaces() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = "return 1 + 2;", - ClassName = "NoAutoInjectionCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Should compile"); - Assert.That(result.AutoInjectedNamespaces, Is.Empty); - } - - [Test] - public async Task CompileAsync_ScriptMode_WithExistingUsing_ShouldNotReportIt() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - using System.Text; - StringBuilder builder = new(); - builder.Append(""already imported""); - return builder.ToString(); - ", - ClassName = "ExistingUsingCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Should compile"); - Assert.That(result.AutoInjectedNamespaces, Is.Empty); - } - - [Test] - public async Task CompileAsync_ScriptMode_MultipleMissing_ShouldReportAll() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - StringBuilder sb = new(); - Regex regex = new(@""\d+""); - return sb.ToString() + regex.ToString(); - ", - ClassName = "MultipleAutoInjectedCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Should compile"); - Assert.That(result.AutoInjectedNamespaces, Does.Contain("System.Text")); - Assert.That(result.AutoInjectedNamespaces, Does.Contain("System.Text.RegularExpressions")); - } - - [Test] - public async Task CompileAsync_RawMode_MissingUsing_ShouldReportAutoInjectedNamespaces() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - /// - /// Test fixture that verifies Raw Mode Missing Using behavior. - /// - public class RawModeMissingUsingTest - { - public async System.Threading.Tasks.Task ExecuteAsync( - System.Collections.Generic.Dictionary parameters = null, - System.Threading.CancellationToken ct = default) - { - StringBuilder sb = new(); - sb.Append(""raw""); - return sb.ToString(); - } - } - ", - ClassName = "RawModeMissingUsingCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Raw mode should compile with auto-using"); - Assert.That(result.AutoInjectedNamespaces, Does.Contain("System.Text")); - } - - [Test] - public async Task CompileAsync_RawMode_FullyQualifiedNames_ShouldReportEmptyAutoInjectedNamespaces() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - /// - /// Test fixture that verifies Raw Mode Auto Injected behavior. - /// - public class RawModeAutoInjectedTest - { - public async System.Threading.Tasks.Task ExecuteAsync( - System.Collections.Generic.Dictionary parameters = null, - System.Threading.CancellationToken ct = default) - { - System.Text.StringBuilder sb = new(); - sb.Append(""raw""); - return sb.ToString(); - } - } - ", - ClassName = "RawModeAutoInjectedCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Raw mode should compile"); - // Raw mode uses fully-qualified names, so no auto-injection needed - Assert.That(result.AutoInjectedNamespaces, Is.Empty); - } - - [Test] - public async Task CompileAsync_ScriptMode_ShouldPopulateCoreTimings() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = "return 1 + 2;", - ClassName = "TimingVisibilityCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, "Timing test should compile"); - Assert.That(result.Timings, Has.Some.StartsWith("[Perf] ReferenceResolution:")); - Assert.That(result.Timings, Has.Some.StartsWith("[Perf] Build:")); - Assert.That(result.Timings, Has.Some.StartsWith("[Perf] AssemblyLoad:")); - Assert.That(result.Timings, Has.Some.StartsWith("[Perf] CompilePlan:")); - Assert.That(result.Timings, Has.Some.StartsWith("[Perf] CompileCacheCheck:")); - Assert.That(result.Timings, Has.Some.StartsWith("[Perf] CompilerTotal:")); - } - } } diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/CommandRunnerTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/CommandRunnerTests.cs deleted file mode 100644 index c3fe961e01..0000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/CommandRunnerTests.cs +++ /dev/null @@ -1,55 +0,0 @@ -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using NUnit.Framework; - -using io.github.hatayama.UnityCliLoop.FirstPartyTools; -using io.github.hatayama.UnityCliLoop.ToolContracts; - -namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests -{ - /// - /// Test fixture that verifies Command Runner behavior. - /// - [TestFixture] - public class CommandRunnerTests - { - [Test] - public async Task ExecuteAsync_WhenCallerCancellationIsRequested_ShouldReturnNeutralCancelledMessage() - { - CommandRunner runner = new(); - using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - cancellationTokenSource.Cancel(); - - io.github.hatayama.UnityCliLoop.FirstPartyTools.ExecutionContext context = new() { - CompiledAssembly = typeof(global::io.github.hatayama.UnityCliLoop.Tests.Editor.Dynamic.DynamicCommand).Assembly, - Parameters = new Dictionary(), - CancellationToken = cancellationTokenSource.Token - }; - - ExecutionResult result = await runner.ExecuteAsync(context); - - Assert.That(result.Success, Is.False); - Assert.That(result.ErrorMessage, Is.EqualTo(UnityCliLoopConstants.ERROR_MESSAGE_EXECUTION_CANCELLED)); - Assert.That(result.Logs, Contains.Item("Execution cancelled")); - Assert.That(result.Logs, Has.No.Member("Execution cancelled due to timeout")); - } - - [Test] - public async Task ExecuteAsync_WhenSyncFallbackAcceptsCancellationToken_ShouldUseSupportedSignature() - { - CommandRunner runner = new(); - - io.github.hatayama.UnityCliLoop.FirstPartyTools.ExecutionContext context = new() { - CompiledAssembly = typeof(global::io.github.hatayama.UnityCliLoop.Tests.Editor.Dynamic.DynamicCommand).Assembly, - Parameters = new Dictionary(), - CancellationToken = CancellationToken.None - }; - - ExecutionResult result = await runner.ExecuteAsync(context); - - Assert.That(result.Success, Is.True); - Assert.That(result.Result, Is.EqualTo("dictionary-and-cancellation")); - } - } -} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/CommandRunnerTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/CommandRunnerTests.cs.meta deleted file mode 100644 index 692517f920..0000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/CommandRunnerTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 81d30515a1a6e44138190d12a50d7635 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyBuilderTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyBuilderTests.cs index 044717e2c5..e9fe0d5e00 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyBuilderTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/CompiledAssemblyBuilderTests.cs @@ -1,7 +1,4 @@ -using System.Threading; -using System.Threading.Tasks; using NUnit.Framework; -using UnityEditor.Compilation; using io.github.hatayama.UnityCliLoop.FirstPartyTools; namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests @@ -24,51 +21,5 @@ public void CreateUniqueCompilationName_WhenClassNameContainsPathCharacters_Shou Assert.That(compilationName, Does.Not.Contain("\\")); Assert.That(compilationName, Does.Not.Contain(":")); } - - [Test] - public void AwaitBuildCompletionAsync_WhenCancellationIsRequested_ShouldCancelAfterBuildCompletion() - { - TaskCompletionSource buildTaskCompletionSource = - new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - - Task waitTask = AssemblyBuilderFallbackCompilerBackend.AwaitBuildCompletionAsync( - buildTaskCompletionSource.Task, - cancellationTokenSource.Token); - - cancellationTokenSource.Cancel(); - - Assert.That(waitTask.IsCompleted, Is.False); - - buildTaskCompletionSource.SetResult(System.Array.Empty()); - - Assert.ThrowsAsync(async () => await waitTask); - } - - [Test] - public async Task RegisterBuildFinishedContinuation_WhenCancellationWins_ShouldWaitForActualBuildCompletion() - { - TaskCompletionSource buildTaskCompletionSource = - new TaskCompletionSource(TaskCreationOptions.RunContinuationsAsynchronously); - using CancellationTokenSource cancellationTokenSource = new CancellationTokenSource(); - bool buildFinished = false; - - Task continuationTask = AssemblyBuilderFallbackCompilerBackend.RegisterBuildFinishedContinuation( - buildTaskCompletionSource.Task, - () => buildFinished = true); - Task waitTask = AssemblyBuilderFallbackCompilerBackend.AwaitBuildCompletionAsync( - buildTaskCompletionSource.Task, - cancellationTokenSource.Token); - - cancellationTokenSource.Cancel(); - - Assert.That(buildFinished, Is.False); - - buildTaskCompletionSource.SetResult(System.Array.Empty()); - Assert.That(async () => await waitTask, Throws.InstanceOf()); - await continuationTask; - - Assert.That(buildFinished, Is.True); - } } } diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorDictionaryErrorTest.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorDictionaryErrorTest.cs deleted file mode 100644 index f314f181d4..0000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorDictionaryErrorTest.cs +++ /dev/null @@ -1,159 +0,0 @@ -#if UNITYCLILOOP_HAS_ROSLYN -using NUnit.Framework; -using System.Threading; -using System.Threading.Tasks; - -using io.github.hatayama.UnityCliLoop.FirstPartyTools; -using io.github.hatayama.UnityCliLoop.ToolContracts; -using io.github.hatayama.UnityCliLoop.Domain; - -namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests -{ - /// - /// Reproduce Dictionary Error in DynamicCodeExecutor - /// TDD Approach: Write failing tests first to reproduce the error - /// - [TestFixture] - public class DynamicCodeExecutorDictionaryErrorTest - { - private IDynamicCodeExecutor _executor; - - [SetUp] - public void SetUp() - { - // v4.0 Stateless design - Remove global configuration changes - // Create Executor in Restricted mode - _executor = DynamicCodeServices.ExecutorFactory.Create(DynamicCodeSecurityLevel.Restricted); - } - - [Test] -[Description("Confirm Dictionary Error occurs even in the simplest code")] - public async Task SimpleReturnStatement_ShouldNotThrowDictionaryError() - { - // Arrange - string simpleCode = "return \"Hello World\";"; - - // Act - ExecutionResult result = await _executor.ExecuteCodeAsync( - simpleCode, - "DynamicCommand", - null, - CancellationToken.None, - false - ); - - // Assert - Expect no error - Assert.IsTrue(result.Success, - $"Simple return statement should succeed. Error: {result.ErrorMessage ?? "No error"}, Logs: {string.Join(", ", result.Logs ?? new System.Collections.Generic.List())}"); - Assert.AreEqual("Hello World", result.Result); - } - - [Test] -[Description("Confirm no Dictionary Error occurs in code with variable declarations")] - public async Task VariableDeclaration_ShouldNotThrowDictionaryError() - { - // Arrange - string codeWithVariable = @" - int x = 5; - int y = 10; - return x + y; - "; - - // Act - ExecutionResult result = await _executor.ExecuteCodeAsync( - codeWithVariable, - "DynamicCommand", - null, - CancellationToken.None, - false - ); - - // Assert - Assert.IsTrue(result.Success, - $"Variable declaration should succeed. Error: {result.ErrorMessage ?? "No error"}, Logs: {string.Join(", ", result.Logs ?? new System.Collections.Generic.List())}"); - // Note: ExecuteDynamicCodeResponse.Result is string type, so compare results as strings - Assert.AreEqual("15", result.Result.ToString()); - } - - - - [Test] - [Description("Verify that incorrect parameter types produce clear error messages")] - public async Task IncorrectParameterType_ShouldProduceClearErrorMessage() - { - // This test verifies the fix for the JSON serialization error - // where Parameters field receives "{}" as string instead of object - - // Note: This test would need to be executed through the CLI JSON-RPC path - // to actually test the JSON deserialization error handling. - // Here we just ensure the executor itself works correctly. - - string testCode = "return \"Parameters handling test\";"; - - ExecutionResult result = await _executor.ExecuteCodeAsync( - testCode, - "DynamicCommand", - null, - CancellationToken.None, - false - ); - - Assert.IsTrue(result.Success, - "Code should execute successfully when parameters are correct"); - Assert.AreEqual("Parameters handling test", result.Result); - } - - [Test] -[Description("Detailed verification of compilation error content")] - public async Task AnalyzeCompilationError_GetDetailedErrorInfo() - { - // Arrange - string testCode = "return 42;"; - - // Act - ExecutionResult result = await _executor.ExecuteCodeAsync( - testCode, - "DynamicCommand", - null, - CancellationToken.None, - true // Test in CompileOnly mode - ); - - // Assert - Output detailed error - if (!result.Success) - { - TestContext.WriteLine("=== Error Message ==="); - TestContext.WriteLine($"Error: {result.ErrorMessage ?? "No error message"}"); - - TestContext.WriteLine("\n=== Logs ==="); - foreach (string log in result.Logs ?? new System.Collections.Generic.List()) - { - TestContext.WriteLine($"Log: {log}"); - } - - // Check if Dictionary error is included - bool hasDictionaryError = false; - if (result.ErrorMessage != null && result.ErrorMessage.Contains("Dictionary")) - { - hasDictionaryError = true; - } - foreach (string log in result.Logs ?? new System.Collections.Generic.List()) - { - if (log.Contains("Dictionary")) - { - hasDictionaryError = true; - break; - } - } - - Assert.IsFalse(hasDictionaryError, - $"Code should not produce Dictionary-related errors. Error: {result.ErrorMessage}, Logs: {string.Join(", ", result.Logs)}"); - } - else - { - Assert.Pass("Compilation succeeded as expected"); - } - } - } -} -#endif diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorDictionaryErrorTest.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorDictionaryErrorTest.cs.meta deleted file mode 100644 index 572121e914..0000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeExecutorDictionaryErrorTest.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 90df43129092f43ef890648f8b99e4ed -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityManagerTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityManagerTests.cs index e32357f65c..525bd4f1f0 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityManagerTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/DynamicCodeSecurityManagerTests.cs @@ -20,26 +20,6 @@ public void SetUp() // No setup required for v4.0 stateless design } - [Test] - public void CanExecute_ReturnsTrueForLevel1() - { - // Act - bool canExecute = DynamicCodeSecurityManager.CanExecute(DynamicCodeSecurityLevel.Restricted); - - // Assert - Assert.IsTrue(canExecute); - } - - [Test] - public void CanExecute_ReturnsTrueForLevel2() - { - // Act - bool canExecute = DynamicCodeSecurityManager.CanExecute(DynamicCodeSecurityLevel.FullAccess); - - // Assert - Assert.IsTrue(canExecute); - } - [Test] public void GetAllowedAssemblies_ReturnsAppropriateListForEachLevel() { diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeParameterValidationTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeParameterValidationTests.cs index 08fc255be6..fd12772202 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeParameterValidationTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeParameterValidationTests.cs @@ -1,12 +1,9 @@ -#if UNITYCLILOOP_HAS_ROSLYN using NUnit.Framework; using System.Threading; -using System.Threading.Tasks; using Newtonsoft.Json.Linq; using io.github.hatayama.UnityCliLoop.Application; using io.github.hatayama.UnityCliLoop.Domain; -using io.github.hatayama.UnityCliLoop.FirstPartyTools; using io.github.hatayama.UnityCliLoop.ToolContracts; namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests @@ -20,6 +17,7 @@ public class ExecuteDynamicCodeParameterValidationTests [Test] public void ExecuteAsync_WithStringParameters_ShouldThrowUnityCliLoopToolParameterValidationException() { + // Verifies that a string Parameters value is rejected with a clear validation error before any compilation starts. // Arrange UnityCliLoopToolRegistry registry = ToolRegistryTestFactory.Create(); UnityCliLoopToolExecutionService executionService = new(); @@ -45,76 +43,5 @@ await executionService.ExecuteToolAsync( StringAssert.Contains("{}", ex.Message); } - [Test] - public async Task ExecuteAsync_WithObjectParameters_ShouldSucceedInCompileOnly() - { - // Arrange - DynamicCodeSecurityLevel prev = ULoopSettings.GetDynamicCodeSecurityLevel(); - ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); - UnityCliLoopToolRegistry registry = ToolRegistryTestFactory.Create(); - UnityCliLoopToolExecutionService executionService = new(); - JObject paramsToken = new() { - ["Code"] = "return \"ok\";", - ["Parameters"] = new JObject(), // valid: object - ["CompileOnly"] = true - }; - - // Act - UnityCliLoopToolResponse baseResponse = null; - try - { - baseResponse = await executionService.ExecuteToolAsync( - registry, - "execute-dynamic-code", - paramsToken, - CancellationToken.None); - } - finally - { - ULoopSettings.SetDynamicCodeSecurityLevel(prev); - } - ExecuteDynamicCodeResponse response = baseResponse as ExecuteDynamicCodeResponse; - - // Assert - Assert.IsNotNull(response, "Response should be ExecuteDynamicCodeResponse"); - Assert.IsTrue(response.Success, $"Expected success but got error: {response.ErrorMessage}"); - Assert.IsTrue(string.IsNullOrEmpty(response.ErrorMessage), "ErrorMessage should be empty on success"); - } - - [Test] - public async Task ExecuteAsync_CodeWithoutReturn_ShouldAutoReturnAndSucceed() - { - // Arrange - DynamicCodeSecurityLevel prev = ULoopSettings.GetDynamicCodeSecurityLevel(); - ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); - UnityCliLoopToolRegistry registry = ToolRegistryTestFactory.Create(); - UnityCliLoopToolExecutionService executionService = new(); - JObject paramsToken = new() { - ["Code"] = "int x = 1; // no explicit return", - ["CompileOnly"] = false - }; - - // Act - UnityCliLoopToolResponse baseResponse = null; - try - { - baseResponse = await executionService.ExecuteToolAsync( - registry, - "execute-dynamic-code", - paramsToken, - CancellationToken.None); - } - finally - { - ULoopSettings.SetDynamicCodeSecurityLevel(prev); - } - ExecuteDynamicCodeResponse response = baseResponse as ExecuteDynamicCodeResponse; - - // Assert - Assert.IsNotNull(response, "Response should be ExecuteDynamicCodeResponse"); - Assert.IsTrue(response.Success, $"Expected success but got error: {response.ErrorMessage}"); - Assert.IsTrue(string.IsNullOrEmpty(response.ErrorMessage), "ErrorMessage should be empty on success"); - } } } -#endif diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolAutoUsingTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolAutoUsingTests.cs deleted file mode 100644 index b4d36c1b6a..0000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolAutoUsingTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using NUnit.Framework; - -using io.github.hatayama.UnityCliLoop.Application; -using io.github.hatayama.UnityCliLoop.Domain; -using io.github.hatayama.UnityCliLoop.FirstPartyTools; -using io.github.hatayama.UnityCliLoop.ToolContracts; - -namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests -{ - /// - /// Test fixture that verifies Execute Dynamic Code Tool Auto Using behavior. - /// - [TestFixture] - public class ExecuteDynamicCodeToolAutoUsingTests - { - [Test] - public async Task ExecuteAsync_CompileOnly_WhenTypeRequiresMissingUsing_ShouldSucceed() - { - DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); - UnityCliLoopToolRegistry registry = ToolRegistryTestFactory.Create(); - UnityCliLoopToolExecutionService executionService = new(); - - try - { - JObject paramsToken = new() { - ["Code"] = "StringBuilder builder = new StringBuilder(); builder.Append(\"ok\"); return builder.ToString();", - ["CompileOnly"] = true - }; - - ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); - UnityCliLoopToolResponse response = await executionService.ExecuteToolAsync( - registry, - "execute-dynamic-code", - paramsToken, - CancellationToken.None); - ExecuteDynamicCodeResponse typedResponse = response as ExecuteDynamicCodeResponse; - - Assert.IsNotNull(typedResponse, "Response should be ExecuteDynamicCodeResponse"); - Assert.IsTrue(typedResponse.Success, $"Tool should compile after injecting missing using directives. Error: {typedResponse.ErrorMessage}"); - } - finally - { - ULoopSettings.SetDynamicCodeSecurityLevel(previous); - } - } - } -} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolAutoUsingTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolAutoUsingTests.cs.meta deleted file mode 100644 index 072b1df814..0000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolAutoUsingTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: b36e34da080c949c4909903fc4e3f7d2 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolSecurityTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolSecurityTests.cs deleted file mode 100644 index b672e29af7..0000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolSecurityTests.cs +++ /dev/null @@ -1,50 +0,0 @@ -using System.Threading; -using System.Threading.Tasks; -using Newtonsoft.Json.Linq; -using NUnit.Framework; - -using io.github.hatayama.UnityCliLoop.Application; -using io.github.hatayama.UnityCliLoop.Domain; -using io.github.hatayama.UnityCliLoop.FirstPartyTools; -using io.github.hatayama.UnityCliLoop.ToolContracts; - -namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests -{ - /// - /// Test fixture that verifies Execute Dynamic Code Tool Security behavior. - /// - [TestFixture] - public class ExecuteDynamicCodeToolSecurityTests - { - [Test] - public async Task ExecuteAsync_Restricted_FileExists_ShouldUseCompilerSecurityRulesInsteadOfToolLocalBlock() - { - DynamicCodeSecurityLevel previous = ULoopSettings.GetDynamicCodeSecurityLevel(); - UnityCliLoopToolRegistry registry = ToolRegistryTestFactory.Create(); - UnityCliLoopToolExecutionService executionService = new(); - - try - { - JObject paramsToken = new() { - ["Code"] = "bool exists = System.IO.File.Exists(\"dummy.txt\"); return exists;", - ["CompileOnly"] = false - }; - - ULoopSettings.SetDynamicCodeSecurityLevel(DynamicCodeSecurityLevel.Restricted); - UnityCliLoopToolResponse response = await executionService.ExecuteToolAsync( - registry, - "execute-dynamic-code", - paramsToken, - CancellationToken.None); - ExecuteDynamicCodeResponse typedResponse = response as ExecuteDynamicCodeResponse; - - Assert.IsNotNull(typedResponse, "Response should be ExecuteDynamicCodeResponse"); - Assert.IsTrue(typedResponse.Success, $"Tool should allow safe File.Exists through centralized security validation. Error: {typedResponse.ErrorMessage}"); - } - finally - { - ULoopSettings.SetDynamicCodeSecurityLevel(previous); - } - } - } -} diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolSecurityTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolSecurityTests.cs.meta deleted file mode 100644 index a0dd68e1f0..0000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/ExecuteDynamicCodeToolSecurityTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 945b1987200004e83ace247494b13669 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs index 3742768e1b..6e025853e7 100644 --- a/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs +++ b/Assets/Tests/Editor/DynamicCodeToolTests/PreUsingResolverTests.cs @@ -1,11 +1,7 @@ using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; using NUnit.Framework; using io.github.hatayama.UnityCliLoop.FirstPartyTools; -using io.github.hatayama.UnityCliLoop.ToolContracts; -using io.github.hatayama.UnityCliLoop.Domain; namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests { @@ -257,275 +253,4 @@ public void Resolve_WhenFullyQualifiedTypeIsUsed_ShouldReportAssemblyReference() Assert.That(result.AddedAssemblyReferences, Has.Count.GreaterThan(0)); } } - - /// - /// Test fixture that verifies Pre Using Resolver Integration behavior. - /// - [TestFixture] - public class PreUsingResolverIntegrationTests - { - private IPreloadAssemblySecurityValidator _previousValidator; - - [SetUp] - public void SetUp() - { - _previousValidator = PreloadAssemblySecurityValidatorRegistry.SwapValidatorForTests( - new SystemReflectionMetadataPreloadValidator()); - } - - [TearDown] - public void TearDown() - { - PreloadAssemblySecurityValidatorRegistry.SwapValidatorForTests(_previousValidator); - } - - [Test] - public async Task CompileAsync_ScriptMode_MissingUsing_ShouldSucceedWithSingleBuild() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - StringBuilder builder = new(); - builder.Append(""hello""); - return builder.ToString(); - ", - ClassName = "PreUsingMissingCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Should compile"); - StringAssert.Contains("using System.Text;", result.UpdatedCode); - // PreUsingResolver pre-injects the using, so AutoUsingResolver needs no retry - Assert.AreEqual(1, compiler.LastBuildCount, "Should compile in a single build (no retry)"); - } - - [Test] - public async Task CompileAsync_RawMode_FullClass_ShouldSucceedWithoutPreUsingIntervention() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - using System.Text; - - /// - /// Test support type used by editor and play mode fixtures. - /// - public class RawModeTestClass - { - public async System.Threading.Tasks.Task ExecuteAsync( - System.Collections.Generic.Dictionary parameters = null, - System.Threading.CancellationToken ct = default) - { - StringBuilder sb = new(); - sb.Append(""raw""); - return sb.ToString(); - } - } - ", - ClassName = "RawModeCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Raw mode should compile"); - Assert.IsNotNull(result.CompiledAssembly); - } - - [Test] - public async Task CompileAsync_ScriptMode_AllUsingsPresent_ShouldCompileNormally() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - using System.Text; - StringBuilder builder = new(); - builder.Append(""already imported""); - return builder.ToString(); - ", - ClassName = "AlreadyImportedCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Should compile"); - Assert.AreEqual(1, compiler.LastBuildCount, "No retry needed when using is already present"); - } - - [Test] - public async Task CompileAsync_ScriptMode_NakedObject_ShouldUseUnityObjectAlias() - { - // Verifies that wrapper-provided Object resolves to UnityEngine.Object while C# object remains available. - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - Object[] unityObjects = new Object[0]; - object boxedObject = new object(); - System.Object systemObject = boxedObject; - return unityObjects.Length == 0 && systemObject != null ? ""ok"" : ""unexpected""; - ", - ClassName = "NakedObjectAliasCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Object alias should compile"); - StringAssert.Contains("using Object = UnityEngine.Object;", result.UpdatedCode); - } - - [Test] - public async Task CompileAsync_ScriptMode_UserObjectAlias_ShouldKeepExplicitAlias() - { - // Verifies that an explicit user Object alias is not shadowed by the wrapper default. - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - using Object = System.Object; - Object boxedObject = new object(); - return boxedObject != null ? ""ok"" : ""unexpected""; - ", - ClassName = "UserObjectAliasCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "User Object alias should compile"); - StringAssert.DoesNotContain("using Object = UnityEngine.Object;", result.UpdatedCode); - } - - [Test] - public async Task CompileAsync_ScriptMode_UserObjectAliasWithCompactSpacing_ShouldKeepExplicitAlias() - { - // Verifies that compact alias spacing is still treated as an explicit Object alias. - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - using Object=System.Object; - Object boxedObject = new object(); - return boxedObject != null ? ""ok"" : ""unexpected""; - ", - ClassName = "CompactUserObjectAliasCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Compact user Object alias should compile"); - StringAssert.DoesNotContain("using Object = UnityEngine.Object;", result.UpdatedCode); - } - - [Test] - public async Task CompileAsync_ScriptMode_UserObjectAliasWithTokenComment_ShouldKeepExplicitAlias() - { - // Verifies that comments between alias tokens do not hide an explicit Object alias. - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - using Object /* explicit */ = System.Object; - Object boxedObject = new object(); - return boxedObject != null ? ""ok"" : ""unexpected""; - ", - ClassName = "CommentedUserObjectAliasCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Commented user Object alias should compile"); - StringAssert.DoesNotContain("using Object = UnityEngine.Object;", result.UpdatedCode); - } - - [Test] - public async Task CompileAsync_ScriptMode_MultipleMissingUsings_ShouldPreInjectAllAndSucceed() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - StringBuilder sb = new(); - Regex regex = new(@""\d+""); - return sb.ToString() + regex.ToString(); - ", - ClassName = "MultiplePreUsingCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Should compile"); - StringAssert.Contains("using System.Text;", result.UpdatedCode); - StringAssert.Contains("using System.Text.RegularExpressions;", result.UpdatedCode); - Assert.AreEqual(1, compiler.LastBuildCount, "Both usings pre-injected, no retry needed"); - } - - [Test] - public async Task CompileAsync_ScriptMode_SimpleArithmetic_ShouldSucceedWithSingleBuild() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = "return 1 + 2;", - ClassName = "SimpleArithmeticPreUsingCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Should compile"); - Assert.IsNotNull(result.CompiledAssembly); - Assert.AreEqual(1, compiler.LastBuildCount, "Simple code needs no retry"); - } - - [Test] - public async Task CompileAsync_ScriptMode_CustomAsmdefType_ShouldResolveAssemblyReference() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - DynamicAssemblyTest test = new(); - return test.HelloWorld(); - ", - ClassName = "CustomAsmdefReferenceCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Custom asmdef type should compile"); - Assert.AreEqual(1, compiler.LastBuildCount, "Unique type resolution should avoid extra retries"); - } - - [Test] - public async Task CompileAsync_ScriptMode_FullyQualifiedCustomAsmdefType_ShouldResolveWithoutRetry() - { - DynamicCodeCompiler compiler = new(DynamicCodeSecurityLevel.Restricted); - CompilationRequest request = new() { - Code = @" - io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests.DynamicAssemblyTest test = new(); - return test.HelloWorld(); - ", - ClassName = "FullyQualifiedCustomAsmdefPreUsingCommand", - Namespace = "TestNamespace" - }; - - CompilationResult result = await compiler.CompileAsync(request, CancellationToken.None); - - Assert.IsTrue(result.Success, - result.Errors != null && result.Errors.Count > 0 ? result.Errors[0].Message : "Fully-qualified custom asmdef type should compile"); - Assert.AreEqual(1, compiler.LastBuildCount, "Qualified assembly resolution should avoid extra retries"); - } - } } diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/RestrictedModeDangerousApiTests.cs b/Assets/Tests/Editor/DynamicCodeToolTests/RestrictedModeDangerousApiTests.cs deleted file mode 100644 index fa7568e2bb..0000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/RestrictedModeDangerousApiTests.cs +++ /dev/null @@ -1,869 +0,0 @@ -#if UNITYCLILOOP_HAS_ROSLYN -using System; -using System.Threading; -using System.Threading.Tasks; -using NUnit.Framework; -using UnityEngine; - -using io.github.hatayama.UnityCliLoop.FirstPartyTools; -using io.github.hatayama.UnityCliLoop.ToolContracts; -using io.github.hatayama.UnityCliLoop.Domain; - -namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests -{ - /// - /// Comprehensive testing that dangerous APIs are blocked in Restricted mode - /// and safe APIs are allowed - /// Verifies behavior of all APIs defined in DangerousApiDetector - /// - [TestFixture] - public class RestrictedModeDangerousApiTests - { - private IDynamicCodeExecutor executor; - - // Temporary test directory and asset path - private const string TEST_TEMP_DIR = "TestTemp_RestrictedMode"; - private const string TEST_ASSET_PATH = "Assets/Tests/Editor/DynamicCodeToolTests/Temp/TestTemp_RestrictedMode_temp_asset.asset"; - - [SetUp] - public void SetUp() - { - // v4.0 stateless design - Remove changes to global settings - // Directly specify level in Executor (keep existing) - executor = DynamicCodeServices.ExecutorFactory.Create( - DynamicCodeSecurityLevel.Restricted - ); - - // Create test directory - if (!System.IO.Directory.Exists(TEST_TEMP_DIR)) - { - System.IO.Directory.CreateDirectory(TEST_TEMP_DIR); - UnityEngine.Debug.Log($"[RestrictedModeDangerousApiTests] Created test directory: {TEST_TEMP_DIR}"); - } - } - - [TearDown] - public void TearDown() - { - // Cleanup after test (delete entire directory) - CleanupTestDirectory(); - - executor = null; - } - - private void CleanupTestDirectory() - { - // Delete entire test directory - try - { - if (System.IO.Directory.Exists(TEST_TEMP_DIR)) - { - System.IO.Directory.Delete(TEST_TEMP_DIR, true); - UnityEngine.Debug.Log($"[RestrictedModeDangerousApiTests] Cleaned up test directory: {TEST_TEMP_DIR}"); - } - - // Delete test asset file via AssetDatabase (meta file will be automatically deleted) - string assetPath = TEST_ASSET_PATH; - if (UnityEditor.AssetDatabase.LoadAssetAtPath(assetPath) != null) - { - UnityEditor.AssetDatabase.DeleteAsset(assetPath); - UnityEngine.Debug.Log($"[RestrictedModeDangerousApiTests] Deleted asset: {assetPath}"); - } - - // Delete Temp folder (if it's empty) - string tempFolder = "Assets/Tests/Editor/DynamicCodeToolTests/Temp"; - if (UnityEditor.AssetDatabase.IsValidFolder(tempFolder)) - { - // Delete only if folder is empty - string[] assets = UnityEditor.AssetDatabase.FindAssets("", new[] { tempFolder }); - if (assets.Length == 0) - { - UnityEditor.AssetDatabase.DeleteAsset(tempFolder); - UnityEngine.Debug.Log($"[RestrictedModeDangerousApiTests] Deleted temp folder: {tempFolder}"); - } - } - } - catch (Exception ex) - { - // Record cleanup errors as warnings - UnityEngine.Debug.LogWarning($"[RestrictedModeDangerousApiTests] Failed to delete test directory: {ex.Message}"); - } - } - - // ================================================================================ - // System.IO.File Tests - // ================================================================================ - - #region System.IO.File - Dangerous APIs (Should be blocked) - - [Test] - public async Task TestRestrictedMode_FileDelete_Blocked() - { - string code = @" - System.IO.File.Delete(""test.txt""); - return ""File deleted""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "File.Delete should be blocked in Restricted mode"); - StringAssert.Contains("Dangerous", result.ErrorMessage, "Should report security violation"); - } - - [Test] - public async Task TestRestrictedMode_FileWriteAllText_Blocked() - { - string code = @" - System.IO.File.WriteAllText(""test.txt"", ""malicious content""); - return ""File written""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "File.WriteAllText should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - [Test] - public async Task TestRestrictedMode_FileWriteAllBytes_Blocked() - { - string code = @" - System.IO.File.WriteAllBytes(""test.bin"", new byte[] { 0x00, 0x01 }); - return ""Bytes written""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "File.WriteAllBytes should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - [Test] - public async Task TestRestrictedMode_FileReplace_Blocked() - { - string code = @" - System.IO.File.Replace(""source.txt"", ""dest.txt"", ""backup.txt""); - return ""File replaced""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "File.Replace should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - #endregion - - #region System.IO.File - Safe APIs (Should be allowed) - - [Test] - public async Task TestRestrictedMode_FileCreate_Allowed() - { - string code = $@" - using (var stream = System.IO.File.Create(""{TEST_TEMP_DIR}/temp_test.txt"")) - {{ - byte[] data = System.Text.Encoding.UTF8.GetBytes(""test""); - stream.Write(data, 0, data.Length); - }} - return ""File created successfully""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"File.Create should be allowed. Error: {result.ErrorMessage}"); - StringAssert.Contains("successfully", result.Result?.ToString() ?? ""); - } - - [Test] - public async Task TestRestrictedMode_FileCopy_Allowed() - { - string code = @" - // Note: Copy requires source file to exist - if (System.IO.File.Exists(""source.txt"")) - { - System.IO.File.Copy(""source.txt"", ""dest.txt""); - return ""File copied""; - } - return ""Source file not found (expected in test)""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"File.Copy should be allowed. Error: {result.ErrorMessage}"); - } - - [Test] - public async Task TestRestrictedMode_FileReadAllText_Allowed() - { - string code = $@" - // Create a file first - using (var stream = System.IO.File.Create(""{TEST_TEMP_DIR}/read_test.txt"")) - {{ - byte[] data = System.Text.Encoding.UTF8.GetBytes(""test content""); - stream.Write(data, 0, data.Length); - }} - - // Now read it - string content = System.IO.File.ReadAllText(""{TEST_TEMP_DIR}/read_test.txt""); - return $""Read content: {{content}}""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"File.ReadAllText should be allowed. Error: {result.ErrorMessage}"); - StringAssert.Contains("test content", result.Result?.ToString() ?? ""); - } - - [Test] - public async Task TestRestrictedMode_FileExists_Allowed() - { - string code = @" - bool exists = System.IO.File.Exists(""any_file.txt""); - return $""File.Exists executed: {exists}""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"File.Exists should be allowed. Error: {result.ErrorMessage}"); - StringAssert.Contains("File.Exists executed", result.Result?.ToString() ?? ""); - } - - #endregion - - // ================================================================================ - // System.IO.Directory Tests - // ================================================================================ - - #region System.IO.Directory - Dangerous APIs - - [Test] - public async Task TestRestrictedMode_DirectoryDelete_Blocked() - { - string code = @" - System.IO.Directory.Delete(""test_dir""); - return ""Directory deleted""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "Directory.Delete should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - #endregion - - #region System.IO.Directory - Safe APIs - - [Test] - public async Task TestRestrictedMode_DirectoryCreate_Allowed() - { - string code = $@" - System.IO.Directory.CreateDirectory(""{TEST_TEMP_DIR}/test_directory""); - return ""Directory created""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"Directory.CreateDirectory should be allowed. Error: {result.ErrorMessage}"); - } - - [Test] - public async Task TestRestrictedMode_DirectoryGetFiles_Allowed() - { - string code = @" - string[] files = System.IO.Directory.GetFiles("".""); - return $""Found {files.Length} files""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"Directory.GetFiles should be allowed. Error: {result.ErrorMessage}"); - StringAssert.Contains("files", result.Result?.ToString() ?? ""); - } - - [Test] - public async Task TestRestrictedMode_DirectoryExists_Allowed() - { - string code = @" - bool exists = System.IO.Directory.Exists("".""); - return $""Directory.Exists executed: {exists}""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"Directory.Exists should be allowed. Error: {result.ErrorMessage}"); - } - - #endregion - - // ================================================================================ - // System.Diagnostics.Process Tests - // ================================================================================ - - #region System.Diagnostics.Process - Dangerous APIs - - [Test] - public async Task TestRestrictedMode_ProcessStart_Blocked() - { - string code = @" - System.Diagnostics.Process.Start(""notepad.exe""); - return ""Process started""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "Process.Start should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - [Test] - public async Task TestRestrictedMode_ProcessKill_Blocked() - { - string code = @" - var process = System.Diagnostics.Process.GetCurrentProcess(); - process.Kill(); - return ""Process killed""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "Process.Kill should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - #endregion - - #region System.Diagnostics.Process - Safe APIs - - [Test] - public async Task TestRestrictedMode_ProcessGetCurrentProcess_Allowed() - { - string code = @" - var process = System.Diagnostics.Process.GetCurrentProcess(); - return $""Current process ID: {process.Id}""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"Process.GetCurrentProcess should be allowed. Error: {result.ErrorMessage}"); - StringAssert.Contains("process ID", result.Result?.ToString() ?? ""); - } - - #endregion - - // ================================================================================ - // System.Reflection.Assembly Tests - // ================================================================================ - - #region System.Reflection.Assembly - Dangerous APIs - - [Test] - public async Task TestRestrictedMode_AssemblyLoad_Blocked() - { - string code = @" - System.Reflection.Assembly.Load(""System""); - return ""Assembly loaded""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "Assembly.Load should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - [Test] - public async Task TestRestrictedMode_AssemblyLoadFrom_Blocked() - { - string code = @" - System.Reflection.Assembly.LoadFrom(""test.dll""); - return ""Assembly loaded from file""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "Assembly.LoadFrom should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - #endregion - - #region System.Reflection.Assembly - Safe APIs - - [Test] - public async Task TestRestrictedMode_AssemblyGetExecutingAssembly_Allowed() - { - string code = @" - var assembly = System.Reflection.Assembly.GetExecutingAssembly(); - return $""Executing assembly: {assembly.GetName().Name}""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"Assembly.GetExecutingAssembly should be allowed. Error: {result.ErrorMessage}"); - StringAssert.Contains("assembly", result.Result?.ToString() ?? ""); - } - - #endregion - - // ================================================================================ - // System.Environment Tests - // ================================================================================ - #region System.Environment - Safe APIs - - [Test] - public async Task TestRestrictedMode_EnvironmentGetEnvironmentVariable_Allowed() - { - string code = @" - string value = System.Environment.GetEnvironmentVariable(""PATH"") ?? ""not set""; - return $""Environment variable retrieved: {value.Length} chars""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"Environment.GetEnvironmentVariable should be allowed. Error: {result.ErrorMessage}"); - StringAssert.Contains("Environment variable", result.Result?.ToString() ?? ""); - } - - #endregion - - // ================================================================================ - // System.Threading.Thread Tests - // ================================================================================ - - #region System.Threading.Thread - Dangerous APIs - - [Test] - public async Task TestRestrictedMode_ThreadAbort_Blocked() - { - string code = @" - var thread = System.Threading.Thread.CurrentThread; - thread.Abort(); - return ""Thread aborted""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "Thread.Abort should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - #endregion - - #region System.Threading.Thread - Safe APIs - - [Test] - public async Task TestRestrictedMode_ThreadCurrentThread_Allowed() - { - string code = @" - var thread = System.Threading.Thread.CurrentThread; - return $""Current thread ID: {thread.ManagedThreadId}""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"Thread.CurrentThread should be allowed. Error: {result.ErrorMessage}"); - StringAssert.Contains("thread ID", result.Result?.ToString() ?? ""); - } - - #endregion - - // ================================================================================ - // System.Type Tests - // ================================================================================ - - #region System.Type - Dangerous APIs - - [Test] - public async Task TestRestrictedMode_TypeInvokeMember_Blocked() - { - string code = @" - var type = typeof(string); - type.InvokeMember(""Concat"", - System.Reflection.BindingFlags.InvokeMethod, - null, null, new object[] { ""a"", ""b"" }); - return ""Method invoked""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "Type.InvokeMember should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - #endregion - - #region System.Type - Safe APIs - - [Test] - public async Task TestRestrictedMode_TypeGetType_Allowed() - { - string code = @" - var type = System.Type.GetType(""System.String""); - return $""Type retrieved: {type?.Name ?? ""null""}""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsTrue(result.Success, $"Type.GetType should be allowed. Error: {result.ErrorMessage}"); - StringAssert.Contains("Type retrieved", result.Result?.ToString() ?? ""); - } - - #endregion - - // ================================================================================ - // System.Activator Tests - // ================================================================================ - - #region System.Activator - Dangerous APIs - - [Test] - public async Task TestRestrictedMode_ActivatorCreateComInstanceFrom_Blocked() - { - string code = @" - System.Activator.CreateComInstanceFrom(""test.dll"", ""TestClass""); - return ""COM instance created""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "Activator.CreateComInstanceFrom should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - #endregion - - #region System.Activator - Blocked APIs - - [Test] - public async Task TestRestrictedMode_ActivatorCreateInstance_Blocked() - { - string code = @" - var obj = System.Activator.CreateInstance(typeof(System.Collections.Generic.List)); - return $""Instance created: {obj.GetType().Name}""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "Activator.CreateInstance should be blocked in Restricted mode"); - StringAssert.Contains("Dangerous", result.ErrorMessage ?? ""); - } - - #endregion - - // ================================================================================ - // Environment / GC tests - // ================================================================================ - - #region Environment and GC - Blocked APIs - - [Test] - public async Task TestRestrictedMode_EnvironmentSetEnvironmentVariable_Blocked() - { - string code = @" - System.Environment.SetEnvironmentVariable(""TEST_VAR_UNITYCLILOOP"", ""x""); - return ""ok""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "Environment.SetEnvironmentVariable should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage ?? ""); - } - - [Test] - public async Task TestRestrictedMode_GCSettingsLatencyModeAssignment_Blocked() - { - string code = @" - System.Runtime.GCSettings.LatencyMode = System.Runtime.GCLatencyMode.LowLatency; - return ""ok""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "GCSettings.LatencyMode assignment should be blocked"); - Assert.IsTrue( - result.ErrorMessage?.Contains("Dangerous") == true || - result.ErrorMessage?.Contains("Security violations detected") == true, - $"Expected security error message. Actual: '{result.ErrorMessage}'"); - } - - #endregion - - // ================================================================================ - // UnityEditor.AssetDatabase Tests - // ================================================================================ - - #region UnityEditor.AssetDatabase - Dangerous APIs - - [Test] - public async Task TestRestrictedMode_AssetDatabaseDeleteAsset_Blocked() - { - string code = @" - UnityEditor.AssetDatabase.DeleteAsset(""Assets/test.txt""); - return ""Asset deleted""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "AssetDatabase.DeleteAsset should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - #endregion - - #region UnityEditor.AssetDatabase - Safe APIs - - [Test] - public async Task TestRestrictedMode_AssetDatabaseCreateAsset_Allowed() - { - string code = @" - // Note: CreateAsset requires a UnityEngine.Object, so we use a ScriptableObject - var obj = UnityEngine.ScriptableObject.CreateInstance(); - // Create Temp folder if it does not exist - string tempDir = @""Assets/Tests/Editor/DynamicCodeToolTests/Temp""; - if (!UnityEditor.AssetDatabase.IsValidFolder(tempDir)) - { - string parent = @""Assets/Tests/Editor/DynamicCodeToolTests""; - UnityEditor.AssetDatabase.CreateFolder(parent, ""Temp""); - } - UnityEditor.AssetDatabase.CreateAsset(obj, @""Assets/Tests/Editor/DynamicCodeToolTests/Temp/TestTemp_RestrictedMode_temp_asset.asset""); - return ""Asset created""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - // CreateAsset is actually an editor function, so it might result in a runtime error - // But it should not be a security violation - if (!result.Success) - { - StringAssert.DoesNotContain("Dangerous", result.ErrorMessage ?? "", - "AssetDatabase.CreateAsset should not be blocked for security reasons"); - } - } - - #endregion - - // ================================================================================ - // Network API Tests (Full ban, including localhost) - // ================================================================================ - - #region Network APIs - Should be blocked in Restricted mode - - [Test] - public async Task TestRestrictedMode_TcpClient_Blocked() - { - string code = @" - var c = new System.Net.Sockets.TcpClient(""127.0.0.1"", 9); - return ""connected""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "TcpClient should be blocked in Restricted mode"); - Assert.IsTrue( - result.ErrorMessage?.Contains("Security violations detected") == true || - result.ErrorMessage?.Contains("Dangerous") == true || - result.ErrorMessage?.Contains("blocked") == true, - $"Expected security error message. Actual: '{result.ErrorMessage}'"); - } - - [Test] - public async Task TestRestrictedMode_UdpClient_Blocked() - { - string code = @" - var c = new System.Net.Sockets.UdpClient(); - return ""ok""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "UdpClient should be blocked in Restricted mode"); - StringAssert.Contains("Dangerous", result.ErrorMessage ?? ""); - } - - [Test] - public async Task TestRestrictedMode_DnsGetHostEntry_Blocked() - { - string code = @" - var e = System.Net.Dns.GetHostEntry(""localhost""); - return e?.HostName ?? ""none""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "Dns.GetHostEntry should be blocked in Restricted mode"); - Assert.IsTrue( - result.ErrorMessage?.Contains("Security violations detected") == true || - result.ErrorMessage?.Contains("Dangerous") == true || - result.ErrorMessage?.Contains("blocked") == true, - $"Expected security error message. Actual: '{result.ErrorMessage}'"); - } - - [Test] - public async Task TestRestrictedMode_ClientWebSocket_Blocked() - { - string code = @" - var ws = new System.Net.WebSockets.ClientWebSocket(); - return ""ok""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "ClientWebSocket should be blocked in Restricted mode"); - Assert.IsTrue( - result.ErrorMessage?.Contains("Security violations detected") == true || - result.ErrorMessage?.Contains("Dangerous") == true || - result.ErrorMessage?.Contains("blocked") == true || - result.ErrorMessage?.Contains("does not exist") == true || - result.ErrorMessage?.Contains("compilation error") == true, - $"Expected security or compilation error message. Actual: '{result.ErrorMessage}'"); - } - - #endregion - - // ================================================================================ - // UnityEditor.FileUtil Tests - // ================================================================================ - - #region UnityEditor.FileUtil - Dangerous APIs - - [Test] - public async Task TestRestrictedMode_FileUtilDeleteFileOrDirectory_Blocked() - { - string code = @" - UnityEditor.FileUtil.DeleteFileOrDirectory(""test""); - return ""File or directory deleted""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - Assert.IsFalse(result.Success, "FileUtil.DeleteFileOrDirectory should be blocked"); - StringAssert.Contains("Dangerous", result.ErrorMessage); - } - - #endregion - - #region UnityEditor.FileUtil - Safe APIs - - [Test] - public async Task TestRestrictedMode_FileUtilCopyFileOrDirectory_Allowed() - { - string code = $@" - // First, create a file that definitely exists - string sourceFile = ""{TEST_TEMP_DIR}/source_file.txt""; - string destFile = ""{TEST_TEMP_DIR}/dest_file.txt""; - - // Confirm that the directory exists - if (!System.IO.Directory.Exists(""{TEST_TEMP_DIR}"")) - {{ - System.IO.Directory.CreateDirectory(""{TEST_TEMP_DIR}""); - }} - - // Create source file - using (var stream = System.IO.File.Create(sourceFile)) - {{ - byte[] data = System.Text.Encoding.UTF8.GetBytes(""test content for copy""); - stream.Write(data, 0, data.Length); - }} - - // Confirm that the file definitely exists - if (!System.IO.File.Exists(sourceFile)) - {{ - return ""Failed to create source file""; - }} - - // Execute FileUtil.CopyFileOrDirectory (this should not be a security violation) - UnityEditor.FileUtil.CopyFileOrDirectory(sourceFile, destFile); - - // Confirm that the copy was successful - if (System.IO.File.Exists(destFile)) - {{ - return ""FileUtil.CopyFileOrDirectory executed successfully""; - }} - - return ""FileUtil.CopyFileOrDirectory API call allowed (copy may have failed for other reasons)""; - "; - - ExecutionResult result = await executor.ExecuteCodeAsync( - code, "TestCommand", null, CancellationToken.None, compileOnly: false - ); - - // The API call itself should not be a security violation - Assert.IsTrue( - result.Success || !result.ErrorMessage?.Contains("Dangerous") == true, - $"FileUtil.CopyFileOrDirectory should not be blocked for security reasons. Error: {result.ErrorMessage}" - ); - } - - #endregion - } -} -#endif diff --git a/Assets/Tests/Editor/DynamicCodeToolTests/RestrictedModeDangerousApiTests.cs.meta b/Assets/Tests/Editor/DynamicCodeToolTests/RestrictedModeDangerousApiTests.cs.meta deleted file mode 100644 index 4f147bc1ee..0000000000 --- a/Assets/Tests/Editor/DynamicCodeToolTests/RestrictedModeDangerousApiTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 0ba970049c5fd43a89403e66b9e36f4e -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/Editor/EditorFrameWaiterTests.cs b/Assets/Tests/Editor/EditorFrameWaiterTests.cs deleted file mode 100644 index 153366090a..0000000000 --- a/Assets/Tests/Editor/EditorFrameWaiterTests.cs +++ /dev/null @@ -1,219 +0,0 @@ -using System; -using System.Collections; -using System.Collections.Generic; -using System.Threading; -using System.Threading.Tasks; -using NUnit.Framework; -using UnityEngine; -using UnityEngine.TestTools; - -using io.github.hatayama.UnityCliLoop.ToolContracts; - -namespace io.github.hatayama.UnityCliLoop.Tests.Editor -{ - /// - /// Verifies Editor frame and wall-clock wait primitives. - /// - [TestFixture] - public sealed class EditorFrameWaiterTests - { - [SetUp] - public void SetUp() - { - EditorFrameWaiter.ClearAllForTests(); - EditorFrameWaiter.ResetFrameCountForTests(); - EditorFrameWaiter.InitializeForEditorStartup(); - } - - [TearDown] - public void TearDown() - { - EditorFrameWaiter.ClearAllForTests(); - } - - [Test] - public async Task WaitFramesAsync_WithZeroFrames_CompletesImmediately() - { - // Verifies that zero-frame waits do not subscribe to Editor update. - int startFrame = EditorFrameWaiter.CurrentFrameCount; - - await EditorFrameWaiter.WaitFramesAsync(0, CancellationToken.None); - - Assert.AreEqual(startFrame, EditorFrameWaiter.CurrentFrameCount); - Assert.AreEqual(0, EditorFrameWaiter.PendingWaitCount); - } - - [UnityTest] - public IEnumerator WaitFramesAsync_WithSingleFrame_CompletesAfterOneEditorUpdate() - { - // Verifies that a one-frame wait resumes after the next Editor update. - bool completed = false; - int completionFrame = -1; - int startFrame = EditorFrameWaiter.CurrentFrameCount; - - WaitAsync().Forget(); - - Assert.IsFalse(completed); - yield return null; - - Assert.IsTrue(completed); - Assert.AreEqual(startFrame + 1, completionFrame); - - async Task WaitAsync() - { - await EditorFrameWaiter.WaitFramesAsync(1, CancellationToken.None); - completed = true; - completionFrame = EditorFrameWaiter.CurrentFrameCount; - } - } - - [UnityTest] - public IEnumerator WaitFramesAsync_WithConcurrentWaits_CompletesByTargetFrame() - { - // Verifies that concurrent frame waits complete in target-frame order. - List executionLog = new List(); - int startFrame = EditorFrameWaiter.CurrentFrameCount; - - WaitAsync("one", 1, executionLog).Forget(); - WaitAsync("three", 3, executionLog).Forget(); - WaitAsync("two", 2, executionLog).Forget(); - - yield return null; - CollectionAssert.AreEqual(new[] { "one" }, executionLog); - Assert.AreEqual(startFrame + 1, EditorFrameWaiter.CurrentFrameCount); - - yield return null; - CollectionAssert.AreEqual(new[] { "one", "two" }, executionLog); - - yield return null; - CollectionAssert.AreEqual(new[] { "one", "two", "three" }, executionLog); - - async Task WaitAsync(string label, int frameCount, List log) - { - await EditorFrameWaiter.WaitFramesAsync(frameCount, CancellationToken.None); - log.Add(label); - } - } - - [UnityTest] - public IEnumerator WaitFramesAsync_WhenCancelledBeforeTarget_CancelsWithoutWaitingForTargetFrame() - { - // Verifies that cancellation releases the pending wait before the target frame arrives. - CancellationTokenSource cts = new CancellationTokenSource(); - Task waitTask = EditorFrameWaiter.WaitFramesAsync(5, cts.Token); - - yield return null; - Assert.AreEqual(1, EditorFrameWaiter.PendingWaitCount); - - cts.Cancel(); - yield return new WaitUntil(() => waitTask.IsCompleted); - - Assert.IsTrue(waitTask.IsCanceled); - Assert.AreEqual(0, EditorFrameWaiter.PendingWaitCount); - cts.Dispose(); - } - - [Test] - public async Task TimerDelay_Wait_CompletesWithoutEditorFrameDependency() - { - // Verifies that wall-clock waits do not register Editor frame wait requests. - Assert.AreEqual(0, EditorFrameWaiter.PendingWaitCount); - - await TimerDelay.Wait(10, CancellationToken.None); - - Assert.AreEqual(0, EditorFrameWaiter.PendingWaitCount); - } - - [UnityTest] - public IEnumerator WaitThenExecuteOnMainThread_WhenActionThrows_FaultsReturnedTask() - { - // Verifies that delayed main-thread action failures are observable by the awaiting caller. - Task waitTask = TimerDelay.WaitThenExecuteOnMainThread( - 1, - () => throw new InvalidOperationException("Delayed action failed."), - CancellationToken.None); - float startTime = Time.realtimeSinceStartup; - - while (!waitTask.IsCompleted && Time.realtimeSinceStartup - startTime < 2f) - { - yield return null; - } - - Assert.IsTrue(waitTask.IsFaulted); - Exception exception = waitTask.Exception?.GetBaseException(); - Assert.IsInstanceOf(exception); - Assert.AreEqual("Delayed action failed.", exception.Message); - } - - [UnityTest] - public IEnumerator WaitThenExecuteOnMainThread_WithZeroMilliseconds_DefersActionUntilEditorUpdate() - { - // Verifies that an already-completed timer wait still defers the action to an Editor update. - bool actionRan = false; - - Task waitTask = TimerDelay.WaitThenExecuteOnMainThread( - 0, - () => actionRan = true, - CancellationToken.None); - - Assert.IsFalse(actionRan); - yield return null; - - Assert.IsTrue(waitTask.IsCompleted); - Assert.IsFalse(waitTask.IsFaulted, $"Delayed action should not fault: {waitTask.Exception}"); - Assert.IsTrue(actionRan); - } - - [UnityTest] - public IEnumerator WaitFramesAsync_DoesNotRunSynchronousContinuationInsideEditorUpdate() - { - // Verifies that frame wait continuations are queued instead of running inline from Editor update. - int mainThreadId = Thread.CurrentThread.ManagedThreadId; - int continuationThreadId = -1; - Task waitTask = EditorFrameWaiter.WaitFramesAsync(1, CancellationToken.None); - Task continuationTask = waitTask.ContinueWith( - _ => continuationThreadId = Thread.CurrentThread.ManagedThreadId, - CancellationToken.None, - TaskContinuationOptions.ExecuteSynchronously, - TaskScheduler.Default); - float startTime = Time.realtimeSinceStartup; - - while (!continuationTask.IsCompleted && Time.realtimeSinceStartup - startTime < 2f) - { - yield return null; - } - - Assert.IsTrue(continuationTask.IsCompleted); - Assert.AreNotEqual(mainThreadId, continuationThreadId); - } - - [UnityTest] - public IEnumerator WaitThenExecuteOnMainThread_RunsActionOnCapturedSynchronizationContext() - { - // Verifies that the delayed action resumes on the SynchronizationContext captured before the timer wait. - SynchronizationContext capturedContext = SynchronizationContext.Current; - int capturedThreadId = Thread.CurrentThread.ManagedThreadId; - SynchronizationContext actionContext = null; - int actionThreadId = -1; - Task waitTask = TimerDelay.WaitThenExecuteOnMainThread( - 1, - () => - { - actionContext = SynchronizationContext.Current; - actionThreadId = Thread.CurrentThread.ManagedThreadId; - }, - CancellationToken.None); - float startTime = Time.realtimeSinceStartup; - - while (!waitTask.IsCompleted && Time.realtimeSinceStartup - startTime < 2f) - { - yield return null; - } - - Assert.IsTrue(waitTask.IsCompleted); - Assert.IsFalse(waitTask.IsFaulted, $"Delayed action should not fault: {waitTask.Exception}"); - Assert.AreSame(capturedContext, actionContext); - Assert.AreEqual(capturedThreadId, actionThreadId); - } - } -} diff --git a/Assets/Tests/Editor/EditorFrameWaiterTests.cs.meta b/Assets/Tests/Editor/EditorFrameWaiterTests.cs.meta deleted file mode 100644 index fb357a1970..0000000000 --- a/Assets/Tests/Editor/EditorFrameWaiterTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 22f2f7d5d15b4f6fb4f9e8818d50e187 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs index 4587b1dacf..d77efe7df4 100644 --- a/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs +++ b/Assets/Tests/Editor/JsonRpcProcessorCliVersionGateTests.cs @@ -87,6 +87,7 @@ public async Task ProcessRequest_WhenFirstToolWaitsForMainThread_ReturnsServerBu // Verifies the single-flight gate is checked before queuing on Unity's main-thread dispatcher. CapturingMainThreadDispatcher dispatcher = new(); MainThreadSwitcher.RegisterService(dispatcher); + UnityCliLoopEditorStateSnapshot.SetPlayStateForTesting(isPlaying: false, isPaused: false); UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); @@ -121,8 +122,8 @@ public async Task ProcessRequest_WhenFirstToolWaitsForMainThread_ReturnsServerBu Assert.That(data["type"]?.ToString(), Is.EqualTo("server_busy")); Assert.That(data["runningToolName"]?.ToString(), Is.EqualTo(SingleFlightTestTool.Name)); Assert.That(data["requestedToolName"]?.ToString(), Is.EqualTo(SingleFlightTestTool.Name)); - Assert.That(data["isPlaying"], Is.Null); - Assert.That(data["isPaused"], Is.Null); + Assert.That(data["isPlaying"]?.ToObject(), Is.False); + Assert.That(data["isPaused"]?.ToObject(), Is.False); LogAssert.NoUnexpectedReceived(); } finally @@ -130,6 +131,7 @@ public async Task ProcessRequest_WhenFirstToolWaitsForMainThread_ReturnsServerBu dispatcher.RunContinuations(); await DrainTaskIfNeeded(firstResponseTask); await DrainTaskIfNeeded(secondResponseTask); + UnityCliLoopEditorStateSnapshot.ClearForTesting(); UnityCliLoopToolRegistrar.RegisterService(previousService); RestoreEditorMainThreadDispatcher(); } @@ -141,6 +143,7 @@ public async Task ProcessRequest_WhenExecuteDynamicCodeWaitsForMainThread_Allows // Verifies dynamic-code handoff stays inside the dynamic-code scheduler while other tools stay single-flight. CapturingMainThreadDispatcher dispatcher = new(); MainThreadSwitcher.RegisterService(dispatcher); + UnityCliLoopEditorStateSnapshot.SetPlayStateForTesting(isPlaying: false, isPaused: false); UnityCliLoopToolRegistrarService previousService = UnityCliLoopToolRegistrar.Service; ToolSettingsService toolSettingsService = new(new ToolSettingsRepository()); @@ -186,8 +189,8 @@ public async Task ProcessRequest_WhenExecuteDynamicCodeWaitsForMainThread_Allows Assert.That(data["type"]?.ToString(), Is.EqualTo("server_busy")); Assert.That(data["runningToolName"]?.ToString(), Is.EqualTo(UnityCliLoopConstants.TOOL_NAME_EXECUTE_DYNAMIC_CODE)); Assert.That(data["requestedToolName"]?.ToString(), Is.EqualTo(SingleFlightTestTool.Name)); - Assert.That(data["isPlaying"], Is.Null); - Assert.That(data["isPaused"], Is.Null); + Assert.That(data["isPlaying"]?.ToObject(), Is.False); + Assert.That(data["isPaused"]?.ToObject(), Is.False); LogAssert.NoUnexpectedReceived(); } finally @@ -196,6 +199,7 @@ public async Task ProcessRequest_WhenExecuteDynamicCodeWaitsForMainThread_Allows await DrainTaskIfNeeded(firstDynamicCodeTask); await DrainTaskIfNeeded(secondDynamicCodeTask); await DrainTaskIfNeeded(otherToolTask); + UnityCliLoopEditorStateSnapshot.ClearForTesting(); UnityCliLoopToolRegistrar.RegisterService(previousService); RestoreEditorMainThreadDispatcher(); } diff --git a/Assets/Tests/Editor/MainThreadSwitcherTests.cs b/Assets/Tests/Editor/MainThreadSwitcherTests.cs index f8e2c8e3c7..64c0a9f03f 100644 --- a/Assets/Tests/Editor/MainThreadSwitcherTests.cs +++ b/Assets/Tests/Editor/MainThreadSwitcherTests.cs @@ -23,42 +23,6 @@ public void Setup() mainThreadId = Thread.CurrentThread.ManagedThreadId; } - /// - /// Verifies that when called from a background thread, it can switch back to main thread - /// - [UnityTest] - public IEnumerator SwitchToMainThread_WhenCalledFromBackgroundThread_ShouldSwitchBackToMainThread() - { - // Arrange - bool executedImmediately = false; - int executionThreadId = -1; - bool completed = false; - - // Act - Task.Run(async () => - { - try - { - await MainThreadSwitcher.SwitchToMainThread(); - executedImmediately = true; - executionThreadId = Thread.CurrentThread.ManagedThreadId; - completed = true; - } - catch (System.Exception ex) - { - UnityEngine.Debug.LogError($"Test failed: {ex.Message}"); - completed = true; - } - }); - - // Wait for completion - yield return new UnityEngine.WaitUntil(() => completed); - - // Assert - Assert.That(executedImmediately, Is.True, "Should execute when called from background thread"); - Assert.That(executionThreadId, Is.EqualTo(mainThreadId), "Should switch to main thread"); - } - /// /// Verifies that when called from a background thread, it switches to the main thread /// diff --git a/Assets/Tests/Editor/RunTestsToolTests.cs b/Assets/Tests/Editor/RunTestsToolTests.cs index 67a9d3bfc1..9e47ba37ab 100644 --- a/Assets/Tests/Editor/RunTestsToolTests.cs +++ b/Assets/Tests/Editor/RunTestsToolTests.cs @@ -31,29 +31,6 @@ public void ToolName_ShouldReturnRunTests() Assert.That(runTestsTool.ToolName, Is.EqualTo("run-tests")); } - /// - /// Parameter parsing test for filtered test execution. - /// - [Test] - public void ParseParameters_ShouldParseCorrectly() - { - // This test is now obsolete as the new implementation uses type-safe Schema classes - // instead of JSON parameter parsing. The parsing is handled by the tool dispatch layer. - - // Arrange - Test the Schema object directly - RunTestsSchema schema = new() - { - TestMode = UnityCliLoopTestMode.PlayMode, - FilterType = TestFilterType.regex, - FilterValue = "TestClass" - }; - - // Assert - Schema properties should match what we set - Assert.That(schema.TestMode, Is.EqualTo(UnityCliLoopTestMode.PlayMode)); - Assert.That(schema.FilterType, Is.EqualTo(TestFilterType.regex)); - Assert.That(schema.FilterValue, Is.EqualTo("TestClass")); - } - /// /// Default value test with default schema. /// diff --git a/Assets/Tests/PlayMode/BasicPlayModeTests.cs b/Assets/Tests/PlayMode/BasicPlayModeTests.cs deleted file mode 100644 index 909e113952..0000000000 --- a/Assets/Tests/PlayMode/BasicPlayModeTests.cs +++ /dev/null @@ -1,68 +0,0 @@ -using System.Collections; -using NUnit.Framework; -using UnityEngine; -using UnityEngine.TestTools; - -namespace io.github.hatayama.UnityCliLoop.Tests.PlayMode -{ - /// - /// Basic PlayMode tests to verify test framework functionality - /// - public class BasicPlayModeTests - { - [Test] - public void BasicTest_ShouldPass() - { - // Arrange - int expected = 5; - int actual = 2 + 3; - - // Act & Assert - Assert.AreEqual(expected, actual, "Basic math should work correctly"); - } - - [Test] - public void StringTest_ShouldPass() - { - // Arrange - string expected = "Hello World"; - string actual = "Hello" + " " + "World"; - - // Act & Assert - Assert.AreEqual(expected, actual, "String concatenation should work"); - } - - [UnityTest] - public IEnumerator FrameTest_ShouldPassAfterWaiting() - { - // Arrange - float startTime = Time.time; - - // Act - Wait for one frame - yield return null; - - // Assert - float endTime = Time.time; - Assert.GreaterOrEqual(endTime, startTime, "Time should progress during frame wait"); - } - - [Test] - public void UnityEngineTest_ShouldPass() - { - // Test Unity engine functionality - Vector3 vector = new(1, 2, 3); - float magnitude = vector.magnitude; - - Assert.Greater(magnitude, 0, "Vector magnitude should be positive"); - Assert.AreEqual(Mathf.Sqrt(1 + 4 + 9), magnitude, 0.001f, "Vector magnitude calculation should be correct"); - } - - [Test] - public void ApplicationTest_ShouldPass() - { - // Test Application properties that are available in PlayMode - Assert.IsTrue(UnityEngine.Application.isPlaying, "Application should be in play mode during PlayMode tests"); - Assert.IsNotNull(UnityEngine.Application.unityVersion, "Unity version should be available"); - } - } -} \ No newline at end of file diff --git a/Assets/Tests/PlayMode/BasicPlayModeTests.cs.meta b/Assets/Tests/PlayMode/BasicPlayModeTests.cs.meta deleted file mode 100644 index 4a7336478e..0000000000 --- a/Assets/Tests/PlayMode/BasicPlayModeTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: 44be2e0f8526643c5ac57106aca12af0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/_Test_For_Test_.meta b/Assets/Tests/_Test_For_Test_.meta deleted file mode 100644 index 4ae9081051..0000000000 --- a/Assets/Tests/_Test_For_Test_.meta +++ /dev/null @@ -1,8 +0,0 @@ -fileFormatVersion: 2 -guid: ce20d8d109e874a668e6725e21dd9837 -folderAsset: yes -DefaultImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/_Test_For_Test_/ConsoleLogRetrieverTests.cs b/Assets/Tests/_Test_For_Test_/ConsoleLogRetrieverTests.cs deleted file mode 100644 index 950b5342a2..0000000000 --- a/Assets/Tests/_Test_For_Test_/ConsoleLogRetrieverTests.cs +++ /dev/null @@ -1,22 +0,0 @@ -using NUnit.Framework; - -namespace io.github.hatayama.UnityCliLoop.Tests.TestForTest -{ - /// - /// Test fixture that verifies Test For behavior. - /// - public class Test_For_Test - { - [Test] - public void Test1() - { - Assert.True(true); - } - - [Test] - public void Test2() - { - Assert.True(true); - } - } -} \ No newline at end of file diff --git a/Assets/Tests/_Test_For_Test_/ConsoleLogRetrieverTests.cs.meta b/Assets/Tests/_Test_For_Test_/ConsoleLogRetrieverTests.cs.meta deleted file mode 100644 index fe9ee4ee4d..0000000000 --- a/Assets/Tests/_Test_For_Test_/ConsoleLogRetrieverTests.cs.meta +++ /dev/null @@ -1,11 +0,0 @@ -fileFormatVersion: 2 -guid: ac213ab952b3e4967bd69fae759402e0 -MonoImporter: - externalObjects: {} - serializedVersion: 2 - defaultReferences: [] - executionOrder: 0 - icon: {instanceID: 0} - userData: - assetBundleName: - assetBundleVariant: diff --git a/Assets/Tests/_Test_For_Test_/UnityCLILoop.test_for_test.Editor.asmdef b/Assets/Tests/_Test_For_Test_/UnityCLILoop.test_for_test.Editor.asmdef deleted file mode 100644 index 9d5833ff02..0000000000 --- a/Assets/Tests/_Test_For_Test_/UnityCLILoop.test_for_test.Editor.asmdef +++ /dev/null @@ -1,21 +0,0 @@ -{ - "name": "UnityCLILoop.test_for_test.Editor", - "rootNamespace": "io.github.hatayama.UnityCliLoop.Tests.TestForTest", - "references": [ - "GUID:214998e563c124e8a88199b2dd1f522d", - "GUID:0acc523941302664db1f4e527237feb3", - "GUID:27619889b8ba8c24980f49ee34dbb44a" - ], - "includePlatforms": [ - "Editor" - ], - "excludePlatforms": [], - "allowUnsafeCode": false, - "overrideReferences": false, - "precompiledReferences": [], - "autoReferenced": false, - "defineConstraints": [], - "versionDefines": [], - "noEngineReferences": false, - "testAssemblies": true -} \ No newline at end of file diff --git a/Assets/Tests/_Test_For_Test_/UnityCLILoop.test_for_test.Editor.asmdef.meta b/Assets/Tests/_Test_For_Test_/UnityCLILoop.test_for_test.Editor.asmdef.meta deleted file mode 100644 index c731994adf..0000000000 --- a/Assets/Tests/_Test_For_Test_/UnityCLILoop.test_for_test.Editor.asmdef.meta +++ /dev/null @@ -1,7 +0,0 @@ -fileFormatVersion: 2 -guid: 2dc6b4611b96a49c5bd291cb7f5dc3ff -AssemblyDefinitionImporter: - externalObjects: {} - userData: - assetBundleName: - assetBundleVariant: