Skip to content
Merged
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
Original file line number Diff line number Diff line change
@@ -1,6 +1,9 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT license. See LICENSE file in the project root for full license information.

using System.Linq.Expressions;
using System.Reflection;

using AwesomeAssertions;

using TestFramework.ForTestingMSTest;
Expand Down Expand Up @@ -1277,4 +1280,72 @@ private readonly struct ValueBox(int value)
{
public int Value { get; } = value;
}

private sealed class MutableContainer
{
#pragma warning disable SA1401 // Fields should be private - intentional: this type tests Expression.Assign on nested members.
public MutableBox Inner = new() { Value = 0 };
#pragma warning restore SA1401
}

private sealed class MutableBox
{
#pragma warning disable SA1401 // Fields should be private - intentional: this type tests Expression.Assign on a field.
public int Value;
#pragma warning restore SA1401
}

public void That_ManuallyConstructedAssignExpression_SurfacesRhsAndPreservesSideEffect()
{
// Construct: (container.Inner.Value = ComputeValue()) < 0 — fails, so message must
// include enough context for diagnosis AND the assignment side-effect must apply.
var container = new MutableContainer();
FieldInfo innerFi = typeof(MutableContainer).GetField(nameof(MutableContainer.Inner))!;
FieldInfo valueFi = typeof(MutableBox).GetField(nameof(MutableBox.Value))!;
Comment thread
Evangelink marked this conversation as resolved.

MemberExpression innerAccess = Expression.Field(Expression.Constant(container), innerFi);
MemberExpression valueAccess = Expression.Field(innerAccess, valueFi);
MethodCallExpression rhs = Expression.Call(typeof(MutableBoxHelper).GetMethod(nameof(MutableBoxHelper.ComputeValue))!);
Comment thread
Evangelink marked this conversation as resolved.
BinaryExpression assign = Expression.Assign(valueAccess, rhs);
BinaryExpression body = Expression.LessThan(assign, Expression.Constant(0));
var lambda = Expression.Lambda<Func<bool>>(body);

Action act = () => Assert.That(lambda);

AssertFailedException ex = act.Should().Throw<AssertFailedException>().Which;
// Assert on the method name only (not the parenthesized rendering) so that future
// renderer tweaks (e.g., dropping/adding parentheses for nullary calls) don't break
// this regression test.
ex.Message.Should().Contain(nameof(MutableBoxHelper.ComputeValue));
// The Inner member of the receiver chain should surface in the failure details.
ex.Message.Should().Contain("Inner");
// The assignment side-effect must apply.
container.Inner.Value.Should().Be(42);
}

// Regression: a member whose static type is Func/Action but whose runtime value is null should
// still appear in the failure details. Filtering must remain runtime-typed (via the cached
// value's GetType()) rather than static-typed at analysis time.
private sealed class HolderWithFuncField
{
#pragma warning disable SA1401 // Fields should be private - intentional: test fixture exposing a delegate-typed field.
public Func<int>? Callback;
#pragma warning restore SA1401
}

public void That_NullDelegateTypedMember_StillAppearsInDetails()
{
Comment thread
Evangelink marked this conversation as resolved.
var holder = new HolderWithFuncField { Callback = null };

Action act = () => Assert.That(() => holder.Callback != null);

AssertFailedException ex = act.Should().Throw<AssertFailedException>().Which;
ex.Message.Should().Contain("Callback");
ex.Message.Should().Contain("null");
}
}

internal static class MutableBoxHelper
{
public static int ComputeValue() => 42;
}
Loading