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
@@ -0,0 +1,113 @@
using System.Collections.Generic;

using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.Infrastructure;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Test fixture that verifies compile-error-log entries are matched against V2 legacy API tokens
/// and reduced to a deduplicated set of migration target file paths.
/// </summary>
public sealed class ThirdPartyToolMigrationCompileErrorLogMatcherTests
{
[Test]
public void Match_WhenNoEntryContainsLegacyToken_ReturnsEmptyResult()
{
// Verifies that unrelated V3 compile errors produce no matched entries and no target file paths.
List<CompileErrorLogEntry> entries = new List<CompileErrorLogEntry>
{
new CompileErrorLogEntry(
"Assets/Editor/MyTool.cs(1,1): error CS0103: The name 'undefinedSymbol' does not exist in the current context",
"Assets/Editor/MyTool.cs",
1)
};

ThirdPartyToolMigrationCompileErrorLogMatchResult result =
ThirdPartyToolMigrationCompileErrorLogMatcher.Match(entries);

Assert.That(result.MatchedEntries, Is.Empty);
Assert.That(result.TargetFilePaths, Is.Empty);
}

[Test]
public void Match_WhenEntryContainsLegacyToken_ReturnsMatchedEntryAndTargetFilePath()
{
// Verifies that an entry whose message contains a legacy API token is included in both
// the matched-entries list and the target file path list.
List<CompileErrorLogEntry> entries = new List<CompileErrorLogEntry>
{
new CompileErrorLogEntry(
"Assets/Editor/MyTool.cs(3,25): error CS0234: The type or namespace name 'uLoopMCP' " +
"does not exist in the namespace 'io.github.hatayama' (are you missing an assembly reference?)",
"Assets/Editor/MyTool.cs",
3)
};

ThirdPartyToolMigrationCompileErrorLogMatchResult result =
ThirdPartyToolMigrationCompileErrorLogMatcher.Match(entries);

Assert.That(result.MatchedEntries.Count, Is.EqualTo(1));
Assert.That(result.TargetFilePaths, Is.EqualTo(new[] { "Assets/Editor/MyTool.cs" }));
}

[Test]
public void Match_WhenMultipleEntriesShareTheSameFilePath_DeduplicatesTargetFilePaths()
{
// Verifies that multiple legacy-token-matching errors in the same file collapse to one target path.
List<CompileErrorLogEntry> entries = new List<CompileErrorLogEntry>
{
new CompileErrorLogEntry(
"Assets/Editor/MyTool.cs(3,25): error CS0234: The type or namespace name 'uLoopMCP' " +
"does not exist in the namespace 'io.github.hatayama' (are you missing an assembly reference?)",
"Assets/Editor/MyTool.cs",
3),
new CompileErrorLogEntry(
"Assets/Editor/MyTool.cs(9,14): error CS0246: The type or namespace name 'AbstractUnityTool' " +
"could not be found (are you missing a using directive or an assembly reference?)",
"Assets/Editor/MyTool.cs",
9)
};

ThirdPartyToolMigrationCompileErrorLogMatchResult result =
ThirdPartyToolMigrationCompileErrorLogMatcher.Match(entries);

Assert.That(result.MatchedEntries.Count, Is.EqualTo(2));
Assert.That(result.TargetFilePaths, Is.EqualTo(new[] { "Assets/Editor/MyTool.cs" }));
}

[Test]
public void Match_WhenEntriesMixMatchingAndNonMatching_OnlyIncludesMatchingFilePaths()
{
// Verifies that a non-matching entry does not contribute its file path even when it is
// interleaved with matching entries from other files.
List<CompileErrorLogEntry> entries = new List<CompileErrorLogEntry>
{
new CompileErrorLogEntry(
"Assets/Editor/Unrelated.cs(1,1): error CS0103: The name 'undefinedSymbol' does not exist in the current context",
"Assets/Editor/Unrelated.cs",
1),
new CompileErrorLogEntry(
"Assets/Editor/LegacyTool.cs(2,6): error CS0246: The type or namespace name 'McpTool' " +
"could not be found (are you missing a using directive or an assembly reference?)",
"Assets/Editor/LegacyTool.cs",
2)
};

ThirdPartyToolMigrationCompileErrorLogMatchResult result =
ThirdPartyToolMigrationCompileErrorLogMatcher.Match(entries);

Assert.That(result.MatchedEntries.Count, Is.EqualTo(1));
Assert.That(result.TargetFilePaths, Is.EqualTo(new[] { "Assets/Editor/LegacyTool.cs" }));
}

[Test]
public void Match_WhenEntriesIsNull_Throws()
{
// Verifies fail-fast behavior when the entry collection itself is missing.
Assert.Throws<System.ArgumentNullException>(() =>
ThirdPartyToolMigrationCompileErrorLogMatcher.Match(null));
}
}
}

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

