Skip to content

Commit 076fb6a

Browse files
authored
Close the object for empty combination results, and keep the space in flattened messages (#1867)
Two edge cases in CombinationResultsConverter: * Write started an object then returned early for empty results without closing it, so anything written after the results was swallowed into them. Reachable by constructing CombinationResults directly, since the runner rejects empty lists. * FlattenMessage only appended ". " when a line did not already end in a period, so a line that did was run straight into the next one. The two line net48 ArgumentNullException rendered as "Value cannot be null.Parameter name: p".
1 parent d3a9e7c commit 076fb6a

7 files changed

Lines changed: 54 additions & 5 deletions

docs/combinations.md

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,7 @@ public class CombinationResultsConverter :
198198
var items = results.Items;
199199
if (items.Count == 0)
200200
{
201+
writer.WriteEndObject();
201202
return;
202203
}
203204

@@ -363,16 +364,19 @@ public class CombinationResultsConverter :
363364
builder.Append(trimmed);
364365
if (!trimmed.EndsWith('.'))
365366
{
366-
builder.Append(". ");
367+
builder.Append('.');
367368
}
369+
370+
// separate from the next line even when the sentence was already terminated
371+
builder.Append(' ');
368372
}
369373

370374
builder.TrimEnd();
371375
return builder.ToString();
372376
}
373377
}
374378
```
375-
<sup><a href='/src/Verify/Combinations/CombinationResultsConverter.cs#L1-L185' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationResultsConverter.cs' title='Start of snippet'>anchor</a></sup>
379+
<sup><a href='/src/Verify/Combinations/CombinationResultsConverter.cs#L1-L189' title='Snippet source file'>snippet source</a> | <a href='#snippet-CombinationResultsConverter.cs' title='Start of snippet'>anchor</a></sup>
376380
<!-- endSnippet -->
377381

378382

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
{}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
results: {},
3+
after: TheValue
4+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
1: ArgumentException: Value cannot be null. Parameter name: p.,
3+
10: ArgumentException: Value cannot be null. Parameter name: p.
4+
}

src/Verify.Tests/CombinationTests.cs

Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -159,6 +159,38 @@ public Task RecordingWithExceptionPausedTest()
159159
.IgnoreStackTrace();
160160
}
161161

162+
static string ThrowMultiLine(int value) =>
163+
throw new ArgumentException(
164+
"""
165+
Value cannot be null.
166+
Parameter name: p
167+
""");
168+
169+
// ArgumentException messages are flattened onto one line, so the parts of the
170+
// message need a separator between them
171+
[Fact]
172+
public Task MultiLineArgumentException() =>
173+
Combination(captureExceptions: true)
174+
.Verify(
175+
ThrowMultiLine,
176+
params1);
177+
178+
// Only reachable by constructing the results directly, since the runner requires
179+
// every list to have at least one item
180+
[Fact]
181+
public Task EmptyResults() =>
182+
Verify(new CombinationResults([], [], null));
183+
184+
// an unclosed object would swallow everything written after it
185+
[Fact]
186+
public Task EmptyResultsNested() =>
187+
Verify(
188+
new
189+
{
190+
results = new CombinationResults([], [], null),
191+
after = "TheValue"
192+
});
193+
162194
[Fact]
163195
public Task RecordingPausedTest()
164196
{

src/Verify/Combinations/CombinationResultsConverter.cs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ public override void Write(VerifyJsonWriter writer, CombinationResults results)
1010
var items = results.Items;
1111
if (items.Count == 0)
1212
{
13+
writer.WriteEndObject();
1314
return;
1415
}
1516

@@ -175,8 +176,11 @@ static string FlattenMessage(string message)
175176
builder.Append(trimmed);
176177
if (!trimmed.EndsWith('.'))
177178
{
178-
builder.Append(". ");
179+
builder.Append('.');
179180
}
181+
182+
// separate from the next line even when the sentence was already terminated
183+
builder.Append(' ');
180184
}
181185

182186
builder.TrimEnd();

src/todo.md

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -67,10 +67,10 @@ All six resolved 2026-08-16 (five fixed here; the inline item resolved as by-des
6767
- [x] **MSTest source generator ignores `record` test classes.**
6868
`Verify.MSTest.SourceGenerator/UsesVerifyGenerator.cs:125-126` — only `ClassDeclarationSyntax` is eligible; `[UsesVerify] [TestClass] partial record` compiles then fails at runtime with the misleading "TestContext is null" error. `Parser.GetParentClasses` similarly stops at a `record struct` parent.
6969

70-
- [ ] **Unclosed JSON object for empty `CombinationResults`.**
70+
- [x] **Unclosed JSON object for empty `CombinationResults`.**
7171
`Verify/Combinations/CombinationResultsConverter.cs:8-14``WriteStartObject()` then early `return` with no `WriteEndObject()`. Only reachable by constructing `CombinationResults([], ...)` directly.
7272

73-
- [ ] **`FlattenMessage` omits the joining space after a line ending in `.`.**
73+
- [x] **`FlattenMessage` omits the joining space after a line ending in `.`.**
7474
`Verify/Combinations/CombinationResultsConverter.cs:168-183` — two-line messages (net48 `ArgumentNullException`) render as `"Value cannot be null.Parameter name: p"`.
7575

7676
- [ ] **Negative sub-hour offsets render unsigned.**

0 commit comments

Comments
 (0)