Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Read this before interpreting unexpected, missing, or truncated captured values,
- The snapshot is taken **before** the resolved line executes, exactly like an IDE breakpoint on that line. To inspect a value after an assignment, place the pause point on the following line.
- The pause itself only takes effect at the next frame boundary: the frame that hit the pause point still runs to completion first, so any event that fires later in that same frame (a chained collision, a cascading destroy) has already happened by the time Unity actually stops. Trust `CapturedVariables` (the pre-line snapshot) as evidence for what was true up to the patched line; do not assume the paused state still matches it for events later in the same frame.
- Rigidbody values read inside a physics callback (`OnCollision*`/`OnTrigger*`) can be mid-solver intermediates — `velocity` may capture as `(0.0, 0.0)` at the callback even though the body visibly moves. `CapturedVariables` faithfully records that intermediate value; a later `execute-dynamic-code` read returning something different means the physics solver has since finished the step, not that the capture was wrong.
- `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result.
- `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result. `EditorPlaying` is always present and reports whether Play Mode is running, so a stopped versus playing Editor is visible even when `EditorPaused` is omitted.

## Scopes and the `this` Entry

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Read this before interpreting unexpected, missing, or truncated captured values,
- The snapshot is taken **before** the resolved line executes, exactly like an IDE breakpoint on that line. To inspect a value after an assignment, place the pause point on the following line.
- The pause itself only takes effect at the next frame boundary: the frame that hit the pause point still runs to completion first, so any event that fires later in that same frame (a chained collision, a cascading destroy) has already happened by the time Unity actually stops. Trust `CapturedVariables` (the pre-line snapshot) as evidence for what was true up to the patched line; do not assume the paused state still matches it for events later in the same frame.
- Rigidbody values read inside a physics callback (`OnCollision*`/`OnTrigger*`) can be mid-solver intermediates — `velocity` may capture as `(0.0, 0.0)` at the callback even though the body visibly moves. `CapturedVariables` faithfully records that intermediate value; a later `execute-dynamic-code` read returning something different means the physics solver has since finished the step, not that the capture was wrong.
- `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result.
- `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result. `EditorPlaying` is always present and reports whether Play Mode is running, so a stopped versus playing Editor is visible even when `EditorPaused` is omitted.

## Scopes and the `this` Entry

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,252 @@
using System;
using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;

