chore: import upstream snapshot with attribution
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
CodeQL / Analyze (csharp) (push) Waiting to run
CodeQL / Analyze (python) (push) Waiting to run
dotnet-build-and-test / dotnet-test-functions (push) Has been cancelled
dotnet-build-and-test / paths-filter (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Debug, windows-latest, net9.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, ubuntu-latest, net8.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-build (Release, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, ubuntu-latest, net10.0) (push) Has been cancelled
dotnet-build-and-test / dotnet-test (Release, integration, true, windows-latest, net472) (push) Has been cancelled
dotnet-build-and-test / dotnet-foundry-hosted-it (push) Has been cancelled
dotnet-build-and-test / dotnet-build-and-test-check (push) Has been cancelled
dotnet-build-and-test / Integration Test Report (push) Has been cancelled
This commit is contained in:
+1162
File diff suppressed because it is too large
Load Diff
+145
@@ -0,0 +1,145 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using System.Collections.Immutable;
|
||||
using System.IO;
|
||||
using System.Linq;
|
||||
using System.Reflection;
|
||||
using System.Threading;
|
||||
using System.Threading.Tasks;
|
||||
using FluentAssertions;
|
||||
using Microsoft.CodeAnalysis;
|
||||
using Microsoft.CodeAnalysis.CSharp;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Generators.UnitTests;
|
||||
|
||||
/// <summary>
|
||||
/// Helper class for testing the ExecutorRouteGenerator.
|
||||
/// </summary>
|
||||
public static class GeneratorTestHelper
|
||||
{
|
||||
/// <summary>
|
||||
/// Runs the ExecutorRouteGenerator on the provided source code and returns the result.
|
||||
/// </summary>
|
||||
public static GeneratorRunResult RunGenerator(string source) => RunGenerator([source]);
|
||||
|
||||
/// <summary>
|
||||
/// Runs the ExecutorRouteGenerator on multiple source files and returns the result.
|
||||
/// Use this to test scenarios with partial classes split across files.
|
||||
/// </summary>
|
||||
public static GeneratorRunResult RunGenerator(params string[] sources)
|
||||
{
|
||||
var syntaxTrees = sources.Select(s => CSharpSyntaxTree.ParseText(s)).ToArray();
|
||||
|
||||
var references = GetMetadataReferences();
|
||||
|
||||
var compilation = CSharpCompilation.Create(
|
||||
assemblyName: "TestAssembly",
|
||||
syntaxTrees: syntaxTrees,
|
||||
references: references,
|
||||
options: new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary));
|
||||
|
||||
var generator = new ExecutorRouteGenerator();
|
||||
|
||||
GeneratorDriver driver = CSharpGeneratorDriver.Create(generator);
|
||||
driver = driver.RunGeneratorsAndUpdateCompilation(compilation, out var outputCompilation, out var diagnostics);
|
||||
|
||||
var runResult = driver.GetRunResult();
|
||||
|
||||
return new GeneratorRunResult(
|
||||
runResult,
|
||||
outputCompilation,
|
||||
diagnostics);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the generator and asserts that it produces exactly one generated file with the expected content.
|
||||
/// </summary>
|
||||
public static void AssertGeneratesSource(string source, string expectedGeneratedSource)
|
||||
{
|
||||
var result = RunGenerator(source);
|
||||
|
||||
result.RunResult.GeneratedTrees.Should().HaveCount(1, "expected exactly one generated file");
|
||||
|
||||
var generatedSource = result.RunResult.GeneratedTrees[0].ToString();
|
||||
generatedSource.Should().Contain(expectedGeneratedSource);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the generator and asserts that no source is generated.
|
||||
/// </summary>
|
||||
public static void AssertGeneratesNoSource(string source)
|
||||
{
|
||||
var result = RunGenerator(source);
|
||||
result.RunResult.GeneratedTrees.Should().BeEmpty("expected no generated files");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the generator and asserts that a specific diagnostic is produced.
|
||||
/// </summary>
|
||||
public static void AssertProducesDiagnostic(string source, string diagnosticId)
|
||||
{
|
||||
var result = RunGenerator(source);
|
||||
|
||||
var generatorDiagnostics = result.RunResult.Diagnostics;
|
||||
generatorDiagnostics.Should().Contain(d => d.Id == diagnosticId,
|
||||
$"expected diagnostic {diagnosticId} to be produced");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Runs the generator and asserts that compilation succeeds with no errors.
|
||||
/// </summary>
|
||||
public static void AssertCompilationSucceeds(string source)
|
||||
{
|
||||
var result = RunGenerator(source);
|
||||
|
||||
var errors = result.OutputCompilation.GetDiagnostics()
|
||||
.Where(d => d.Severity == DiagnosticSeverity.Error)
|
||||
.ToList();
|
||||
|
||||
errors.Should().BeEmpty("compilation should succeed without errors");
|
||||
}
|
||||
|
||||
private static ImmutableArray<MetadataReference> GetMetadataReferences()
|
||||
{
|
||||
var assemblies = new[]
|
||||
{
|
||||
typeof(object).Assembly, // System.Runtime
|
||||
typeof(Attribute).Assembly, // System.Runtime
|
||||
typeof(ValueTask).Assembly, // System.Threading.Tasks.Extensions
|
||||
typeof(CancellationToken).Assembly, // System.Threading
|
||||
typeof(ISet<>).Assembly, // System.Collections
|
||||
typeof(Executor).Assembly, // Microsoft.Agents.AI.Workflows
|
||||
};
|
||||
|
||||
var references = new List<MetadataReference>();
|
||||
|
||||
foreach (var assembly in assemblies)
|
||||
{
|
||||
references.Add(MetadataReference.CreateFromFile(assembly.Location));
|
||||
}
|
||||
|
||||
// Add netstandard reference
|
||||
var netstandardAssembly = Assembly.Load("netstandard, Version=2.0.0.0");
|
||||
references.Add(MetadataReference.CreateFromFile(netstandardAssembly.Location));
|
||||
|
||||
// Add System.Runtime reference for core types
|
||||
var runtimeAssemblyPath = Path.GetDirectoryName(typeof(object).Assembly.Location)!;
|
||||
var systemRuntimePath = Path.Combine(runtimeAssemblyPath, "System.Runtime.dll");
|
||||
if (File.Exists(systemRuntimePath))
|
||||
{
|
||||
references.Add(MetadataReference.CreateFromFile(systemRuntimePath));
|
||||
}
|
||||
|
||||
return [.. references.Distinct()];
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Contains the results of running the generator.
|
||||
/// </summary>
|
||||
public record GeneratorRunResult(
|
||||
GeneratorDriverRunResult RunResult,
|
||||
Compilation OutputCompilation,
|
||||
ImmutableArray<Diagnostic> Diagnostics);
|
||||
+23
@@ -0,0 +1,23 @@
|
||||
<Project Sdk="Microsoft.NET.Sdk">
|
||||
|
||||
<PropertyGroup>
|
||||
<!-- Generator tests need to reference Roslyn which requires newer TFMs -->
|
||||
<TargetFrameworks>net10.0</TargetFrameworks>
|
||||
<!-- Suppress "mark as const" warnings for test source strings -->
|
||||
<NoWarn>$(NoWarn);RCS1118</NoWarn>
|
||||
</PropertyGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Workflows\Microsoft.Agents.AI.Workflows.csproj" />
|
||||
<!-- Generator builds as netstandard2.0, reference it with special handling -->
|
||||
<ProjectReference Include="..\..\src\Microsoft.Agents.AI.Workflows.Generators\Microsoft.Agents.AI.Workflows.Generators.csproj"
|
||||
PrivateAssets="all"
|
||||
GlobalPropertiesToRemove="TargetFramework" />
|
||||
</ItemGroup>
|
||||
|
||||
<ItemGroup>
|
||||
<PackageReference Include="FluentAssertions" />
|
||||
<PackageReference Include="Microsoft.CodeAnalysis.CSharp" />
|
||||
</ItemGroup>
|
||||
|
||||
</Project>
|
||||
+220
@@ -0,0 +1,220 @@
|
||||
// Copyright (c) Microsoft. All rights reserved.
|
||||
|
||||
using System;
|
||||
using FluentAssertions;
|
||||
using FluentAssertions.Execution;
|
||||
using FluentAssertions.Primitives;
|
||||
using Microsoft.CodeAnalysis;
|
||||
|
||||
namespace Microsoft.Agents.AI.Workflows.Generators.UnitTests;
|
||||
|
||||
internal sealed class SyntaxTreeAssertions : ObjectAssertions<SyntaxTree, SyntaxTreeAssertions>
|
||||
{
|
||||
private readonly string _syntaxString;
|
||||
|
||||
public SyntaxTreeAssertions(SyntaxTree instance, AssertionChain assertionChain) : base(instance, assertionChain)
|
||||
{
|
||||
this._syntaxString = instance.ToString();
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> AddHandler(string handlerName)
|
||||
{
|
||||
string expectedRegistration = $".AddHandler({handlerName})";
|
||||
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(this._syntaxString.Contains(expectedRegistration))
|
||||
.BecauseOf($"expected handler {handlerName} to be registered")
|
||||
.FailWith("Expected {context} to contain handler registration {0}{reason}, but it was not found. Actual syntax: {1}",
|
||||
expectedRegistration, this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> AddHandler(string handlerName, string inTypeParam)
|
||||
{
|
||||
string expectedRegistration = $".AddHandler<{inTypeParam}>({handlerName})";
|
||||
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(this._syntaxString.Contains(expectedRegistration))
|
||||
.BecauseOf($"expected handler {handlerName} to be registered")
|
||||
.FailWith("Expected {context} to contain handler registration {0}{reason}, but it was not found. Actual syntax: {1}",
|
||||
expectedRegistration, this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> AddHandler(string handlerName, string inTypeParam, string outTypeParam)
|
||||
{
|
||||
string expectedRegistration = $".AddHandler<{inTypeParam},{outTypeParam}>({handlerName})";
|
||||
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(this._syntaxString.Contains(expectedRegistration))
|
||||
.BecauseOf($"expected handler {handlerName} to be registered")
|
||||
.FailWith("Expected {context} to contain handler registration {0}{reason}, but it was not found. Actual syntax: {1}",
|
||||
expectedRegistration, this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> AddHandler<TIn>(string handlerName, bool globalQualified = false)
|
||||
{
|
||||
Type inType = typeof(TIn);
|
||||
string inTypeParam = globalQualified ? $"global::{inType.FullName}" : inType.Name;
|
||||
return this.AddHandler(handlerName, inTypeParam);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> AddHandler<TIn, TOut>(string handlerName, bool globalQualified = false)
|
||||
{
|
||||
Type inType = typeof(TIn), outType = typeof(TOut);
|
||||
string inTypeParam = globalQualified ? $"global::{inType.FullName}" : inType.Name;
|
||||
string outTypeParam = globalQualified ? $"global::{outType.FullName}" : outType.Name;
|
||||
return this.AddHandler(handlerName, inTypeParam, outTypeParam);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> HaveNoHandlers()
|
||||
{
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(!this._syntaxString.Contains(".AddHandler("))
|
||||
.BecauseOf("expected no handlers to be registered")
|
||||
.FailWith("Expected {context} to have no handler registrations{reason}, but found at least one. Actual syntax: {1}",
|
||||
this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> RegisterSentMessageType(string messageTypeParam)
|
||||
{
|
||||
string expectedRegistration = $".SendsMessage<{messageTypeParam}>()";
|
||||
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(this._syntaxString.Contains(expectedRegistration))
|
||||
.BecauseOf($"expected message type {messageTypeParam} to be registered")
|
||||
.FailWith("Expected {context} to contain message type registration {0}{reason}, but it was not found. Actual syntax: {1}",
|
||||
expectedRegistration, this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> RegisterSentMessageType<TMessage>(bool globalQualified = true)
|
||||
{
|
||||
Type messageType = typeof(TMessage);
|
||||
string messageTypeParam = globalQualified ? $"global::{messageType.FullName}" : messageType.Name;
|
||||
return this.RegisterSentMessageType(messageTypeParam);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> NotRegisterSentMessageTypes()
|
||||
{
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(!this._syntaxString.Contains(".SendsMessage<"))
|
||||
.BecauseOf("expected no message types to be registered")
|
||||
.FailWith("Expected {context} to have no message type registrations{reason}, but found at least one. Actual syntax: {1}",
|
||||
this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> RegisterYieldedOutputType(string outputTypeParam)
|
||||
{
|
||||
string expectedRegistration = $".YieldsOutput<{outputTypeParam}>()";
|
||||
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(this._syntaxString.Contains(expectedRegistration))
|
||||
.BecauseOf($"expected output type {outputTypeParam} to be registered")
|
||||
.FailWith("Expected {context} to contain output type registration {0}{reason}, but it was not found. Actual syntax: {1}",
|
||||
expectedRegistration, this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> RegisterYieldedOutputType<TOutput>(bool globalQualified = true)
|
||||
{
|
||||
Type outputType = typeof(TOutput);
|
||||
string outputTypeParam = globalQualified ? $"global::{outputType.FullName}" : outputType.Name;
|
||||
return this.RegisterYieldedOutputType(outputTypeParam);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> NotRegisterYieldedOutputTypes()
|
||||
{
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(!this._syntaxString.Contains(".YieldsOutput<"))
|
||||
.BecauseOf("expected no output types to be registered")
|
||||
.FailWith("Expected {context} to have no output type registrations{reason}, but found at least one. Actual syntax: {1}",
|
||||
this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
private AndConstraint<SyntaxTreeAssertions> ContainPartialDeclaration(int level, int index, string className)
|
||||
{
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(index > 0)
|
||||
.BecauseOf($"expected \"partial class {className}\" at nesting level {level}")
|
||||
.FailWith("Expected {context} to contain \"partial class {0}\" at nesting level {1}{reason}, but it was not found. Actual syntax: {2}",
|
||||
className, level, this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
private AndConstraint<SyntaxTreeAssertions> DeclarePartialsInCorrectOrder(int prevIndex, int currIndex, string prevClass, string currClass)
|
||||
{
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(prevIndex < currIndex)
|
||||
.BecauseOf($"expected \"partial class {prevClass}\" before \"partial class {currClass}\"")
|
||||
.FailWith("Expected {context} to have \"partial class {0}\" before \"partial class {1}\"{reason}, but the order was incorrect. Actual syntax: {2}",
|
||||
prevClass, currClass, this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> HaveHierarchy(params string[] expectedNesting)
|
||||
{
|
||||
if (expectedNesting.Length == 0)
|
||||
{
|
||||
return new AndConstraint<SyntaxTreeAssertions>(this);
|
||||
}
|
||||
|
||||
int[] indicies = new int[expectedNesting.Length];
|
||||
|
||||
for (int i = 0; i < expectedNesting.Length; i++)
|
||||
{
|
||||
indicies[i] = this._syntaxString.IndexOf($"partial class {expectedNesting[i]}", StringComparison.Ordinal);
|
||||
}
|
||||
|
||||
// Verify partial declarations are present
|
||||
AndConstraint<SyntaxTreeAssertions> runningResult = this.ContainPartialDeclaration(0, indicies[0], expectedNesting[0]);
|
||||
for (int i = 1; i < expectedNesting.Length; i++)
|
||||
{
|
||||
runningResult = runningResult.And.ContainPartialDeclaration(i, indicies[i], expectedNesting[i])
|
||||
.And.DeclarePartialsInCorrectOrder(indicies[i - 1], indicies[i], expectedNesting[i - 1], expectedNesting[i]);
|
||||
}
|
||||
|
||||
return runningResult;
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> HaveNamespace()
|
||||
{
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(this._syntaxString.Contains("namespace "))
|
||||
.BecauseOf("expected namespace declaration")
|
||||
.FailWith("Expected {context} to contain a namespace declaration{reason}, but it was found. Actual syntax: {0}",
|
||||
this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
|
||||
public AndConstraint<SyntaxTreeAssertions> NotHaveNamespace()
|
||||
{
|
||||
this.CurrentAssertionChain
|
||||
.ForCondition(!this._syntaxString.Contains("namespace "))
|
||||
.BecauseOf("expected no namespace declaration")
|
||||
.FailWith("Expected {context} to not contain a namespace declaration{reason}, but it was found. Actual syntax: {0}",
|
||||
this._syntaxString);
|
||||
|
||||
return new(this);
|
||||
}
|
||||
}
|
||||
|
||||
internal static class SyntaxTreeFluentExtensions
|
||||
{
|
||||
public static SyntaxTreeAssertions Should(this SyntaxTree syntaxTree) => new(syntaxTree, AssertionChain.GetOrCreate());
|
||||
}
|
||||
Reference in New Issue
Block a user