diff --git a/src/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.ConcurrentAccessDetection.netcoreapp.cs b/src/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.ConcurrentAccessDetection.netcoreapp.cs new file mode 100644 index 000000000000..d14a965c26c7 --- /dev/null +++ b/src/System.Collections/tests/Generic/Dictionary/Dictionary.Generic.Tests.ConcurrentAccessDetection.netcoreapp.cs @@ -0,0 +1,129 @@ +// Licensed to the .NET Foundation under one or more agreements. +// The .NET Foundation licenses this file to you under the MIT license. +// See the LICENSE file in the project root for more information. + +using System; +using System.Collections; +using System.Collections.Generic; +using System.Reflection; +using System.Threading.Tasks; +using Xunit; + +namespace Generic.Dictionary +{ + public class DictionaryConcurrentAccessDetectionTests + { + private async Task DictionaryConcurrentAccessDetection(Dictionary dictionary, bool isValueType, object comparer, Action> add, Action> get, Action> remove, Action> removeOutParam) + { + Task task = Task.Factory.StartNew(() => + { + // Get the Dictionary into a corrupted state, as if it had been corrupted by concurrent access. + // We this deterministically by clearing the _entries array using reflection; + // this means that every Entry struct has a 'next' field of zero, which causes the infinite loop + // that we want Dictionary to break out of + FieldInfo entriesType = dictionary.GetType().GetField("_entries", BindingFlags.NonPublic | BindingFlags.Instance); + Array entriesInstance = (Array)entriesType.GetValue(dictionary); + Array entryArray = (Array)Activator.CreateInstance(entriesInstance.GetType(), new object[] { ((IDictionary)dictionary).Count }); + entriesType.SetValue(dictionary, entryArray); + + Assert.Equal(comparer, dictionary.GetType().GetField("_comparer", BindingFlags.NonPublic | BindingFlags.Instance).GetValue(dictionary)); + Assert.Equal(isValueType, dictionary.GetType().GetGenericArguments()[0].IsValueType); + Assert.Equal("ThrowInvalidOperationException_ConcurrentOperationsNotSupported", Assert.Throws(() => add(dictionary)).TargetSite.Name); + Assert.Equal("ThrowInvalidOperationException_ConcurrentOperationsNotSupported", Assert.Throws(() => get(dictionary)).TargetSite.Name); + Assert.Equal("ThrowInvalidOperationException_ConcurrentOperationsNotSupported", Assert.Throws(() => remove(dictionary)).TargetSite.Name); + Assert.Equal("ThrowInvalidOperationException_ConcurrentOperationsNotSupported", Assert.Throws(() => removeOutParam(dictionary)).TargetSite.Name); + }, TaskCreationOptions.LongRunning); + + // If Dictionary regresses, we do not want to hang here indefinitely + Assert.True((await Task.WhenAny(task, Task.Delay(TimeSpan.FromSeconds(60))) == task) && task.IsCompletedSuccessfully); + } + + [Theory] + [InlineData(null)] + [InlineData(typeof(CustomEqualityComparerInt32ValueType))] + public async Task DictionaryConcurrentAccessDetection_ValueTypeKey(Type comparerType) + { + IEqualityComparer customComparer = null; + + Dictionary dic = comparerType == null ? + new Dictionary() : + new Dictionary((customComparer = (IEqualityComparer)Activator.CreateInstance(comparerType))); + + dic.Add(1, 1); + + await DictionaryConcurrentAccessDetection(dic, + typeof(int).IsValueType, + customComparer, + d => d.Add(1, 1), + d => { var v = d[1]; }, + d => d.Remove(1), + d => d.Remove(1, out int value)); + } + + [Theory] + [InlineData(null)] + [InlineData(typeof(CustomEqualityComparerDummyRefType))] + public async Task DictionaryConcurrentAccessDetection_ReferenceTypeKey(Type comparerType) + { + IEqualityComparer customComparer = null; + + Dictionary dic = comparerType == null ? + new Dictionary() : + new Dictionary((customComparer = (IEqualityComparer)Activator.CreateInstance(comparerType))); + + var keyValueSample = new DummyRefType() { Value = 1 }; + + dic.Add(keyValueSample, keyValueSample); + + await DictionaryConcurrentAccessDetection(dic, + typeof(DummyRefType).IsValueType, + customComparer, + d => d.Add(keyValueSample, keyValueSample), + d => { var v = d[keyValueSample]; }, + d => d.Remove(keyValueSample), + d => d.Remove(keyValueSample, out DummyRefType value)); + } + } + + // We use a custom type instead of string because string use optimized comparer https://github.com/dotnet/coreclr/blob/master/src/System.Private.CoreLib/shared/System/Collections/Generic/Dictionary.cs#L79 + // We want to test case with _comparer = null + class DummyRefType + { + public int Value { get; set; } + public override bool Equals(object obj) + { + return ((DummyRefType)obj).Equals(this.Value); + } + + public override int GetHashCode() + { + return Value.GetHashCode(); + } + } + + class CustomEqualityComparerDummyRefType : EqualityComparer + { + public override bool Equals(DummyRefType x, DummyRefType y) + { + return x.Value == y.Value; + } + + public override int GetHashCode(DummyRefType obj) + { + return obj.GetHashCode(); + } + } + + class CustomEqualityComparerInt32ValueType : EqualityComparer + { + public override bool Equals(int x, int y) + { + return EqualityComparer.Default.Equals(x, y); + } + + public override int GetHashCode(int obj) + { + return EqualityComparer.Default.GetHashCode(obj); + } + } +} diff --git a/src/System.Collections/tests/System.Collections.Tests.csproj b/src/System.Collections/tests/System.Collections.Tests.csproj index 40039d447f5d..8e1ec81bb0a1 100644 --- a/src/System.Collections/tests/System.Collections.Tests.csproj +++ b/src/System.Collections/tests/System.Collections.Tests.csproj @@ -22,15 +22,9 @@ Common\System\Collections\IDictionary.NonGeneric.Tests.cs - - Common\System\Collections\IDictionary.NonGeneric.Tests.netcoreapp.cs - Common\System\Collections\IDictionary.Generic.Tests.cs - - Common\System\Collections\IDictionary.Generic.Tests.netcoreapp.cs - Common\System\Collections\IEnumerable.NonGeneric.Tests.cs @@ -77,14 +71,11 @@ Common\System\Runtime\Serialization\Formatters\BinaryFormatterHelpers.cs - - - @@ -99,7 +90,6 @@ - @@ -131,7 +121,6 @@ - @@ -145,12 +134,10 @@ - - Common\System\Diagnostics\DebuggerAttributes.cs @@ -162,6 +149,22 @@ Common\System\Collections\IEnumerable.Generic.Serialization.Tests.cs + + + + + + + + + + + Common\System\Collections\IDictionary.Generic.Tests.netcoreapp.cs + + + Common\System\Collections\IDictionary.NonGeneric.Tests.netcoreapp.cs + +