83 changes: 83 additions & 0 deletions Assets/Tests/Editor/ThirdPartyToolMigrationDetectionRulesTests.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,83 @@
using NUnit.Framework;

using io.github.hatayama.UnityCliLoop.Infrastructure;

namespace io.github.hatayama.UnityCliLoop.Tests.Editor
{
/// <summary>
/// Test fixture that verifies compile-error-message token matching for V2 legacy API detection.
/// </summary>
public sealed class ThirdPartyToolMigrationDetectionRulesTests
{
[Test]
public void ContainsLegacyApiToken_WhenMessageContainsLegacyNamespaceSegment_ReturnsTrue()
{
// Verifies that a CS0234 message shaped like Roslyn's actual segment-only report is detected.
const string message =
"Assets/Editor/MyTool.cs(3,25): error CS0234: The type or namespace name 'uLoopMCP' " +
"does not exist in the namespace 'io.github.hatayama' (are you missing an assembly reference?)";

Assert.That(ThirdPartyToolMigrationDetectionRules.ContainsLegacyApiToken(message), Is.True);
}

[Test]
public void ContainsLegacyApiToken_WhenMessageContainsLegacyAssemblyName_ReturnsTrue()
{
// Verifies that asmdef-reference-shaped errors referencing the legacy assembly name are detected.
const string message = "error: Assembly 'uLoopMCP.Editor' could not be resolved.";

Assert.That(ThirdPartyToolMigrationDetectionRules.ContainsLegacyApiToken(message), Is.True);
}

[Test]
public void ContainsLegacyApiToken_WhenMessageContainsRenamedLegacyTypeName_ReturnsTrue()
{
// Verifies that CS0246-shaped errors for a renamed legacy base type are detected without depending on the error code.
const string message = "Assets/Editor/MyTool.cs(5,14): error CS0246: The type or namespace name " +
"'AbstractUnityTool' could not be found (are you missing a using directive or an assembly reference?)";

Assert.That(ThirdPartyToolMigrationDetectionRules.ContainsLegacyApiToken(message), Is.True);
}

[Test]
public void ContainsLegacyApiToken_WhenMessageContainsAttributeSuffixStrippedForm_ReturnsTrue()
{
// Verifies that Roslyn's attribute-usage error, which reports 'McpTool' without the 'Attribute' suffix, is still detected.
const string message = "Assets/Editor/MyTool.cs(2,6): error CS0246: The type or namespace name " +
"'McpTool' could not be found (are you missing a using directive or an assembly reference?)";

Assert.That(ThirdPartyToolMigrationDetectionRules.ContainsLegacyApiToken(message), Is.True);
}

[Test]
public void ContainsLegacyApiToken_WhenTokenIsSubstringOfUnrelatedIdentifier_ReturnsFalse()
{
// Verifies identifier-boundary matching: 'IUnityTool' must not match inside 'IUnityToolbarButton'.
const string message = "Assets/Editor/MyTool.cs(9,10): error CS0246: The type or namespace name " +
"'IUnityToolbarButton' could not be found (are you missing a using directive or an assembly reference?)";

Assert.That(ThirdPartyToolMigrationDetectionRules.ContainsLegacyApiToken(message), Is.False);
}

[Test]
public void ContainsLegacyApiToken_WhenMessageIsUnrelatedV3CompileError_ReturnsFalse()
{
// Verifies that ordinary V3 compile errors unrelated to legacy migration do not match.
const string message = "Assets/Editor/MyTool.cs(1,1): error CS0103: The name 'undefinedSymbol' " +
"does not exist in the current context";

Assert.That(ThirdPartyToolMigrationDetectionRules.ContainsLegacyApiToken(message), Is.False);
}

[Test]
public void ContainsLegacyApiToken_WhenMessageReferencesTypeSharedByLegacyAndCurrentNames_ReturnsFalse()
{
// Verifies that names unchanged between V2 and V3 (e.g. ServiceResult) are excluded from the token set,
// since matching them would false-positive on unrelated V3-only compile errors.
const string message = "Assets/Editor/MyTool.cs(4,9): error CS0246: The type or namespace name " +
"'ServiceResult' could not be found (are you missing a using directive or an assembly reference?)";

Assert.That(ThirdPartyToolMigrationDetectionRules.ContainsLegacyApiToken(message), Is.False);
}
}
}

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,86 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Linq;

