// ------------------------------------------------------------------------ // Copyright 2023 The Dapr Authors // Licensed under the Apache License, Version 2.0 (the "License"); // you may not use this file except in compliance with the License. // You may obtain a copy of the License at // http://www.apache.org/licenses/LICENSE-2.0 // Unless required by applicable law or agreed to in writing, software // distributed under the License is distributed on an "AS IS" BASIS, // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. // See the License for the specific language governing permissions and // limitations under the License. // ------------------------------------------------------------------------ namespace Dapr.Actors.Generators; using System.Text; using Microsoft.CodeAnalysis; using Microsoft.CodeAnalysis.Testing; using Microsoft.CodeAnalysis.Text; using VerifyCS = CSharpSourceGeneratorVerifier; public sealed class ActorClientGeneratorTests { private const string ActorMethodAttributeText = $@"// #nullable enable using System; namespace Dapr.Actors.Generators {{ [AttributeUsage(AttributeTargets.Method, AllowMultiple = false, Inherited = false)] internal sealed class ActorMethodAttribute : Attribute {{ public string? Name {{ get; set; }} }} }}"; private static readonly (string, SourceText) ActorMethodAttributeSource = ( Path.Combine("Dapr.Actors.Generators", "Dapr.Actors.Generators.ActorClientGenerator", "Dapr.Actors.Generators.ActorMethodAttribute.g.cs"), SourceText.From(ActorMethodAttributeText, Encoding.UTF8)); private const string GenerateActorClientAttributeText = $@"// #nullable enable using System; namespace Dapr.Actors.Generators {{ [AttributeUsage(AttributeTargets.Interface, AllowMultiple = false, Inherited = false)] internal sealed class GenerateActorClientAttribute : Attribute {{ public string? Name {{ get; set; }} public string? Namespace {{ get; set; }} }} }}"; private static readonly (string, SourceText) GenerateActorClientAttributeSource = ( Path.Combine("Dapr.Actors.Generators", "Dapr.Actors.Generators.ActorClientGenerator", "Dapr.Actors.Generators.GenerateActorClientAttribute.g.cs"), SourceText.From(GenerateActorClientAttributeText, Encoding.UTF8)); private static VerifyCS.Test CreateTest(string originalSource, string? generatedName = null, string? generatedSource = null) { var test = new VerifyCS.Test { TestState = { AdditionalReferences = { AdditionalMetadataReferences.Actors }, Sources = { originalSource }, GeneratedSources = { ActorMethodAttributeSource, GenerateActorClientAttributeSource }, } }; if (generatedName is not null && generatedSource is not null) { test.TestState.GeneratedSources.Add(( Path.Combine("Dapr.Actors.Generators", "Dapr.Actors.Generators.ActorClientGenerator", generatedName), SourceText.From(generatedSource, Encoding.UTF8))); } return test; } [Fact] public async Task TestMethodWithNoArgumentsOrReturnValue() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { [GenerateActorClient] public interface ITestActor { Task TestMethod(); } }"; var generatedSource = @"// #nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod() { return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestInternalInterface() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { [GenerateActorClient] internal interface ITestActor { Task TestMethod(); } }"; var generatedSource = @"// #nullable enable namespace Test { internal sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod() { return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestSingleGenericInternalInterface() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { [GenerateActorClient] internal interface ITestActor { Task TestMethod(); } }"; var generatedSource = @"// #nullable enable namespace Test { internal sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod() { return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestMultipleGenericsInternalInterface() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { [GenerateActorClient] internal interface ITestActor { Task TestMethod(); } }"; var generatedSource = @"// #nullable enable namespace Test { internal sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod() { return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestRenamedClient() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { [GenerateActorClient(Name = ""MyTestActorClient"")] internal interface ITestActor { Task TestMethod(); } }"; var generatedSource = @"// #nullable enable namespace Test { internal sealed class MyTestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod() { return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } }"; await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestSingleGenericRenamedClient() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { [GenerateActorClient(Name = ""MyTestActorClient"")] internal interface ITestActor { Task TestMethod(); } }"; var generatedSource = @"// #nullable enable namespace Test { internal sealed class MyTestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod() { return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } }"; await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestMultipleGenericsRenamedClient() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { [GenerateActorClient(Name = ""MyTestActorClient"")] internal interface ITestActor { Task TestMethod(); } }"; var generatedSource = @"// #nullable enable namespace Test { internal sealed class MyTestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public MyTestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod() { return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } }"; await CreateTest(originalSource, "Test.MyTestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestCustomNamespace() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { [GenerateActorClient(Namespace = ""MyTest"")] internal interface ITestActor { Task TestMethod(); } }"; var generatedSource = @"// #nullable enable namespace MyTest { internal sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod() { return this.actorProxy.InvokeMethodAsync(""TestMethod""); } } }"; await CreateTest(originalSource, "MyTest.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestRenamedMethod() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { [GenerateActorClient] public interface ITestActor { [ActorMethod(Name = ""MyTestMethod"")] Task TestMethod(); } } "; var generatedSource = @"// #nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod() { return this.actorProxy.InvokeMethodAsync(""MyTestMethod""); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestMethodWithArgumentsButNoReturnValue() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { public record TestValue(int Value); [GenerateActorClient] public interface ITestActor { Task TestMethod(TestValue value); } } "; var generatedSource = @"// #nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethod(Test.TestValue value) { return this.actorProxy.InvokeMethodAsync(""TestMethod"", value); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestMethodWithNoArgumentsButReturnValue() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { public record TestValue(int Value); [GenerateActorClient] public interface ITestActor { Task TestMethodAsync(); } }"; var generatedSource = @"// #nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethodAsync() { return this.actorProxy.InvokeMethodAsync(""TestMethodAsync""); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestMethodWithArgumentsAndReturnValue() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading.Tasks; namespace Test { public record TestRequestValue(int Value); public record TestReturnValue(int Value); [GenerateActorClient] public interface ITestActor { Task TestMethodAsync(TestRequestValue value); } }"; var generatedSource = @"// #nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethodAsync(Test.TestRequestValue value) { return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestMethodWithCancellationTokenArgument() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading; using System.Threading.Tasks; namespace Test { [GenerateActorClient] public interface ITestActor { Task TestMethodAsync(CancellationToken cancellationToken); } }"; var generatedSource = @"// #nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethodAsync(System.Threading.CancellationToken cancellationToken) { return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestMethodWithDefaultCancellationTokenArgument() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading; using System.Threading.Tasks; namespace Test { [GenerateActorClient] public interface ITestActor { Task TestMethodAsync(CancellationToken cancellationToken = default); } }"; var generatedSource = @"// #nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethodAsync(System.Threading.CancellationToken cancellationToken = default) { return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", cancellationToken); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestMethodWithValueAndCancellationTokenArguments() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading; using System.Threading.Tasks; namespace Test { public record TestValue(int Value); [GenerateActorClient] public interface ITestActor { Task TestMethodAsync(TestValue value, CancellationToken cancellationToken); } } "; var generatedSource = @"// #nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethodAsync(Test.TestValue value, System.Threading.CancellationToken cancellationToken) { return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestMethodWithValueAndDefaultCancellationTokenArguments() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading; using System.Threading.Tasks; namespace Test { public record TestValue(int Value); [GenerateActorClient] public interface ITestActor { Task TestMethodAsync(TestValue value, CancellationToken cancellationToken = default); } } "; var generatedSource = @"// #nullable enable namespace Test { public sealed class TestActorClient : Test.ITestActor { private readonly Dapr.Actors.Client.ActorProxy actorProxy; public TestActorClient(Dapr.Actors.Client.ActorProxy actorProxy) { if (actorProxy is null) { throw new System.ArgumentNullException(nameof(actorProxy)); } this.actorProxy = actorProxy; } public System.Threading.Tasks.Task TestMethodAsync(Test.TestValue value, System.Threading.CancellationToken cancellationToken = default) { return this.actorProxy.InvokeMethodAsync(""TestMethodAsync"", value, cancellationToken); } } }"; await CreateTest(originalSource, "Test.TestActorClient.g.cs", generatedSource).RunAsync(); } [Fact] public async Task TestMethodWithReversedArguments() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading; using System.Threading.Tasks; namespace Test { public record TestValue(int Value); [GenerateActorClient] public interface ITestActor { Task TestMethodAsync(CancellationToken cancellationToken, int value); } }"; var test = CreateTest(originalSource); test.TestState.ExpectedDiagnostics.Add( new DiagnosticResult("DAPR0001", DiagnosticSeverity.Error) .WithSpan(13, 48, 13, 65) .WithMessage("Cancellation tokens must be the last argument")); await test.RunAsync(); } [Fact] public async Task TestMethodWithTooManyArguments() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading; using System.Threading.Tasks; namespace Test { public record TestValue(int Value); [GenerateActorClient] public interface ITestActor { Task TestMethodAsync(int value1, int value2); } }"; var test = CreateTest(originalSource); test.TestState.ExpectedDiagnostics.Add( new DiagnosticResult("DAPR0002", DiagnosticSeverity.Error) .WithSpan(13, 14, 13, 29) .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported")); await test.RunAsync(); } [Fact] public async Task TestMethodWithFarTooManyArguments() { var originalSource = @" using Dapr.Actors.Generators; using System.Threading; using System.Threading.Tasks; namespace Test { public record TestValue(int Value); [GenerateActorClient] public interface ITestActor { Task TestMethodAsync(int value1, int value2, CancellationToken cancellationToken); } }"; var test = CreateTest(originalSource); test.TestState.ExpectedDiagnostics.Add( new DiagnosticResult("DAPR0002", DiagnosticSeverity.Error) .WithSpan(13, 14, 13, 29) .WithMessage("Only methods with a single argument or a single argument followed by a cancellation token are supported")); await test.RunAsync(); } }