Skip to content
Merged
Show file tree
Hide file tree
Changes from 1 commit
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
44 changes: 28 additions & 16 deletions Source/aweXpect.Core/Core/Nodes/WhichNode.cs
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,7 @@
=> _inner = node;

/// <inheritdoc />
public override async Task<ConstraintResult> IsMetBy<TValue>(

Check failure on line 65 in Source/aweXpect.Core/Core/Nodes/WhichNode.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Refactor this method to reduce its Cognitive Complexity from 16 to the 15 allowed.

See more on https://sonarcloud.io/project/issues?id=Testably_aweXpect&issues=AZ40xIicdn0D8X7NKuxJ&open=AZ40xIicdn0D8X7NKuxJ&pullRequest=958
TValue? value,
IEvaluationContext context,
CancellationToken cancellationToken) where TValue : default
Expand Down Expand Up @@ -90,26 +90,38 @@
FurtherProcessingStrategy.IgnoreResult, default);
}

if (value is TSource typedValue)
TSource? source;
if (value is TSource directValue)
{
TMember? matchingValue;
if (_memberAccessor != null)
{
matchingValue = _memberAccessor(typedValue);
}
else
{
matchingValue = await _asyncMemberAccessor!.Invoke(typedValue);
}
source = directValue;
}
else if (parentResult != null && parentResult.TryGetValue(out TSource? projectedValue))
{
// A parent WhichNode already projected the outer subject to TSource;
// use that projection so chained ForWhich calls compose
// (e.g. .Which.X.Which.Y, where Y's accessor operates on X's output).
source = projectedValue;
}
else
{
throw new InvalidOperationException(
$"The member type for the actual value in the which node did not match.{Environment.NewLine} Found: {Formatter.Format(value.GetType())}{Environment.NewLine} Expected: {Formatter.Format(typeof(TSource))}")
.LogTrace();
}

ConstraintResult result = await _inner.IsMetBy(matchingValue, context, cancellationToken);
return CombineResults(parentResult, result, _separator ?? "", FurtherProcessingStrategy.IgnoreResult,
matchingValue);
TMember? matchingValue;
if (_memberAccessor != null)
{
matchingValue = source is null ? default : _memberAccessor(source);

Check warning on line 115 in Source/aweXpect.Core/Core/Nodes/WhichNode.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this condition so that it does not always evaluate to 'False'. Some code paths are unreachable.

See more on https://sonarcloud.io/project/issues?id=Testably_aweXpect&issues=AZ40xIicdn0D8X7NKuxH&open=AZ40xIicdn0D8X7NKuxH&pullRequest=958
}
else
{
matchingValue = source is null ? default : await _asyncMemberAccessor!.Invoke(source);

Check warning on line 119 in Source/aweXpect.Core/Core/Nodes/WhichNode.cs

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Change this condition so that it does not always evaluate to 'False'. Some code paths are unreachable.

See more on https://sonarcloud.io/project/issues?id=Testably_aweXpect&issues=AZ40xIicdn0D8X7NKuxI&open=AZ40xIicdn0D8X7NKuxI&pullRequest=958
}

throw new InvalidOperationException(
$"The member type for the actual value in the which node did not match.{Environment.NewLine} Found: {Formatter.Format(value.GetType())}{Environment.NewLine} Expected: {Formatter.Format(typeof(TSource))}")
.LogTrace();
ConstraintResult innerResult = await _inner.IsMetBy(matchingValue, context, cancellationToken);
return CombineResults(parentResult, innerResult, _separator ?? "", FurtherProcessingStrategy.IgnoreResult,
matchingValue);
}

/// <inheritdoc cref="object.Equals(object?)" />
Expand Down
60 changes: 59 additions & 1 deletion Tests/aweXpect.Core.Tests/Core/ExpectationBuilderTests.cs
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
using System.Threading;
using System.Globalization;
using System.Threading;
using aweXpect.Core.Constraints;
using aweXpect.Core.Tests.TestHelpers;

