using CSharp.SourceGenerators.Extensions; using CSharp.SourceGenerators.Extensions.Models; using FluentAssertions; using Microsoft.CodeAnalysis.CSharp.Syntax; using ProxyInterfaceSourceGeneratorTests.Source.Disposable; using Speckle.ProxyGenerator; namespace ProxyInterfaceSourceGeneratorTests; [Flags] public enum ImplementationOptions { None = 0, ProxyBaseClasses = 1, ProxyInterfaces = 2, UseExtendedInterfaces = 4, ProxyForBaseInterface = 8 } public class InheritedInterfaceTests { private const string Namespace = "ProxyInterfaceSourceGeneratorTests.Source.Disposable"; private const string OutputPath = "../../../Destination/Disposable/"; private readonly ProxyInterfaceCodeGenerator _sut; public InheritedInterfaceTests() { if (!Directory.Exists(OutputPath)) { Directory.CreateDirectory(OutputPath); } _sut = new ProxyInterfaceCodeGenerator(); } [Theory] [InlineData(ImplementationOptions.None, false)] [InlineData( ImplementationOptions.ProxyBaseClasses | ImplementationOptions.ProxyInterfaces, true )] public void GenerateFiles_InheritedInterface_InheritFromBaseClass( ImplementationOptions options, bool inheritBaseInterface ) { var name = "Child"; var interfaceName = "I" + name; var proxyName = name + "Proxy"; // Arrange string[] fileNames = [$"{Namespace}.{interfaceName}.g.cs", $"{Namespace}.{proxyName}.g.cs"]; var path = $"./Source/Disposable/{interfaceName}.cs"; SourceFile sourceFile = CreateSourceFile(path, name, options); // Act var result = _sut.Execute([sourceFile]); result.Valid.Should().BeTrue(); result.Files.Should().HaveCount(fileNames.Length + 1); WriteFiles(fileNames, result); var interfaceIndex = 0; var tree = result.Files[interfaceIndex].SyntaxTree; var root = tree.GetRoot(); var interfaceDeclarations = root.DescendantNodes().OfType(); // Assert Assert.Single(interfaceDeclarations); var baseList = interfaceDeclarations.First().BaseList; bool didWeInherit = baseList is not null; Assert.Equal(didWeInherit, inheritBaseInterface); } [Theory] [InlineData("Parent")] [InlineData("Child")] public void GenerateFiles_InheritedInterface_Should_InheritTheInterface(string name) { var interfaceName = "I" + name; var proxyName = name + "Proxy"; // Arrange string[] fileNames = [$"{Namespace}.{interfaceName}.g.cs", $"{Namespace}.{proxyName}.g.cs"]; var path = $"./Source/Disposable/{interfaceName}.cs"; SourceFile sourceFile = CreateSourceFile( path, name, ImplementationOptions.ProxyInterfaces | ImplementationOptions.ProxyBaseClasses | ImplementationOptions.UseExtendedInterfaces ); // Act var result = _sut.Execute([sourceFile]); result.Valid.Should().BeTrue(); result.Files.Should().HaveCount(fileNames.Length + 1); WriteFiles(fileNames, result); var interfaceIndex = 0; var tree = result.Files[interfaceIndex].SyntaxTree; var root = tree.GetRoot(); var interfaceDeclarations = root.DescendantNodes().OfType(); // Assert Assert.Single(interfaceDeclarations); var baseList = interfaceDeclarations.First().BaseList; Assert.Equal(2, baseList?.Types.Count); var type1 = (QualifiedNameSyntax)baseList!.Types[0].Type; var type2 = (QualifiedNameSyntax)baseList.Types[1].Type; Assert.Equal(nameof(IDisposable), type1.Right.Identifier.Text); Assert.Equal(nameof(IUpdate), type2.Right.Identifier.Text); } [Fact] public void GenerateFiles_InheritedInterface_Should_InheritTheInterfaceAndNotNew() { var className1 = "LocationPoint"; var interfaceName1 = "IRevitLocationPointProxy"; var proxyName1 = $"{className1}Proxy"; // Arrange var path1 = $"./Source/Disposable/{interfaceName1}.cs"; var sourceFile1 = CreateSourceFile( path1, className1, ImplementationOptions.ProxyForBaseInterface | ImplementationOptions.UseExtendedInterfaces ); var className2 = "Location"; var interfaceName2 = "IRevitLocationProxy"; var proxyName2 = $"{className2}Proxy"; // Arrange var path2 = $"./Source/Disposable/{interfaceName2}.cs"; var sourceFile2 = CreateSourceFile( path2, className2, ImplementationOptions.ProxyForBaseInterface | ImplementationOptions.UseExtendedInterfaces ); // Act var result = _sut.Execute([sourceFile1, sourceFile2]); string[] fileNames = [ $"{Namespace}.{interfaceName1}.g.cs", $"{Namespace}.{proxyName1}.g.cs", $"{Namespace}.{interfaceName2}.g.cs", $"{Namespace}.{proxyName2}.g.cs" ]; result.Valid.Should().BeTrue(); result.Files.Should().HaveCount(fileNames.Length + 1); foreach (var fileName in fileNames.Select((fileName, index) => new { fileName, index })) { var builder = result.Files[fileName.index + 1]; // +1 means skip the attribute File.WriteAllText($"{OutputPath}{fileName.fileName}", builder.Text); } } [Fact] public void GenerateFiles_InheritedInterface_Should_Not_InheritExplicitImplementedInterfaces() { var name = "Explicit"; var interfaceName = "I" + name; var proxyName = name + "Proxy"; // Arrange string[] fileNames = [$"{Namespace}.{interfaceName}.g.cs", $"{Namespace}.{proxyName}.g.cs"]; var path = $"./Source/Disposable/{interfaceName}.cs"; SourceFile sourceFile = CreateSourceFile( path, name, ImplementationOptions.UseExtendedInterfaces ); // Act var result = _sut.Execute([sourceFile]); result.Valid.Should().BeTrue(); result.Files.Should().HaveCount(fileNames.Length + 1); WriteFiles(fileNames, result); var interfaceIndex = 0; var tree = result.Files[interfaceIndex].SyntaxTree; var root = tree.GetRoot(); var interfaceDeclarations = root.DescendantNodes().OfType(); // Assert //This actually could work, we just need to implenent the logic inside the Proxy (and interface). //⚠ Dispose is not a public member of the 'Explicit' class and also not of the Proxy. //e.g. new Explicit().Dipose() is not possible. Assert.Single(interfaceDeclarations); var baseList = interfaceDeclarations.First().BaseList; bool noInterfaceImplementationFound = baseList is null; Assert.True(noInterfaceImplementationFound); } private static SourceFile CreateSourceFile( string path, string name, ImplementationOptions options ) { var o = string.Empty; foreach (var val in Enum.GetValues()) { if (val == ImplementationOptions.None || !options.HasFlag(val)) { continue; } if (o.Length > 0) { o += " | "; } o += "ImplementationOptions." + val; } if (o.Length == 0) { o = "ImplementationOptions.None"; } return new SourceFile { Path = path, Text = File.ReadAllText(path), AttributeToAddToInterface = new ExtraAttribute { Name = "Speckle.ProxyGenerator.Proxy", ArgumentList = $"typeof({Namespace}.{name}), {o}" } }; } private static void WriteFiles(string[] fileNames, ExecuteResult result) { foreach (var fileName in fileNames.Select((fileName, index) => new { fileName, index })) { var builder = result.Files[fileName.index]; // attribute is always last builder.Path.Should().EndWith(fileName.fileName); File.WriteAllText($"{OutputPath}{fileName.fileName}", builder.Text); builder.Text.Should().Be(File.ReadAllText($"{OutputPath}{fileName.fileName}")); } } }