From 24b630e42af4058f914d97f06ea107da2646d814 Mon Sep 17 00:00:00 2001 From: hwndmaster <5339929+hwndmaster@users.noreply.github.com> Date: Sun, 20 Jun 2021 20:05:48 +0200 Subject: [PATCH] Unit tests added --- .gitignore | 1 + .../Repositories/SettingsRepositoryTests.cs | 96 +++++++++++ .../Services/PriceSeekerTests.cs | 151 ++++++++++++++++++ PriceChecker.Core.Tests/TestHelpers.cs | 20 +++ PriceChecker.Core/Module.cs | 1 + PriceChecker.Core/Services/IoService.cs | 26 +++ PriceChecker.Core/Services/Persister.cs | 19 ++- PriceChecker.Core/Services/PriceSeeker.cs | 23 +-- .../PriceChecker.UI.Forms.Tests.csproj | 13 ++ .../PriceChecker.UI.Tests.csproj | 13 ++ .../MustBeUniqueValidationRuleTests.cs | 78 +++++++++ .../ValueCannotBeEmptyValidationRuleTests.cs | 85 ++++++++++ .../ValueCannotBeEmptyValidationRule.cs | 7 +- PriceChecker.sln | 28 ++++ 14 files changed, 531 insertions(+), 30 deletions(-) create mode 100644 PriceChecker.Core.Tests/Repositories/SettingsRepositoryTests.cs create mode 100644 PriceChecker.Core.Tests/Services/PriceSeekerTests.cs create mode 100644 PriceChecker.Core.Tests/TestHelpers.cs create mode 100644 PriceChecker.Core/Services/IoService.cs create mode 100644 PriceChecker.UI.Forms.Tests/PriceChecker.UI.Forms.Tests.csproj create mode 100644 PriceChecker.UI.Tests/PriceChecker.UI.Tests.csproj create mode 100644 PriceChecker.UI.Tests/Validation/MustBeUniqueValidationRuleTests.cs create mode 100644 PriceChecker.UI.Tests/Validation/ValueCannotBeEmptyValidationRuleTests.cs diff --git a/.gitignore b/.gitignore index 9bbe183..7ffe022 100644 --- a/.gitignore +++ b/.gitignore @@ -9,6 +9,7 @@ *.user *.userosscache *.sln.docstates +experiments.dib # User-specific files (MonoDevelop/Xamarin Studio) *.userprefs diff --git a/PriceChecker.Core.Tests/Repositories/SettingsRepositoryTests.cs b/PriceChecker.Core.Tests/Repositories/SettingsRepositoryTests.cs new file mode 100644 index 0000000..be767a7 --- /dev/null +++ b/PriceChecker.Core.Tests/Repositories/SettingsRepositoryTests.cs @@ -0,0 +1,96 @@ +using System; +using AutoFixture; +using Genius.PriceChecker.Core.Messages; +using Genius.PriceChecker.Core.Models; +using Genius.PriceChecker.Core.Repositories; +using Genius.PriceChecker.Core.Services; +using Genius.PriceChecker.Infrastructure.Events; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Genius.PriceChecker.Core.Tests.Repositories +{ + public class SettingsRepositoryTests + { + private readonly Fixture _fixture = new(); + private readonly Mock _eventBusMock = new(); + private readonly Mock _persisterMock = new(); + + [Fact] + public void Constructor__Previous_settings_exist__Loaded() + { + // Arrange + var settings = _fixture.Create(); + + // Act + var sut = CreateSystemUnderTest(settings); + + // Verify + Assert.Equal(settings, sut.Get()); + } + + [Fact] + public void Constructor__Previous_settings_dont_exist__Loaded_default() + { + // Arrange + Settings settings = null; + + // Act + var sut = CreateSystemUnderTest(settings); + + // Verify + var result = sut.Get(); + Assert.False(result.AutoRefreshEnabled); + Assert.Equal(1440, result.AutoRefreshMinutes); + } + + [Fact] + public void Get__returns_currently_loaded_settings() + { + // Arrange + var settings = _fixture.Create(); + var sut = CreateSystemUnderTest(settings); + + // Act + var result = sut.Get(); + + // Verify + Assert.Equal(settings, result); + } + + [Fact] + public void Store__Argument_not_provided__throws_exception() + { + // Arrange + var sut = CreateSystemUnderTest(); + + // Act & Verify + Assert.Throws(() => sut.Store(null)); + } + + [Fact] + public void Store__Replaces_existing_settings_and_updates_cache_and_fires_event() + { + // Arrange + var sut = CreateSystemUnderTest(); + var newSettings = _fixture.Create(); + + // Act + sut.Store(newSettings); + + // Verify + Assert.Equal(newSettings, sut.Get()); + _persisterMock.Verify(x => x.Store(It.IsAny(), newSettings)); + _eventBusMock.Verify(x => x.Publish(It.Is(e => e.Settings == newSettings)), Times.Once); + } + + private SettingsRepository CreateSystemUnderTest(Settings settings = null) + { + _persisterMock.Setup(x => x.Load(It.IsAny())) + .Returns(settings); + return new SettingsRepository(_eventBusMock.Object, _persisterMock.Object, + Mock.Of>()); + } + } +} diff --git a/PriceChecker.Core.Tests/Services/PriceSeekerTests.cs b/PriceChecker.Core.Tests/Services/PriceSeekerTests.cs new file mode 100644 index 0000000..544fed4 --- /dev/null +++ b/PriceChecker.Core.Tests/Services/PriceSeekerTests.cs @@ -0,0 +1,151 @@ +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using AutoFixture; +using Genius.PriceChecker.Core.Models; +using Genius.PriceChecker.Core.Services; +using Microsoft.Extensions.Logging; +using Moq; +using Xunit; + +namespace Genius.PriceChecker.Core.Tests.Services +{ + public class PriceSeekerTests + { + private readonly Fixture _fixture = new(); + private readonly Mock _httpMock = new(); + private readonly Mock _ioMock = new(); + private readonly Mock> _loggerMock = new(); + + private readonly PriceSeeker _sut; + + public PriceSeekerTests() + { + _fixture.Behaviors.Add(new OmitOnRecursionBehavior(recursionDepth: 2)); + + _sut = new PriceSeeker(_httpMock.Object, _ioMock.Object, _loggerMock.Object); + } + + [Fact] + public async Task SeekAsync__Happy_flow_scenario() + { + // Arrange + var product = CreateSampleProduct(); + + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); + + // Verify + Assert.Equal(product.Sources.Length, result.Length); + _ioMock.Verify(x => x.WriteTextToFile(It.IsAny(), It.IsAny()), Times.Never); + TestHelpers.VerifyLogger(_loggerMock, LogLevel.Warning, Times.Never()); + TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error, Times.Never()); + } + + [Fact] + public async Task SeekAsync__Content_wasnt_downloaded__Returns_null() + { + // Arrange + var product = CreateSampleProduct(sourcesCount: 1); + _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) + .ReturnsAsync((string)null); + + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); + + // Verify + Assert.Empty(result); + } + + [Fact] + public async Task SeekAsync__Content_not_matched_the_pattern__Returns_null_and_dumps_file() + { + // Arrange + var product = CreateSampleProduct(sourcesCount: 1); + var contentNotMatchingAnything = _fixture.Create(); + _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) + .ReturnsAsync(contentNotMatchingAnything); + + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); + + // Verify + Assert.Empty(result); + _ioMock.Verify(x => x.WriteTextToFile(It.IsAny(), It.IsAny()), Times.Once); + TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error); + } + + [Fact] + public async Task SeekAsync__DecimalDelimiter_isnt_default__Considered_in_price_parse() + { + // Arrange + var product = CreateSampleProduct(sourcesCount: 1, delimiter: ';'); + + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); + + // Verify + Assert.Single(result); + Assert.NotEqual(0, result[0].Price); + Assert.NotEqual((int)result[0].Price, result[0].Price); // Check if it is decimal + } + + [Fact] + public async Task SeekAsync__Price_is_invalid__Returns_null() + { + // Arrange + var product = CreateSampleProduct(sourcesCount: 1); + var contentPriceInvalid = "`0`"; + _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) + .ReturnsAsync(contentPriceInvalid); + + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); + + // Verify + Assert.Empty(result); + TestHelpers.VerifyLogger(_loggerMock, LogLevel.Warning); + } + + [Fact] + public async Task SeekAsync__Price_isnt_convertible__Returns_null() + { + // Arrange + var product = CreateSampleProduct(sourcesCount: 1); + var contentPriceInvalid = "`not-a-number`"; + _httpMock.Setup(x => x.DownloadContent(It.IsAny(), It.IsAny())) + .ReturnsAsync(contentPriceInvalid); + + // Act + var result = await _sut.SeekAsync(product, new CancellationToken()); + + // Verify + Assert.Empty(result); + TestHelpers.VerifyLogger(_loggerMock, LogLevel.Error); + } + + private Product CreateSampleProduct(int sourcesCount = 3, char delimiter = '.') + { + var product = _fixture.Build() + .With(x => x.Sources, _fixture.CreateMany(sourcesCount).ToArray()) + .Create(); + foreach (var productSource in product.Sources) + { + productSource.Agent = _fixture.Build() + .With(x => x.Url, _fixture.Create() + "{0}") + .With(x => x.PricePattern, $@"`(?[\d\{delimiter}]+)`") + .With(x => x.DecimalDelimiter, delimiter) + .Create(); + + var priceDec = _fixture.Create(); + var priceFlt = _fixture.Create(); + var content = $"{_fixture.Create()}`{priceDec}{delimiter}{priceFlt}`{_fixture.Create()}"; + + _httpMock.Setup(x => x.DownloadContent( + It.Is(url => url == string.Format(productSource.Agent.Url, productSource.AgentArgument)), It.IsAny())) + .ReturnsAsync(content); + } + return product; + } + } +} diff --git a/PriceChecker.Core.Tests/TestHelpers.cs b/PriceChecker.Core.Tests/TestHelpers.cs new file mode 100644 index 0000000..4d65596 --- /dev/null +++ b/PriceChecker.Core.Tests/TestHelpers.cs @@ -0,0 +1,20 @@ +using System; +using Microsoft.Extensions.Logging; +using Moq; + +namespace Genius.PriceChecker.Core.Tests +{ + public static class TestHelpers + { + public static void VerifyLogger(Mock> loggerMock, LogLevel logLevel, Times? times = null) + { + if (times == null) + times = Times.Once(); + loggerMock.Verify(x => x.Log(logLevel, + It.IsAny(), + It.IsAny(), + It.IsAny(), + (Func) It.IsAny()), times.Value); + } + } +} \ No newline at end of file diff --git a/PriceChecker.Core/Module.cs b/PriceChecker.Core/Module.cs index 3e7b323..2e98503 100644 --- a/PriceChecker.Core/Module.cs +++ b/PriceChecker.Core/Module.cs @@ -17,6 +17,7 @@ public static void Configure(IServiceCollection services) services.AddSingleton(); // Services + services.AddSingleton(); services.AddTransient(); services.AddTransient(); services.AddTransient(); diff --git a/PriceChecker.Core/Services/IoService.cs b/PriceChecker.Core/Services/IoService.cs new file mode 100644 index 0000000..c182c8a --- /dev/null +++ b/PriceChecker.Core/Services/IoService.cs @@ -0,0 +1,26 @@ +using System.Diagnostics.CodeAnalysis; +using System.IO; +using System.Text; + +namespace Genius.PriceChecker.Core.Services +{ + public interface IIoService + { + bool FileExists(string path); + string ReadTextFromFile(string path); + void WriteTextToFile(string path, string content); + } + + [ExcludeFromCodeCoverage] + public class IoService : IIoService + { + public bool FileExists(string path) + => File.Exists(path); + + public string ReadTextFromFile(string path) + => File.ReadAllText(path); + + public void WriteTextToFile(string path, string content) + => File.WriteAllText(path, content, Encoding.UTF8); + } +} diff --git a/PriceChecker.Core/Services/Persister.cs b/PriceChecker.Core/Services/Persister.cs index 33e8ece..f8495ee 100644 --- a/PriceChecker.Core/Services/Persister.cs +++ b/PriceChecker.Core/Services/Persister.cs @@ -1,25 +1,24 @@ -using System.Diagnostics.CodeAnalysis; -using System.IO; using System.Text.Json; using System.Threading; namespace Genius.PriceChecker.Core.Services { - public interface IPersister + public interface IPersister { T Load(string filePath); T[] LoadCollection(string filePath); void Store(string filePath, object data); } - [ExcludeFromCodeCoverage] internal sealed class Persister : IPersister { + private readonly IIoService _io; private readonly JsonSerializerOptions _jsonOptions; private static ReaderWriterLockSlim _locker = new(); - public Persister() + public Persister(IIoService io) { + _io = io; _jsonOptions = new JsonSerializerOptions { PropertyNameCaseInsensitive = true, WriteIndented = true @@ -31,11 +30,11 @@ public T Load(string filePath) _locker.EnterReadLock(); try { - if (!File.Exists(filePath)) + if (!_io.FileExists(filePath)) { return default(T); } - var content = File.ReadAllText(filePath); + var content = _io.ReadTextFromFile(filePath); return JsonSerializer.Deserialize(content, _jsonOptions); } finally @@ -49,11 +48,11 @@ public T[] LoadCollection(string filePath) _locker.EnterReadLock(); try { - if (!File.Exists(filePath)) + if (!_io.FileExists(filePath)) { return new T[0]; } - var content = File.ReadAllText(filePath); + var content = _io.ReadTextFromFile(filePath); return JsonSerializer.Deserialize(content, _jsonOptions); } finally @@ -68,7 +67,7 @@ public void Store(string filePath, object data) try { var json = JsonSerializer.Serialize(data, _jsonOptions); - File.WriteAllText(filePath, json); + _io.WriteTextToFile(filePath, json); } finally { diff --git a/PriceChecker.Core/Services/PriceSeeker.cs b/PriceChecker.Core/Services/PriceSeeker.cs index 9a47eff..4471828 100644 --- a/PriceChecker.Core/Services/PriceSeeker.cs +++ b/PriceChecker.Core/Services/PriceSeeker.cs @@ -1,12 +1,9 @@ -using System.IO; using System.Linq; -using System.Text; using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Genius.PriceChecker.Core.Messages; using Genius.PriceChecker.Core.Models; -using Genius.PriceChecker.Core.Repositories; using Microsoft.Extensions.Logging; namespace Genius.PriceChecker.Core.Services @@ -18,19 +15,18 @@ public interface IPriceSeeker internal sealed class PriceSeeker : IPriceSeeker { - private readonly IAgentRepository _agentRepo; private readonly ITrickyHttpClient _trickyHttpClient; + private readonly IIoService _io; private readonly ILogger _logger; private const char DEFAULT_DECIMAL_DELIMITER = '.'; private static object _locker = new(); - public PriceSeeker(IAgentRepository agentRepo, ITrickyHttpClient trickyHttpClient, - ILogger logger) + public PriceSeeker(ITrickyHttpClient trickyHttpClient, IIoService io, ILogger logger) { - _agentRepo = agentRepo; _trickyHttpClient = trickyHttpClient; + _io = io; _logger = logger; } @@ -46,13 +42,7 @@ public async Task SeekAsync(Product product, CancellationToke private async Task Seek(ProductSource productSource, CancellationToken cancel) { - var agent = _agentRepo.FindById(productSource.AgentId); - if (agent == null) - { - _logger.LogError($"Source not found: {productSource.AgentId}"); - return null; - } - + var agent = productSource.Agent; var url = string.Format(agent.Url, productSource.AgentArgument); var content = await _trickyHttpClient.DownloadContent(url, cancel); if (content == null) @@ -62,11 +52,12 @@ private async Task Seek(ProductSource productSource, Cancellati var match = re.Match(content); if (!match.Success) { + var dumpFileName = $"dump ({productSource.Id}).log"; lock(_locker) { - File.WriteAllText($"dump ({productSource.Id}).log", content, Encoding.UTF8); + _io.WriteTextToFile(dumpFileName, content); } - _logger.LogError($"Cannot match price from the given content. File = 'content.log', Url = '{url}'"); + _logger.LogError($"Cannot match price from the given content. File = '{dumpFileName}', Url = '{url}'"); return null; } diff --git a/PriceChecker.UI.Forms.Tests/PriceChecker.UI.Forms.Tests.csproj b/PriceChecker.UI.Forms.Tests/PriceChecker.UI.Forms.Tests.csproj new file mode 100644 index 0000000..36f317f --- /dev/null +++ b/PriceChecker.UI.Forms.Tests/PriceChecker.UI.Forms.Tests.csproj @@ -0,0 +1,13 @@ + + + + net5.0-windows + Genius.PriceChecker.UI.Forms.Tests + false + + + + + + + diff --git a/PriceChecker.UI.Tests/PriceChecker.UI.Tests.csproj b/PriceChecker.UI.Tests/PriceChecker.UI.Tests.csproj new file mode 100644 index 0000000..41776a9 --- /dev/null +++ b/PriceChecker.UI.Tests/PriceChecker.UI.Tests.csproj @@ -0,0 +1,13 @@ + + + + net5.0-windows + Genius.PriceChecker.UI.Tests + false + + + + + + + diff --git a/PriceChecker.UI.Tests/Validation/MustBeUniqueValidationRuleTests.cs b/PriceChecker.UI.Tests/Validation/MustBeUniqueValidationRuleTests.cs new file mode 100644 index 0000000..0ccd93d --- /dev/null +++ b/PriceChecker.UI.Tests/Validation/MustBeUniqueValidationRuleTests.cs @@ -0,0 +1,78 @@ +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using AutoFixture; +using Genius.PriceChecker.UI.Forms.ViewModels; +using Genius.PriceChecker.UI.Validation; +using Xunit; + +namespace Genius.PriceChecker.UI.Tests.Validation +{ + public class MustBeUniqueValidationRuleTests + { + private readonly Fixture _fixture = new(); + private readonly MustBeUniqueValidationRule _sut; + private readonly TestViewModel _testVm = new(); + + public MustBeUniqueValidationRuleTests() + { + _sut = new MustBeUniqueValidationRule(_testVm, nameof(TestViewModel.SampleSet)); + } + + [Fact] + public void Value_not_string__Returns_valid() + { + // Act + var result = _sut.Validate(new object(), _fixture.Create()); + + // Verify + Assert.True(result.IsValid); + } + + [Fact] + public void Value__Already_exists_twice_in_collection__Returns_not_valid() + { + // Arrange + _testVm.SampleSet = _fixture.CreateMany().ToList(); + _testVm.SampleSet.Add(_testVm.SampleSet[1]); + + // Act + var result = _sut.Validate(_testVm.SampleSet[1], _fixture.Create()); + + // Verify + Assert.False(result.IsValid); + } + + [Fact] + public void Value__Only_one_exists_in_collection__Returns_valid() + { + // Arrange + _testVm.SampleSet = _fixture.CreateMany().ToList(); + + // Act + var result = _sut.Validate(_testVm.SampleSet[1], _fixture.Create()); + + // Verify + Assert.True(result.IsValid); + } + + [Fact] + public void Value__Not_exists_in_collection__Returns_valid() + { + // Arrange + _testVm.SampleSet = _fixture.CreateMany().ToList(); + var valueToValidate = _fixture.Create(); + + // Act + var result = _sut.Validate(valueToValidate, _fixture.Create()); + + // Verify + Assert.True(result.IsValid); + } + + class TestViewModel : ViewModelBase + { + public List SampleSet { get; set; } + } + } +} diff --git a/PriceChecker.UI.Tests/Validation/ValueCannotBeEmptyValidationRuleTests.cs b/PriceChecker.UI.Tests/Validation/ValueCannotBeEmptyValidationRuleTests.cs new file mode 100644 index 0000000..8742812 --- /dev/null +++ b/PriceChecker.UI.Tests/Validation/ValueCannotBeEmptyValidationRuleTests.cs @@ -0,0 +1,85 @@ +using System.Globalization; +using AutoFixture; +using Genius.PriceChecker.UI.Validation; +using Xunit; + +namespace Genius.PriceChecker.UI.Tests.Validation +{ + public class ValueCannotBeEmptyValidationRuleTests + { + private readonly Fixture _fixture = new(); + private readonly ValueCannotBeEmptyValidationRule _sut = new(); + + [Fact] + public void Value_is_null__Returns_not_valid() + { + // Act + var result = _sut.Validate(null, _fixture.Create()); + + // Verify + Assert.False(result.IsValid); + } + + [Fact] + public void Value_is_not_string__Returns_not_valid() + { + // Act + var result = _sut.Validate(new object(), _fixture.Create()); + + // Verify + Assert.False(result.IsValid); + } + + [Fact] + public void Value_is_null_string__Returns_not_valid() + { + // Arrange + string value = null; + + // Act + var result = _sut.Validate(value, _fixture.Create()); + + // Verify + Assert.False(result.IsValid); + } + + [Fact] + public void Value_is_empty_string__Returns_not_valid() + { + // Arrange + string value = string.Empty; + + // Act + var result = _sut.Validate(value, _fixture.Create()); + + // Verify + Assert.False(result.IsValid); + } + + [Fact] + public void Value_is_whitespaced_string__Returns_not_valid() + { + // Arrange + string value = " "; + + // Act + var result = _sut.Validate(value, _fixture.Create()); + + // Verify + Assert.False(result.IsValid); + } + + [Fact] + public void Value_is_string__Returns_valid() + { + // Arrange + string value = _fixture.Create(); + + // Act + var result = _sut.Validate(value, _fixture.Create()); + + // Verify + Assert.True(result.IsValid); + } + } +} diff --git a/PriceChecker.UI/Validation/ValueCannotBeEmptyValidationRule.cs b/PriceChecker.UI/Validation/ValueCannotBeEmptyValidationRule.cs index 3e822a6..dbff13f 100644 --- a/PriceChecker.UI/Validation/ValueCannotBeEmptyValidationRule.cs +++ b/PriceChecker.UI/Validation/ValueCannotBeEmptyValidationRule.cs @@ -7,13 +7,12 @@ public class ValueCannotBeEmptyValidationRule : ValidationRule { public override ValidationResult Validate(object value, CultureInfo cultureInfo) { - if (value == null - || (value is string valueString && string.IsNullOrWhiteSpace(valueString))) + if (value is string valueString && !string.IsNullOrWhiteSpace(valueString)) { - return new ValidationResult(false, "Value cannot be empty."); + return ValidationResult.ValidResult; } - return ValidationResult.ValidResult; + return new ValidationResult(false, "Value cannot be empty."); } } } diff --git a/PriceChecker.sln b/PriceChecker.sln index 3572102..c2fb0c2 100644 --- a/PriceChecker.sln +++ b/PriceChecker.sln @@ -13,6 +13,10 @@ Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PriceChecker.UI.Forms", "Pr EndProject Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PriceChecker.Core.Tests", "PriceChecker.Core.Tests\PriceChecker.Core.Tests.csproj", "{00DFA49D-E5D8-4102-B887-9E154EEABA9C}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PriceChecker.UI.Tests", "PriceChecker.UI.Tests\PriceChecker.UI.Tests.csproj", "{51D8DB71-C58D-45A0-B733-6BBEE2A64C44}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "PriceChecker.UI.Forms.Tests", "PriceChecker.UI.Forms.Tests\PriceChecker.UI.Forms.Tests.csproj", "{1AFF9770-2023-4DB1-B9DC-257948B55FD5}" +EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution Debug|Any CPU = Debug|Any CPU @@ -86,5 +90,29 @@ Global {00DFA49D-E5D8-4102-B887-9E154EEABA9C}.Release|x64.Build.0 = Release|Any CPU {00DFA49D-E5D8-4102-B887-9E154EEABA9C}.Release|x86.ActiveCfg = Release|Any CPU {00DFA49D-E5D8-4102-B887-9E154EEABA9C}.Release|x86.Build.0 = Release|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Debug|Any CPU.Build.0 = Debug|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Debug|x64.ActiveCfg = Debug|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Debug|x64.Build.0 = Debug|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Debug|x86.ActiveCfg = Debug|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Debug|x86.Build.0 = Debug|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Release|Any CPU.ActiveCfg = Release|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Release|Any CPU.Build.0 = Release|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Release|x64.ActiveCfg = Release|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Release|x64.Build.0 = Release|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Release|x86.ActiveCfg = Release|Any CPU + {51D8DB71-C58D-45A0-B733-6BBEE2A64C44}.Release|x86.Build.0 = Release|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Debug|Any CPU.Build.0 = Debug|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Debug|x64.ActiveCfg = Debug|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Debug|x64.Build.0 = Debug|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Debug|x86.ActiveCfg = Debug|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Debug|x86.Build.0 = Debug|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Release|Any CPU.ActiveCfg = Release|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Release|Any CPU.Build.0 = Release|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Release|x64.ActiveCfg = Release|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Release|x64.Build.0 = Release|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Release|x86.ActiveCfg = Release|Any CPU + {1AFF9770-2023-4DB1-B9DC-257948B55FD5}.Release|x86.Build.0 = Release|Any CPU EndGlobalSection EndGlobal