csharp-sdk/tests/ModelContextProtocol.Analyzers.Tests/XmlToDescriptionGeneratorTests.cs

2178 lines
71 KiB
C#

using Microsoft.CodeAnalysis;
using Microsoft.CodeAnalysis.CSharp;
using System.Diagnostics.CodeAnalysis;
using Xunit;
namespace ModelContextProtocol.Analyzers.Tests;
public partial class XmlToDescriptionGeneratorTests
{
[Fact]
public void Generator_WithSummaryOnly_GeneratesMethodDescription()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Test tool description
/// </summary>
[McpServerTool]
public static partial string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Test tool description")]
public static partial string TestMethod(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithSummaryAndRemarks_CombinesInMethodDescription()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Test tool summary
/// </summary>
/// <remarks>
/// Additional remarks
/// </remarks>
[McpServerTool]
public static partial string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Test tool summary\nAdditional remarks")]
public static partial string TestMethod(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithParameterDocs_GeneratesParameterDescriptions()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Test tool
/// </summary>
/// <param name="input">Input parameter description</param>
/// <param name="count">Count parameter description</param>
[McpServerTool]
public static partial string TestMethod(string input, int count)
{
return input;
}
}
""");
Assert.True(result.Success);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Test tool")]
public static partial string TestMethod([Description("Input parameter description")] string input, [Description("Count parameter description")] int count);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithReturnDocs_GeneratesReturnDescription()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Test tool
/// </summary>
/// <returns>The result of the operation</returns>
[McpServerTool]
public static partial string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Test tool")]
[return: Description("The result of the operation")]
public static partial string TestMethod(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithExistingMethodDescription_DoesNotGenerateMethodDescription()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Test tool summary
/// </summary>
/// <returns>Result</returns>
[McpServerTool]
[Description("Already has description")]
public static partial string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[return: Description("Result")]
public static partial string TestMethod(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithExistingParameterDescription_SkipsThatParameter()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Test tool
/// </summary>
/// <param name="input">Input description</param>
/// <param name="count">Count description</param>
[McpServerTool]
public static partial string TestMethod(string input, [Description("Already has")] int count)
{
return input;
}
}
""");
Assert.True(result.Success);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Test tool")]
public static partial string TestMethod([Description("Input description")] string input, int count);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithoutMcpServerToolAttribute_DoesNotGenerate()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
public partial class TestTools
{
/// <summary>
/// Test tool
/// </summary>
public static partial string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Empty(result.GeneratedSources);
}
[Fact]
public void Generator_WithoutPartialKeyword_DoesNotGenerate()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public class TestTools
{
/// <summary>
/// Test tool
/// </summary>
[McpServerTool]
public static string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Empty(result.GeneratedSources);
}
[Fact]
public void Generator_NonPartialMethodWithXmlDocs_ReportsMCP002Diagnostic()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public class TestTools
{
/// <summary>
/// Test tool with documentation
/// </summary>
[McpServerTool]
public static string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Empty(result.GeneratedSources);
// Should report MCP002 diagnostic
var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002");
Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity);
Assert.Contains("TestMethod", diagnostic.GetMessage());
Assert.Contains("partial", diagnostic.GetMessage());
}
[Fact]
public void Generator_NonPartialMethodWithParameterDocs_ReportsMCP002Diagnostic()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public class TestTools
{
/// <param name="input">Input parameter</param>
[McpServerTool]
public static string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Empty(result.GeneratedSources);
// Should report MCP002 diagnostic because parameter has documentation
var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002");
Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity);
}
[Fact]
public void Generator_NonPartialMethodWithReturnDocs_ReportsMCP002Diagnostic()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public class TestTools
{
/// <returns>Return value</returns>
[McpServerTool]
public static string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Empty(result.GeneratedSources);
// Should report MCP002 diagnostic because return has documentation
var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002");
Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity);
}
[Fact]
public void Generator_NonPartialMethodWithoutXmlDocs_DoesNotReportDiagnostic()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public class TestTools
{
[McpServerTool]
public static string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Empty(result.GeneratedSources);
// Should NOT report MCP002 diagnostic because there's no XML documentation
Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002");
}
[Fact]
public void Generator_NonPartialMethodWithEmptyXmlDocs_DoesNotReportDiagnostic()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public class TestTools
{
/// <summary></summary>
[McpServerTool]
public static string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Empty(result.GeneratedSources);
// Should NOT report MCP002 diagnostic because XML documentation is empty
Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002");
}
[Fact]
public void Generator_NonPartialMethodWithExistingDescriptions_DoesNotReportDiagnostic()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public class TestTools
{
/// <summary>Test tool</summary>
/// <param name="input">Input param</param>
/// <returns>Return value</returns>
[McpServerTool]
[Description("Already has method description")]
[return: Description("Already has return description")]
public static string TestMethod([Description("Already has param description")] string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Empty(result.GeneratedSources);
// Should NOT report MCP002 diagnostic because all descriptions already exist
Assert.DoesNotContain(result.Diagnostics, d => d.Id == "MCP002");
}
[Fact]
public void Generator_NonPartialMethodWithPartialExistingDescriptions_ReportsMCP002Diagnostic()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public class TestTools
{
/// <summary>Test tool</summary>
/// <param name="input">Input param</param>
[McpServerTool]
[Description("Already has method description")]
public static string TestMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Empty(result.GeneratedSources);
// Should report MCP002 diagnostic because parameter description would be generated
var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002");
Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity);
}
[Fact]
public void Generator_NonPartialPromptWithXmlDocs_ReportsMCP002Diagnostic()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerPromptType]
public class TestPrompts
{
/// <summary>
/// Test prompt
/// </summary>
[McpServerPrompt]
public static string TestPrompt(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Empty(result.GeneratedSources);
// Should report MCP002 diagnostic for prompts too
var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002");
Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity);
}
[Fact]
public void Generator_NonPartialResourceWithXmlDocs_ReportsMCP002Diagnostic()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerResourceType]
public class TestResources
{
/// <summary>
/// Test resource
/// </summary>
[McpServerResource("test://resource")]
public static string TestResource(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Empty(result.GeneratedSources);
// Should report MCP002 diagnostic for resources too
var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP002");
Assert.Equal(DiagnosticSeverity.Info, diagnostic.Severity);
}
[Fact]
public void Generator_WithSpecialCharacters_EscapesCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Test with "quotes", \backslash, newline
/// and tab characters.
/// </summary>
/// <param name="input">Parameter with "quotes"</param>
[McpServerTool]
public static partial string TestEscaping(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Test with \"quotes\", \\backslash, newline and tab characters.")]
public static partial string TestEscaping([Description("Parameter with \"quotes\"")] string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithInvalidXml_GeneratesPartialAndReportsDiagnostic()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Test with <unclosed tag
/// </summary>
[McpServerTool]
public static partial string TestInvalidXml(string input)
{
return input;
}
}
""");
// Should not throw, generates partial implementation without Description attributes
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
public static partial string TestInvalidXml(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
// Should report a warning diagnostic
var diagnostic = Assert.Single(result.Diagnostics, d => d.Id == "MCP001");
Assert.Equal(DiagnosticSeverity.Warning, diagnostic.Severity);
Assert.Contains("invalid", diagnostic.GetMessage(), StringComparison.OrdinalIgnoreCase);
}
[Fact]
public void Generator_WithGenericType_GeneratesCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools<T>
{
/// <summary>
/// Test generic
/// </summary>
[McpServerTool]
public static partial string TestGeneric(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Test generic")]
public static partial string TestGeneric(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithEmptyXmlComments_GeneratesPartialWithoutDescription()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// </summary>
[McpServerTool]
public static partial string TestEmpty(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
public static partial string TestEmpty(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithMultilineComments_CombinesIntoSingleLine()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// First line
/// Second line
/// Third line
/// </summary>
[McpServerTool]
public static partial string TestMultiline(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("First line Second line Third line")]
public static partial string TestMultiline(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithParametersOnly_GeneratesParameterDescriptions()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <param name="input">Input parameter</param>
/// <param name="count">Count parameter</param>
[McpServerTool]
public static partial string TestMethod(string input, int count)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
public static partial string TestMethod([Description("Input parameter")] string input, [Description("Count parameter")] int count);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithNestedType_GeneratesCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
public partial class OuterClass
{
[McpServerToolType]
public partial class InnerClass
{
/// <summary>
/// Nested tool
/// </summary>
[McpServerTool]
public static partial string NestedMethod(string input)
{
return input;
}
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class OuterClass
{
partial class InnerClass
{
[Description("Nested tool")]
public static partial string NestedMethod(string input);
}
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithManyToolsAcrossMultipleNestedTypes_GeneratesCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test.Outer;
[McpServerToolType]
public partial class RootTools
{
/// <summary>Root level tool 1</summary>
[McpServerTool]
public static partial string RootTool1(string input) => input;
/// <summary>Root level tool 2</summary>
/// <param name="count">The count</param>
[McpServerTool]
public static partial string RootTool2(string input, int count) => input;
}
public partial class OuterContainer
{
[McpServerToolType]
public partial class Level2A
{
/// <summary>Level 2A tool</summary>
[McpServerTool]
public static partial string Level2ATool(string input) => input;
public partial class Level3
{
[McpServerToolType]
public partial class Level4
{
/// <summary>Deep nested tool</summary>
/// <returns>The result</returns>
[McpServerTool]
public static partial string DeepTool(string input) => input;
}
}
}
[McpServerToolType]
public partial class Level2B
{
/// <summary>Level 2B tool 1</summary>
[McpServerTool]
public static partial string Level2BTool1(string input) => input;
/// <summary>Level 2B tool 2</summary>
[McpServerTool]
public static partial string Level2BTool2(string input) => input;
}
}
namespace Test.Resources;
[McpServerResourceType]
public partial class ResourceProviders
{
/// <summary>Test resource 1</summary>
/// <param name="path">The path</param>
[McpServerResource("test:///{path}")]
public static partial string Resource1(string path) => path;
/// <summary>Test resource 2</summary>
[McpServerResource("test2:///{id}")]
public static partial string Resource2(string id) => id;
}
[McpServerPromptType]
public partial class GlobalPrompts
{
/// <summary>Global prompt</summary>
[McpServerPrompt]
public static partial string GlobalPrompt(string input) => input;
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test.Outer
{
partial class RootTools
{
[Description("Root level tool 1")]
public static partial string RootTool1(string input);
[Description("Root level tool 2")]
public static partial string RootTool2(string input, [Description("The count")] int count);
}
partial class OuterContainer
{
partial class Level2A
{
[Description("Level 2A tool")]
public static partial string Level2ATool(string input);
}
}
partial class OuterContainer
{
partial class Level2A
{
partial class Level3
{
partial class Level4
{
[Description("Deep nested tool")]
[return: Description("The result")]
public static partial string DeepTool(string input);
}
}
}
}
partial class OuterContainer
{
partial class Level2B
{
[Description("Level 2B tool 1")]
public static partial string Level2BTool1(string input);
[Description("Level 2B tool 2")]
public static partial string Level2BTool2(string input);
}
}
}
namespace Test.Outer.Test.Resources
{
partial class GlobalPrompts
{
[Description("Global prompt")]
public static partial string GlobalPrompt(string input);
}
partial class ResourceProviders
{
[Description("Test resource 1")]
public static partial string Resource1([Description("The path")] string path);
[Description("Test resource 2")]
public static partial string Resource2(string id);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithRecordClass_GeneratesCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial record TestTools
{
/// <summary>
/// Record tool
/// </summary>
[McpServerTool]
public static partial string RecordMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial record class TestTools
{
[Description("Record tool")]
public static partial string RecordMethod(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithRecordStruct_GeneratesCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial record struct TestTools
{
/// <summary>
/// Record struct tool
/// </summary>
[McpServerTool]
public static partial string RecordStructMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial record struct TestTools
{
[Description("Record struct tool")]
public static partial string RecordStructMethod(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithVirtualMethod_GeneratesCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Virtual tool
/// </summary>
[McpServerTool]
public virtual partial string VirtualMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Virtual tool")]
public virtual partial string VirtualMethod(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithAbstractMethod_GeneratesCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public abstract partial class TestTools
{
/// <summary>
/// Abstract tool
/// </summary>
[McpServerTool]
public abstract partial string AbstractMethod(string input);
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Abstract tool")]
public abstract partial string AbstractMethod(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithMcpServerPrompt_GeneratesCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerPromptType]
public partial class TestPrompts
{
/// <summary>
/// Test prompt
/// </summary>
[McpServerPrompt]
public static partial string TestPrompt(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestPrompts
{
[Description("Test prompt")]
public static partial string TestPrompt(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithMcpServerResource_GeneratesCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerResourceType]
public partial class TestResources
{
/// <summary>
/// Test resource
/// </summary>
[McpServerResource("test://resource")]
public static partial string TestResource(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestResources
{
[Description("Test resource")]
public static partial string TestResource(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithGlobalNamespace_GeneratesCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
[McpServerToolType]
public partial class GlobalTools
{
/// <summary>
/// Tool in global namespace
/// </summary>
[McpServerTool]
public static partial string GlobalMethod(string input)
{
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
partial class GlobalTools
{
[Description("Tool in global namespace")]
public static partial string GlobalMethod(string input);
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithDefaultParameterValues_PreservesDefaults()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Test tool with defaults
/// </summary>
/// <param name="project">The project name</param>
/// <param name="flag">Enable flag</param>
/// <param name="count">Item count</param>
[McpServerTool]
public static partial string TestMethod(
string? project = null,
bool flag = false,
int count = 42)
{
return project ?? "default";
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Test tool with defaults")]
public static partial string TestMethod([Description("The project name")] string? project = null, [Description("Enable flag")] bool flag = false, [Description("Item count")] int count = 42);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithAsyncMethod_ExcludesAsyncModifier()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Threading.Tasks;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Async tool
/// </summary>
[McpServerTool]
public async partial Task<string> DoWorkAsync(string input)
{
await Task.Delay(100);
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Async tool")]
public partial Task<string> DoWorkAsync(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithAsyncStaticMethod_ExcludesAsyncModifier()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Threading.Tasks;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Static async tool
/// </summary>
[McpServerTool]
public static async partial Task<string> StaticAsyncMethod(string input)
{
await Task.Delay(100);
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Static async tool")]
public static partial Task<string> StaticAsyncMethod(string input);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithDefaultParameterValuesAndAsync_HandlesBothCorrectly()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
using System.Threading.Tasks;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Async tool with defaults
/// </summary>
/// <param name="input">The input</param>
/// <param name="timeout">Timeout in ms</param>
[McpServerTool]
public static async partial Task<string> AsyncWithDefaults(string input, int timeout = 1000)
{
await Task.Delay(timeout);
return input;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Async tool with defaults")]
public static partial Task<string> AsyncWithDefaults([Description("The input")] string input, [Description("Timeout in ms")] int timeout = 1000);
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Generator_WithStringDefaultValue_PreservesQuotedDefault()
{
var result = RunGenerator("""
using ModelContextProtocol.Server;
using System.ComponentModel;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>
/// Test tool with string default
/// </summary>
[McpServerTool]
public static partial string TestMethod(string name = "default value")
{
return name;
}
}
""");
Assert.True(result.Success);
Assert.Single(result.GeneratedSources);
var expected = $$"""
// <auto-generated/>
// ModelContextProtocol.Analyzers {{typeof(XmlToDescriptionGenerator).Assembly.GetName().Version}}
#pragma warning disable
using System.ComponentModel;
using ModelContextProtocol.Server;
namespace Test
{
partial class TestTools
{
[Description("Test tool with string default")]
public static partial string TestMethod(string name = "default value");
}
}
""";
AssertGeneratedSourceEquals(expected, result.GeneratedSources[0].SourceText.ToString());
}
private GeneratorRunResult RunGenerator([StringSyntax("C#-test")] string source)
{
var syntaxTree = CSharpSyntaxTree.ParseText(source);
// Get reference assemblies - we need to include all the basic runtime types
List<MetadataReference> referenceList =
[
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.ComponentModel.DescriptionAttribute).Assembly.Location),
];
// Add all necessary runtime assemblies
var runtimePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
referenceList.Add(MetadataReference.CreateFromFile(Path.Combine(runtimePath, "System.Runtime.dll")));
referenceList.Add(MetadataReference.CreateFromFile(Path.Combine(runtimePath, "netstandard.dll")));
// Try to find and add ModelContextProtocol.Core
try
{
var coreAssemblyPath = Path.Combine(AppContext.BaseDirectory, "ModelContextProtocol.Core.dll");
if (File.Exists(coreAssemblyPath))
{
referenceList.Add(MetadataReference.CreateFromFile(coreAssemblyPath));
}
}
catch
{
// If we can't find it, the compilation will fail with appropriate errors
}
var compilation = CSharpCompilation.Create(
"TestAssembly",
[syntaxTree],
referenceList,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
var driver = (CSharpGeneratorDriver)CSharpGeneratorDriver
.Create(new XmlToDescriptionGenerator())
.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics);
var runResult = driver.GetRunResult();
return new GeneratorRunResult
{
Success = !diagnostics.Any(d => d.Severity == DiagnosticSeverity.Error),
GeneratedSources = runResult.GeneratedTrees.Select(t => (t.FilePath, t.GetText())).ToList(),
Diagnostics = diagnostics.ToList(),
Compilation = outputCompilation
};
}
private static void AssertGeneratedSourceEquals(
[StringSyntax("C#-test")] string expected,
[StringSyntax("C#-test")] string actual)
{
// Normalize line endings to \n, remove trailing whitespace from each line, and trim the end
static string Normalize(string s)
{
var lines = s.Replace("\r\n", "\n").Replace("\r", "\n").Split('\n');
for (int i = 0; i < lines.Length; i++)
{
lines[i] = lines[i].TrimEnd();
}
return string.Join('\n', lines).TrimEnd();
}
var normalizedExpected = Normalize(expected);
var normalizedActual = Normalize(actual);
Assert.Equal(normalizedExpected, normalizedActual);
}
private class GeneratorRunResult
{
public bool Success { get; set; }
public List<(string FilePath, Microsoft.CodeAnalysis.Text.SourceText SourceText)> GeneratedSources { get; set; } = [];
public List<Diagnostic> Diagnostics { get; set; } = [];
public Compilation? Compilation { get; set; }
}
[Fact]
public void Caching_WithIdenticalCompilation_AllOutputsCached()
{
// This tests that running the same compilation twice uses cached results
const string Source = """
using ModelContextProtocol.Server;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>Test tool</summary>
[McpServerTool]
public static partial string TestMethod(string input) => input;
}
""";
var compilation = CreateCompilation(Source);
var driver = CreateTrackedDriver();
// Run #1
driver = driver.RunGenerators(compilation, TestContext.Current.CancellationToken);
var result1 = driver.GetRunResult();
Assert.Single(result1.Results);
Assert.Single(result1.Results[0].GeneratedSources);
// Run #2 with same compilation - should be fully cached
driver = driver.RunGenerators(compilation, TestContext.Current.CancellationToken);
var result2 = driver.GetRunResult();
Assert.Single(result2.Results);
var allOutputs = result2.Results[0].TrackedSteps.Values
.SelectMany(steps => steps.SelectMany(step => step.Outputs))
.ToList();
Assert.NotEmpty(allOutputs);
Assert.All(allOutputs, output =>
Assert.True(output.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged,
$"Expected Cached or Unchanged but got {output.Reason}"));
}
[Fact]
public void Caching_WithNewCompilationSameSource_OutputsCached()
{
const string Source = """
using ModelContextProtocol.Server;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>Test tool</summary>
[McpServerTool]
public static partial string TestMethod(string input) => input;
}
""";
var driver = CreateTrackedDriver();
// Run #1 with first compilation
var compilation1 = CreateCompilation(Source);
driver = driver.RunGenerators(compilation1, TestContext.Current.CancellationToken);
var result1 = driver.GetRunResult();
Assert.Single(result1.Results);
// Run #2 with NEW compilation from same source
// This creates new syntax trees and new symbol instances
var compilation2 = CreateCompilation(Source);
Assert.NotSame(compilation1, compilation2); // Verify these are different instances
driver = driver.RunGenerators(compilation2, TestContext.Current.CancellationToken);
var result2 = driver.GetRunResult();
Assert.Single(result2.Results);
// The source generation output should be cached because the extracted data is semantically identical
var sourceOutputSteps = result2.Results[0].TrackedSteps
.Where(kvp => kvp.Key.Contains("SourceOutput") || kvp.Key.Contains("RegisterSourceOutput"))
.SelectMany(kvp => kvp.Value.SelectMany(step => step.Outputs))
.ToList();
// At minimum, check that we're not regenerating everything from scratch
var allOutputs = result2.Results[0].TrackedSteps.Values
.SelectMany(steps => steps.SelectMany(step => step.Outputs))
.ToList();
Assert.NotEmpty(allOutputs);
// With proper value equality, the final output should be unchanged
// (the source text should be identical even if intermediate steps ran)
Assert.Equal(
result1.Results[0].GeneratedSources[0].SourceText.ToString(),
result2.Results[0].GeneratedSources[0].SourceText.ToString());
}
[Fact]
public void Caching_WithUnrelatedFileChange_McpMethodCached()
{
// Adding an unrelated file should not cause MCP method extraction to re-run
const string McpSource = """
using ModelContextProtocol.Server;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>Test tool</summary>
[McpServerTool]
public static partial string TestMethod(string input) => input;
}
""";
const string UnrelatedSource1 = """
namespace Other;
public class Unrelated { public int Value { get; set; } }
""";
const string UnrelatedSource2 = """
namespace Other;
public class Unrelated { public int Value { get; set; } public string Name { get; set; } }
""";
var driver = CreateTrackedDriver();
// Run #1 with MCP file + unrelated file
driver = driver.RunGenerators(CreateCompilation(McpSource, UnrelatedSource1), TestContext.Current.CancellationToken);
var result1 = driver.GetRunResult();
Assert.Single(result1.Results);
var output1 = result1.Results[0].GeneratedSources[0].SourceText.ToString();
// Run #2 with MCP file + MODIFIED unrelated file
driver = driver.RunGenerators(CreateCompilation(McpSource, UnrelatedSource2), TestContext.Current.CancellationToken);
var result2 = driver.GetRunResult();
Assert.Single(result2.Results);
var output2 = result2.Results[0].GeneratedSources[0].SourceText.ToString();
// Output should be identical
Assert.Equal(output1, output2);
// Check that ForAttributeWithMetadataName steps for the MCP method are cached
var forAttributeSteps = result2.Results[0].TrackedSteps
.Where(kvp => kvp.Key.Contains("ForAttributeWithMetadataName"))
.SelectMany(kvp => kvp.Value.SelectMany(step => step.Outputs))
.ToList();
// The MCP method should be cached since it didn't change
Assert.Contains(forAttributeSteps, output =>
output.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged);
}
[Fact]
public void Caching_WithXmlDocChange_OutputRegenerated()
{
// Changing XML docs should cause regeneration
const string Source1 = """
using ModelContextProtocol.Server;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>Original description</summary>
[McpServerTool]
public static partial string TestMethod(string input) => input;
}
""";
const string Source2 = """
using ModelContextProtocol.Server;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>Modified description</summary>
[McpServerTool]
public static partial string TestMethod(string input) => input;
}
""";
var driver = CreateTrackedDriver();
// Run #1
driver = driver.RunGenerators(CreateCompilation(Source1), TestContext.Current.CancellationToken);
var result1 = driver.GetRunResult();
var output1 = result1.Results[0].GeneratedSources[0].SourceText.ToString();
Assert.Contains("Original description", output1);
// Run #2 with modified XML docs
driver = driver.RunGenerators(CreateCompilation(Source2), TestContext.Current.CancellationToken);
var result2 = driver.GetRunResult();
var output2 = result2.Results[0].GeneratedSources[0].SourceText.ToString();
Assert.Contains("Modified description", output2);
Assert.DoesNotContain("Original description", output2);
// Verify that there was actual regeneration (not just cached)
var allOutputs = result2.Results[0].TrackedSteps.Values
.SelectMany(steps => steps.SelectMany(step => step.Outputs))
.ToList();
Assert.Contains(allOutputs, output =>
output.Reason is IncrementalStepRunReason.Modified or IncrementalStepRunReason.New);
}
[Fact]
public void Caching_WithAddedMethod_ExistingMethodCached()
{
const string Source1 = """
using ModelContextProtocol.Server;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>First tool</summary>
[McpServerTool]
public static partial string FirstMethod(string input) => input;
}
""";
const string Source2 = """
using ModelContextProtocol.Server;
namespace Test;
[McpServerToolType]
public partial class TestTools
{
/// <summary>First tool</summary>
[McpServerTool]
public static partial string FirstMethod(string input) => input;
/// <summary>Second tool</summary>
[McpServerTool]
public static partial string SecondMethod(string input) => input;
}
""";
var driver = CreateTrackedDriver();
// Run #1
driver = driver.RunGenerators(CreateCompilation(Source1), TestContext.Current.CancellationToken);
var result1 = driver.GetRunResult();
Assert.Single(result1.Results[0].GeneratedSources);
// Run #2 with added method
driver = driver.RunGenerators(CreateCompilation(Source2), TestContext.Current.CancellationToken);
var result2 = driver.GetRunResult();
Assert.Single(result2.Results[0].GeneratedSources);
var output2 = result2.Results[0].GeneratedSources[0].SourceText.ToString();
Assert.Contains("First tool", output2);
Assert.Contains("Second tool", output2);
// The ForAttributeWithMetadataName step should have some cached outputs (the first method)
var forAttributeSteps = result2.Results[0].TrackedSteps
.Where(kvp => kvp.Key.Contains("ForAttributeWithMetadataName"))
.SelectMany(kvp => kvp.Value.SelectMany(step => step.Outputs))
.ToList();
// Should have both cached (first method) and new (second method) outputs
Assert.Contains(forAttributeSteps, output =>
output.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged);
Assert.Contains(forAttributeSteps, output =>
output.Reason is IncrementalStepRunReason.New or IncrementalStepRunReason.Modified);
}
[Fact]
public void Caching_MultipleMethodsAcrossFiles_IndependentCaching()
{
const string File1 = """
using ModelContextProtocol.Server;
namespace Test;
[McpServerToolType]
public partial class Tools1
{
/// <summary>Tool in file 1</summary>
[McpServerTool]
public static partial string Method1(string input) => input;
}
""";
const string File2Original = """
using ModelContextProtocol.Server;
namespace Test;
[McpServerToolType]
public partial class Tools2
{
/// <summary>Tool in file 2</summary>
[McpServerTool]
public static partial string Method2(string input) => input;
}
""";
const string File2Modified = """
using ModelContextProtocol.Server;
namespace Test;
[McpServerToolType]
public partial class Tools2
{
/// <summary>Modified tool in file 2</summary>
[McpServerTool]
public static partial string Method2(string input) => input;
}
""";
var driver = CreateTrackedDriver();
// Run #1
driver = driver.RunGenerators(CreateCompilation(File1, File2Original), TestContext.Current.CancellationToken);
var result1 = driver.GetRunResult();
Assert.Single(result1.Results[0].GeneratedSources);
// Run #2 - only File2 changed
driver = driver.RunGenerators(CreateCompilation(File1, File2Modified), TestContext.Current.CancellationToken);
var result2 = driver.GetRunResult();
Assert.Single(result2.Results[0].GeneratedSources);
var output2 = result2.Results[0].GeneratedSources[0].SourceText.ToString();
Assert.Contains("Tool in file 1", output2); // Unchanged
Assert.Contains("Modified tool in file 2", output2); // Changed
// Method1 extraction should be cached, Method2 should be modified
var forAttributeSteps = result2.Results[0].TrackedSteps
.Where(kvp => kvp.Key.Contains("ForAttributeWithMetadataName"))
.SelectMany(kvp => kvp.Value.SelectMany(step => step.Outputs))
.ToList();
Assert.Contains(forAttributeSteps, output =>
output.Reason is IncrementalStepRunReason.Cached or IncrementalStepRunReason.Unchanged);
Assert.Contains(forAttributeSteps, output =>
output.Reason is IncrementalStepRunReason.Modified or IncrementalStepRunReason.New);
}
/// <summary>
/// Creates a compilation with the specified source and standard references.
/// Each call creates a NEW compilation instance to ensure we're testing value equality, not reference equality.
/// </summary>
private static CSharpCompilation CreateCompilation(params string[] sources)
{
var syntaxTrees = sources.Select(s => CSharpSyntaxTree.ParseText(s)).ToArray();
var runtimePath = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
List<MetadataReference> referenceList =
[
MetadataReference.CreateFromFile(typeof(object).Assembly.Location),
MetadataReference.CreateFromFile(typeof(System.ComponentModel.DescriptionAttribute).Assembly.Location),
MetadataReference.CreateFromFile(Path.Combine(runtimePath, "System.Runtime.dll")),
MetadataReference.CreateFromFile(Path.Combine(runtimePath, "netstandard.dll")),
];
try
{
var coreAssemblyPath = Path.Combine(AppContext.BaseDirectory, "ModelContextProtocol.Core.dll");
if (File.Exists(coreAssemblyPath))
{
referenceList.Add(MetadataReference.CreateFromFile(coreAssemblyPath));
}
}
catch
{
// Ignore
}
return CSharpCompilation.Create(
"TestAssembly",
syntaxTrees,
referenceList,
new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
}
/// <summary>
/// Creates a generator driver with step tracking enabled.
/// </summary>
private static GeneratorDriver CreateTrackedDriver() =>
CSharpGeneratorDriver.Create(
generators: [new XmlToDescriptionGenerator().AsSourceGenerator()],
driverOptions: new GeneratorDriverOptions(
disabledOutputs: default,
trackIncrementalGeneratorSteps: true));
}