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