Create connector tests (#52)

* Use shared IdleManager

* Fix usage of _hasSubscribed by using Try methods on concurrent dictionaries

* use top exception handler in idle manager subscription

* Connectors with TopLevel handler tests

* fix handler

* start idle call manager tests

* add remove tests

* merge fixes

* add a bit more coverage

* Fix tests
This commit is contained in:
Adam Hathcock
2024-07-23 13:13:28 +01:00
committed by GitHub
parent 27fde075b8
commit a554a2ab9d
11 changed files with 906 additions and 101 deletions
@@ -0,0 +1,64 @@
using FluentAssertions;
using Moq;
using NUnit.Framework;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Testing;
namespace Speckle.Connectors.DUI.Tests.Bridge;
public class IdleCallManagerTests : MoqTest
{
[Test]
public void SubscribeToIdleTest()
{
var handler = Create<ITopLevelExceptionHandler>();
var sut = new IdleCallManager(handler.Object);
var action = Create<Action>();
var addEvent = Create<Action>();
handler.Setup(x => x.CatchUnhandled(It.IsAny<Action>())).Returns(new Result());
sut.SubscribeToIdle("id", action.Object, addEvent.Object);
}
[Test]
public void SubscribeInternalTest()
{
var handler = Create<ITopLevelExceptionHandler>();
var sut = new IdleCallManager(handler.Object);
var action = Create<Action>();
var addEvent = Create<Action>();
addEvent.Setup(x => x.Invoke());
sut.IdleSubscriptionCalled.Should().BeFalse();
sut.SubscribeInternal("id", action.Object, addEvent.Object);
sut.Calls.Count.Should().Be(1);
sut.Calls.Should().ContainKey("id");
sut.IdleSubscriptionCalled.Should().BeTrue();
}
[Test]
public void AppOnIdleTest()
{
var handler = Create<ITopLevelExceptionHandler>();
var sut = new IdleCallManager(handler.Object);
var removeEvent = Create<Action>();
handler.Setup(x => x.CatchUnhandled(It.IsAny<Action>())).Returns(new Result());
sut.AppOnIdle(removeEvent.Object);
}
[Test]
public void AppOnIdleInternalTest()
{
var handler = Create<ITopLevelExceptionHandler>();
var sut = new IdleCallManager(handler.Object);
var removeEvent = Create<Action>();
removeEvent.Setup(x => x.Invoke());
sut.SubscribeInternal("Test", () => { }, () => { });
sut.IdleSubscriptionCalled.Should().BeTrue();
sut.Calls.Count.Should().Be(1);
sut.AppOnIdleInternal(removeEvent.Object);
sut.Calls.Count.Should().Be(0);
sut.IdleSubscriptionCalled.Should().BeFalse();
}
}
@@ -0,0 +1,125 @@
using FluentAssertions;
using Microsoft.Extensions.Logging;
using Moq;
using NUnit.Framework;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Testing;
namespace Speckle.Connectors.DUI.Tests.Bridge;
public class TopLevelExceptionHandlerTests : MoqTest
{
[Test]
public void CatchUnhandledAction_Happy()
{
var logger = Create<ILogger<TopLevelExceptionHandler>>(MockBehavior.Loose);
var bridge = Create<IBridge>();
var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object);
sut.CatchUnhandled(() => { });
}
[Test]
public void CatchUnhandledAction_Exception()
{
var logger = Create<ILogger<TopLevelExceptionHandler>>(MockBehavior.Loose);
var bridge = Create<IBridge>();
bridge.Setup(x => x.Send(BasicConnectorBindingCommands.SET_GLOBAL_NOTIFICATION, It.IsAny<object>()));
var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object);
sut.CatchUnhandled(() => throw new InvalidOperationException());
}
[Test]
public void CatchUnhandledFunc_Happy()
{
var val = 2;
var logger = Create<ILogger<TopLevelExceptionHandler>>(MockBehavior.Loose);
var bridge = Create<IBridge>();
var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object);
var returnVal = sut.CatchUnhandled(() => val);
returnVal.Value.Should().Be(val);
returnVal.Exception.Should().BeNull();
returnVal.IsSuccess.Should().BeTrue();
}
[Test]
public void CatchUnhandledFunc_Exception()
{
var logger = Create<ILogger<TopLevelExceptionHandler>>(MockBehavior.Loose);
var bridge = Create<IBridge>();
bridge.Setup(x => x.Send(BasicConnectorBindingCommands.SET_GLOBAL_NOTIFICATION, It.IsAny<object>()));
var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object);
var returnVal = sut.CatchUnhandled((Func<string>)(() => throw new InvalidOperationException()));
returnVal.Value.Should().BeNull();
returnVal.Exception.Should().BeOfType<InvalidOperationException>();
returnVal.IsSuccess.Should().BeFalse();
}
[Test]
public void CatchUnhandledFunc_Exception_Fatal()
{
var logger = Create<ILogger<TopLevelExceptionHandler>>(MockBehavior.Loose);
var bridge = Create<IBridge>();
var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object);
#pragma warning disable CA2201
var exception = Assert.Throws<AggregateException>(
() => sut.CatchUnhandled(new Func<string>(() => throw new AppDomainUnloadedException()))
);
#pragma warning restore CA2201
exception.InnerExceptions.Single().Should().BeOfType<AppDomainUnloadedException>();
}
[Test]
public async Task CatchUnhandledFuncAsync_Happy()
{
var val = 2;
var logger = Create<ILogger<TopLevelExceptionHandler>>(MockBehavior.Loose);
var bridge = Create<IBridge>();
var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object);
var returnVal = await sut.CatchUnhandled(() => Task.FromResult(val));
returnVal.Value.Should().Be(val);
returnVal.Exception.Should().BeNull();
returnVal.IsSuccess.Should().BeTrue();
}
[Test]
public async Task CatchUnhandledFuncAsync_Exception()
{
var logger = Create<ILogger<TopLevelExceptionHandler>>(MockBehavior.Loose);
var bridge = Create<IBridge>();
bridge.Setup(x => x.Send(BasicConnectorBindingCommands.SET_GLOBAL_NOTIFICATION, It.IsAny<object>()));
var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object);
var returnVal = await sut.CatchUnhandled(new Func<Task<string>>(() => throw new InvalidOperationException()));
returnVal.Value.Should().BeNull();
returnVal.Exception.Should().BeOfType<InvalidOperationException>();
returnVal.IsSuccess.Should().BeFalse();
}
[Test]
public void CatchUnhandledFuncAsync_Exception_Fatal()
{
var logger = Create<ILogger<TopLevelExceptionHandler>>(MockBehavior.Loose);
var bridge = Create<IBridge>();
var sut = new TopLevelExceptionHandler(logger.Object, bridge.Object);
#pragma warning disable CA2201
var exception = Assert.ThrowsAsync<AppDomainUnloadedException>(
async () => await sut.CatchUnhandled(new Func<Task<string>>(() => throw new AppDomainUnloadedException()))
);
#pragma warning restore CA2201
exception.Should().BeOfType<AppDomainUnloadedException>();
}
}
@@ -0,0 +1,26 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>net8.0</TargetFramework>
<IsPackable>false</IsPackable>
<IsTestProject>true</IsTestProject>
<Configurations>Debug;Release;Local</Configurations>
<!-- Ingored warnings, some aspirational but too noisy for now, some by design. -->
<NoWarn>
CA2007;
$(NoWarn)
</NoWarn>
</PropertyGroup>
<ItemGroup>
<PackageReference Include="altcover" />
<PackageReference Include="FluentAssertions" />
<PackageReference Include="Microsoft.NET.Test.Sdk" />
<PackageReference Include="Moq" />
<PackageReference Include="NUnit" />
<PackageReference Include="NUnit.Analyzers" />
<PackageReference Include="NUnit3TestAdapter" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\Sdk\Speckle.Testing\Speckle.Testing.csproj" />
<ProjectReference Include="..\Speckle.Connectors.DUI\Speckle.Connectors.DUI.csproj" />
</ItemGroup>
</Project>
@@ -0,0 +1,540 @@
{
"version": 2,
"dependencies": {
"net8.0": {
"altcover": {
"type": "Direct",
"requested": "[8.8.74, )",
"resolved": "8.8.74",
"contentHash": "e8RZNE0vZnuBk/gOAWu9K5wm3S7dOrOlZje3PHI9PJUHqvP1cxVJD1eXAAmddFVlixowB7C7/zvC16GnunC2LQ=="
},
"FluentAssertions": {
"type": "Direct",
"requested": "[6.12.0, )",
"resolved": "6.12.0",
"contentHash": "ZXhHT2YwP9lajrwSKbLlFqsmCCvFJMoRSK9t7sImfnCyd0OB3MhgxdoMcVqxbq1iyxD6mD2fiackWmBb7ayiXQ==",
"dependencies": {
"System.Configuration.ConfigurationManager": "4.4.0"
}
},
"Microsoft.NET.Test.Sdk": {
"type": "Direct",
"requested": "[17.10.0, )",
"resolved": "17.10.0",
"contentHash": "0/2HeACkaHEYU3wc83YlcD2Fi4LMtECJjqrtvw0lPi9DCEa35zSPt1j4fuvM8NagjDqJuh1Ja35WcRtn1Um6/A==",
"dependencies": {
"Microsoft.CodeCoverage": "17.10.0",
"Microsoft.TestPlatform.TestHost": "17.10.0"
}
},
"Microsoft.SourceLink.GitHub": {
"type": "Direct",
"requested": "[8.0.0, )",
"resolved": "8.0.0",
"contentHash": "G5q7OqtwIyGTkeIOAc3u2ZuV/kicQaec5EaRnc0pIeSnh9LUjj+PYQrJYBURvDt7twGl2PKA7nSN0kz1Zw5bnQ==",
"dependencies": {
"Microsoft.Build.Tasks.Git": "8.0.0",
"Microsoft.SourceLink.Common": "8.0.0"
}
},
"Moq": {
"type": "Direct",
"requested": "[4.20.70, )",
"resolved": "4.20.70",
"contentHash": "4rNnAwdpXJBuxqrOCzCyICXHSImOTRktCgCWXWykuF1qwoIsVvEnR7PjbMk/eLOxWvhmj5Kwt+kDV3RGUYcNwg==",
"dependencies": {
"Castle.Core": "5.1.1"
}
},
"NUnit": {
"type": "Direct",
"requested": "[4.1.0, )",
"resolved": "4.1.0",
"contentHash": "MT/DpAhjtiytzhTgTqIhBuWx4y26PKfDepYUHUM+5uv4TsryHC2jwFo5e6NhWkApCm/G6kZ80dRjdJFuAxq3rg=="
},
"NUnit.Analyzers": {
"type": "Direct",
"requested": "[4.2.0, )",
"resolved": "4.2.0",
"contentHash": "4fJojPkzdoa4nB2+p6U+fITvPnVvwWSnsmiJ/Dl30xqiL3oxNbYvfeSLVd91hOmEjoUqSwN3Z7j1aFedjqWbUA=="
},
"NUnit3TestAdapter": {
"type": "Direct",
"requested": "[4.5.0, )",
"resolved": "4.5.0",
"contentHash": "s8JpqTe9bI2f49Pfr3dFRfoVSuFQyraTj68c3XXjIS/MRGvvkLnrg6RLqnTjdShX+AdFUCCU/4Xex58AdUfs6A=="
},
"PolySharp": {
"type": "Direct",
"requested": "[1.14.1, )",
"resolved": "1.14.1",
"contentHash": "mOOmFYwad3MIOL14VCjj02LljyF1GNw1wP0YVlxtcPvqdxjGGMNdNJJxHptlry3MOd8b40Flm8RPOM8JOlN2sQ=="
},
"Speckle.InterfaceGenerator": {
"type": "Direct",
"requested": "[0.9.5, )",
"resolved": "0.9.5",
"contentHash": "oU/L7pN1R7q8KkbrpQ3WJnHirPHqn+9DEA7asOcUiggV5dzVg1A/VYs7GOSusD24njxXh03tE3a2oTLOjt3cVg=="
},
"Castle.Core": {
"type": "Transitive",
"resolved": "5.1.1",
"contentHash": "rpYtIczkzGpf+EkZgDr9CClTdemhsrwA/W5hMoPjLkRFnXzH44zDLoovXeKtmxb1ykXK9aJVODSpiJml8CTw2g==",
"dependencies": {
"System.Diagnostics.EventLog": "6.0.0"
}
},
"GraphQL.Client": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "8yPNBbuVBpTptivyAlak4GZvbwbUcjeQTL4vN1HKHRuOykZ4r7l5fcLS6vpyPyLn0x8FsL31xbOIKyxbmR9rbA==",
"dependencies": {
"GraphQL.Client.Abstractions": "6.0.0",
"GraphQL.Client.Abstractions.Websocket": "6.0.0",
"System.Reactive": "5.0.0"
}
},
"GraphQL.Client.Abstractions": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "h7uzWFORHZ+CCjwr/ThAyXMr0DPpzEANDa4Uo54wqCQ+j7qUKwqYTgOrb1W40sqbvNaZm9v/X7It31SUw0maHA==",
"dependencies": {
"GraphQL.Primitives": "6.0.0"
}
},
"GraphQL.Client.Abstractions.Websocket": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "Nr9bPf8gIOvLuXpqEpqr9z9jslYFJOvd0feHth3/kPqeR3uMbjF5pjiwh4jxyMcxHdr8Pb6QiXkV3hsSyt0v7A==",
"dependencies": {
"GraphQL.Client.Abstractions": "6.0.0"
}
},
"GraphQL.Primitives": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "yg72rrYDapfsIUrul7aF6wwNnTJBOFvuA9VdDTQpPa8AlAriHbufeXYLBcodKjfUdkCnaiggX1U/nEP08Zb5GA=="
},
"Microsoft.AspNetCore.Http": {
"type": "Transitive",
"resolved": "2.2.2",
"contentHash": "BAibpoItxI5puk7YJbIGj95arZueM8B8M5xT1fXBn3hb3L2G3ucrZcYXv1gXdaroLbntUs8qeV8iuBrpjQsrKw==",
"dependencies": {
"Microsoft.AspNetCore.Http.Abstractions": "2.2.0",
"Microsoft.AspNetCore.WebUtilities": "2.2.0",
"Microsoft.Extensions.ObjectPool": "2.2.0",
"Microsoft.Extensions.Options": "2.2.0",
"Microsoft.Net.Http.Headers": "2.2.0"
}
},
"Microsoft.AspNetCore.Http.Abstractions": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "Nxs7Z1q3f1STfLYKJSVXCs1iBl+Ya6E8o4Oy1bCxJ/rNI44E/0f6tbsrVqAWfB7jlnJfyaAtIalBVxPKUPQb4Q==",
"dependencies": {
"Microsoft.AspNetCore.Http.Features": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.AspNetCore.Http.Features": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "ziFz5zH8f33En4dX81LW84I6XrYXKf9jg6aM39cM+LffN9KJahViKZ61dGMSO2gd3e+qe5yBRwsesvyqlZaSMg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0"
}
},
"Microsoft.AspNetCore.WebUtilities": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "9ErxAAKaDzxXASB/b5uLEkLgUWv1QbeVxyJYEHQwMaxXOeFFVkQxiq8RyfVcifLU7NR0QY0p3acqx4ZpYfhHDg==",
"dependencies": {
"Microsoft.Net.Http.Headers": "2.2.0",
"System.Text.Encodings.Web": "4.5.0"
}
},
"Microsoft.Build.Tasks.Git": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "bZKfSIKJRXLTuSzLudMFte/8CempWjVamNUR5eHJizsy+iuOuO/k2gnh7W0dHJmYY0tBf+gUErfluCv5mySAOQ=="
},
"Microsoft.CodeCoverage": {
"type": "Transitive",
"resolved": "17.10.0",
"contentHash": "yC7oSlnR54XO5kOuHlVOKtxomNNN1BWXX8lK1G2jaPXT9sUok7kCOoA4Pgs0qyFaCtMrNsprztYMeoEGqCm4uA=="
},
"Microsoft.CSharp": {
"type": "Transitive",
"resolved": "4.7.0",
"contentHash": "pTj+D3uJWyN3My70i2Hqo+OXixq3Os2D1nJ2x92FFo6sk8fYS1m1WLNTs0Dc1uPaViH0YvEEwvzddQ7y4rhXmA=="
},
"Microsoft.Data.Sqlite": {
"type": "Transitive",
"resolved": "8.0.6",
"contentHash": "YVzVtU1IoGsTiMAe0BNV9ssKyrUm6lBQJP6w2N4W29YrBYYtyCmTFmrNGjxaJtJXVqttu0sYkPxmStj/OnMFdg==",
"dependencies": {
"Microsoft.Data.Sqlite.Core": "8.0.6",
"SQLitePCLRaw.bundle_e_sqlite3": "2.1.6"
}
},
"Microsoft.Data.Sqlite.Core": {
"type": "Transitive",
"resolved": "8.0.6",
"contentHash": "umhZ0ZF2RI81rGFTnYmCxI+Euj4Aqe/6Y4+8CxN9OVJNGDNIqB5laJ3wxQTU8zXCcm2k9F7FL+/6RVoOT4z1Fw==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.6"
}
},
"Microsoft.Extensions.DependencyInjection": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "elNeOmkeX3eDVG6pYVeV82p29hr+UKDaBhrZyWvWLw/EVZSYEkZlQdkp0V39k/Xehs2Qa0mvoCvkVj3eQxNQ1Q==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0"
}
},
"Microsoft.Extensions.DependencyInjection.Abstractions": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "h3j/QfmFN4S0w4C2A6X7arXij/M/OVw3uQHSOFxnND4DyAzO1F9eMX7Eti7lU/OkSthEE0WzRsfT/Dmx86jzCw=="
},
"Microsoft.Extensions.Logging": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "Nw2muoNrOG5U5qa2ZekXwudUn2BJcD41e65zwmDHb1fQegTX66UokLWZkJRpqSSHXDOWZ5V0iqhbxOEky91atA==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection": "7.0.0",
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Logging.Abstractions": "7.0.0",
"Microsoft.Extensions.Options": "7.0.0"
}
},
"Microsoft.Extensions.ObjectPool": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "gA8H7uQOnM5gb+L0uTNjViHYr+hRDqCdfugheGo/MxQnuHzmhhzCBTIPm19qL1z1Xe0NEMabfcOBGv9QghlZ8g=="
},
"Microsoft.Extensions.Options": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "lP1yBnTTU42cKpMozuafbvNtQ7QcBjr/CcK3bYOGEMH55Fjt+iecXjT6chR7vbgCMqy3PG3aNQSZgo/EuY/9qQ==",
"dependencies": {
"Microsoft.Extensions.DependencyInjection.Abstractions": "7.0.0",
"Microsoft.Extensions.Primitives": "7.0.0"
}
},
"Microsoft.Extensions.Primitives": {
"type": "Transitive",
"resolved": "7.0.0",
"contentHash": "um1KU5kxcRp3CNuI8o/GrZtD4AIOXDk+RLsytjZ9QPok3ttLUelLKpilVPuaFT3TFjOhSibUAso0odbOaCDj3Q=="
},
"Microsoft.Net.Http.Headers": {
"type": "Transitive",
"resolved": "2.2.0",
"contentHash": "iZNkjYqlo8sIOI0bQfpsSoMTmB/kyvmV2h225ihyZT33aTp48ZpF6qYnXxzSXmHt8DpBAwBTX+1s1UFLbYfZKg==",
"dependencies": {
"Microsoft.Extensions.Primitives": "2.2.0",
"System.Buffers": "4.5.0"
}
},
"Microsoft.SourceLink.Common": {
"type": "Transitive",
"resolved": "8.0.0",
"contentHash": "dk9JPxTCIevS75HyEQ0E4OVAFhB2N+V9ShCXf8Q6FkUQZDkgLI12y679Nym1YqsiSysuQskT7Z+6nUf3yab6Vw=="
},
"Microsoft.TestPlatform.ObjectModel": {
"type": "Transitive",
"resolved": "17.10.0",
"contentHash": "KkwhjQevuDj0aBRoPLY6OLAhGqbPUEBuKLbaCs0kUVw29qiOYncdORd4mLVJbn9vGZ7/iFGQ/+AoJl0Tu5Umdg==",
"dependencies": {
"System.Reflection.Metadata": "1.6.0"
}
},
"Microsoft.TestPlatform.TestHost": {
"type": "Transitive",
"resolved": "17.10.0",
"contentHash": "LWpMdfqhHvcUkeMCvNYJO8QlPLlYz9XPPb+ZbaXIKhdmjAV0wqTSrTiW5FLaf7RRZT50AQADDOYMOe0HxDxNgA==",
"dependencies": {
"Microsoft.TestPlatform.ObjectModel": "17.10.0",
"Newtonsoft.Json": "13.0.1"
}
},
"Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.1",
"contentHash": "ppPFpBcvxdsfUonNcvITKqLl3bqxWbDCZIzDWHzjpdAHRFfZe0Dw9HmA0+za13IdyrgJwpkDTDA9fHaxOrt20A=="
},
"Polly": {
"type": "Transitive",
"resolved": "7.2.3",
"contentHash": "DeCY0OFbNdNxsjntr1gTXHJ5pKUwYzp04Er2LLeN3g6pWhffsGuKVfMBLe1lw7x76HrPkLxKEFxBlpRxS2nDEQ=="
},
"Polly.Contrib.WaitAndRetry": {
"type": "Transitive",
"resolved": "1.1.1",
"contentHash": "1MUQLiSo4KDkQe6nzQRhIU05lm9jlexX5BVsbuw0SL82ynZ+GzAHQxJVDPVBboxV37Po3SG077aX8DuSy8TkaA=="
},
"Polly.Extensions.Http": {
"type": "Transitive",
"resolved": "3.0.0",
"contentHash": "drrG+hB3pYFY7w1c3BD+lSGYvH2oIclH8GRSehgfyP5kjnFnHKQuuBhuHLv+PWyFuaTDyk/vfRpnxOzd11+J8g==",
"dependencies": {
"Polly": "7.1.0"
}
},
"Sentry": {
"type": "Transitive",
"resolved": "3.33.0",
"contentHash": "8vbD2o6IR2wrRrkSiRbnodWGWUOqIlwYtzpjvPNOb5raJdOf+zxMwfS8f6nx9bmrTTfDj7KrCB8C/5OuicAc8A=="
},
"Sentry.Serilog": {
"type": "Transitive",
"resolved": "3.33.0",
"contentHash": "V8BU7QGWg2qLYfNPqtuTBhC1opysny5l+Ifp6J6PhOeAxU0FssR7nYfbJVetrnLIoh2rd3DlJ6hHYYQosQYcUQ==",
"dependencies": {
"Sentry": "3.33.0",
"Serilog": "2.10.0"
}
},
"Serilog": {
"type": "Transitive",
"resolved": "2.12.0",
"contentHash": "xaiJLIdu6rYMKfQMYUZgTy8YK7SMZjB4Yk50C/u//Z4OsvxkUfSPJy4nknfvwAC34yr13q7kcyh4grbwhSxyZg=="
},
"Serilog.Enrichers.ClientInfo": {
"type": "Transitive",
"resolved": "1.3.0",
"contentHash": "mTc7PM+wC9Hr7LWSwqt5mmnlAr7RJs+eTb3PGPRhwdOackk95MkhUZognuxXEdlW19HAFNmEBTSBY5DfLwM8jQ==",
"dependencies": {
"Microsoft.AspNetCore.Http": "2.2.2",
"Serilog": "2.9.0"
}
},
"Serilog.Exceptions": {
"type": "Transitive",
"resolved": "8.4.0",
"contentHash": "nc/+hUw3lsdo0zCj0KMIybAu7perMx79vu72w0za9Nsi6mWyNkGXxYxakAjWB7nEmYL6zdmhEQRB4oJ2ALUeug==",
"dependencies": {
"Serilog": "2.8.0",
"System.Reflection.TypeExtensions": "4.7.0"
}
},
"Serilog.Formatting.Compact": {
"type": "Transitive",
"resolved": "1.1.0",
"contentHash": "pNroKVjo+rDqlxNG5PXkRLpfSCuDOBY0ri6jp9PLe505ljqwhwZz8ospy2vWhQlFu5GkIesh3FcDs4n7sWZODA==",
"dependencies": {
"Serilog": "2.8.0"
}
},
"Serilog.Sinks.Console": {
"type": "Transitive",
"resolved": "4.1.0",
"contentHash": "K6N5q+5fetjnJPvCmkWOpJ/V8IEIoMIB1s86OzBrbxwTyHxdx3pmz4H+8+O/Dc/ftUX12DM1aynx/dDowkwzqg==",
"dependencies": {
"Serilog": "2.10.0"
}
},
"Serilog.Sinks.File": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "uwV5hdhWPwUH1szhO8PJpFiahqXmzPzJT/sOijH/kFgUx+cyoDTMM8MHD0adw9+Iem6itoibbUXHYslzXsLEAg==",
"dependencies": {
"Serilog": "2.10.0"
}
},
"Serilog.Sinks.PeriodicBatching": {
"type": "Transitive",
"resolved": "3.1.0",
"contentHash": "NDWR7m3PalVlGEq3rzoktrXikjFMLmpwF0HI4sowo8YDdU+gqPlTHlDQiOGxHfB0sTfjPA9JjA7ctKG9zqjGkw==",
"dependencies": {
"Serilog": "2.0.0"
}
},
"Serilog.Sinks.Seq": {
"type": "Transitive",
"resolved": "5.2.2",
"contentHash": "1Csmo5ua7NKUe0yXUx+zsRefjAniPWcXFhUXxXG8pwo0iMiw2gjn9SOkgYnnxbgWqmlGv236w0N/dHc2v5XwMg==",
"dependencies": {
"Serilog": "2.12.0",
"Serilog.Formatting.Compact": "1.1.0",
"Serilog.Sinks.File": "5.0.0",
"Serilog.Sinks.PeriodicBatching": "3.1.0"
}
},
"SerilogTimings": {
"type": "Transitive",
"resolved": "3.0.1",
"contentHash": "Zs28eTgszAMwpIrbBnWHBI50yuxL50p/dmAUWmy75+axdZYK/Sjm5/5m1N/CisR8acJUhTVcjPZrsB1P5iv0Uw==",
"dependencies": {
"Serilog": "2.10.0"
}
},
"Speckle.DoubleNumerics": {
"type": "Transitive",
"resolved": "4.0.1",
"contentHash": "MzEQ1Im0zTja+tEsdRIk/WlPiKqb22NmTOJcR1ZKm/mz46pezyyID3/wRz6vJUELMpSLnG7LhsxBL+nxbr7V0w=="
},
"Speckle.Newtonsoft.Json": {
"type": "Transitive",
"resolved": "13.0.2",
"contentHash": "g1BejUZwax5PRfL6xHgLEK23sqHWOgOj9hE7RvfRRlN00AGt8GnPYt8HedSK7UB3HiRW8zCA9Pn0iiYxCK24BA=="
},
"SQLitePCLRaw.bundle_e_sqlite3": {
"type": "Transitive",
"resolved": "2.1.6",
"contentHash": "BmAf6XWt4TqtowmiWe4/5rRot6GerAeklmOPfviOvwLoF5WwgxcJHAxZtySuyW9r9w+HLILnm8VfJFLCUJYW8A==",
"dependencies": {
"SQLitePCLRaw.lib.e_sqlite3": "2.1.6",
"SQLitePCLRaw.provider.e_sqlite3": "2.1.6"
}
},
"SQLitePCLRaw.core": {
"type": "Transitive",
"resolved": "2.1.6",
"contentHash": "wO6v9GeMx9CUngAet8hbO7xdm+M42p1XeJq47ogyRoYSvNSp0NGLI+MgC0bhrMk9C17MTVFlLiN6ylyExLCc5w==",
"dependencies": {
"System.Memory": "4.5.3"
}
},
"SQLitePCLRaw.lib.e_sqlite3": {
"type": "Transitive",
"resolved": "2.1.6",
"contentHash": "2ObJJLkIUIxRpOUlZNGuD4rICpBnrBR5anjyfUFQep4hMOIeqW+XGQYzrNmHSVz5xSWZ3klSbh7sFR6UyDj68Q=="
},
"SQLitePCLRaw.provider.e_sqlite3": {
"type": "Transitive",
"resolved": "2.1.6",
"contentHash": "PQ2Oq3yepLY4P7ll145P3xtx2bX8xF4PzaKPRpw9jZlKvfe4LE/saAV82inND9usn1XRpmxXk7Lal3MTI+6CNg==",
"dependencies": {
"SQLitePCLRaw.core": "2.1.6"
}
},
"System.Buffers": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "pL2ChpaRRWI/p4LXyy4RgeWlYF2sgfj/pnVMvBqwNFr5cXg7CXNnWZWxrOONLg8VGdFB8oB+EG2Qw4MLgTOe+A=="
},
"System.Configuration.ConfigurationManager": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "gWwQv/Ug1qWJmHCmN17nAbxJYmQBM/E94QxKLksvUiiKB1Ld3Sc/eK1lgmbSjDFxkQhVuayI/cGFZhpBSodLrg==",
"dependencies": {
"System.Security.Cryptography.ProtectedData": "4.4.0"
}
},
"System.Diagnostics.EventLog": {
"type": "Transitive",
"resolved": "6.0.0",
"contentHash": "lcyUiXTsETK2ALsZrX+nWuHSIQeazhqPphLfaRxzdGaG93+0kELqpgEHtwWOlQe7+jSFnKwaCAgL4kjeZCQJnw=="
},
"System.Memory": {
"type": "Transitive",
"resolved": "4.5.3",
"contentHash": "3oDzvc/zzetpTKWMShs1AADwZjQ/36HnsufHRPcOjyRAAMLDlu2iD33MBI2opxnezcVUtXyqDXXjoFMOU9c7SA=="
},
"System.Reactive": {
"type": "Transitive",
"resolved": "5.0.0",
"contentHash": "erBZjkQHWL9jpasCE/0qKAryzVBJFxGHVBAvgRN1bzM0q2s1S4oYREEEL0Vb+1kA/6BKb5FjUZMp5VXmy+gzkQ=="
},
"System.Reflection.Metadata": {
"type": "Transitive",
"resolved": "1.6.0",
"contentHash": "COC1aiAJjCoA5GBF+QKL2uLqEBew4JsCkQmoHKbN3TlOZKa2fKLz5CpiRQKDz0RsAOEGsVKqOD5bomsXq/4STQ=="
},
"System.Reflection.TypeExtensions": {
"type": "Transitive",
"resolved": "4.7.0",
"contentHash": "VybpaOQQhqE6siHppMktjfGBw1GCwvCqiufqmP8F1nj7fTUNtW35LOEt3UZTEsECfo+ELAl/9o9nJx3U91i7vA=="
},
"System.Security.Cryptography.ProtectedData": {
"type": "Transitive",
"resolved": "4.4.0",
"contentHash": "cJV7ScGW7EhatRsjehfvvYVBvtiSMKgN8bOVI0bQhnF5bU7vnHVIsH49Kva7i7GWaWYvmEzkYVk1TC+gZYBEog=="
},
"System.Text.Encodings.Web": {
"type": "Transitive",
"resolved": "4.5.0",
"contentHash": "Xg4G4Indi4dqP1iuAiMSwpiWS54ZghzR644OtsRCm/m/lBMG8dUBhLVN7hLm8NNrNTR+iGbshCPTwrvxZPlm4g=="
},
"speckle.autofac": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[7.0.0, )"
}
},
"speckle.connectors.dui": {
"type": "Project",
"dependencies": {
"Microsoft.Extensions.Logging.Abstractions": "[7.0.0, )",
"Speckle.Autofac": "[1.0.0, )",
"Speckle.Connectors.Utils": "[1.0.0, )",
"Speckle.Core": "[3.1.0-dev.75, )",
"System.Threading.Tasks.Dataflow": "[6.0.0, )"
}
},
"speckle.connectors.utils": {
"type": "Project",
"dependencies": {
"Serilog.Extensions.Logging": "[7.0.0, )",
"Speckle.Autofac": "[1.0.0, )",
"Speckle.Core": "[3.1.0-dev.75, )"
}
},
"speckle.testing": {
"type": "Project",
"dependencies": {
"Moq": "[4.20.70, )",
"NUnit": "[4.1.0, )"
}
},
"Microsoft.Extensions.Logging.Abstractions": {
"type": "CentralTransitive",
"requested": "[7.0.0, )",
"resolved": "7.0.0",
"contentHash": "kmn78+LPVMOWeITUjIlfxUPDsI0R6G0RkeAMBmQxAJ7vBJn4q2dTva7pWi65ceN5vPGjJ9q/Uae2WKgvfktJAw=="
},
"Serilog.Extensions.Logging": {
"type": "CentralTransitive",
"requested": "[7.0.0, )",
"resolved": "7.0.0",
"contentHash": "9faU0zNQqU7I6soVhLUMYaGNpgWv6cKlKb2S5AnS8gXxzW/em5Ladm/6FMrWTnX41cdbdGPOWNAo6adi4WaJ6A==",
"dependencies": {
"Microsoft.Extensions.Logging": "7.0.0",
"Serilog": "2.12.0"
}
},
"Speckle.Core": {
"type": "CentralTransitive",
"requested": "[3.1.0-dev.75, )",
"resolved": "3.1.0-dev.75",
"contentHash": "RW+ZNmMoVtJnj+FxwX4pxy9oEFrYvZ6GEVA1AtyQQ3jPFVdApL9GDsfp7iNJDTtzY4sGY+a3EcudslYMfGk8jA==",
"dependencies": {
"GraphQL.Client": "6.0.0",
"Microsoft.CSharp": "4.7.0",
"Microsoft.Data.Sqlite": "8.0.6",
"Polly": "7.2.3",
"Polly.Contrib.WaitAndRetry": "1.1.1",
"Polly.Extensions.Http": "3.0.0",
"Sentry": "3.33.0",
"Sentry.Serilog": "3.33.0",
"Serilog": "2.12.0",
"Serilog.Enrichers.ClientInfo": "1.3.0",
"Serilog.Exceptions": "8.4.0",
"Serilog.Sinks.Console": "4.1.0",
"Serilog.Sinks.Seq": "5.2.2",
"SerilogTimings": "3.0.1",
"Speckle.DoubleNumerics": "4.0.1",
"Speckle.Newtonsoft.Json": "13.0.2"
}
},
"System.Threading.Tasks.Dataflow": {
"type": "CentralTransitive",
"requested": "[6.0.0, )",
"resolved": "6.0.0",
"contentHash": "+tyDCU3/B1lDdOOAJywHQoFwyXIUghIaP2BxG79uvhfTnO+D9qIgjVlL/JV2NTliYbMHpd6eKDmHp2VHpij7MA=="
}
}
}
}
@@ -45,7 +45,7 @@ public class BasicConnectorBindingCommands
private const string NOTIFY_DOCUMENT_CHANGED_EVENT_NAME = "documentChanged";
private const string SET_MODEL_PROGRESS_UI_COMMAND_NAME = "setModelProgress";
private const string SET_MODEL_ERROR_UI_COMMAND_NAME = "setModelError";
internal const string SET_GLOBAL_NOTIFICATION = "setGlobalNotification";
public const string SET_GLOBAL_NOTIFICATION = "setGlobalNotification";
protected IBridge Bridge { get; }
@@ -1,52 +1,62 @@
using System.Collections.Concurrent;
using System.Collections.Concurrent;
using System.Diagnostics.CodeAnalysis;
using Speckle.InterfaceGenerator;
namespace Speckle.Connectors.DUI.Bridge;
public interface IIdleCallManager
{
void SubscribeToIdle(string id, Action action, Action addEvent);
void AppOnIdle(Action removeEvent);
}
//should be registered as singleton
[GenerateAutoInterface]
[SuppressMessage("ReSharper", "InconsistentlySynchronizedField")]
public class IdleCallManager(ITopLevelExceptionHandler topLevelExceptionHandler) : IIdleCallManager
{
private readonly ConcurrentDictionary<string, Action> _calls = new();
private bool _idleSubscriptionCalled;
public ConcurrentDictionary<string, Action> Calls { get; } = new();
private readonly object _lock = new();
public bool IdleSubscriptionCalled { get; private set; }
public void SubscribeToIdle(string id, Action action, Action addEvent) =>
topLevelExceptionHandler.CatchUnhandled(() =>
topLevelExceptionHandler.CatchUnhandled(() => SubscribeInternal(id, action, addEvent));
public void SubscribeInternal(string id, Action action, Action addEvent)
{
Calls.TryAdd(id, action);
if (!IdleSubscriptionCalled)
{
_calls.TryAdd(id, action);
if (!_idleSubscriptionCalled)
lock (_lock)
{
lock (_calls)
if (!IdleSubscriptionCalled)
{
if (!_idleSubscriptionCalled)
{
addEvent.Invoke();
_idleSubscriptionCalled = true;
}
addEvent.Invoke();
IdleSubscriptionCalled = true;
}
}
});
}
}
public void AppOnIdle(Action removeEvent) =>
topLevelExceptionHandler.CatchUnhandled(() =>
topLevelExceptionHandler.CatchUnhandled(() => AppOnIdleInternal(removeEvent));
public void AppOnIdleInternal(Action removeEvent)
{
foreach (KeyValuePair<string, Action> kvp in Calls)
{
foreach (KeyValuePair<string, Action> kvp in _calls)
kvp.Value.Invoke();
}
Calls.Clear();
if (IdleSubscriptionCalled)
{
lock (_lock)
{
kvp.Value.Invoke();
}
_calls.Clear();
if (_idleSubscriptionCalled)
{
lock (_calls)
if (IdleSubscriptionCalled)
{
if (_idleSubscriptionCalled)
{
removeEvent.Invoke();
_idleSubscriptionCalled = false;
}
removeEvent.Invoke();
IdleSubscriptionCalled = false;
}
}
});
}
}
}
@@ -0,0 +1,58 @@
using System.Diagnostics.CodeAnalysis;
using Speckle.Connectors.Utils;
namespace Speckle.Connectors.DUI.Bridge;
/// <summary>
/// Result Pattern struct
/// </summary>
/// <typeparam name="T"></typeparam>
[ExcludeFromCodeCoverage]
public readonly struct Result<T>
{
//Don't add new members to this struct, it is perfect.
public T? Value { get; }
public Exception? Exception { get; }
[MemberNotNullWhen(false, nameof(Exception))]
public bool IsSuccess => Exception is null;
/// <summary>
/// Create a successful result
/// </summary>
/// <param name="result"></param>
public Result(T result)
{
Value = result;
}
/// <summary>
/// Create a non-successful result
/// </summary>
/// <param name="result"></param>
/// <exception cref="ArgumentNullException"><paramref name="result"/> was null</exception>
public Result([NotNull] Exception? result)
{
Exception = result.NotNull();
}
}
[ExcludeFromCodeCoverage]
public readonly struct Result
{
//Don't add new members to this struct, it is perfect.
public Exception? Exception { get; }
[MemberNotNullWhen(false, nameof(Exception))]
public bool IsSuccess => Exception is null;
/// <summary>
/// Create a non-successful result
/// </summary>
/// <param name="result"></param>
/// <exception cref="ArgumentNullException"><paramref name="result"/> was null</exception>
public Result([NotNull] Exception? result)
{
Exception = result.NotNull();
}
}
@@ -1,46 +1,11 @@
using System.Diagnostics.CodeAnalysis;
using Microsoft.Extensions.Logging;
using Speckle.Connectors.DUI.Bindings;
using Speckle.Connectors.Utils;
using Speckle.Core.Logging;
using Speckle.Core.Models.Extensions;
using Speckle.InterfaceGenerator;
namespace Speckle.Connectors.DUI.Bridge;
/// <summary>
/// Result Pattern struct
/// </summary>
/// <typeparam name="T"></typeparam>
public readonly struct Result<T>
{
//Don't add new members to this struct, it is perfect.
public T? Value { get; }
public Exception? Exception { get; }
[MemberNotNullWhen(false, nameof(Exception))]
public bool IsSuccess => Exception is null;
/// <summary>
/// Create a successful result
/// </summary>
/// <param name="result"></param>
public Result(T result)
{
Value = result;
}
/// <summary>
/// Create a non-sucessful result
/// </summary>
/// <param name="result"></param>
/// <exception cref="ArgumentNullException"><paramref name="result"/> was null</exception>
public Result([NotNull] Exception? result)
{
Exception = result.NotNull();
}
}
/// <summary>
/// The functions provided by this class are designed to be used in all "top level" scenarios (e.g. Plugin, UI, and Event callbacks)
/// To provide "last ditch effort" handling of unexpected exceptions that have not been handled.
@@ -55,19 +20,11 @@ public readonly struct Result<T>
/// Attempting to swallow them may lead to data corruption, deadlocking, or things worse than a managed host app crash.
/// </remarks>
[GenerateAutoInterface]
public sealed class TopLevelExceptionHandler : ITopLevelExceptionHandler
public sealed class TopLevelExceptionHandler(ILogger<TopLevelExceptionHandler> logger, IBridge bridge)
: ITopLevelExceptionHandler
{
private readonly ILogger<TopLevelExceptionHandler> _logger;
private readonly IBridge _bridge;
private const string UNHANDLED_LOGGER_TEMPLATE = "An unhandled Exception occured";
public TopLevelExceptionHandler(ILogger<TopLevelExceptionHandler> logger, IBridge bridge)
{
_logger = logger;
_bridge = bridge;
}
/// <summary>
/// Invokes the given function <paramref name="function"/> within a <see langword="try"/>/<see langword="catch"/> block,
/// and provides exception handling for unexpected exceptions that have not been handled.<br/>
@@ -75,35 +32,18 @@ public sealed class TopLevelExceptionHandler : ITopLevelExceptionHandler
/// <param name="function">The function to invoke and provide error handling for</param>
/// <exception cref="Exception"><see cref="ExceptionHelpers.IsFatal"/> will be rethrown, these should be allowed to bubble up to the host app</exception>
/// <seealso cref="ExceptionHelpers.IsFatal"/>
public void CatchUnhandled(Action function)
{
CatchUnhandled(() =>
{
function.Invoke();
return (object?)null;
});
}
/// <inheritdoc cref="CatchUnhandled(Action)"/>
/// <typeparam name="T"><paramref name="function"/> return type</typeparam>
/// <returns>A result pattern struct (where exceptions have been handled)</returns>
public Result<T> CatchUnhandled<T>(Func<T> function)
{
return CatchUnhandled(() => Task.FromResult(function.Invoke())).Result;
}
///<inheritdoc cref="CatchUnhandled{T}(Func{T})"/>
public async Task<Result<T>> CatchUnhandled<T>(Func<Task<T>> function)
public Result CatchUnhandled(Action function)
{
try
{
try
{
return new(await function.Invoke().ConfigureAwait(false));
function.Invoke();
return new();
}
catch (Exception ex) when (!ex.IsFatal())
{
_logger.LogError(ex, UNHANDLED_LOGGER_TEMPLATE);
logger.LogError(ex, UNHANDLED_LOGGER_TEMPLATE);
SetGlobalNotification(
ToastNotificationType.DANGER,
@@ -116,13 +56,48 @@ public sealed class TopLevelExceptionHandler : ITopLevelExceptionHandler
}
catch (Exception ex)
{
_logger.LogCritical(ex, UNHANDLED_LOGGER_TEMPLATE);
logger.LogCritical(ex, UNHANDLED_LOGGER_TEMPLATE);
throw;
}
}
/// <inheritdoc cref="CatchUnhandled(Action)"/>
/// <typeparam name="T"><paramref name="function"/> return type</typeparam>
/// <returns>A result pattern struct (where exceptions have been handled)</returns>
public Result<T> CatchUnhandled<T>(Func<T> function) =>
CatchUnhandled(() => Task.FromResult(function.Invoke())).Result;
///<inheritdoc cref="CatchUnhandled{T}(Func{T})"/>
public async Task<Result<T>> CatchUnhandled<T>(Func<Task<T>> function)
{
try
{
try
{
return new(await function.Invoke().ConfigureAwait(false));
}
catch (Exception ex) when (!ex.IsFatal())
{
logger.LogError(ex, UNHANDLED_LOGGER_TEMPLATE);
SetGlobalNotification(
ToastNotificationType.DANGER,
"Unhandled Exception Occured",
ex.ToFormattedString(),
false
);
return new(ex);
}
}
catch (Exception ex)
{
logger.LogCritical(ex, UNHANDLED_LOGGER_TEMPLATE);
throw;
}
}
private void SetGlobalNotification(ToastNotificationType type, string title, string message, bool autoClose) =>
_bridge.Send(
bridge.Send(
BasicConnectorBindingCommands.SET_GLOBAL_NOTIFICATION, //TODO: We could move these constants into a DUI3 constants static class
new
{
@@ -1,4 +1,4 @@
using Speckle.Autofac.DependencyInjection;
using Speckle.Autofac.DependencyInjection;
using Speckle.Connectors.DUI.Bridge;
using Speckle.Connectors.DUI.Models.Card.SendFilter;
using Speckle.Connectors.DUI.Utils;
+2 -2
View File
@@ -15,6 +15,6 @@ public abstract class MoqTest
protected MockRepository Repository { get; private set; } = new(MockBehavior.Strict);
protected Mock<T> Create<T>()
where T : class => Repository.Create<T>();
protected Mock<T> Create<T>(MockBehavior behavior = MockBehavior.Strict)
where T : class => Repository.Create<T>(behavior);
}
+7
View File
@@ -137,6 +137,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Speckle.Converters.Revit202
EndProject
Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "2022", "2022", "{0AF38BA3-65A0-481B-8CBB-B82E406E1575}"
EndProject
Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Speckle.Connectors.DUI.Tests", "DUI3\Speckle.Connectors.DUI.Tests\Speckle.Connectors.DUI.Tests.csproj", "{EB83A3A3-F9B6-4281-8EBF-F7289FB5D885}"
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Speckle.Converters.Revit2022.Tests", "Converters\Revit\Speckle.Converters.Revit2022.Tests\Speckle.Converters.Revit2022.Tests.csproj", "{D8069A23-AD2E-4C9E-8574-7E8C45296A46}"
EndProject
Global
@@ -293,6 +295,10 @@ Global
{881D71A3-D276-4108-98C6-0FFD32129B9C}.Debug|Any CPU.Build.0 = Debug|Any CPU
{881D71A3-D276-4108-98C6-0FFD32129B9C}.Release|Any CPU.ActiveCfg = Release|Any CPU
{881D71A3-D276-4108-98C6-0FFD32129B9C}.Release|Any CPU.Build.0 = Release|Any CPU
{EB83A3A3-F9B6-4281-8EBF-F7289FB5D885}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{EB83A3A3-F9B6-4281-8EBF-F7289FB5D885}.Debug|Any CPU.Build.0 = Debug|Any CPU
{EB83A3A3-F9B6-4281-8EBF-F7289FB5D885}.Release|Any CPU.ActiveCfg = Release|Any CPU
{EB83A3A3-F9B6-4281-8EBF-F7289FB5D885}.Release|Any CPU.Build.0 = Release|Any CPU
{D8069A23-AD2E-4C9E-8574-7E8C45296A46}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{D8069A23-AD2E-4C9E-8574-7E8C45296A46}.Debug|Any CPU.Build.0 = Debug|Any CPU
{D8069A23-AD2E-4C9E-8574-7E8C45296A46}.Release|Any CPU.ActiveCfg = Release|Any CPU
@@ -357,6 +363,7 @@ Global
{19424B55-058C-4E9C-B86F-700AEF9EAEC3} = {0AF38BA3-65A0-481B-8CBB-B82E406E1575}
{881D71A3-D276-4108-98C6-0FFD32129B9C} = {0AF38BA3-65A0-481B-8CBB-B82E406E1575}
{0AF38BA3-65A0-481B-8CBB-B82E406E1575} = {D92751C8-1039-4005-90B2-913E55E0B8BD}
{EB83A3A3-F9B6-4281-8EBF-F7289FB5D885} = {FD4D6594-D81E-456F-8F2E-35B09E04A755}
{D8069A23-AD2E-4C9E-8574-7E8C45296A46} = {0AF38BA3-65A0-481B-8CBB-B82E406E1575}
EndGlobalSection
GlobalSection(ExtensibilityGlobals) = postSolution