Skip to content
Closed
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 0 additions & 1 deletion src/DotJEM.NUnit3.Tests/Class1.cs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -83,4 +83,79 @@ public void Test_InRunnerDisplay2()
Assert.That(new { Test = "243", Age = 42 }, Has.Properties.NotEqualTo(new { Test = "243", Age = 43 }).Strict());
}
}

/// <summary>
/// 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.
/// </summary>
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<WithDateTime>(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<WithDateTime>(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>(parent);
});
}

[Test]
public void InitializeProperties_ObjectWithTypeProperty_DoesNotStackOverflow()
{
var expected = new WithTypeProperty { ObjectType = typeof(string), Name = "test" };
Assert.DoesNotThrow(() =>
{
var constraint = new ObjectPropertiesEqualsConstraint<WithTypeProperty>(expected);
});
}
}
}
7 changes: 4 additions & 3 deletions src/DotJEM.NUnit3.Tests/DotJEM.NUnit3.Tests.csproj
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net48</TargetFramework>
<TargetFrameworks>net48;net9.0</TargetFrameworks>
<AssemblyTitle>DotJEM.NUnit3.Tests</AssemblyTitle>
<Product>DotJEM.NUnit3.Tests</Product>
<Copyright>Copyright © 2019</Copyright>
Expand All @@ -15,10 +15,11 @@
</PropertyGroup>
<ItemGroup>
<PackageReference Include="Microsoft.NET.Test.Sdk" Version="17.4.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
<PackageReference Include="NUnit" Version="3.13.3" />
<PackageReference Include="NUnit3TestAdapter" Version="4.5.0" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.2" />
</ItemGroup>
<ItemGroup>
<ItemGroup Condition="'$(TargetFramework)' == 'net48'">
<Reference Include="System.Data.DataSetExtensions" />
<Reference Include="Microsoft.CSharp" />
<Reference Include="System.Net.Http" />
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ public class ObjectPropertiesEqualsConstraint<T> : BaseConstraint, IObjectProper
protected T Expected { get; }
public bool ExplicitTypesFlag { get; set; }

private readonly HashSet<object> references = new HashSet<object>(/*new ReferenceComparer()*/);
private readonly HashSet<object> references = new HashSet<object>(new ReferenceComparer());

public ObjectPropertiesEqualsConstraint(T expected)
{
Expand Down Expand Up @@ -54,15 +54,26 @@ 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);

//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 (TargetInvocationException)
{
// Skip properties whose getter throws (e.g. Type.GenericParameterAttributes
// throws InvalidOperationException for non-generic types)
continue;
}

if (references.Contains(expectedObject))
continue;

Expand Down