Skip to content

Commit 3604155

Browse files
committed
fix(should): restore dictionary size/count methods lost to new shadowing
The dictionary-continuation `public new` overloads added to DictionaryAssertionBase/MutableDictionaryAssertionBase (IsEmpty, IsNotEmpty, HasSingleItem, HasAtLeast, HasAtMost, HasCountBetween) return the abstract dictionary assertion base, which has no public constructor. The Should wrapper generator maps an instance method only when its return type exposes a constructible ctor, so these shadowed methods were silently skipped and the generated BeEmpty/NotBeEmpty/HaveSingleItem/HaveAtLeast/HaveAtMost/ HaveCountBetween counterparts disappeared from ShouldDictionarySource and ShouldMutableDictionarySource — a public API regression caught by the PublicAPI snapshot test. Hand-write the six methods on each Should dictionary source (mirroring the existing ContainKey etc.), returning ShouldAssertion<dict> backed by the collection assertions, restoring the exact prior surface. Add functional coverage so the regression can't recur silently.
1 parent 4124178 commit 3604155

2 files changed

Lines changed: 147 additions & 0 deletions

File tree

TUnit.Assertions.Should.Tests/CollectionTests.cs

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -108,6 +108,45 @@ public async Task Dictionary_ContainKeyWithValue()
108108
await dict.Should().ContainKeyWithValue("one", 1);
109109
}
110110

111+
[Test]
112+
public async Task Dictionary_size_and_count_methods()
113+
{
114+
IReadOnlyDictionary<string, int> dict = new Dictionary<string, int>
115+
{
116+
["one"] = 1,
117+
["two"] = 2,
118+
};
119+
120+
await dict.Should().NotBeEmpty();
121+
await dict.Should().HaveAtLeast(1);
122+
await dict.Should().HaveAtMost(5);
123+
await dict.Should().HaveCountBetween(1, 3);
124+
125+
IReadOnlyDictionary<string, int> empty = new Dictionary<string, int>();
126+
await empty.Should().BeEmpty();
127+
128+
IReadOnlyDictionary<string, int> single = new Dictionary<string, int> { ["only"] = 1 };
129+
await single.Should().HaveSingleItem();
130+
}
131+
132+
[Test]
133+
public async Task MutableDictionary_size_and_count_methods()
134+
{
135+
IDictionary<string, int> dict = new Dictionary<string, int>
136+
{
137+
["one"] = 1,
138+
["two"] = 2,
139+
};
140+
141+
await dict.Should().NotBeEmpty();
142+
await dict.Should().HaveAtLeast(1);
143+
await dict.Should().HaveAtMost(5);
144+
await dict.Should().HaveCountBetween(1, 3);
145+
146+
IDictionary<string, int> single = new Dictionary<string, int> { ["only"] = 1 };
147+
await single.Should().HaveSingleItem();
148+
}
149+
111150
[Test]
112151
public async Task HashSet_BeSupersetOf()
113152
{

TUnit.Assertions.Should/Core/ShouldDictionarySource.cs

Lines changed: 108 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,61 @@ public ShouldAssertion<IReadOnlyDictionary<TKey, TValue>> AnyValue(
117117
var inner = ApplyBecause(new DictionaryAnyValueAssertion<IReadOnlyDictionary<TKey, TValue>, TKey, TValue>(Context, predicate));
118118
return new ShouldAssertion<IReadOnlyDictionary<TKey, TValue>>(Context, inner);
119119
}
120+
121+
// The count/size methods below are hand-written because the source DictionaryAssertion shadows the
122+
// inherited collection methods (IsEmpty, HasSingleItem, ...) with dictionary-typed `public new`
123+
// overloads whose abstract return type the Should generator can't construct. Without these the
124+
// generated Be/Have counterparts would silently disappear from the dictionary Should surface.
125+
126+
public ShouldAssertion<IReadOnlyDictionary<TKey, TValue>> BeEmpty()
127+
{
128+
Context.ExpressionBuilder.Append(".BeEmpty()");
129+
var inner = ApplyBecause(new CollectionIsEmptyAssertion<IReadOnlyDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context));
130+
return new ShouldAssertion<IReadOnlyDictionary<TKey, TValue>>(Context, inner);
131+
}
132+
133+
public ShouldAssertion<IReadOnlyDictionary<TKey, TValue>> NotBeEmpty()
134+
{
135+
Context.ExpressionBuilder.Append(".NotBeEmpty()");
136+
var inner = ApplyBecause(new CollectionIsNotEmptyAssertion<IReadOnlyDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context));
137+
return new ShouldAssertion<IReadOnlyDictionary<TKey, TValue>>(Context, inner);
138+
}
139+
140+
public ShouldAssertion<IReadOnlyDictionary<TKey, TValue>> HaveSingleItem()
141+
{
142+
Context.ExpressionBuilder.Append(".HaveSingleItem()");
143+
var inner = ApplyBecause(new HasSingleItemAssertion<IReadOnlyDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context));
144+
return new ShouldAssertion<IReadOnlyDictionary<TKey, TValue>>(Context, inner);
145+
}
146+
147+
public ShouldAssertion<IReadOnlyDictionary<TKey, TValue>> HaveAtLeast(
148+
int minCount,
149+
[CallerArgumentExpression(nameof(minCount))] string? expression = null)
150+
{
151+
Context.ExpressionBuilder.Append(".HaveAtLeast(").Append(expression).Append(')');
152+
var inner = ApplyBecause(new CollectionHasAtLeastAssertion<IReadOnlyDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context, minCount));
153+
return new ShouldAssertion<IReadOnlyDictionary<TKey, TValue>>(Context, inner);
154+
}
155+
156+
public ShouldAssertion<IReadOnlyDictionary<TKey, TValue>> HaveAtMost(
157+
int maxCount,
158+
[CallerArgumentExpression(nameof(maxCount))] string? expression = null)
159+
{
160+
Context.ExpressionBuilder.Append(".HaveAtMost(").Append(expression).Append(')');
161+
var inner = ApplyBecause(new CollectionHasAtMostAssertion<IReadOnlyDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context, maxCount));
162+
return new ShouldAssertion<IReadOnlyDictionary<TKey, TValue>>(Context, inner);
163+
}
164+
165+
public ShouldAssertion<IReadOnlyDictionary<TKey, TValue>> HaveCountBetween(
166+
int min,
167+
int max,
168+
[CallerArgumentExpression(nameof(min))] string? minExpression = null,
169+
[CallerArgumentExpression(nameof(max))] string? maxExpression = null)
170+
{
171+
Context.ExpressionBuilder.Append($".HaveCountBetween({minExpression}, {maxExpression})");
172+
var inner = ApplyBecause(new CollectionHasCountBetweenAssertion<IReadOnlyDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context, min, max));
173+
return new ShouldAssertion<IReadOnlyDictionary<TKey, TValue>>(Context, inner);
174+
}
120175
}
121176

