diff --git a/docs/recording.md b/docs/recording.md
index 0a1c738fb..03928388e 100644
--- a/docs/recording.md
+++ b/docs/recording.md
@@ -25,7 +25,7 @@ public Task Usage()
return Verify("TheValue");
}
```
-snippet source | anchor
+snippet source | anchor
Results in:
@@ -61,7 +61,7 @@ public Task TryAdd()
return Verify("TheValue");
}
```
-snippet source | anchor
+snippet source | anchor
@@ -85,7 +85,7 @@ public Task RecordingScoped()
return Verify();
}
```
-snippet source | anchor
+snippet source | anchor
Results in:
@@ -117,7 +117,7 @@ public Task SameKey()
return Verify("TheValue");
}
```
-snippet source | anchor
+snippet source | anchor
Results in:
@@ -156,7 +156,7 @@ public Task Identifier()
return Verify(Recording.Stop("identifier"));
}
```
-snippet source | anchor
+snippet source | anchor
Results in:
@@ -188,7 +188,7 @@ public Task Case()
return Verify("TheValue");
}
```
-snippet source | anchor
+snippet source | anchor
Results in:
@@ -223,7 +223,7 @@ public Task Stop()
return Verify(appends.Where(_ => _.Name != "name1"));
}
```
-snippet source | anchor
+snippet source | anchor
Results in:
@@ -255,7 +255,7 @@ public Task StopNotInResult()
return Verify("other data");
}
```
-snippet source | anchor
+snippet source | anchor
Results in:
@@ -284,7 +284,7 @@ public void IsRecording()
Assert.True(Recording.IsRecording());
}
```
-snippet source | anchor
+snippet source | anchor
This can be helpful if the cost of capturing data, to add to recording, is high.
@@ -307,7 +307,7 @@ public Task Clear()
return Verify();
}
```
-snippet source | anchor
+snippet source | anchor
Results in:
@@ -343,7 +343,7 @@ public Task PauseResume()
return Verify();
}
```
-snippet source | anchor
+snippet source | anchor
Results in:
diff --git a/src/Verify.Tests/RecordingTests.cs b/src/Verify.Tests/RecordingTests.cs
index 3364ddbf6..0214ad808 100644
--- a/src/Verify.Tests/RecordingTests.cs
+++ b/src/Verify.Tests/RecordingTests.cs
@@ -21,6 +21,33 @@ public async Task StoppedInChildContextIsNotRecording()
Assert.False(Recording.IsRecording());
}
+ // The engine consumes the recording in a child context, so the null does not flow back
+ // and the test's own context still holds the stopped state. Starting again after that
+ // is the supported pattern, not a double start.
+ [Fact]
+ public async Task StartAfterConsumedInChildContext()
+ {
+ Recording.Start();
+ Recording.Add("name", "value1");
+ await Task.Run(() => Recording.TryStop(out _));
+
+ Recording.Start();
+ Recording.Add("name", "value2");
+
+ Assert.True(Recording.TryStop(out var recorded));
+ Assert.Equal(["value2"], recorded.Select(_ => _.Data));
+ }
+
+ [Fact]
+ public void StartWhileRecordingStillThrows()
+ {
+ using (Recording.Start())
+ {
+ var exception = Assert.Throws(Recording.Start);
+ Assert.Equal("Recording already started", exception.Message);
+ }
+ }
+
[Fact]
public void DisposeAfterStopDoesNotThrow()
{
diff --git a/src/Verify/Recording/Recording.cs b/src/Verify/Recording/Recording.cs
index f24828ca4..2012a6736 100644
--- a/src/Verify/Recording/Recording.cs
+++ b/src/Verify/Recording/Recording.cs
@@ -79,8 +79,7 @@ public static bool TryStop([NotNullWhen(true)] out IReadOnlyCollection
// Snapshot the items, then stop the shared State so the stop is observable
// through the caller's reference.
recorded = value.Items.ToList();
- value.Clear();
- value.Pause();
+ value.Stop();
asyncLocal.Value = null;
return true;
}
@@ -125,7 +124,10 @@ public static IDisposable Start()
{
var value = asyncLocal.Value;
- if (value != null)
+ // A stopped state is one the caller's context is still holding after the recording
+ // was consumed, for example by a Verify. That is finished, so starting again is
+ // valid: only a live recording is a double start.
+ if (value is {Stopped: false})
{
throw new("Recording already started");
}
diff --git a/src/Verify/Recording/State.cs b/src/Verify/Recording/State.cs
index 73b4371c5..aa0471958 100644
--- a/src/Verify/Recording/State.cs
+++ b/src/Verify/Recording/State.cs
@@ -23,6 +23,20 @@ public void Add(string name, object item)
items.Enqueue(append);
}
+ ///
+ /// Set once the recording has been consumed. Nulling the AsyncLocal does not flow back
+ /// to the caller when the engine stops a recording from inside the verification, so the
+ /// stop has to be observable through the caller's own reference to this state.
+ ///
+ public bool Stopped { get; private set; }
+
+ public void Stop()
+ {
+ Clear();
+ Pause();
+ Stopped = true;
+ }
+
public void Pause() =>
Paused = true;