namespace io.github.hatayama.UnityCliLoop.Infrastructure
{
/// <summary>
/// A single compile-error console log entry, already carrying the file path and line number the
/// caller resolved from the raw log (see ThirdPartyToolMigrationCompileErrorLogMatcher for why the
/// matcher itself does not parse these out of the message text).
/// </summary>
internal readonly struct CompileErrorLogEntry
{
public CompileErrorLogEntry(string message, string filePath, int lineNumber)
{
Debug.Assert(!string.IsNullOrEmpty(message), "message must not be null or empty");
Debug.Assert(!string.IsNullOrEmpty(filePath), "filePath must not be null or empty");
Debug.Assert(lineNumber >= 0, "lineNumber must not be negative");

Message = message ?? throw new ArgumentNullException(nameof(message));
FilePath = filePath ?? throw new ArgumentNullException(nameof(filePath));
LineNumber = lineNumber;
}

public string Message { get; }
public string FilePath { get; }
public int LineNumber { get; }
}

/// <summary>
/// The result of matching compile-error log entries against V2 legacy API tokens: the matched
/// entries themselves, plus the deduplicated set of file paths they point to.
/// </summary>
internal readonly struct ThirdPartyToolMigrationCompileErrorLogMatchResult
{
public ThirdPartyToolMigrationCompileErrorLogMatchResult(
List<CompileErrorLogEntry> matchedEntries,
List<string> targetFilePaths)
{
Debug.Assert(matchedEntries != null, "matchedEntries must not be null");
Debug.Assert(targetFilePaths != null, "targetFilePaths must not be null");

MatchedEntries = matchedEntries ?? throw new ArgumentNullException(nameof(matchedEntries));
TargetFilePaths = targetFilePaths ?? throw new ArgumentNullException(nameof(targetFilePaths));
}

public List<CompileErrorLogEntry> MatchedEntries { get; }
public List<string> TargetFilePaths { get; }
}

/// <summary>
/// Matches compile-error log entries against V2 legacy API tokens (see
/// ThirdPartyToolMigrationDetectionRules) to drive the compile-error-driven auto-scan trigger.
/// This is a pure filter/dedup step: parsing file path and line number out of raw console log text
/// is a separate concern owned by the caller wiring this to the real log source.
/// </summary>
internal static class ThirdPartyToolMigrationCompileErrorLogMatcher
{
internal static ThirdPartyToolMigrationCompileErrorLogMatchResult Match(
IReadOnlyList<CompileErrorLogEntry> entries)
{
Debug.Assert(entries != null, "entries must not be null");
if (entries == null)
{
throw new ArgumentNullException(nameof(entries));
}

List<CompileErrorLogEntry> matchedEntries = new List<CompileErrorLogEntry>();
foreach (CompileErrorLogEntry entry in entries)
{
if (ThirdPartyToolMigrationDetectionRules.ContainsLegacyApiToken(entry.Message))
{
matchedEntries.Add(entry);
}
}

List<string> targetFilePaths = matchedEntries
.Select(entry => entry.FilePath)
.Distinct(StringComparer.Ordinal)
.ToList();

return new ThirdPartyToolMigrationCompileErrorLogMatchResult(matchedEntries, targetFilePaths);
}
}
}

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

Loading