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
72 changes: 72 additions & 0 deletions Assets/Tests/Editor/BridgeTransportEndpointTests.cs
Original file line number Diff line number Diff line change
@@ -1,7 +1,12 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Runtime.InteropServices;
using Newtonsoft.Json.Linq;
using NUnit.Framework;

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

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
Expand All @@ -10,6 +15,8 @@ namespace io.github.hatayama.UnityCliLoop.Tests.Editor
/// </summary>
public class BridgeTransportEndpointTests
{
private const string SharedEndpointContractPath = "tests/contracts/endpoint_contract.json";

[Test]
public void CanonicalizeProjectRoot_WhenPathIsFilesystemRoot_ShouldPreserveRoot()
{
Expand All @@ -20,5 +27,70 @@ public void CanonicalizeProjectRoot_WhenPathIsFilesystemRoot_ShouldPreserveRoot(

Assert.That(canonicalProjectRoot, Is.EqualTo(filesystemRoot));
}

[Test]
public void CreateProjectIpc_WhenCanonicalRootsFromSharedContract_MatchExpectedEndpointPaths()
{
// Verifies Go and C# derive the same IPC endpoint names from already-canonicalized roots
// listed in the shared endpoint contract (symlink resolution is out of scope).
foreach (JObject contractCase in ReadEndpointContractCases())
{
string caseId = contractCase.Value<string>("id");
string canonicalProjectRoot = contractCase.Value<string>("canonicalProjectRoot");
if (!CanExerciseProjectRootOnCurrentPlatform(canonicalProjectRoot))
{
continue;
}

string expectedPath = ExpectedEndpointPathForCurrentPlatform(contractCase);
BridgeTransportEndpoint endpoint = BridgeTransportEndpoint.CreateProjectIpc(canonicalProjectRoot);
Assert.That(endpoint.Path, Is.EqualTo(expectedPath), $"case {caseId} canonical root");

JArray trimOnlyEquivalentRoots = contractCase.Value<JArray>("trimOnlyEquivalentRoots") ?? new JArray();
foreach (JToken equivalentRootToken in trimOnlyEquivalentRoots)
{
string equivalentRoot = equivalentRootToken.Value<string>();
BridgeTransportEndpoint trimmedEndpoint = BridgeTransportEndpoint.CreateProjectIpc(equivalentRoot);
Assert.That(
trimmedEndpoint.Path,
Is.EqualTo(expectedPath),
$"case {caseId} trim-only equivalent root {equivalentRoot}");
}
}
}

private static IEnumerable<JObject> ReadEndpointContractCases()
{
string projectRoot = UnityCliLoopPathResolver.GetProjectRoot();
string json = File.ReadAllText(Path.Combine(projectRoot, SharedEndpointContractPath));
JObject contract = JObject.Parse(json);
JArray cases = contract.Value<JArray>("cases");
Assert.That(cases, Is.Not.Null.And.Not.Empty);
foreach (JToken caseToken in cases)
{
yield return (JObject)caseToken;
}
}

private static bool CanExerciseProjectRootOnCurrentPlatform(string projectRoot)
{
bool looksLikeWindowsPath = projectRoot.Length >= 2 && projectRoot[1] == ':';
if (looksLikeWindowsPath)
{
return RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
}

return !RuntimeInformation.IsOSPlatform(OSPlatform.Windows);
}

private static string ExpectedEndpointPathForCurrentPlatform(JObject contractCase)
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
return contractCase.Value<string>("windowsPipePath");
}

return contractCase.Value<string>("unixSocketPath");
}
}
}
90 changes: 90 additions & 0 deletions Assets/Tests/Editor/CliUpdateRequiredErrorContractTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,90 @@
using System.IO;
using Newtonsoft.Json.Linq;
using NUnit.Framework;

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

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Freezes the cli_update_required JSON-RPC error.data frame that old CLIs must keep parsing forever.
/// </summary>
public sealed class CliUpdateRequiredErrorContractTests
{
private const string SharedContractPath = "tests/contracts/cli_update_required_error_contract.json";

[Test]
public void CreateCliProtocolMismatchResponse_WhenOlderProtocol_MatchesSharedErrorDataFieldShape()
{
// This frame must remain backward compatible forever — old CLIs parse it to learn they must update.
// Verifies JsonRpcResponseFactory still emits the frozen error.data field shape for older CLIs.
JObject expected = ReadSharedErrorDataFieldShape();
string responseJson = JsonRpcResponseFactory.CreateCliProtocolMismatchResponse(
id: "contract-test",
currentCliVersion: "3.0.0-beta.5",
currentProtocolVersion: 1);
JObject actualData = (JObject)JObject.Parse(responseJson)["error"]!["data"]!;
JObject actual = NormalizeFieldShape(actualData);

Assert.That(JToken.DeepEquals(actual, expected), Is.True, $"Expected {expected} but got {actual}");
Assert.That(actualData.Value<string>("type"), Is.EqualTo("cli_update_required"));
Assert.That(
actualData.Value<int>("requiredProtocolVersion"),
Is.EqualTo(CliConstants.REQUIRED_CLI_PROTOCOL_VERSION));
Assert.That(actualData.Value<string>("updateCommand"), Is.EqualTo("uloop update"));
}

private static JObject ReadSharedErrorDataFieldShape()
{
string projectRoot = UnityCliLoopPathResolver.GetProjectRoot();
string json = File.ReadAllText(Path.Combine(projectRoot, SharedContractPath));
JObject contract = JObject.Parse(json);
return NormalizeFieldShape((JObject)contract["errorData"]!);
}

private static JObject NormalizeFieldShape(JObject value)
{
JObject shape = new();
foreach (JProperty property in value.Properties())
{
shape[property.Name] = NormalizeTokenFieldShape(property.Value);
}
return shape;
}

private static JToken NormalizeTokenFieldShape(JToken value)
{
if (value is JObject objectValue)
{
return NormalizeFieldShape(objectValue);
}

if (value is JArray arrayValue)
{
JArray arrayShape = new();
if (arrayValue.Count > 0)
{
arrayShape.Add(NormalizeTokenFieldShape(arrayValue[0]));
}
return arrayShape;
}

return new JValue(NormalizeScalarType(value.Type));
}

private static string NormalizeScalarType(JTokenType type)
{
return type switch
{
JTokenType.Boolean => "boolean",
JTokenType.Float => "number",
JTokenType.Integer => "number",
JTokenType.Null => "null",
JTokenType.String => "string",
_ => "unknown"
};
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/CliUpdateRequiredErrorContractTests.cs.meta

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

123 changes: 123 additions & 0 deletions Assets/Tests/Editor/GetCompileStatusResponseContractTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,123 @@
using System;
using System.IO;
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
{
/// <summary>
/// Test fixture that verifies Unity get-compile-status responses stay aligned with the shared Go CLI contract.
/// </summary>
public sealed class GetCompileStatusResponseContractTests
{
private const string SharedContractPath = "tests/contracts/compile_status_response_contract.json";

[Test]
public void GetCompileStatusResponse_WhenSerialized_MatchesSharedContractFieldShape()
{
// Verifies C# does not add, remove, or rename get-compile-status fields without updating the
// shared CLI contract. Result must come from a real CompileResponse serialization because that
// is what CompileStatusBridgeCommand restores from ResultJson for Go compileResultStatus.Success.
JObject expected = ReadSharedContractFieldShape();
JObject compileResultJson = SerializeCompileResponseResult();
GetCompileStatusResponse response = new()
{
Ready = true,
HasResult = true,
IsCompiling = true,
IsUpdating = true,
IsDomainReloadInProgress = true,
Result = compileResultJson,
Message = "Compile result is available."
};
string json = JsonConvert.SerializeObject(
response,
Formatting.None,
UnityCliLoopJsonResponseSerializerSettings.Settings);
JObject actual = NormalizeFieldShape(JObject.Parse(json));

Assert.That(JToken.DeepEquals(actual, expected), Is.True, $"Expected {expected} but got {actual}");
}

[Test]
public void CompileResponse_WhenSerializedForCompileStatusResult_IncludesSuccessProperty()
{
// Verifies the wire Result payload still exposes Success under the name Go unmarshals into
// compileResultStatus — a rename on CompileResponse must fail this test.
JObject compileResultJson = SerializeCompileResponseResult();
Assert.That(compileResultJson.Property("Success"), Is.Not.Null);
Assert.That(compileResultJson["Success"]!.Type, Is.EqualTo(JTokenType.Boolean));
}

private static JObject SerializeCompileResponseResult()
{
CompileResponse compileResult = new(
success: true,
errorCount: 0,
warningCount: 0,
errors: Array.Empty<CompileIssue>(),
warnings: Array.Empty<CompileIssue>(),
message: "Compile result is available.");
string resultJson = JsonConvert.SerializeObject(
compileResult,
Formatting.None,
UnityCliLoopJsonResponseSerializerSettings.Settings);
return JObject.Parse(resultJson);
}

private static JObject ReadSharedContractFieldShape()
{
string projectRoot = UnityCliLoopPathResolver.GetProjectRoot();
string json = File.ReadAllText(Path.Combine(projectRoot, SharedContractPath));
return NormalizeFieldShape(JObject.Parse(json));
}

private static JObject NormalizeFieldShape(JObject value)
{
JObject shape = new();
foreach (JProperty property in value.Properties())
{
shape[property.Name] = NormalizeTokenFieldShape(property.Value);
}
return shape;
}

private static JToken NormalizeTokenFieldShape(JToken value)
{
if (value is JObject objectValue)
{
return NormalizeFieldShape(objectValue);
}

if (value is JArray arrayValue)
{
JArray arrayShape = new();
if (arrayValue.Count > 0)
{
arrayShape.Add(NormalizeTokenFieldShape(arrayValue[0]));
}
return arrayShape;
}

return new JValue(NormalizeScalarType(value.Type));
}

private static string NormalizeScalarType(JTokenType type)
{
return type switch
{
JTokenType.Boolean => "boolean",
JTokenType.Float => "number",
JTokenType.Integer => "number",
JTokenType.Null => "null",
JTokenType.String => "string",
_ => "unknown"
};
}
}
}
11 changes: 11 additions & 0 deletions Assets/Tests/Editor/GetCompileStatusResponseContractTests.cs.meta

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
@@ -0,0 +1,35 @@
using Newtonsoft.Json.Linq;
using NUnit.Framework;

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

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Verifies JSON-RPC success serialization failures become error frames instead of fake success payloads.
/// </summary>
public sealed class JsonRpcResponseFactorySerializationFallbackTests
{
[Test]
public void CreateSuccessResponse_WhenResultCannotBeSerialized_ReturnsJsonRpcError()
{
// Verifies serialization failure returns INTERNAL_ERROR with internal_error data instead of a
// success frame that embeds an error-shaped object in result.
string responseJson = JsonRpcResponseFactory.CreateSuccessResponse(
id: 1,
result: new UnserializableToolResponse());
JObject response = JObject.Parse(responseJson);

Assert.That(response["result"], Is.Null);
Assert.That(response["error"], Is.Not.Null);
Assert.That(response["error"]!["code"]!.Value<int>(), Is.EqualTo(UnityCliLoopServerConfig.INTERNAL_ERROR_CODE));
Assert.That(response["error"]!["data"]!["type"]!.Value<string>(), Is.EqualTo(JsonRpcErrorTypes.InternalError));
}

private sealed class UnserializableToolResponse : UnityCliLoopToolResponse
{
public string Boom => throw new System.InvalidOperationException("intentional serialization failure");
}
}
}

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

2 changes: 1 addition & 1 deletion Assets/Tests/Editor/RunTestsTestFrameworkResultTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -192,7 +192,7 @@ public void TrySaveFailureXml_WhenExporterThrows_ReturnsNull()
TestResultStatus.Failed,
0.1,
new List<ITestResultAdaptor>());
Regex warningPattern = new Regex("^Failed to save NUnit XML result file: ");
Regex warningPattern = new Regex("^Failed to save failure XML result file: ");

LogAssert.Expect(LogType.Warning, warningPattern);

Expand Down
Loading
Loading