Expand Down Expand Up @@ -157,6 +158,45 @@ await That(result.GetExpectationText())
.IsEqualTo("is foo whose upper is FOO and whose doubled is foofoo");
}

[Fact]
public async Task ForWhich_Async_CalledTwice_WhereSecondProjectsFromFirstResult_ShouldChainProjections()
{
Func<int, Task<string?>> stringify = i =>
Task.FromResult<string?>(i.ToString(CultureInfo.InvariantCulture));
Func<string, Task<string?>> doubled = s => Task.FromResult<string?>(s + s);
ManualExpectationBuilder<int> sut = new(null);
sut.AddConstraint((_, _) => new DummyConstraint<int>(i => i == 12, "is 12"));
sut.ForWhich(stringify, " whose string ");
sut.AddConstraint((_, _) => new DummyConstraint<string>(s => s == "12", "is \"12\""));
sut.ForWhich(doubled, " and whose doubled ");
sut.AddConstraint((_, _) => new DummyConstraint<string>(s => s == "1212", "is \"1212\""));

ConstraintResult result = await sut.IsMetBy(12, null!, CancellationToken.None);

await That(result.Outcome).IsEqualTo(Outcome.Success);
await That(result.GetExpectationText())
.IsEqualTo("is 12 whose string is \"12\" and whose doubled is \"1212\"");
}

[Fact]
public async Task ForWhich_CalledThreeTimes_EachProjectionChainsFromPrevious_ShouldEvaluateDeeply()
{
ManualExpectationBuilder<string> sut = new(null);
sut.AddConstraint((_, _) => new DummyConstraint<string>(s => s == "foo", "is foo"));
sut.ForWhich<string, char>(s => s[0], " whose first char ");
sut.AddConstraint((_, _) => new DummyConstraint<char>(c => c == 'f', "is 'f'"));
sut.ForWhich<char, int>(c => c, " whose code point ");
sut.AddConstraint((_, _) => new DummyConstraint<int>(i => i == 'f', "is 102"));
sut.ForWhich<int, bool>(i => i % 2 == 0, " whose is-even ");
sut.AddConstraint((_, _) => new DummyConstraint<bool>(b => b, "is true"));

ConstraintResult result = await sut.IsMetBy("foo", null!, CancellationToken.None);

await That(result.Outcome).IsEqualTo(Outcome.Success);
await That(result.GetExpectationText())
.IsEqualTo("is foo whose first char is 'f' whose code point is 102 whose is-even is true");
}

[Fact]
public async Task ForWhich_CalledTwice_OuterConstraintFails_ShouldStillEvaluateOuterConstraint()
{
Expand Down Expand Up @@ -208,6 +248,24 @@ await That(result.GetExpectationText())
.IsEqualTo("is foo whose length is 3 and whose first char is 'f'");
}

[Fact]
public async Task ForWhich_CalledTwice_WhereSecondProjectsFromFirstResult_ShouldChainProjections()
{
ManualExpectationBuilder<int> sut = new(null);
sut.AddConstraint((_, _) => new DummyConstraint<int>(i => i == 123, "is 123"));
sut.ForWhich<int, string>(i => i.ToString(CultureInfo.InvariantCulture),
" whose string ");
sut.AddConstraint((_, _) => new DummyConstraint<string>(s => s == "123", "is \"123\""));
sut.ForWhich<string, int>(s => s.Length, " and whose length ");
sut.AddConstraint((_, _) => new DummyConstraint<int>(i => i == 3, "is 3"));

ConstraintResult result = await sut.IsMetBy(123, null!, CancellationToken.None);

await That(result.Outcome).IsEqualTo(Outcome.Success);
await That(result.GetExpectationText())
.IsEqualTo("is 123 whose string is \"123\" and whose length is 3");
}

[Fact]
public async Task WhenSubjectHasMultipleLines_ShouldTrimCommonWhiteSpace()
{
Expand Down
79 changes: 78 additions & 1 deletion Tests/aweXpect.Core.Tests/Core/Nodes/WhichNodeTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -280,10 +280,87 @@ public async Task IsMetBy_ExpectationNodeWithConstraint_ShouldApplyConstraint()
await That(sb.ToString()).IsEqualTo("e1e2");
}

