Occasionally I get the following error on my (team city) build server:
System.Collections.Generic.KeyNotFoundException : The given key was not present in the dictionary.
at FluentAssertions.Common.BaseDictionary`2.get_Item(TKey key)
at FluentAssertions.EventMonitoring.EventMonitoringExtensions.ShouldRaise(Object eventSource, String eventName, String reason, Object[] reasonParameters)
at FluentAssertions.EventMonitoring.EventMonitoringExtensions.ShouldRaisePropertyChangeFor[T](T eventSource, Expression`1 propertyExpression, String reason, Object[] reasonParameters)
at FluentAssertions.EventMonitoring.EventMonitoringExtensions.ShouldRaisePropertyChangeFor[T](T eventSource, Expression`1 propertyExpression)
at XXX.Tests.ViewModels.XXXXViewModelTests.WhenXXXIsSet_ThenPropertyChangedShouldBeRaised()
The problem seems to be located in the ShouldRaise extension method:
public static EventRecorder ShouldRaise(this object eventSource, string eventName, string reason, params object[] reasonParameters)
{
<>c__DisplayClass4 class2;
if (!eventRecordersMap.ContainsKey(eventSource))
{
throw new InvalidOperationException(string.Format("Object <{0}> is not being monitored for events. Use the MonitorEvents() extension method to start monitoring events.", eventSource));
}
EventRecorder source = Enumerable.FirstOrDefault<EventRecorder>(
eventRecordersMap[eventSource], //<= !!!Here the exception gets thrown!!!
new Func<EventRecorder, bool>(class2, (IntPtr) this.<ShouldRaise>b__3));
if (source == null)
{
throw new InvalidOperationException(string.Format("Object <{0}> does not expose an event named \"{1}\".", eventSource, eventName));
}
if (!source.Any<RecordedEvent>())
{
Verification.Fail("Expected object {1} to raise event {0}{2}, but it did not.", eventName, eventSource, reason, reasonParameters, new object[0]);
}
return source;
}
The eventRecordersMap instance member is a WeakDictionary<TKey, TValue>. What happens is that the ContainsKey and TryGetValue (used by the indexer) have a different way of evaluating. ContainsKey will verify that a key is present in the dictionary (it won't verify if the key is alive). TryGetValue will lookup the value associated with the key and verify if the value is alive. This is why the first if statement succeeds and the second one fails, causing the aforementioned exception.
All this must mean the garbage collector has kicked in and marked an instance of the event monitor (the thing that is the value in the WeakDictionary, an enumerable of event recorders if I'm not mistaken) as dead (not alive). How did this happen? I've setup a [SetUp] method where I put something along the lines "_viewModel.MonitorEvents();". Notice how I'm not holding on to the event monitor. By not holding on to it, it becomes eligible for garbage collection, hence the fact that only "sometimes" this failure manifests itself. I'm not saying FluentAssertions is at fault, but this is a nasty side-effect that can bite you later on (depending on how you've written your tests). Some may never even notice it (it works every time on my machine, but not on the build server).
Comments: Resolved with changeset 62591.
System.Collections.Generic.KeyNotFoundException : The given key was not present in the dictionary.
at FluentAssertions.Common.BaseDictionary`2.get_Item(TKey key)
at FluentAssertions.EventMonitoring.EventMonitoringExtensions.ShouldRaise(Object eventSource, String eventName, String reason, Object[] reasonParameters)
at FluentAssertions.EventMonitoring.EventMonitoringExtensions.ShouldRaisePropertyChangeFor[T](T eventSource, Expression`1 propertyExpression, String reason, Object[] reasonParameters)
at FluentAssertions.EventMonitoring.EventMonitoringExtensions.ShouldRaisePropertyChangeFor[T](T eventSource, Expression`1 propertyExpression)
at XXX.Tests.ViewModels.XXXXViewModelTests.WhenXXXIsSet_ThenPropertyChangedShouldBeRaised()
The problem seems to be located in the ShouldRaise extension method:
public static EventRecorder ShouldRaise(this object eventSource, string eventName, string reason, params object[] reasonParameters)
{
<>c__DisplayClass4 class2;
if (!eventRecordersMap.ContainsKey(eventSource))
{
throw new InvalidOperationException(string.Format("Object <{0}> is not being monitored for events. Use the MonitorEvents() extension method to start monitoring events.", eventSource));
}
EventRecorder source = Enumerable.FirstOrDefault<EventRecorder>(
eventRecordersMap[eventSource], //<= !!!Here the exception gets thrown!!!
new Func<EventRecorder, bool>(class2, (IntPtr) this.<ShouldRaise>b__3));
if (source == null)
{
throw new InvalidOperationException(string.Format("Object <{0}> does not expose an event named \"{1}\".", eventSource, eventName));
}
if (!source.Any<RecordedEvent>())
{
Verification.Fail("Expected object {1} to raise event {0}{2}, but it did not.", eventName, eventSource, reason, reasonParameters, new object[0]);
}
return source;
}
The eventRecordersMap instance member is a WeakDictionary<TKey, TValue>. What happens is that the ContainsKey and TryGetValue (used by the indexer) have a different way of evaluating. ContainsKey will verify that a key is present in the dictionary (it won't verify if the key is alive). TryGetValue will lookup the value associated with the key and verify if the value is alive. This is why the first if statement succeeds and the second one fails, causing the aforementioned exception.
All this must mean the garbage collector has kicked in and marked an instance of the event monitor (the thing that is the value in the WeakDictionary, an enumerable of event recorders if I'm not mistaken) as dead (not alive). How did this happen? I've setup a [SetUp] method where I put something along the lines "_viewModel.MonitorEvents();". Notice how I'm not holding on to the event monitor. By not holding on to it, it becomes eligible for garbage collection, hence the fact that only "sometimes" this failure manifests itself. I'm not saying FluentAssertions is at fault, but this is a nasty side-effect that can bite you later on (depending on how you've written your tests). Some may never even notice it (it works every time on my machine, but not on the build server).
Comments: Resolved with changeset 62591.