-
Notifications
You must be signed in to change notification settings - Fork 2
Expand file tree
/
Copy pathEventPublisherSpecs.cs
More file actions
342 lines (300 loc) · 10.7 KB
/
Copy pathEventPublisherSpecs.cs
File metadata and controls
342 lines (300 loc) · 10.7 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading;
using FakeItEasy;
using OSPSuite.BDDHelper;
using OSPSuite.BDDHelper.Extensions;
using OSPSuite.Utility.Events;
using OSPSuite.Utility.Exceptions;
namespace OSPSuite.Utility.Tests
{
public abstract class concern_for_EventPublisher : ContextSpecification<IEventPublisher>
{
private IExceptionManager _exceptionManager;
protected RecordingSynchronizationContext _synchronizationContext;
protected override void Context()
{
_exceptionManager = new ExceptionManagerForTests();
_synchronizationContext = new RecordingSynchronizationContext();
sut = new EventPublisher(_synchronizationContext, _exceptionManager);
}
internal class ExceptionManagerForTests : ExceptionManagerBase
{
public override void LogException(Exception ex)
{
Exception = ex;
}
internal Exception Exception { get; set; }
}
// Runs the given action on a fresh thread whose current SynchronizationContext is the one
// supplied, so a test can control whether PublishEvent sees itself as running on the
// publisher's context thread (pass _synchronizationContext) or off it (pass null). Joining
// the thread before returning makes the recorded flags safe to read on the test thread.
protected static void runOnThreadWithSynchronizationContext(SynchronizationContext context, Action action)
{
Exception thrown = null;
var thread = new Thread(() =>
{
SynchronizationContext.SetSynchronizationContext(context);
try
{
action();
}
catch (Exception ex)
{
thrown = ex;
}
});
thread.Start();
thread.Join();
if (thrown != null)
throw thrown;
}
// Both Send and Post run the callback synchronously so listener notification stays
// deterministic in tests (the default SynchronizationContext.Post would queue to the
// ThreadPool and race the observations). The flags record which path PublishEvent chose.
protected class RecordingSynchronizationContext : SynchronizationContext
{
public bool PostWasCalled { get; private set; }
public bool SendWasCalled { get; private set; }
public override void Post(SendOrPostCallback d, object state)
{
PostWasCalled = true;
d(state);
}
public override void Send(SendOrPostCallback d, object state)
{
SendWasCalled = true;
d(state);
}
}
}
public abstract class concern_for_publishing_an_event : concern_for_EventPublisher
{
protected object _eventToPublish;
protected IListener<object> _listener;
protected override void Context()
{
base.Context();
_eventToPublish = A.Fake<object>();
_listener = A.Fake<IListener<object>>();
sut.AddListener(_listener);
}
}
public class When_publishing_an_event_on_behalf_of_a_publisher : concern_for_publishing_an_event
{
protected override void Because()
{
sut.PublishEvent(_eventToPublish);
}
[Observation]
public void should_notify_all_listeners_of_the_event()
{
A.CallTo(() => _listener.Handle(_eventToPublish)).MustHaveHappened();
}
}
public class When_publishing_an_event_from_the_thread_that_owns_the_synchronization_context : concern_for_publishing_an_event
{
protected override void Because()
{
// simulate publishing from the UI thread: the publishing thread's current context
// is the very context the publisher dispatches to
runOnThreadWithSynchronizationContext(_synchronizationContext, () => sut.PublishEvent(_eventToPublish));
}
[Observation]
public void should_dispatch_synchronously_through_the_blocking_send_path()
{
_synchronizationContext.SendWasCalled.ShouldBeTrue();
_synchronizationContext.PostWasCalled.ShouldBeFalse();
}
[Observation]
public void should_notify_all_listeners_of_the_event()
{
A.CallTo(() => _listener.Handle(_eventToPublish)).MustHaveHappened();
}
}
public class When_publishing_an_event_from_a_thread_that_does_not_own_the_synchronization_context : concern_for_publishing_an_event
{
protected override void Because()
{
// simulate publishing from a background thread: the publishing thread does not share
// the publisher's context
runOnThreadWithSynchronizationContext(null, () => sut.PublishEvent(_eventToPublish));
}
[Observation]
public void should_dispatch_through_the_non_blocking_post_path()
{
_synchronizationContext.PostWasCalled.ShouldBeTrue();
_synchronizationContext.SendWasCalled.ShouldBeFalse();
}
[Observation]
public void should_notify_all_listeners_of_the_event()
{
A.CallTo(() => _listener.Handle(_eventToPublish)).MustHaveHappened();
}
}
public class When_asked_to_add_a_listener : concern_for_EventPublisher
{
private IListener<object> _listener;
[Observation]
public void should_register_the_listener_into_the_underlaying_structure()
{
sut.Contains(_listener).ShouldBeTrue();
}
protected override void Because()
{
sut.AddListener(_listener);
}
protected override void Context()
{
base.Context();
_listener = A.Fake<IListener<object>>();
}
}
public class When_asked_to_remove_a_listener : concern_for_EventPublisher
{
private IListener<object> listener1;
private IListener<object> listener2;
[Observation]
public void should_remove_the_listener_from_the_underlaying_structure()
{
sut.Contains(listener1).ShouldBeTrue();
sut.Contains(listener2).ShouldBeFalse();
}
protected override void Because()
{
sut.RemoveListener(listener2);
}
protected override void Context()
{
base.Context();
listener1 = A.Fake<IListener<object>>();
listener2 = A.Fake<IListener<object>>();
sut.AddListener(listener1);
sut.AddListener(listener2);
}
}
public class When_publishing_an_event_for_a_class_that_listens_to_many_events : concern_for_EventPublisher
{
private IFakeListener listener;
private FakeEvent1 fakeEvent1;
private FakeEvent2 fakeEvent2;
[Observation]
public void shoulld_notify_the_class_only_once()
{
A.CallTo(() => listener.Handle(fakeEvent1)).MustHaveHappened();
A.CallTo(() => listener.Handle(fakeEvent2)).MustNotHaveHappened();
}
protected override void Because()
{
sut.PublishEvent(fakeEvent1);
}
protected override void Context()
{
base.Context();
listener = A.Fake<IFakeListener>();
fakeEvent1 = A.Fake<FakeEvent1>();
fakeEvent2 = A.Fake<FakeEvent2>();
sut.AddListener(listener);
}
}
public class When_publishing_events_concurrently_while_listeners_are_added_and_removed : concern_for_EventPublisher
{
private List<Exception> _exceptions;
private int _completedOperations;
private const int _threadCount = 8;
private const int _iterations = 5000;
// publisher loops + mutator loops, each running _threadCount x _iterations operations
private const int _expectedOperationCount = 2 * _threadCount * _iterations;
protected override void Context()
{
base.Context();
_exceptions = new List<Exception>();
// seed a handful of stable listeners so every publish has a non-trivial list to snapshot
for (var i = 0; i < 50; i++)
{
sut.AddListener(new StressListener());
}
}
protected override void Because()
{
var startSignal = new ManualResetEventSlim(false);
var threads = new List<Thread>();
// publisher threads: each PublishEvent prunes and snapshots the listener list
for (var t = 0; t < _threadCount; t++)
{
threads.Add(threadRunning(startSignal, () =>
{
for (var i = 0; i < _iterations; i++)
{
sut.PublishEvent(new object());
Interlocked.Increment(ref _completedOperations);
}
}));
}
// mutator threads: churn the listener list so Add (which can grow the backing
// array) overlaps the publishers' snapshot
for (var t = 0; t < _threadCount; t++)
{
threads.Add(threadRunning(startSignal, () =>
{
for (var i = 0; i < _iterations; i++)
{
var listener = new StressListener();
sut.AddListener(listener);
sut.RemoveListener(listener);
Interlocked.Increment(ref _completedOperations);
}
}));
}
threads.ForEach(x => x.Start());
startSignal.Set();
threads.ForEach(x => x.Join());
}
private Thread threadRunning(ManualResetEventSlim startSignal, Action action)
{
return new Thread(() =>
{
startSignal.Wait();
try
{
action();
}
catch (Exception ex)
{
lock (_exceptions)
{
_exceptions.Add(ex);
}
}
});
}
// Single observation on purpose: the BDD harness re-runs Context/Because per [Observation],
// so a second observation would silently re-run the whole stress loop.
[Observation]
public void should_complete_all_concurrent_operations_without_throwing()
{
var messages = string.Join(Environment.NewLine, _exceptions.Select(ex => $"{ex.GetType().Name}: {ex.Message}"));
messages.ShouldBeEqualTo(string.Empty);
// guard against a vacuous pass: confirm every publisher and mutator thread ran its full loop
_completedOperations.ShouldBeEqualTo(_expectedOperationCount);
}
private class StressListener : IListener<object>
{
public void Handle(object eventToHandle)
{
// no-op; the test exercises the publish/listener-list machinery, not the handler
}
}
}
public class FakeEvent1
{
}
public class FakeEvent2
{
}
public interface IFakeListener : IListener<FakeEvent1>, IListener<FakeEvent2>
{
}
}