[Fact]
public async Task IsMetBy_WhenParentWhichNodeProjectsToInnerSource_ShouldEvaluateAgainstProjectedValue()
{
// Outer subject is `string` "food" (length 4).
// Parent WhichNode projects string → int (length).
// Inner WhichNode (TSource = int) projects int → bool (even).
// Without the fix the inner WhichNode receives the outer string instead of the projected int
// and throws "the member type for the actual value in the which node did not match".
WhichNode<string, int> outerWhich = new(null, s => s!.Length);
outerWhich.AddNode(new ExpectationNode());
outerWhich.AddConstraint(new DummyConstraint<int>(_ => true, "length"));

bool? observed = null;
WhichNode<int, bool> innerWhich = new(outerWhich, i => i % 2 == 0);
innerWhich.AddNode(new ExpectationNode());
innerWhich.AddConstraint(new DummyConstraint<bool>(b =>
{
observed = b;
return b;
}, "is true"));

ConstraintResult result = await innerWhich.IsMetBy("food", null!, CancellationToken.None);

await That(result.Outcome).IsEqualTo(Outcome.Success);
await That(observed).IsEqualTo(true);
}

[Fact]
public async Task IsMetBy_WhenParentChainProjectsThroughThreeLevels_ShouldPropagateInnermostValue()
{
// string ("foo") → first char ('f') → code point (102) → is-even (true)
WhichNode<string, char> level1 = new(null, s => s![0]);
level1.AddNode(new ExpectationNode());
level1.AddConstraint(new DummyConstraint<char>(_ => true, "first"));

WhichNode<char, int> level2 = new(level1, c => c);
level2.AddNode(new ExpectationNode());
level2.AddConstraint(new DummyConstraint<int>(_ => true, "code"));

WhichNode<int, bool> level3 = new(level2, i => i % 2 == 0);
level3.AddNode(new ExpectationNode());
bool? observed = null;
level3.AddConstraint(new DummyConstraint<bool>(b =>
{
observed = b;
return b;
}, "is true"));

ConstraintResult result = await level3.IsMetBy("foo", null!, CancellationToken.None);

await That(result.Outcome).IsEqualTo(Outcome.Success);
await That(observed).IsEqualTo(true);
}

[Fact]
public async Task IsMetBy_WhenOuterTypeDoesNotMatchButParentExposesProjectedValue_ShouldFallBackToParentProjection()
{
// Parent's result carries a string "abcd". WhichNode<string, int> receives an unrelated
// outer DateTime, falls back to the parent's projected string, and projects its length.
DummyNode node1 = new("", () => new DummyConstraintResult<string?>(Outcome.Success, "abcd", ""));
WhichNode<string, int> whichNode = new(node1, s => s.Length);
whichNode.AddNode(new ExpectationNode());
int? observed = null;
whichNode.AddConstraint(new DummyConstraint<int>(i =>
{
observed = i;
return true;
}, "is anything"));

ConstraintResult result = await whichNode.IsMetBy(DateTime.Now, null!, CancellationToken.None);

await That(result.Outcome).IsEqualTo(Outcome.Success);
await That(observed).IsEqualTo(4);
}

[Fact]
public async Task IsMetBy_WhenTypeDoesNotMatch_ShouldThrowInvalidOperationException()
{
DummyNode node1 = new("", () => new DummyConstraintResult<string?>(Outcome.Success, "1", ""));
// The parent node deliberately exposes no value of TSource, so the WhichNode cannot
// recover the projected value from the parent chain and must surface the type mismatch.
DummyNode node1 = new("", () => new DummyConstraintResult(Outcome.Success));
WhichNode<string, int> whichNode = new(node1, s => s.Length);
whichNode.AddNode(new ExpectationNode());
whichNode.AddConstraint(new DummyConstraint("c2",
Expand Down
Loading