Skip to content
Merged
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
26 changes: 26 additions & 0 deletions TUnit.Assertions.Tests/NullabilityInferenceTests.cs
Original file line number Diff line number Diff line change
Expand Up @@ -42,4 +42,30 @@ public async Task NotNull_ValueType_Chaining()

Console.WriteLine(notNullValue.ToString());
}

[Test]
public async Task NotNull_Dictionary_Chaining_With_Contains_Lambda()
{
// Regression test for issue #3471
// This should compile and work correctly
var dictionary = new Dictionary<string, string>
{
{ "key", "value" }
};

// This works fine:
await Assert.That(dictionary).Contains(x => x.Key == "key");

// This should also work (was failing to compile before fix):
await Assert.That(dictionary).IsNotNull().And.Contains(x => x.Key == "key");
}

[Test]
public async Task NotNull_List_Chaining_With_Contains_Lambda()
{
var list = new List<int> { 1, 2, 3, 4, 5 };

// Verify that lambda type inference works after IsNotNull().And for lists
await Assert.That(list).IsNotNull().And.Contains(x => x > 3);
}
}
32 changes: 32 additions & 0 deletions TUnit.Assertions/Conditions/CollectionNullAssertion.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,32 @@
using System.Collections;
using TUnit.Assertions.Core;
using TUnit.Assertions.Sources;

namespace TUnit.Assertions.Conditions;

/// <summary>
/// Asserts that a collection is not null, preserving collection type information.
/// Extends CollectionAssertionBase to ensure .And and .Or return collection-specific continuations.
/// </summary>
public class CollectionNotNullAssertion<TCollection, TItem> : CollectionAssertionBase<TCollection, TItem>
where TCollection : IEnumerable<TItem>
{
public CollectionNotNullAssertion(AssertionContext<TCollection> context)
: base(context)
{
}

protected override Task<AssertionResult> CheckAsync(EvaluationMetadata<TCollection> metadata)
{
var value = metadata.Value;

if (value != null)
{
return Task.FromResult(AssertionResult.Passed);
}

return Task.FromResult(AssertionResult.Failed("value is null"));
}

protected override string GetExpectation() => "to not be null";
}
16 changes: 16 additions & 0 deletions TUnit.Assertions/Extensions/AssertionExtensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,22 @@ public static NotNullAssertion<TValue> IsNotNull<TValue>(
return new NotNullAssertion<TValue>(mappedContext);
}

/// <summary>
/// Asserts that a collection is not null, preserving collection type information.
/// Returns a collection-aware assertion that maintains TItem type for proper chaining.
/// This overload enables: Assert.That(collection).IsNotNull().And.Contains(x => predicate).
/// </summary>
public static CollectionNotNullAssertion<TCollection, TItem> IsNotNull<TCollection, TItem>(
this CollectionAssertionBase<TCollection, TItem> source)
where TCollection : class, IEnumerable<TItem>
{
var assertionSource = (IAssertionSource<TCollection>)source;
assertionSource.Context.ExpressionBuilder.Append(".IsNotNull()");
// Map from TCollection? to TCollection (nullable to non-nullable)
var mappedContext = assertionSource.Context.Map((TCollection? v) => v!);
return new CollectionNotNullAssertion<TCollection, TItem>(mappedContext);
}

