// Copyright (c) Microsoft Open Technologies, Inc. All rights reserved. See License.txt in the project root for license information. namespace ProductivityApiUnitTests { using System; using System.Collections.Generic; using System.Data.Entity; using System.Data.Entity.Core.EntityClient; using System.Data.Entity.Core.Objects; using System.Data.Entity.Infrastructure; using System.Data.Entity.Internal; using System.Data.Entity.Internal.Linq; using System.Threading.Tasks; using Moq; using Xunit; /// /// Unit tests that execute the various places where we have thread-safe code with multiple threads /// such that we have at least some chance of finding issues in this code. As with any test of this /// type just because these tests pass does not mean that the code is correct. On the other hand, /// if any test ever fails (EVEN ONCE) then we know there is a problem to be investigated. /// public class MultiThreadingTests : TestBase { #region Access to cached property type/getter/setter delegates [Fact] public void DbHelpers_GetPropertyTypes_can_be_accessed_from_multiple_threads_concurrently() { ExecuteInParallel( () => { var types = DbHelpers.GetPropertyTypes(typeof(TypeWithALotOfProperties)); Assert.Equal(50, types.Count); Assert.Equal(typeof(string), types["Property3"]); Assert.Equal(typeof(int), types["Property26"]); }); } [Fact] public void DbHelpers_GetPropertySetters_can_be_accessed_from_multiple_threads_concurrently() { ExecuteInParallel( () => { var setters = DbHelpers.GetPropertySetters(typeof(TypeWithALotOfProperties)); Assert.Equal(40, setters.Count); var testType = new TypeWithALotOfProperties(); setters["Property10"](testType, "UnicornsOnTheRun"); Assert.Equal("UnicornsOnTheRun", testType.Property10); setters["Property47"](testType, "UnicornNeXTcube"); Assert.Equal("UnicornNeXTcube", testType.Property47); }); } [Fact] public void DbHelpers_GetPropertyGetters_can_be_accessed_from_multiple_threads_concurrently() { ExecuteInParallel( () => { var getters = DbHelpers.GetPropertyGetters(typeof(TypeWithALotOfProperties)); var testType = new TypeWithALotOfProperties(); Assert.Equal(47, getters.Count); Assert.Equal("Hello", getters["Property3"](testType)); Assert.Equal(77, getters["Property26"](testType)); }); } #endregion #region Access to cached ObjectContext constructor delegates public class DummyObjectContext : ObjectContext { public DummyObjectContext(EntityConnection connection) : base(connection) { } } [Fact] public void DbCompiledModel_GetConstructorDelegate_can_be_accessed_from_multiple_threads_concurrently() { DbCompiledModel_GetConstructorDelegate_can_be_accessed_from_multiple_threads_concurrently_implementation(); } [Fact] public void DbCompiledModel_GetConstructorDelegate_for_non_derived_ObjectContext_can_be_accessed_from_multiple_threads_concurrently( ) { DbCompiledModel_GetConstructorDelegate_can_be_accessed_from_multiple_threads_concurrently_implementation(); } private void DbCompiledModel_GetConstructorDelegate_can_be_accessed_from_multiple_threads_concurrently_implementation() where TContext : ObjectContext { ExecuteInParallel( () => { var constructor = DbCompiledModel.GetConstructorDelegate(); try { // We can't make an ObjectContext inexpensively so we don't want to make a real one // in a unit test, therefore we just pass null and check for the exception. constructor(null); Assert.True(false); } catch (ArgumentNullException ex) { Assert.Equal("connection", ex.ParamName); } }); } #endregion #region Setting and calling database initializers for a context type // This is used instead of a Moq initializer in cases where the initializer is called multiple times // from different threads because the Moq initializer is not thread-safe. public class ThreadSafeCountingInitializer : IDatabaseInitializer where TContext : DbContext { private readonly object _lock = new object(); private readonly bool _throwFoFirstFive; public ThreadSafeCountingInitializer(bool throwFoFirstFive = false) { _throwFoFirstFive = throwFoFirstFive; } public int Count { get; set; } public void InitializeDatabase(TContext context) { lock (_lock) { Count++; if (_throwFoFirstFive && Count <= 5) { throw new Exception("Fail!"); } } } } public class ContextForSettingInitializer : DbContextUsingMockInternalContext { } [Fact] public void Database_initializer_can_be_set_by_multiple_threads() { var countingInitializer = new ThreadSafeCountingInitializer(); ExecuteInParallel(() => Database.SetInitializer(countingInitializer)); GetDatabaseForInitialization().Initialize(force: false); Assert.Equal(1, countingInitializer.Count); } private Database GetDatabaseForInitialization() where TContext : DbContextUsingMockInternalContext, new() { var mock = new Mock> { CallBase = true }; mock.Setup(c => c.UseTempObjectContext()).Callback(() => { }); return new Database(mock.Object); } public class ContextForCallingInitializer : DbContextUsingMockInternalContext { } [Fact] public void Database_Initialize_without_force_can_be_called_by_multiple_threads_and_initialization_only_happens_once() { var countingInitializer = new ThreadSafeCountingInitializer(); Database.SetInitializer(countingInitializer); ExecuteInParallel(() => GetDatabaseForInitialization().Initialize(force: false)); Assert.Equal(1, countingInitializer.Count); } public class ContextForCallingInitializerWithForce : DbContextUsingMockInternalContext { } [Fact] public void Database_Initialize_with_force_can_be_called_by_multiple_threads_and_initialization_happens_every_time() { var countingInitializer = new ThreadSafeCountingInitializer(); Database.SetInitializer(countingInitializer); ExecuteInParallel(() => GetDatabaseForInitialization().Initialize(force: true)); Assert.Equal(20, countingInitializer.Count); } public class ContextForCallingInitializerWithFailures : DbContextUsingMockInternalContext { } [Fact] public void Database_Initialize_without_force_can_be_called_by_multiple_threads_and_initialization_is_attempted_until_one_thread_succeeds() { var countingInitializer = new ThreadSafeCountingInitializer(throwFoFirstFive: true); Database.SetInitializer(countingInitializer); try { ExecuteInParallel(() => GetDatabaseForInitialization().Initialize(force: false)); } catch (AggregateException ex) { Assert.Equal(5, ex.InnerExceptions.Count); foreach (var innerException in ex.InnerExceptions) { Assert.Equal("Fail!", innerException.Message); } } Assert.Equal(6, countingInitializer.Count); } #endregion #region Using DbSet discovery from multiple threads public class ContextForSetDiscovery : DbContext { // Having a lot of sets makes it more likely that the discovery // service will be running concurrently in two threads. public DbSet Set1 { get; set; } public DbSet Set2 { get; set; } public DbSet Set3 { get; set; } public DbSet Set4 { get; set; } public DbSet Set5 { get; set; } public DbSet Set6 { get; set; } public DbSet Set7 { get; set; } public DbSet Set8 { get; set; } public DbSet Set9 { get; set; } public DbSet Set10 { get; set; } public DbSet Set11 { get; set; } public DbSet Set12 { get; set; } public DbSet Set13 { get; set; } public DbSet Set14 { get; set; } public DbSet Set15 { get; set; } public DbSet Set16 { get; set; } public DbSet Set17 { get; set; } public DbSet Set18 { get; set; } public DbSet Set19 { get; set; } public DbSet Set20 { get; set; } public DbSet Set21 { get; set; } public DbSet Set22 { get; set; } public DbSet Set23 { get; set; } public DbSet Set24 { get; set; } public DbSet Set25 { get; set; } public DbSet Set26 { get; set; } public DbSet Set27 { get; set; } public DbSet Set28 { get; set; } public DbSet Set29 { get; set; } public DbSet Set30 { get; set; } } [Fact] public void Set_discovery_can_be_called_from_multiple_threads_at_the_same_time() { ExecuteInParallel( () => { using (var context = new ContextForSetDiscovery()) { new DbSetDiscoveryService(context).InitializeSets(); Assert.NotNull(context.Set1); Assert.NotNull(context.Set30); } }); } #endregion #region Various factory methods called from multiple threads [Fact] public void DbMemberEntry_Create_can_be_called_from_multiple_threads() { ExecuteInParallel( () => { var collectionMetadata = new NavigationEntryMetadata( typeof(FakeWithProps), typeof(FakeEntity), "Collection", isCollection: true); var internalEntry = new InternalCollectionEntry( new Mock>().Object, collectionMetadata); var entry = internalEntry.CreateDbMemberEntry>(); Assert.IsAssignableFrom>>(entry); }); } [Fact] public void InternalPropertyValues_ToObject_for_entity_type_can_be_called_from_multiple_threads() { ExecuteInParallel( () => { var values = new TestInternalPropertyValues(null, isEntityValues: true); values.MockInternalContext.Setup(c => c.CreateObject(typeof(DbPropertyValuesTests.FakeTypeWithProps))).Returns( new DbPropertyValuesTests.FakeDerivedTypeWithProps()); var clone = values.ToObject(); Assert.IsType(clone); }); } [Fact] public void InternalPropertyValues_ToObject_for_non_entity_type_can_be_called_from_multiple_threads() { ExecuteInParallel( () => { var values = new TestInternalPropertyValues(null, isEntityValues: true); values.MockInternalContext.Setup(c => c.CreateObject(typeof(DbPropertyValuesTests.FakeTypeWithProps))).Returns( new DbPropertyValuesTests.FakeDerivedTypeWithProps()); var clone = values.ToObject(); Assert.IsType(clone); }); } [Fact] public void Non_generic_DbSet_creation_can_be_called_from_multiple_threads() { ExecuteInParallel( () => { var internalContext = new Mock { CallBase = true }.Object; var set = internalContext.Set(typeof(FakeEntity)); Assert.IsType>(set); }); } [Fact] public void ObjectContextTypeCache_GetObjectType_can_be_called_from_multiple_threads() { ExecuteInParallel( () => { var type = ObjectContextTypeCache.GetObjectType(typeof(FakeEntity)); Assert.Same(typeof(FakeEntity), type); }); } #endregion #region RetryLazy tests [Fact] public void RetryLazy_only_runs_the_lazy_initializer_once_even_when_called_from_multiple_threads() { var count = 0; var lockObject = new object(); var initializer = new RetryLazy( i => { // Locking here to ensure that count is incremented correctly even if RetryLazy isn't working correctly. lock (lockObject) { count++; return ""; } }); ExecuteInParallel(() => initializer.GetValue("")); Assert.Equal(1, count); } [Fact] public void RetryLazy_keeps_trying_to_initialize_until_an_attempt_succeeds() { var count = 0; var lockObject = new object(); var initializer = new RetryLazy( i => { // Locking here to ensure that count is incremented correctly even if RetryLazy isn't working correctly. lock (lockObject) { count++; if (count <= 5) { throw new Exception("Fail!"); } return ""; } }); try { ExecuteInParallel(() => initializer.GetValue("")); } catch (AggregateException ex) { Assert.Equal(5, ex.InnerExceptions.Count); foreach (var innerException in ex.InnerExceptions) { Assert.Equal("Fail!", innerException.Message); } } Assert.Equal(6, count); } public class InitializerOutput { public int Input { get; set; } public int Count { get; set; } } [Fact] public void RetryLazy_uses_the_given_input_for_each_attempt_at_initialization_until_an_attempt_succeeds() { var count = 0; var lockObject = new object(); InitializerOutput result = null; var inputs = new List(); var initializer = new RetryLazy( i => { // Locking here to ensure that count is incremented correctly even if RetryLazy isn't working correctly. lock (lockObject) { inputs.Add(i); count++; if (count <= 5) { throw new Exception("Fail!"); } return new InitializerOutput { Input = i, Count = count }; } }); var tests = new Action[20]; for (var i = 0; i < 20; i++) { var outside = i; // Make sure i is used from outside the closure tests[i] = () => result = initializer.GetValue(outside); } try { Parallel.Invoke(tests); } catch (AggregateException ex) { Assert.Equal(5, ex.InnerExceptions.Count); } Assert.Equal(6, count); Assert.Equal(6, result.Count); Assert.Equal(6, inputs.Count); Assert.Equal(inputs[5], result.Input); for (var i = 0; i < inputs.Count; i++) { for (var j = 0; j < inputs.Count; j++) { if (i != j) { Assert.NotEqual(inputs[i], inputs[j]); } } } } #endregion #region RetryAction tests [Fact] public void RetryAction_only_runs_the_action_once_even_when_called_from_multiple_threads() { var count = 0; var lockObject = new object(); var initializer = new RetryAction( i => { // Locking here to ensure that count is incremented correctly even if RetryAction isn't working correctly. lock (lockObject) { count++; } }); ExecuteInParallel(() => initializer.PerformAction("")); Assert.Equal(1, count); } [Fact] public void RetryAction_keeps_trying_to_run_the_action_until_an_attempt_succeeds() { var count = 0; var lockObject = new object(); var initializer = new RetryAction( i => { // Locking here to ensure that count is incremented correctly even if RetryAction isn't working correctly. lock (lockObject) { count++; if (count <= 5) { throw new Exception("Fail!"); } } }); try { ExecuteInParallel(() => initializer.PerformAction("")); } catch (AggregateException ex) { Assert.Equal(5, ex.InnerExceptions.Count); foreach (var innerException in ex.InnerExceptions) { Assert.Equal("Fail!", innerException.Message); } } Assert.Equal(6, count); } public void RetryAction_uses_the_given_input_for_each_attempt_until_an_attempt_succeeds() { var count = 0; var lockObject = new object(); var inputs = new List(); var initializer = new RetryAction( i => { // Locking here to ensure that count is incremented correctly even if RetryAction isn't working correctly. lock (lockObject) { inputs.Add(i); count++; if (count <= 5) { throw new Exception("Fail!"); } } }); var tests = new Action[20]; for (var i = 0; i < 20; i++) { var outside = i; // Make sure i is used from outside the closure tests[i] = () => initializer.PerformAction(outside); } try { Parallel.Invoke(tests); } catch (AggregateException ex) { Assert.Equal(5, ex.InnerExceptions.Count); } Assert.Equal(6, count); Assert.Equal(6, inputs.Count); for (var i = 0; i < inputs.Count; i++) { for (var j = 0; j < inputs.Count; j++) { if (i != j) { Assert.NotEqual(inputs[i], inputs[j]); } } } } #endregion } #region public class TypeWithALotOfProperties { public int Property0 { get; set; } private byte Property1 { get; set; } protected int Property2 { get { return 0; } } public string Property3 { get { return "Hello"; } } public int Property4 { get; set; } public object Property5 { get; set; } internal int Property6 { get; set; } public int Property7 { get; set; } public int Property8 { get; set; } public int Property9 { get; set; } public string Property10 { get; set; } protected int Property11 { get; set; } private int Property12 { get; set; } public int Property13 { get; set; } public byte Property14 { get; set; } internal int Property15 { set { } } private int Property16 { get; set; } public object Property17 { get; set; } public int Property18 { get; set; } public int Property19 { get; set; } public string Property20 { get { return ""; } } public int Property21 { get; set; } public int Property22 { get { return 0; } } private int Property23 { get; set; } private string Property24 { get; set; } public int Property25 { get; set; } public int Property26 { get { return 77; } } protected long Property27 { get; set; } public int Property28 { get; set; } private int Property29 { get; set; } protected string Property30 { set { } } protected int Property31 { get; set; } private int Property32 { get; set; } public object Property33 { get; set; } internal int Property34 { get { return 0; } } public string Property35 { get { return ""; } } public int Property36 { get; set; } protected int Property37 { get; set; } public int Property38 { get { return 0; } } public byte Property39 { get; set; } protected int Property40 { get { return 0; } } public object Property41 { get { return null; } } public int Property42 { get; set; } protected string Property43 { get; set; } public int Property44 { get; set; } public byte Property45 { set { } } public byte Property46 { get; set; } public string Property47 { get; set; } public object Property48 { get; set; } public int Property49 { get; set; } } #endregion }