From 58a25eae7ca8397b95e41611e9508dc20870fb2b Mon Sep 17 00:00:00 2001 From: Frank Bakker Date: Tue, 13 Dec 2022 22:07:35 +0100 Subject: [PATCH 1/2] Fix #728 where extension methods were generated for typed entity classes that do not exist because the user has no entities for them --- .../ExtensionMethodsGenerator.cs | 52 +++---- .../Helpers/SyntaxFactoryHelper.cs | 5 - .../Model/HassService.cs | 2 +- .../Model/HassServiceDomain.cs | 4 +- .../CodeGenerator/CodeGenTestHelper.cs | 61 ++++++++ .../CodeGenerator/CodeGeneratorTest.cs | 135 ++-------------- .../CodeGenerator/ServicesGeneratorTest.cs | 144 ++++++++++++++++++ 7 files changed, 243 insertions(+), 160 deletions(-) create mode 100644 src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/CodeGenTestHelper.cs create mode 100644 src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/ServicesGeneratorTest.cs diff --git a/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ExtensionMethodsGenerator.cs b/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ExtensionMethodsGenerator.cs index 5b4677f07..e6b356579 100644 --- a/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ExtensionMethodsGenerator.cs +++ b/src/HassModel/NetDaemon.HassModel.CodeGenerator/CodeGeneration/ExtensionMethodsGenerator.cs @@ -1,4 +1,4 @@ -using NetDaemon.Client.HomeAssistant.Model; +using Microsoft.CodeAnalysis.CSharp; namespace NetDaemon.HassModel.CodeGenerator; @@ -25,46 +25,38 @@ internal static class ExtensionMethodsGenerator { public static IEnumerable Generate(IEnumerable serviceDomains, IReadOnlyCollection entityDomains) { - var entityDomainNames = entityDomains.Select(d => d.Domain).ToHashSet(); - - // we only want to generate these classes for entities that + var entityClassNameByDomain = entityDomains.ToLookup(e => e.Domain, e => e.EntityClassName); + return serviceDomains - .Where(sd => - sd.Services?.Any(s => s.Target?.Entity?.Domain != null && entityDomainNames.Contains(s.Target.Entity.Domain)) == true) - .GroupBy(x => x.Domain, x => x.Services) - .Select(GenerateClass); + .Select(sd => GenerateDomainExtensionClass(sd, entityClassNameByDomain)) + .OfType(); // filter out nulls } - private static ClassDeclarationSyntax GenerateClass(IGrouping?> domainServicesGroup) + private static ClassDeclarationSyntax? GenerateDomainExtensionClass(HassServiceDomain serviceDomain, ILookup entityClassNameByDomain) { - var domain = domainServicesGroup.Key!; - - var domainServices = domainServicesGroup - .SelectMany(services => services!) - .Where(s => s.Target?.Entity?.Domain != null) - .Select(group => @group) + var serviceMethodDeclarations = serviceDomain.Services .OrderBy(x => x.Service) - .ToList(); - - return GenerateDomainExtensionClass(domain, domainServices); - } - - private static ClassDeclarationSyntax GenerateDomainExtensionClass(string domain, IEnumerable services) - { - var serviceTypeDeclaration = Class(GetEntityDomainExtensionMethodClassName(domain)).ToPublic().ToStatic(); - - var serviceMethodDeclarations = services - .SelectMany(service => GenerateExtensionMethod(domain, service)) + .SelectMany(service => GenerateExtensionMethods(serviceDomain.Domain, service, entityClassNameByDomain)) .ToArray(); - return serviceTypeDeclaration.AddMembers(serviceMethodDeclarations); + if (!serviceMethodDeclarations.Any()) return null; + + return SyntaxFactory.ClassDeclaration(GetEntityDomainExtensionMethodClassName(serviceDomain.Domain)) + .AddMembers(serviceMethodDeclarations) + .ToPublic() + .ToStatic(); } - private static IEnumerable GenerateExtensionMethod(string domain, HassService service) + private static IEnumerable GenerateExtensionMethods(string domain, HassService service, ILookup entityClassNameByDomain) { - var serviceName = service.Service!; + var targetEntityDomain = service.Target?.Entity?.Domain; + if (targetEntityDomain == null) yield break; + + var entityTypeName = entityClassNameByDomain[targetEntityDomain].FirstOrDefault(); + if (entityTypeName == null) yield break; + + var serviceName = service.Service; var serviceArguments = ServiceArguments.Create(domain, service); - var entityTypeName = GetDomainEntityTypeName(service.Target?.Entity?.Domain!); var enumerableTargetTypeName = $"IEnumerable<{entityTypeName}>"; if (serviceArguments is null) diff --git a/src/HassModel/NetDaemon.HassModel.CodeGenerator/Helpers/SyntaxFactoryHelper.cs b/src/HassModel/NetDaemon.HassModel.CodeGenerator/Helpers/SyntaxFactoryHelper.cs index a4c515662..2fa020a16 100644 --- a/src/HassModel/NetDaemon.HassModel.CodeGenerator/Helpers/SyntaxFactoryHelper.cs +++ b/src/HassModel/NetDaemon.HassModel.CodeGenerator/Helpers/SyntaxFactoryHelper.cs @@ -68,11 +68,6 @@ public static ClassDeclarationSyntax ClassWithInjected(string classNa return ParseClass(classCode); } - public static ClassDeclarationSyntax Class(string name) - { - return ClassDeclaration(name); - } - public static TypeDeclarationSyntax Interface(string name) { return InterfaceDeclaration(name); diff --git a/src/HassModel/NetDaemon.HassModel.CodeGenerator/Model/HassService.cs b/src/HassModel/NetDaemon.HassModel.CodeGenerator/Model/HassService.cs index 76307d306..dad83e97b 100644 --- a/src/HassModel/NetDaemon.HassModel.CodeGenerator/Model/HassService.cs +++ b/src/HassModel/NetDaemon.HassModel.CodeGenerator/Model/HassService.cs @@ -2,7 +2,7 @@ internal record HassService { - public string? Service { get; init; } + public required string Service { get; init; } public string? Description { get; init; } public IReadOnlyCollection? Fields { get; init; } public TargetSelector? Target { get; init; } diff --git a/src/HassModel/NetDaemon.HassModel.CodeGenerator/Model/HassServiceDomain.cs b/src/HassModel/NetDaemon.HassModel.CodeGenerator/Model/HassServiceDomain.cs index 6b110824f..5b14fa7b6 100644 --- a/src/HassModel/NetDaemon.HassModel.CodeGenerator/Model/HassServiceDomain.cs +++ b/src/HassModel/NetDaemon.HassModel.CodeGenerator/Model/HassServiceDomain.cs @@ -2,6 +2,6 @@ internal record HassServiceDomain { - public string? Domain { get; init; } - public IReadOnlyCollection? Services { get; init; } + public required string Domain { get; init; } + public required IReadOnlyCollection Services { get; init; } } \ No newline at end of file diff --git a/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/CodeGenTestHelper.cs b/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/CodeGenTestHelper.cs new file mode 100644 index 000000000..f420bd396 --- /dev/null +++ b/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/CodeGenTestHelper.cs @@ -0,0 +1,61 @@ +using System.Collections.Generic; +using System.IO; +using System.Text; +using Microsoft.CodeAnalysis; +using Microsoft.CodeAnalysis.CSharp; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NetDaemon.Client.HomeAssistant.Model; +using NetDaemon.HassModel.CodeGenerator; +using NetDaemon.HassModel.CodeGenerator.Model; + +namespace NetDaemon.HassModel.Tests.CodeGenerator; + +internal class CodeGenTestHelper +{ + public static CompilationUnitSyntax GenerateCompilationUnit( + CodeGenerationSettings codeGenerationSettings, + IReadOnlyCollection states, + IReadOnlyCollection services) + { + var metaData = EntityMetaDataGenerator.GetEntityDomainMetaData(states); + metaData = EntityMetaDataMerger.Merge(codeGenerationSettings, new EntitiesMetaData(), metaData); + var generatedTypes = Generator.GenerateTypes(metaData.Domains, services).ToArray(); + return Generator.BuildCompilationUnit(codeGenerationSettings.Namespace, generatedTypes); + + } + + public static void AssertCodeCompiles(string generated, string appCode) + { + var syntaxtrees = new [] + { + SyntaxFactory.ParseSyntaxTree(generated, path: "generated.cs"), + SyntaxFactory.ParseSyntaxTree(appCode, path: "appcode.cs") + }; + + var compilation = CSharpCompilation.Create("tempAssembly", + syntaxtrees, + AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => MetadataReference.CreateFromFile(a.Location)).ToArray(), + new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable) + ); + + var emitResult = compilation.Emit(Stream.Null); + + var warningsOrErrors = emitResult.Diagnostics + .Where(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning).ToList(); + + if (!warningsOrErrors.Any()) return; + + var msg = new StringBuilder("Compile of generated code failed.\r\n"); + foreach (var diagnostic in warningsOrErrors) + { + msg.AppendLine(diagnostic.ToString()); + } + + msg.AppendLine(); + msg.AppendLine("generated.cs"); + // output the generated code including line numbers to help debugging + msg.AppendLine(string.Join(Environment.NewLine, generated.Split('\n').Select((l, i) => $"{i+1,4}: {l}"))); + + Assert.Fail(msg.ToString()); + } +} \ No newline at end of file diff --git a/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/CodeGeneratorTest.cs b/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/CodeGeneratorTest.cs index 86c5e65d0..610e53219 100644 --- a/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/CodeGeneratorTest.cs +++ b/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/CodeGeneratorTest.cs @@ -19,11 +19,11 @@ public class CodeGeneratorTest [Fact] public void RunCodeGenEmpy() { - var code = GenerateCompilationUnit(_settings, Array.Empty(), new HassServiceDomain[0]); + var code = CodeGenTestHelper.GenerateCompilationUnit(_settings, Array.Empty(), new HassServiceDomain[0]); code.DescendantNodes().OfType().First().Name.ToString().Should().Be("RootNameSpace"); - - AssertCodeCompiles(code.ToString(), string.Empty); + + CodeGenTestHelper.AssertCodeCompiles(code.ToString(), string.Empty); } [Fact] @@ -37,7 +37,7 @@ public void TestIEntityGeneration() new() { EntityId = "switch.switch2" }, }; - var generatedCode = GenerateCompilationUnit(_settings, entityStates, Array.Empty()); + var generatedCode = CodeGenTestHelper.GenerateCompilationUnit(_settings, entityStates, Array.Empty()); var appCode = """ using NetDaemon.HassModel.Entities; using NetDaemon.HassModel; @@ -55,7 +55,7 @@ public void Run(IHaContext ha) } } """; - AssertCodeCompiles(generatedCode.ToString(), appCode); + CodeGenTestHelper.AssertCodeCompiles(generatedCode.ToString(), appCode); } [Fact] @@ -84,7 +84,7 @@ public void TestNumericSensorEntityGeneration() }, }; - var generatedCode = GenerateCompilationUnit(_settings, entityStates, Array.Empty()); + var generatedCode = CodeGenTestHelper.GenerateCompilationUnit(_settings, entityStates, Array.Empty()); var appCode = """ using NetDaemon.HassModel.Entities; using NetDaemon.HassModel; @@ -109,7 +109,7 @@ public void Run(IHaContext ha) } } """; - AssertCodeCompiles(generatedCode.ToString(), appCode); + CodeGenTestHelper.AssertCodeCompiles(generatedCode.ToString(), appCode); } [Fact] @@ -146,7 +146,7 @@ public void TestNumberExtensionMethodGeneration() } }; - var generatedCode = GenerateCompilationUnit(_settings, entityStates, hassServiceDomains); + var generatedCode = CodeGenTestHelper.GenerateCompilationUnit(_settings, entityStates, hassServiceDomains); var appCode = """ using NetDaemon.HassModel.Entities; using NetDaemon.HassModel; @@ -162,7 +162,7 @@ public void Run(IHaContext ha) } } """; - AssertCodeCompiles(generatedCode.ToString(), appCode); + CodeGenTestHelper.AssertCodeCompiles(generatedCode.ToString(), appCode); } [Fact] @@ -188,7 +188,7 @@ public void TestAttributeClassGeneration_UseAttributeBaseClassesFalse() }, }; - var generatedCode = GenerateCompilationUnit(_settings with { UseAttributeBaseClasses = false }, entityStates, Array.Empty()).ToString(); + var generatedCode = CodeGenTestHelper.GenerateCompilationUnit(_settings with { UseAttributeBaseClasses = false }, entityStates, Array.Empty()).ToString(); var appCode = """ using NetDaemon.HassModel.Entities; @@ -210,7 +210,7 @@ public void Run(IHaContext ha) } } """; - AssertCodeCompiles(generatedCode, appCode); + CodeGenTestHelper.AssertCodeCompiles(generatedCode, appCode); } @@ -235,7 +235,7 @@ public void TestAttributeClassGenerationSkipBaseProperties() }, }; - var generatedCode = GenerateCompilationUnit(_settings with { UseAttributeBaseClasses = true }, entityStates, Array.Empty()).ToString(); + var generatedCode = CodeGenTestHelper.GenerateCompilationUnit(_settings with { UseAttributeBaseClasses = true }, entityStates, Array.Empty()).ToString(); generatedCode.Should().NotContain("Brightness", because: "It is in the base class"); var appCode = """ @@ -258,116 +258,7 @@ public void Run(IHaContext ha) } } """; - AssertCodeCompiles(generatedCode, appCode); - } - - [Fact] - public void TestServicesGeneration() - { - var readOnlyCollection = new HassState[] - { - new() { EntityId = "light.light1" }, - }; - - var hassServiceDomains = new HassServiceDomain[] - { - new() - { - Domain = "light", - Services = new HassService[] { - new() { - Service = "turn_off", - Target = new TargetSelector { Entity = new() { Domain = "light" } } - }, - new() { - Service = "turn_on", - Fields = new HassServiceField[] { - new() { Field = "transition", Selector = new NumberSelector(), }, - new() { Field = "brightness", Selector = new NumberSelector { Step = 0.2f }, } - }, - Target = new TargetSelector { Entity = new() { Domain = "light" } } - } - } - } - }; - - // Act: - var code = GenerateCompilationUnit(_settings, readOnlyCollection, hassServiceDomains); - - var appCode = """ - using NetDaemon.HassModel; - using NetDaemon.HassModel.Entities; - using RootNameSpace; - - public class Root - { - public void Run(IHaContext ha) - { - var s = new RootNameSpace.Services(ha); - - s.Light.TurnOn(new ServiceTarget() ); - s.Light.TurnOn(new ServiceTarget(), transition: 12, brightness: 324.5f); - s.Light.TurnOn(new ServiceTarget(), new (){ Transition = 12L, Brightness = 12.3f }); - s.Light.TurnOn(new ServiceTarget(), new (){ Brightness = 12.3f }); - - s.Light.TurnOff(new ServiceTarget()); - - var light = new RootNameSpace.LightEntity(ha, "light.testLight"); - - light.TurnOn(); - light.TurnOn(transition: 12, brightness: 324.5f); - light.TurnOn(new (){ Transition = 12L, Brightness = 12.3f }); - light.TurnOff(); - } - } - """; - AssertCodeCompiles(code.ToString(), appCode); - } - - private void AssertCodeCompiles(string generated, string appCode) - { - var syntaxtrees = new [] - { - SyntaxFactory.ParseSyntaxTree(generated, path: "generated.cs"), - SyntaxFactory.ParseSyntaxTree(appCode, path: "appcode.cs") - }; - - var compilation = CSharpCompilation.Create("tempAssembly", - syntaxtrees, - AppDomain.CurrentDomain.GetAssemblies().Where(a => !a.IsDynamic).Select(a => MetadataReference.CreateFromFile(a.Location)).ToArray(), - new CSharpCompilationOptions(OutputKind.DynamicallyLinkedLibrary, nullableContextOptions: NullableContextOptions.Enable) - ); - - var emitResult = compilation.Emit(Stream.Null); - - var warningsAndErrors = emitResult.Diagnostics - .Where(d => d.Severity is DiagnosticSeverity.Error or DiagnosticSeverity.Warning).ToList(); - - if (warningsAndErrors.Any()) - { - var msg = new StringBuilder("Compile of generated code failed.\r\n"); - foreach (var diagnostic in warningsAndErrors) - { - msg.AppendLine(diagnostic.ToString()); - } - - msg.AppendLine(); - msg.AppendLine("generated.cs"); - // output the generated code including line numbers to help debugging - msg.AppendLine(string.Join(Environment.NewLine, generated.Split('\n').Select((l, i) => $"{i+1,4}: {l}"))); - - Assert.Fail(msg.ToString()); - } + CodeGenTestHelper.AssertCodeCompiles(generatedCode, appCode); } - private static CompilationUnitSyntax GenerateCompilationUnit( - CodeGenerationSettings codeGenerationSettings, - IReadOnlyCollection states, - IReadOnlyCollection services) - { - var metaData = EntityMetaDataGenerator.GetEntityDomainMetaData(states); - metaData = EntityMetaDataMerger.Merge(codeGenerationSettings, new EntitiesMetaData(), metaData); - var generatedTypes = Generator.GenerateTypes(metaData.Domains, services).ToArray(); - return Generator.BuildCompilationUnit(codeGenerationSettings.Namespace, generatedTypes); - } } diff --git a/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/ServicesGeneratorTest.cs b/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/ServicesGeneratorTest.cs new file mode 100644 index 000000000..52164c611 --- /dev/null +++ b/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/ServicesGeneratorTest.cs @@ -0,0 +1,144 @@ +using NetDaemon.Client.HomeAssistant.Model; +using NetDaemon.HassModel.CodeGenerator; +using NetDaemon.HassModel.CodeGenerator.Model; + +namespace NetDaemon.HassModel.Tests.CodeGenerator; + +public class ServicesGeneratorTest +{ + private readonly CodeGenerationSettings _settings = new() { Namespace = "RootNameSpace" }; + + [Fact] + public void TestServicesGeneration() + { + var readOnlyCollection = new HassState[] + { + new() { EntityId = "light.light1" }, + }; + + var hassServiceDomains = new HassServiceDomain[] + { + new() + { + Domain = "light", + Services = new HassService[] { + new() { + Service = "turn_off", + Target = new TargetSelector { Entity = new() { Domain = "light" } } + }, + new() { + Service = "turn_on", + Fields = new HassServiceField[] { + new() { Field = "transition", Selector = new NumberSelector(), }, + new() { Field = "brightness", Selector = new NumberSelector { Step = 0.2f }, } + }, + Target = new TargetSelector { Entity = new() { Domain = "light" } } + } + } + } + }; + + // Act: + var code = CodeGenTestHelper.GenerateCompilationUnit(_settings, readOnlyCollection, hassServiceDomains); + + var appCode = """ + using NetDaemon.HassModel; + using NetDaemon.HassModel.Entities; + using RootNameSpace; + + public class Root + { + public void Run(IHaContext ha) + { + var s = new RootNameSpace.Services(ha); + + s.Light.TurnOn(new ServiceTarget() ); + s.Light.TurnOn(new ServiceTarget(), transition: 12, brightness: 324.5f); + s.Light.TurnOn(new ServiceTarget(), new (){ Transition = 12L, Brightness = 12.3f }); + s.Light.TurnOn(new ServiceTarget(), new (){ Brightness = 12.3f }); + + s.Light.TurnOff(new ServiceTarget()); + + var light = new RootNameSpace.LightEntity(ha, "light.testLight"); + + light.TurnOn(); + light.TurnOn(transition: 12, brightness: 324.5f); + light.TurnOn(new (){ Transition = 12L, Brightness = 12.3f }); + light.TurnOff(); + } + } + """; + + CodeGenTestHelper.AssertCodeCompiles(code.ToString(), appCode); + } + + [Fact] + public void TestServiceWithoutAnyTargetEntity_ExtensionMethodSkipped() + { + var readOnlyCollection = new HassState[] + { + new() { EntityId = "light.light1" }, + }; + + var hassServiceDomains = new HassServiceDomain[] + { + new() + { + Domain = "smart_things", + Services = new HassService[] { + new() { + Service = "set_fan_mode", + Target = new TargetSelector { Entity = new() { Domain = "humidifiers" } }, + // Because there is no entity for the humidifiers domain we should not generate extension + // methods for those + }, + new() { + Service = "orbit", + Target = new TargetSelector { Entity = new() { Domain = "light" } }, + } + }, + }, + new() + { + Domain = "dumbthings", + Services = new HassService[] { + new() { + Service = "push_button", + Target = new TargetSelector { Entity = new() { Domain = "uselessbox" } }, + // Because there is no entity for the uselessbox domain we should not generate extension + // methods for those + }, + }, + } + }; + + // Act: + var code = CodeGenTestHelper.GenerateCompilationUnit(_settings, readOnlyCollection, hassServiceDomains); + + code.ToString().Should().NotContain("humidifiers", because:"There is no entity for domain humidifiers"); + code.ToString().Should().NotContain("DumbthingsEntityExtensionMethods", because:"There is no entity for any of the services in dumbthings"); + + var appCode = """ + using NetDaemon.HassModel; + using NetDaemon.HassModel.Entities; + using RootNameSpace; + + public class Root + { + public void Run(Entities entities, Services services) + { + // Test the extension Method exists + SmartThingsEntityExtensionMethods.Orbit(entities.Light.Light1); + entities.Light.Light1.Orbit(); + + // Test the Methods on the service classes exist + services.SmartThings.SetFanMode(new ServiceTarget()); + services.SmartThings.Orbit(new ServiceTarget()); + services.Dumbthings.PushButton(new ServiceTarget()); + } + } + """; + + CodeGenTestHelper.AssertCodeCompiles(code.ToString(), appCode); + } +} \ No newline at end of file From 75d3e86dc093a7ea936ec2c2eccfe92cb113956d Mon Sep 17 00:00:00 2001 From: Frank Bakker Date: Wed, 21 Dec 2022 20:17:09 +0100 Subject: [PATCH 2/2] split test --- .../CodeGenerator/ServicesGeneratorTest.cs | 91 ++++++++++++------- 1 file changed, 57 insertions(+), 34 deletions(-) diff --git a/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/ServicesGeneratorTest.cs b/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/ServicesGeneratorTest.cs index 52164c611..090c6569a 100644 --- a/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/ServicesGeneratorTest.cs +++ b/src/HassModel/NetDaemon.HassModel.Tests/CodeGenerator/ServicesGeneratorTest.cs @@ -1,4 +1,5 @@ -using NetDaemon.Client.HomeAssistant.Model; +using Microsoft.CodeAnalysis.CSharp.Syntax; +using NetDaemon.Client.HomeAssistant.Model; using NetDaemon.HassModel.CodeGenerator; using NetDaemon.HassModel.CodeGenerator.Model; @@ -11,15 +12,12 @@ public class ServicesGeneratorTest [Fact] public void TestServicesGeneration() { - var readOnlyCollection = new HassState[] - { + var readOnlyCollection = new HassState[] { new() { EntityId = "light.light1" }, }; - var hassServiceDomains = new HassServiceDomain[] - { - new() - { + var hassServiceDomains = new HassServiceDomain[] { + new() { Domain = "light", Services = new HassService[] { new() { @@ -75,38 +73,69 @@ public void Run(IHaContext ha) [Fact] public void TestServiceWithoutAnyTargetEntity_ExtensionMethodSkipped() { - var readOnlyCollection = new HassState[] - { - new() { EntityId = "light.light1" }, + var readOnlyCollection = new HassState[] { + new() { EntityId = "orbiter.cassini" }, }; var hassServiceDomains = new HassServiceDomain[] { - new() - { + new() { Domain = "smart_things", Services = new HassService[] { new() { - Service = "set_fan_mode", - Target = new TargetSelector { Entity = new() { Domain = "humidifiers" } }, - // Because there is no entity for the humidifiers domain we should not generate extension - // methods for those + Service = "dig", + Target = new TargetSelector { Entity = new() { Domain = "rover" } }, }, new() { Service = "orbit", - Target = new TargetSelector { Entity = new() { Domain = "light" } }, + Target = new TargetSelector { Entity = new() { Domain = "orbiter" } }, } }, - }, - new() - { + } + }; + + // Act: + var code = CodeGenTestHelper.GenerateCompilationUnit(_settings, readOnlyCollection, hassServiceDomains); + + code.ToString().Should().NotContain("rover", because:"There is no entity for domain rover"); + + var appCode = """ + using NetDaemon.HassModel; + using NetDaemon.HassModel.Entities; + using RootNameSpace; + + public class Root + { + public void Run(Entities entities, Services services) + { + // Test the Orbit extension Method exists + SmartThingsEntityExtensionMethods.Orbit(entities.Orbiter.Cassini); + entities.Orbiter.Cassini.Orbit(); + + // Test the Methods on the service classes do exist + services.SmartThings.Dig(new ServiceTarget()); + services.SmartThings.Orbit(new ServiceTarget()); + } + } + """; + + CodeGenTestHelper.AssertCodeCompiles(code.ToString(), appCode); + } + + [Fact] + public void TestServiceWithoutAnyMethods_ClassSkipped() + { + var readOnlyCollection = new HassState[] { + new() { EntityId = "light.light1" }, + }; + + var hassServiceDomains = new HassServiceDomain[] { + new() { Domain = "dumbthings", Services = new HassService[] { new() { Service = "push_button", Target = new TargetSelector { Entity = new() { Domain = "uselessbox" } }, - // Because there is no entity for the uselessbox domain we should not generate extension - // methods for those }, }, } @@ -114,10 +143,9 @@ public void TestServiceWithoutAnyTargetEntity_ExtensionMethodSkipped() // Act: var code = CodeGenTestHelper.GenerateCompilationUnit(_settings, readOnlyCollection, hassServiceDomains); - - code.ToString().Should().NotContain("humidifiers", because:"There is no entity for domain humidifiers"); - code.ToString().Should().NotContain("DumbthingsEntityExtensionMethods", because:"There is no entity for any of the services in dumbthings"); - + code.ToString().Should().NotContain("DumbthingsEntityExtensionMethods", + because:"There is no entity for any of the services in dumbthings"); + var appCode = """ using NetDaemon.HassModel; using NetDaemon.HassModel.Entities; @@ -127,13 +155,7 @@ public class Root { public void Run(Entities entities, Services services) { - // Test the extension Method exists - SmartThingsEntityExtensionMethods.Orbit(entities.Light.Light1); - entities.Light.Light1.Orbit(); - - // Test the Methods on the service classes exist - services.SmartThings.SetFanMode(new ServiceTarget()); - services.SmartThings.Orbit(new ServiceTarget()); + // But the Method on the Dumbthings service class should still exist services.Dumbthings.PushButton(new ServiceTarget()); } } @@ -141,4 +163,5 @@ public void Run(Entities entities, Services services) CodeGenTestHelper.AssertCodeCompiles(code.ToString(), appCode); } -} \ No newline at end of file +} + \ No newline at end of file