using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.FirstPartyTools;
using io.github.hatayama.UnityCliLoop.Infrastructure;
using io.github.hatayama.UnityCliLoop.ToolContracts;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor.DynamicCodeToolTests
{
/// <summary>
/// Verifies error responses serialize EditorPlaying from the injected editor-state reader.
/// </summary>
[TestFixture]
public sealed class ExecuteDynamicCodeErrorEditorStateTests
{
[SetUp]
public void SetUp()
{
DynamicCodeForegroundWarmupState.Reset();
}

/// <summary>
/// What: a compile-failure UseCase response JSON reports injected EditorPlaying=true.
/// </summary>
[Test]
public async Task ExecuteAsync_WhenCompilationFails_SerializesInjectedEditorPlaying()
{
MarkForegroundWarmupCompleted();
FakeDynamicCodeExecutionRuntime runtime = new(
new ExecutionResult
{
Success = false,
ErrorMessage = "Compilation error occurred",
CompilationErrors = new List<CompilationError>
{
new CompilationError
{
ErrorCode = "CS0103",
Message = "The name 'Nope' does not exist in the current context"
}
}
});
ExecuteDynamicCodeUseCase useCase = CreateUseCase(runtime);

ExecuteDynamicCodeResponse response = await useCase.ExecuteAsync(
new ExecuteDynamicCodeSchema
{
Code = "return Nope;",
CompileOnly = true
},
CancellationToken.None);

Assert.That(response.Success, Is.False);
Assert.That(SerializedEditorState(response), Is.EqualTo("{\"EditorPlaying\":true}"));
}

/// <summary>
/// What: a cancelled-result UseCase response JSON reports injected EditorPlaying=true.
/// </summary>
[Test]
public async Task ExecuteAsync_WhenResultIsCancelled_SerializesInjectedEditorPlaying()
{
MarkForegroundWarmupCompleted();
FakeDynamicCodeExecutionRuntime runtime = new(
new ExecutionResult
{
Success = false,
ErrorMessage = UnityCliLoopConstants.ERROR_MESSAGE_EXECUTION_CANCELLED
});
ExecuteDynamicCodeUseCase useCase = CreateUseCase(runtime);

ExecuteDynamicCodeResponse response = await useCase.ExecuteAsync(
new ExecuteDynamicCodeSchema { Code = "return 1;" },
CancellationToken.None);

Assert.That(response.Success, Is.False);
Assert.That(SerializedEditorState(response), Is.EqualTo("{\"EditorPlaying\":true}"));
}

/// <summary>
/// What: a runtime-restarting UseCase response JSON reports injected EditorPlaying=true.
/// </summary>
[Test]
public async Task ExecuteAsync_WhenResultIsRuntimeRestarting_SerializesInjectedEditorPlaying()
{
MarkForegroundWarmupCompleted();
FakeDynamicCodeExecutionRuntime runtime = new(
DynamicCodeExecutionResponseFactory.CreateRuntimeRestartingExecutionResult());
ExecuteDynamicCodeUseCase useCase = CreateUseCase(runtime);

ExecuteDynamicCodeResponse response = await useCase.ExecuteAsync(
new ExecuteDynamicCodeSchema { Code = "return 1;" },
CancellationToken.None);

Assert.That(response.Success, Is.False);
Assert.That(SerializedEditorState(response), Is.EqualTo("{\"EditorPlaying\":true}"));
}

/// <summary>
/// What: the OperationCanceledException catch path JSON reports injected EditorPlaying=true.
/// </summary>
[Test]
public async Task ExecuteAsync_WhenRuntimeThrowsOperationCanceled_SerializesInjectedEditorPlaying()
{
MarkForegroundWarmupCompleted();
ExecuteDynamicCodeUseCase useCase = CreateUseCase(new OperationCanceledDynamicCodeExecutionRuntime());

ExecuteDynamicCodeResponse response = await useCase.ExecuteAsync(
new ExecuteDynamicCodeSchema { Code = "return 1;" },
CancellationToken.None);

Assert.That(response.Success, Is.False);
Assert.That(SerializedEditorState(response), Is.EqualTo("{\"EditorPlaying\":true}"));
}

/// <summary>
/// What: EditorPlaying=false still appears in JSON instead of being omitted.
/// </summary>
[Test]
public void ExecuteDynamicCodeResponse_WhenNotPlaying_StillSerializesEditorPlayingFalse()
{
ExecuteDynamicCodeResponse response = new()
{
Success = false,
EditorPlaying = false
};

Assert.That(SerializedEditorState(response), Is.EqualTo("{\"EditorPlaying\":false}"));
}

/// <summary>
/// What: a paused response still serializes EditorPaused and ActivePausePointId next to EditorPlaying.
/// </summary>
[Test]
public void ExecuteDynamicCodeResponse_WhenPaused_SerializesPlayingPausedAndActiveId()
{
ExecuteDynamicCodeResponse response = new()
{
Success = false,
EditorPlaying = true,
EditorPaused = true,
ActivePausePointId = "jump"
};

Assert.That(
SerializedEditorState(response),
Is.EqualTo("{\"EditorPlaying\":true,\"EditorPaused\":true,\"ActivePausePointId\":\"jump\"}"));
}

private static ExecuteDynamicCodeUseCase CreateUseCase(IDynamicCodeExecutionRuntime runtime)
{
return new ExecuteDynamicCodeUseCase(
runtime,
new FakeDynamicCodeEditorStateReader(isPlaying: true, isPaused: false));
}

private static string SerializedEditorState(ExecuteDynamicCodeResponse response)
{
JObject serialized = JObject.Parse(
JsonConvert.SerializeObject(response, JsonRpcResponseSerializer.Settings));
JObject state = new JObject();
if (serialized["EditorPlaying"] != null)
{
state["EditorPlaying"] = serialized["EditorPlaying"];
}

if (serialized["EditorPaused"] != null)
{
state["EditorPaused"] = serialized["EditorPaused"];
}

if (serialized["ActivePausePointId"] != null)
{
state["ActivePausePointId"] = serialized["ActivePausePointId"];
}

return state.ToString(Formatting.None);
}

private static void MarkForegroundWarmupCompleted()
{
bool started = DynamicCodeForegroundWarmupState.TryBegin();
if (!started)
{
return;
}

DynamicCodeForegroundWarmupState.MarkCompleted();
}

private sealed class FakeDynamicCodeEditorStateReader : IDynamicCodeEditorStateReader
{
public FakeDynamicCodeEditorStateReader(bool isPlaying, bool isPaused)
{
IsPlaying = isPlaying;
IsPaused = isPaused;
}

public bool IsPlaying { get; }

public bool IsPaused { get; }
}

private sealed class FakeDynamicCodeExecutionRuntime : IDynamicCodeExecutionRuntime
{
private readonly Queue<ExecutionResult> _results;

public FakeDynamicCodeExecutionRuntime(params ExecutionResult[] results)
{
_results = new Queue<ExecutionResult>(results);
}

public Task<ExecutionResult> ExecuteAsync(
DynamicCodeExecutionRequest request,
CancellationToken cancellationToken = default)
{
return Task.FromResult(_results.Dequeue());
}

public Task<(bool Entered, ExecutionResult Result)> TryExecuteIfIdleAsync(
DynamicCodeExecutionRequest request,
CancellationToken cancellationToken = default)
{
return Task.FromResult<(bool, ExecutionResult)>((true, _results.Dequeue()));
}
}

private sealed class OperationCanceledDynamicCodeExecutionRuntime : IDynamicCodeExecutionRuntime
{
public Task<ExecutionResult> ExecuteAsync(
DynamicCodeExecutionRequest request,
CancellationToken cancellationToken = default)
{
throw new OperationCanceledException();
}

public Task<(bool Entered, ExecutionResult Result)> TryExecuteIfIdleAsync(
DynamicCodeExecutionRequest request,
CancellationToken cancellationToken = default)
{
throw new OperationCanceledException();
}
}
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,7 @@ Read this before interpreting unexpected, missing, or truncated captured values,
- The snapshot is taken **before** the resolved line executes, exactly like an IDE breakpoint on that line. To inspect a value after an assignment, place the pause point on the following line.
- The pause itself only takes effect at the next frame boundary: the frame that hit the pause point still runs to completion first, so any event that fires later in that same frame (a chained collision, a cascading destroy) has already happened by the time Unity actually stops. Trust `CapturedVariables` (the pre-line snapshot) as evidence for what was true up to the patched line; do not assume the paused state still matches it for events later in the same frame.
- Rigidbody values read inside a physics callback (`OnCollision*`/`OnTrigger*`) can be mid-solver intermediates — `velocity` may capture as `(0.0, 0.0)` at the callback even though the body visibly moves. `CapturedVariables` faithfully records that intermediate value; a later `execute-dynamic-code` read returning something different means the physics solver has since finished the step, not that the capture was wrong.
- `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result.
- `execute-dynamic-code` during the pause sees the interrupted method's **post-interrupt** state, not this pre-line snapshot. Use `CapturedVariables` for pre-line evidence; use the raw capture API below when you need live references while paused. If you suspect a captured value is stale or wrong, cross-check it against the live scene object with `execute-dynamic-code` (for example reading `transform.position` off the instance found via `UnityObjectPath`) rather than trusting either source alone. `execute-dynamic-code` responses also carry `EditorPaused` and `ActivePausePointId` — these fields appear only while the Editor is paused, so a call made while a pause point still has Unity paused is unambiguous instead of looking like a stale or buggy result. `EditorPlaying` is always present and reports whether Play Mode is running, so a stopped versus playing Editor is visible even when `EditorPaused` is omitted.

## Scopes and the `this` Entry

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
using UnityEditor;

namespace io.github.hatayama.UnityCliLoop.FirstPartyTools
{
/// <summary>
/// Reads Editor Play/Pause flags for execute-dynamic-code pause-state application.
/// </summary>
internal interface IDynamicCodeEditorStateReader
{
bool IsPlaying { get; }
bool IsPaused { get; }
}

/// <summary>
/// Forwards Play/Pause reads to EditorApplication.
/// </summary>
internal sealed class DynamicCodeEditorStateReader : IDynamicCodeEditorStateReader
{
public bool IsPlaying => EditorApplication.isPlaying;

public bool IsPaused => EditorApplication.isPaused;
}
}

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,12 @@ public string Error
/// </summary>
public string[] NextActions { get; set; }

/// <summary>
/// Whether Play Mode is running as this response is returned. Always serialized so a
/// stopped versus playing Editor is visible even when EditorPaused is omitted.
/// </summary>
public bool EditorPlaying { get; set; }

/// <summary>
/// Whether the Editor is paused as this response is returned. Lets an agent recognize a
/// post-interrupt state (e.g. a pause point hit during this execution) instead of mistaking
Expand Down
Loading
Loading