From 92a610e2b723d690003f33d7d32841dcfceeb091 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:49:09 +0000 Subject: [PATCH 1/3] Fix stack overflow in InitializeProperties under .NET 9 --- src/DotJEM.NUnit3.Tests/Class1.cs | 1 - .../ObjectPropertiesEqualsConstraintTest.cs | 75 +++++++++++++++++++ .../DotJEM.NUnit3.Tests.csproj | 7 +- .../ObjectPropertiesEqualsConstraint.cs | 14 +++- 4 files changed, 91 insertions(+), 6 deletions(-) diff --git a/src/DotJEM.NUnit3.Tests/Class1.cs b/src/DotJEM.NUnit3.Tests/Class1.cs index 7b205fe..5195337 100644 --- a/src/DotJEM.NUnit3.Tests/Class1.cs +++ b/src/DotJEM.NUnit3.Tests/Class1.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using System.Linq; -using System.Runtime.Remoting.Messaging; using System.Text; using System.Threading.Tasks; using NUnit.Framework; diff --git a/src/DotJEM.NUnit3.Tests/Constraints/Objects/ObjectPropertiesEqualsConstraintTest.cs b/src/DotJEM.NUnit3.Tests/Constraints/Objects/ObjectPropertiesEqualsConstraintTest.cs index d8f82df..8bf3b62 100644 --- a/src/DotJEM.NUnit3.Tests/Constraints/Objects/ObjectPropertiesEqualsConstraintTest.cs +++ b/src/DotJEM.NUnit3.Tests/Constraints/Objects/ObjectPropertiesEqualsConstraintTest.cs @@ -83,4 +83,79 @@ public void Test_InRunnerDisplay2() Assert.That(new { Test = "243", Age = 42 }, Has.Properties.NotEqualTo(new { Test = "243", Age = 43 }).Strict()); } } + + /// + /// Tests for stack overflow regression introduced under .NET 9.0 where + /// InitializeProperties was recursing into static properties (e.g. DateTime.Now, DateTime.Today) + /// that return new values on each access, defeating the cycle-detection in the references set. + /// + public class ObjectPropertiesEqualsConstraintStackOverflowRegressionTest + { + private class WithDateTime + { + public DateTime CreatedAt { get; set; } + public string Name { get; set; } + } + + private class Parent + { + public Child Child { get; set; } + } + + private class Child + { + public Parent Parent { get; set; } + } + + private class WithTypeProperty + { + public Type ObjectType { get; set; } + public string Name { get; set; } + } + + [Test] + public void InitializeProperties_ObjectWithDateTimeProperty_DoesNotStackOverflow() + { + var expected = new WithDateTime { CreatedAt = new DateTime(2024, 1, 15, 10, 30, 0), Name = "test" }; + Assert.DoesNotThrow(() => + { + var constraint = new ObjectPropertiesEqualsConstraint(expected); + }); + } + + [Test] + public void ApplyTo_ObjectWithDateTimeProperty_Passes() + { + var expected = new WithDateTime { CreatedAt = new DateTime(2024, 1, 15, 10, 30, 0), Name = "test" }; + var actual = new WithDateTime { CreatedAt = new DateTime(2024, 1, 15, 10, 30, 0), Name = "test" }; + + var constraint = new ObjectPropertiesEqualsConstraint(expected); + ConstraintResult result = constraint.ApplyTo(actual); + + Assert.That(result.IsSuccess, Is.True, result.ToString()); + } + + [Test] + public void InitializeProperties_ObjectWithCircularReference_DoesNotStackOverflow() + { + var parent = new Parent(); + var child = new Child { Parent = parent }; + parent.Child = child; + + Assert.DoesNotThrow(() => + { + var constraint = new ObjectPropertiesEqualsConstraint(parent); + }); + } + + [Test] + public void InitializeProperties_ObjectWithTypeProperty_DoesNotStackOverflow() + { + var expected = new WithTypeProperty { ObjectType = typeof(string), Name = "test" }; + Assert.DoesNotThrow(() => + { + var constraint = new ObjectPropertiesEqualsConstraint(expected); + }); + } + } } diff --git a/src/DotJEM.NUnit3.Tests/DotJEM.NUnit3.Tests.csproj b/src/DotJEM.NUnit3.Tests/DotJEM.NUnit3.Tests.csproj index f00ffb8..c7eef21 100644 --- a/src/DotJEM.NUnit3.Tests/DotJEM.NUnit3.Tests.csproj +++ b/src/DotJEM.NUnit3.Tests/DotJEM.NUnit3.Tests.csproj @@ -1,6 +1,6 @@  - net48 + net48;net9.0 DotJEM.NUnit3.Tests DotJEM.NUnit3.Tests Copyright © 2019 @@ -15,10 +15,11 @@ - + + - + diff --git a/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs b/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs index 5cc609f..6eb2ddf 100644 --- a/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs +++ b/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs @@ -54,7 +54,7 @@ protected virtual void InitializeProperties() } Type type = Expected.GetType(); - PropertyInfo[] properties = type.GetProperties(); + PropertyInfo[] properties = type.GetProperties(BindingFlags.Public | BindingFlags.Instance); if (properties.Length == 0) primitive = SetupPrimitive(Expected); @@ -62,7 +62,17 @@ protected virtual void InitializeProperties() //Note: No need for "Else", foreach automatically terminates on an empty collection. foreach (PropertyInfo property in properties.Where(property => property.GetIndexParameters().Length == 0)) { - object expectedObject = property.GetValue(Expected, null); + object expectedObject; + try + { + expectedObject = property.GetValue(Expected, null); + } + catch (Exception) + { + // Skip properties that cannot be read (e.g. throw InvalidOperationException for certain states) + continue; + } + if (references.Contains(expectedObject)) continue; From 78af27847228700765952cd4df938ce34de74271 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 08:51:08 +0000 Subject: [PATCH 2/3] Narrow exception catch to TargetInvocationException per code review --- .../Constraints/Objects/ObjectPropertiesEqualsConstraint.cs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs b/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs index 6eb2ddf..75c98c0 100644 --- a/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs +++ b/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs @@ -67,9 +67,10 @@ protected virtual void InitializeProperties() { expectedObject = property.GetValue(Expected, null); } - catch (Exception) + catch (TargetInvocationException) { - // Skip properties that cannot be read (e.g. throw InvalidOperationException for certain states) + // Skip properties whose getter throws (e.g. Type.GenericParameterAttributes + // throws InvalidOperationException for non-generic types) continue; } From 73b7ae1f063b093f0d440e87de5be4c509a546b5 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Mon, 1 Jun 2026 12:22:05 +0000 Subject: [PATCH 3/3] Fix StackOverflow with circular references by restoring ReferenceComparer in HashSet --- .../Constraints/Objects/ObjectPropertiesEqualsConstraint.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs b/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs index 75c98c0..ad63cbb 100644 --- a/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs +++ b/src/DotJEM.NUnit3/Constraints/Objects/ObjectPropertiesEqualsConstraint.cs @@ -24,7 +24,7 @@ public class ObjectPropertiesEqualsConstraint : BaseConstraint, IObjectProper protected T Expected { get; } public bool ExplicitTypesFlag { get; set; } - private readonly HashSet references = new HashSet(/*new ReferenceComparer()*/); + private readonly HashSet references = new HashSet(new ReferenceComparer()); public ObjectPropertiesEqualsConstraint(T expected) {