122177
[ShouldGeneratePartial(typeof(MutableDictionaryAssertion<,>))]
@@ -228,4 +283,57 @@ public ShouldAssertion<IDictionary<TKey, TValue>> AnyValue(
228283
var inner = ApplyBecause(new MutableDictionaryAnyValueAssertion<IDictionary<TKey, TValue>, TKey, TValue>(Context, predicate));
229284
return new ShouldAssertion<IDictionary<TKey, TValue>>(Context, inner);
230285
}
286+
287+
// See ShouldDictionarySource: hand-written because MutableDictionaryAssertion shadows the inherited
288+
// collection methods with dictionary-typed `public new` overloads the Should generator can't construct.
289+
290+
public ShouldAssertion<IDictionary<TKey, TValue>> BeEmpty()
291+
{
292+
Context.ExpressionBuilder.Append(".BeEmpty()");
293+
var inner = ApplyBecause(new CollectionIsEmptyAssertion<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context));
294+
return new ShouldAssertion<IDictionary<TKey, TValue>>(Context, inner);
295+
}
296+
297+
public ShouldAssertion<IDictionary<TKey, TValue>> NotBeEmpty()
298+
{
299+
Context.ExpressionBuilder.Append(".NotBeEmpty()");
300+
var inner = ApplyBecause(new CollectionIsNotEmptyAssertion<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context));
301+
return new ShouldAssertion<IDictionary<TKey, TValue>>(Context, inner);
302+
}
303+
304+
public ShouldAssertion<IDictionary<TKey, TValue>> HaveSingleItem()
305+
{
306+
Context.ExpressionBuilder.Append(".HaveSingleItem()");
307+
var inner = ApplyBecause(new HasSingleItemAssertion<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context));
308+
return new ShouldAssertion<IDictionary<TKey, TValue>>(Context, inner);
309+
}
310+
311+
public ShouldAssertion<IDictionary<TKey, TValue>> HaveAtLeast(
312+
int minCount,
313+
[CallerArgumentExpression(nameof(minCount))] string? expression = null)
314+
{
315+
Context.ExpressionBuilder.Append(".HaveAtLeast(").Append(expression).Append(')');
316+
var inner = ApplyBecause(new CollectionHasAtLeastAssertion<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context, minCount));
317+
return new ShouldAssertion<IDictionary<TKey, TValue>>(Context, inner);
318+
}
319+
320+
public ShouldAssertion<IDictionary<TKey, TValue>> HaveAtMost(
321+
int maxCount,
322+
[CallerArgumentExpression(nameof(maxCount))] string? expression = null)
323+
{
324+
Context.ExpressionBuilder.Append(".HaveAtMost(").Append(expression).Append(')');
325+
var inner = ApplyBecause(new CollectionHasAtMostAssertion<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context, maxCount));
326+
return new ShouldAssertion<IDictionary<TKey, TValue>>(Context, inner);
327+
}
328+
329+
public ShouldAssertion<IDictionary<TKey, TValue>> HaveCountBetween(
330+
int min,
331+
int max,
332+
[CallerArgumentExpression(nameof(min))] string? minExpression = null,
333+
[CallerArgumentExpression(nameof(max))] string? maxExpression = null)
334+
{
335+
Context.ExpressionBuilder.Append($".HaveCountBetween({minExpression}, {maxExpression})");
336+
var inner = ApplyBecause(new CollectionHasCountBetweenAssertion<IDictionary<TKey, TValue>, KeyValuePair<TKey, TValue>>(Context, min, max));
337+
return new ShouldAssertion<IDictionary<TKey, TValue>>(Context, inner);
338+
}
231339
}

0 commit comments

Comments
 (0)