From 9b84fb442090f74ffd1e2c078a0db47aa907c2a4 Mon Sep 17 00:00:00 2001 From: Ed Lenoir Date: Fri, 18 Jul 2025 13:55:10 -0700 Subject: [PATCH 1/2] Add Equal.object to ServiceTarget so equivalency can be used in Fluent Assertions. Without this since ServiceTarget is a class the only check is if two objects are the SAME object (i.e. reference check only) --- .../AppScopedHaContextProviderTest.cs | 2 +- .../ServiceTargetTest.cs | 46 +++++++++++- .../NetDaemon.HassModel/ServiceTarget.cs | 72 +++++++++++++++++++ 3 files changed, 118 insertions(+), 2 deletions(-) diff --git a/src/HassModel/NetDaemon.HassModel.Tests/Internal/AppScopedHaContextProviderTest.cs b/src/HassModel/NetDaemon.HassModel.Tests/Internal/AppScopedHaContextProviderTest.cs index 1a3f4a5c0..6a412476b 100644 --- a/src/HassModel/NetDaemon.HassModel.Tests/Internal/AppScopedHaContextProviderTest.cs +++ b/src/HassModel/NetDaemon.HassModel.Tests/Internal/AppScopedHaContextProviderTest.cs @@ -149,7 +149,7 @@ public async Task TestCallServiceWithResponseAsync() executeCommand!.Sequence[0].GetType().GetProperty("service")!.GetValue(executeCommand.Sequence[0])!.Should().Be("domain.service"); executeCommand.Sequence[0].GetType().GetProperty("data")!.GetValue(executeCommand.Sequence[0])!.Should().BeEquivalentTo(serviceData); - executeCommand.Sequence[0].GetType().GetProperty("target")!.GetValue(executeCommand.Sequence[0])!.Should().BeEquivalentTo(serviceTarget); + executeCommand.Sequence[0].GetType().GetProperty("target")!.GetValue(executeCommand.Sequence[0])!.Should().BeEquivalentTo(serviceTarget, options => options.ComparingByMembers()); } [Fact] diff --git a/src/HassModel/NetDaemon.HassModel.Tests/ServiceTargetTest.cs b/src/HassModel/NetDaemon.HassModel.Tests/ServiceTargetTest.cs index f47a19419..efbdb1e97 100644 --- a/src/HassModel/NetDaemon.HassModel.Tests/ServiceTargetTest.cs +++ b/src/HassModel/NetDaemon.HassModel.Tests/ServiceTargetTest.cs @@ -27,4 +27,48 @@ public void ServiceTargetShouldContainCorrectEntitiesUsingParams() serviceTarget.EntityIds.Should().BeEquivalentTo("light.kitchen", "light.livingroom"); } -} \ No newline at end of file + + [Fact] + public void ServiceTargetAndObjectShouldBeEqual() + { + var serviceTarget1 = ServiceTarget.FromEntity("light.kitchen"); + object serviceTarget2 = ServiceTarget.FromEntity("light.kitchen"); + serviceTarget1.Should().Be(serviceTarget2); + } + + [Fact] + public void ServiceTargetsShouldBeEqual() + { + var serviceTarget1 = ServiceTarget.FromEntity("light.kitchen"); + var serviceTarget2 = ServiceTarget.FromEntity("light.kitchen"); + serviceTarget1.Should().Be(serviceTarget2); + } + + [Fact] + public void ServiceTargetsOperatorEqualNotOverridden() + { + var serviceTarget1 = ServiceTarget.FromEntities("light.kitchen", "light.livingroom"); + var serviceTarget2 = ServiceTarget.FromEntities("light.kitchen", "light.livingroom"); + + // The == operator is not overridden, so the == operator for ServiceTarget will only work on reference equality. + Assert.False((serviceTarget1 == serviceTarget2), "ServiceTargets should not be equal using =="); + } + + [Fact] + public void ServiceTargetsOperatorEqualReference() + { + var serviceTarget1 = ServiceTarget.FromEntities("light.kitchen", "light.livingroom"); + var serviceTarget1b = serviceTarget1; // Reference to the same object + + Assert.True((serviceTarget1 == serviceTarget1b), "ServiceTargets references should be equal using =="); + } + + [Fact] + public void ServiceTargetObjectEqualShouldBeTrue() + { + var serviceTarget1 = ServiceTarget.FromEntity("light.kitchen"); + object serviceTarget2 = ServiceTarget.FromEntity("light.kitchen"); + + Assert.True(serviceTarget1.Equals(serviceTarget2), "ServiceTargets should be equal using Equals.object"); + } +} diff --git a/src/HassModel/NetDaemon.HassModel/ServiceTarget.cs b/src/HassModel/NetDaemon.HassModel/ServiceTarget.cs index 1556afc05..2da96c818 100644 --- a/src/HassModel/NetDaemon.HassModel/ServiceTarget.cs +++ b/src/HassModel/NetDaemon.HassModel/ServiceTarget.cs @@ -29,6 +29,77 @@ public static ServiceTarget FromEntities(IEnumerable entityIds) => public static ServiceTarget FromEntities(params string[] entityIds) => new() { EntityIds = [.. entityIds] }; + /// + /// Override low level object Equals. This is used by fluent assertions and other libraries to compare objects + /// + /// The object that we are comparing to + /// true if the two objects are the same (Reference equal), or if the two objects are ServiceTargets and passes the Equal test + public override bool Equals(object? obj) + { + if (ReferenceEquals(this, obj)) + { + return true; + } + + if (obj is null) + { + return false; + } + + if (obj.GetType() != GetType()) + { + return false; + } + + ServiceTarget other = (ServiceTarget)obj; + //return Equals((ServiceTarget)obj); + return AreCollectionsEqual(EntityIds, other.EntityIds) + && AreCollectionsEqual(DeviceIds, other.DeviceIds) + && AreCollectionsEqual(AreaIds, other.AreaIds) + && AreCollectionsEqual(FloorIds, other.FloorIds) + && AreCollectionsEqual(LabelIds, other.LabelIds); + + } + + private static bool AreCollectionsEqual(IReadOnlyCollection? a, IReadOnlyCollection? b) + { + if (ReferenceEquals(a, b)) + return true; + if (a is null || b is null) + return false; + bool bReturn = a.Count == b.Count && !a.Except(b).Any(); + return bReturn; + } + + /// + /// Override the GetHashCode for completeness for equality checking + /// + /// integer hash code for this ServiceTarget + public override int GetHashCode() + { + int hash = 17; + hash = hash * 23 + (EntityIds is null ? 0 : GetCollectionHashCode(EntityIds)); + hash = hash * 23 + (DeviceIds is null ? 0 : GetCollectionHashCode(DeviceIds)); + hash = hash * 23 + (AreaIds is null ? 0 : GetCollectionHashCode(AreaIds)); + hash = hash * 23 + (FloorIds is null ? 0 : GetCollectionHashCode(FloorIds)); + hash = hash * 23 + (LabelIds is null ? 0 : GetCollectionHashCode(LabelIds)); + return hash; + } + + private static int GetCollectionHashCode(IReadOnlyCollection collection) + { + unchecked + { + int hash = 19; + foreach (var item in collection.OrderBy(x => x)) + { + hash = hash * 31 + item.GetHashCode(StringComparison.Ordinal); + } + return hash; + } + } + + /// /// Creates a new empty ServiceTarget /// @@ -59,4 +130,5 @@ public ServiceTarget() /// Ids of labels to invoke a service on /// public IReadOnlyCollection? LabelIds { get; init; } + } From 6bcfe26ea7d7e31e6c09cad4b1131d08a83a22cc Mon Sep 17 00:00:00 2001 From: Ed Lenoir Date: Thu, 31 Jul 2025 20:12:42 -0700 Subject: [PATCH 2/2] Added non-equal test cases to ServiceTargetTest --- .../ServiceTargetTest.cs | 47 +++++++++++++++++++ 1 file changed, 47 insertions(+) diff --git a/src/HassModel/NetDaemon.HassModel.Tests/ServiceTargetTest.cs b/src/HassModel/NetDaemon.HassModel.Tests/ServiceTargetTest.cs index efbdb1e97..fac0cdb12 100644 --- a/src/HassModel/NetDaemon.HassModel.Tests/ServiceTargetTest.cs +++ b/src/HassModel/NetDaemon.HassModel.Tests/ServiceTargetTest.cs @@ -36,6 +36,14 @@ public void ServiceTargetAndObjectShouldBeEqual() serviceTarget1.Should().Be(serviceTarget2); } + [Fact] + public void ServiceTargetAndObjectShouldNotBeEqual() + { + var serviceTarget1 = ServiceTarget.FromEntity("light.kitchen"); + object serviceTarget2 = ServiceTarget.FromEntity("light.livingroom"); + serviceTarget1.Should().NotBe(serviceTarget2); + } + [Fact] public void ServiceTargetsShouldBeEqual() { @@ -44,6 +52,14 @@ public void ServiceTargetsShouldBeEqual() serviceTarget1.Should().Be(serviceTarget2); } + [Fact] + public void ServiceTargetsShouldNotBeEqual() + { + var serviceTarget1 = ServiceTarget.FromEntity("light.kitchen"); + var serviceTarget2 = ServiceTarget.FromEntity("light.livingroom"); + serviceTarget1.Should().NotBe(serviceTarget2); + } + [Fact] public void ServiceTargetsOperatorEqualNotOverridden() { @@ -54,6 +70,16 @@ public void ServiceTargetsOperatorEqualNotOverridden() Assert.False((serviceTarget1 == serviceTarget2), "ServiceTargets should not be equal using =="); } + [Fact] + public void ServiceTargetsOperatorNotEqualNorOverridden() + { + var serviceTarget1 = ServiceTarget.FromEntities("light.kitchen", "light.livingroom"); + var serviceTarget2 = ServiceTarget.FromEntities("light.kitchen", "light.kitchen"); + + // The == operator is not overridden, so the == operator for ServiceTarget will only work on reference equality. You will ALWAYS get true for not equal if not the same reference object + Assert.True((serviceTarget1 != serviceTarget2), "ServiceTargets should not be equal using !="); + } + [Fact] public void ServiceTargetsOperatorEqualReference() { @@ -63,6 +89,17 @@ public void ServiceTargetsOperatorEqualReference() Assert.True((serviceTarget1 == serviceTarget1b), "ServiceTargets references should be equal using =="); } + [Fact] + public void ServiceTargetsOperatorNotEqualReference() + { + var serviceTarget1 = ServiceTarget.FromEntity("light.kitchen"); + var serviceTarget2 = ServiceTarget.FromEntity("light.livingroom"); + var serviceTarget1b = serviceTarget1; // Reference to the same object + var serviceTarget2b = serviceTarget2; + + Assert.True((serviceTarget1b != serviceTarget2b), "ServiceTargets references of different objects should not be equal using !="); + } + [Fact] public void ServiceTargetObjectEqualShouldBeTrue() { @@ -71,4 +108,14 @@ public void ServiceTargetObjectEqualShouldBeTrue() Assert.True(serviceTarget1.Equals(serviceTarget2), "ServiceTargets should be equal using Equals.object"); } + + [Fact] + public void ServiceTargetObjectEqualShouldNotBeTrue() + { + var serviceTarget1 = ServiceTarget.FromEntity("light.kitchen"); + object serviceTarget2 = ServiceTarget.FromEntity("light.livingroom"); + + Assert.False(serviceTarget1.Equals(serviceTarget2), "ServiceTargets should not be equal using Equals.object"); + } + }