// ============ EQUALITY ============
// IsEqualTo methods are now generated by AssertionExtensionGenerator for all assertion types

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,13 @@ namespace .Conditions
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public class CollectionNotNullAssertion<TCollection, TItem> : .<TCollection, TItem>
where TCollection : .<TItem>
{
public CollectionNotNullAssertion(.<TCollection> context) { }
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public abstract class ComparerBasedAssertion<TValue, TItem> : .<TValue>
{
protected ComparerBasedAssertion(.<TValue> context) { }
Expand Down Expand Up @@ -1850,6 +1857,8 @@ namespace .Extensions
where TValue : class { }
public static .<TValue> IsNotNull<TValue>(this .<TValue?> source)
where TValue : struct { }
public static .<TCollection, TItem> IsNotNull<TCollection, TItem>(this .<TCollection, TItem> source)
where TCollection : class, .<TItem> { }
public static ..IsNotParsableIntoAssertion<T> IsNotParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this .<string> source) { }
public static .<TValue> IsOfType<TValue>(this .<TValue> source, expectedType, [.("expectedType")] string? expression = null) { }
public static ..IsParsableIntoAssertion<T> IsParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this .<string> source) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -504,6 +504,13 @@ namespace .Conditions
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public class CollectionNotNullAssertion<TCollection, TItem> : .<TCollection, TItem>
where TCollection : .<TItem>
{
public CollectionNotNullAssertion(.<TCollection> context) { }
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public abstract class ComparerBasedAssertion<TValue, TItem> : .<TValue>
{
protected ComparerBasedAssertion(.<TValue> context) { }
Expand Down Expand Up @@ -1847,6 +1854,8 @@ namespace .Extensions
where TValue : class { }
public static .<TValue> IsNotNull<TValue>(this .<TValue?> source)
where TValue : struct { }
public static .<TCollection, TItem> IsNotNull<TCollection, TItem>(this .<TCollection, TItem> source)
where TCollection : class, .<TItem> { }
public static ..IsNotParsableIntoAssertion<T> IsNotParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this .<string> source) { }
public static .<TValue> IsOfType<TValue>(this .<TValue> source, expectedType, [.("expectedType")] string? expression = null) { }
public static ..IsParsableIntoAssertion<T> IsParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this .<string> source) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -507,6 +507,13 @@ namespace .Conditions
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public class CollectionNotNullAssertion<TCollection, TItem> : .<TCollection, TItem>
where TCollection : .<TItem>
{
public CollectionNotNullAssertion(.<TCollection> context) { }
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public abstract class ComparerBasedAssertion<TValue, TItem> : .<TValue>
{
protected ComparerBasedAssertion(.<TValue> context) { }
Expand Down Expand Up @@ -1850,6 +1857,8 @@ namespace .Extensions
where TValue : class { }
public static .<TValue> IsNotNull<TValue>(this .<TValue?> source)
where TValue : struct { }
public static .<TCollection, TItem> IsNotNull<TCollection, TItem>(this .<TCollection, TItem> source)
where TCollection : class, .<TItem> { }
public static ..IsNotParsableIntoAssertion<T> IsNotParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this .<string> source) { }
public static .<TValue> IsOfType<TValue>(this .<TValue> source, expectedType, [.("expectedType")] string? expression = null) { }
public static ..IsParsableIntoAssertion<T> IsParsableInto<[.(..None | ..PublicMethods | ..Interfaces)] T>(this .<string> source) { }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -502,6 +502,13 @@ namespace .Conditions
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public class CollectionNotNullAssertion<TCollection, TItem> : .<TCollection, TItem>
where TCollection : .<TItem>
{
public CollectionNotNullAssertion(.<TCollection> context) { }
protected override .<.> CheckAsync(.<TCollection> metadata) { }
protected override string GetExpectation() { }
}
public abstract class ComparerBasedAssertion<TValue, TItem> : .<TValue>
{
protected ComparerBasedAssertion(.<TValue> context) { }
Expand Down Expand Up @@ -1729,6 +1736,8 @@ namespace .Extensions
where TValue : class { }
public static .<TValue> IsNotNull<TValue>(this .<TValue?> source)
where TValue : struct { }
public static .<TCollection, TItem> IsNotNull<TCollection, TItem>(this .<TCollection, TItem> source)
where TCollection : class, .<TItem> { }
public static ..IsNotParsableIntoAssertion<T> IsNotParsableInto<T>(this .<string> source) { }
public static .<TValue> IsOfType<TValue>(this .<TValue> source, expectedType, [.("expectedType")] string? expression = null) { }
public static ..IsParsableIntoAssertion<T> IsParsableInto<T>(this .<string> source) { }
Expand Down
Loading